feat: build module selection

This commit is contained in:
Ben Allfree
2025-11-23 09:37:10 -08:00
parent dfea7a358d
commit 6ddf13f2e8
7 changed files with 302 additions and 65 deletions

View File

@@ -99,11 +99,9 @@ jobs:
echo "Building for target: ${{ inputs.target }}"
echo "Flags: ${{ inputs.flags }}"
# Inject flags into platformio.ini or environment if needed
# For now, we rely on PIO's ability to take env vars or just run the target
# Real implementation might need more complex flag handling
# Example: export PLATFORMIO_BUILD_FLAGS="${{ inputs.flags }}"
# Inject flags into platformio.ini or environment
export PLATFORMIO_BUILD_FLAGS="${{ inputs.flags }}"
echo "PLATFORMIO_BUILD_FLAGS set to: $PLATFORMIO_BUILD_FLAGS"
pio run -e ${{ inputs.target }}

View File

@@ -1,7 +1,8 @@
import { getAuthUserId } from "@convex-dev/auth/server";
import { v } from "convex/values";
import { api, } from "./_generated/api";
import { api } from "./_generated/api";
import { internalMutation, mutation, query } from "./_generated/server";
import modulesData from "./modules.json";
/**
* Normalizes a config object to a stable JSON string for hashing.
@@ -95,16 +96,17 @@ export const triggerBuild = mutation({
}
// Convert config object to flags string
const flags = Object.entries(profile.config)
.map(([key, value]) => {
if (value === true) return `-D${key}`;
if (typeof value === "number") return `-D${key}=${value}`;
if (typeof value === "string" && value.trim() !== "")
return `-D${key}=${value}`;
return null;
})
.filter(Boolean)
.join(" ");
const flags: string[] = [];
// Handle Modules (Inverted Logic: Default Excluded)
for (const module of modulesData.modules) {
// If config[id] is NOT false (explicitly included), we exclude it.
if (profile.config[module.id] !== false) {
flags.push(`-D${module.id}=1`);
}
}
const flagsString = flags.join(" ");
// Create build records for each target
for (const target of profile.targets) {
@@ -126,7 +128,7 @@ export const triggerBuild = mutation({
if (cached) {
// Use cached artifact, skip GitHub workflow
const artifactUrl = getR2ArtifactUrl(buildHash);
const buildId = await ctx.db.insert("builds", {
const _buildId = await ctx.db.insert("builds", {
profileId: profile._id,
target: target,
githubRunId: 0,
@@ -153,7 +155,7 @@ export const triggerBuild = mutation({
await ctx.scheduler.runAfter(0, api.actions.dispatchGithubBuild, {
buildId: buildId,
target: target,
flags: flags,
flags: flagsString,
version: profile.version,
buildHash: buildHash,
});
@@ -237,17 +239,18 @@ export const retryBuild = mutation({
completedAt: undefined,
});
// Retry the build
const flags = Object.entries(profile.config)
.map(([key, value]) => {
if (value === true) return `-D${key}`;
if (typeof value === "number") return `-D${key}=${value}`;
if (typeof value === "string" && value.trim() !== "")
return `-D${key}=${value}`;
return null;
})
.filter(Boolean)
.join(" ");
// Convert config object to flags string
const flags: string[] = [];
// Handle Modules (Inverted Logic: Default Excluded)
for (const module of modulesData.modules) {
// If config[id] is NOT false (explicitly included), we exclude it.
if (profile.config[module.id] !== false) {
flags.push(`-D${module.id}=1`);
}
}
const flagsString = flags.join(" ");
// Compute build hash for retry
const buildHash = await computeBuildHash(
@@ -259,7 +262,7 @@ export const retryBuild = mutation({
await ctx.scheduler.runAfter(0, api.actions.dispatchGithubBuild, {
buildId: args.buildId,
target: build.target,
flags: flags,
flags: flagsString,
version: profile.version,
buildHash: buildHash,
});

159
convex/modules.json Normal file
View File

@@ -0,0 +1,159 @@
{
"modules": [
{
"id": "MESHTASTIC_EXCLUDE_ADMIN",
"name": "Admin",
"description": "Remote device configuration and management. Allows changing settings, reading device info, and rebooting nodes over the mesh network."
},
{
"id": "MESHTASTIC_EXCLUDE_ATAK",
"name": "ATAK Plugin",
"description": "Integration with ATAK (Android Team Awareness Kit) for tactical situational awareness. Enables military/emergency response coordination."
},
{
"id": "MESHTASTIC_EXCLUDE_AUDIO",
"name": "Audio",
"description": "Audio codec support for voice communication over the mesh."
},
{
"id": "MESHTASTIC_EXCLUDE_BLUETOOTH",
"name": "Bluetooth",
"description": "Bluetooth connectivity for pairing with phones and apps. Required for mobile app communication on most devices."
},
{
"id": "MESHTASTIC_EXCLUDE_CANNEDMESSAGES",
"name": "Canned Messages",
"description": "Pre-defined quick messages that can be sent with button presses. Useful for devices with limited input (no keyboard). Includes on-screen keyboard for some devices."
},
{
"id": "MESHTASTIC_EXCLUDE_DETECTIONSENSOR",
"name": "Detection Sensor",
"description": "Motion/presence detection sensor integration. Broadcasts detection events when sensors trigger (PIR, door switches, etc.)."
},
{
"id": "MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR",
"name": "Environmental Sensor",
"description": "Environmental monitoring sensors including temperature, humidity, pressure, air quality, and light sensors. Broadcasts telemetry data to the mesh."
},
{
"id": "MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION",
"name": "External Notification",
"description": "Drive external LEDs, buzzers, and speakers for notifications. Plays RTTTL ringtones and can control GPIO outputs when messages arrive."
},
{
"id": "MESHTASTIC_EXCLUDE_GPS",
"name": "GPS",
"description": "GPS receiver support for position tracking and sharing. Disabling prevents position broadcasts but the device still relays position packets for other nodes."
},
{
"id": "MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY",
"name": "Health Telemetry",
"description": "Heart rate and health monitoring sensors (like MAX30102 pulse oximeter). Broadcasts health metrics to the mesh."
},
{
"id": "MESHTASTIC_EXCLUDE_I2C",
"name": "I2C",
"description": "I2C bus support for external sensors and peripherals. Required for most sensor modules and OLED displays."
},
{
"id": "MESHTASTIC_EXCLUDE_INPUTBROKER",
"name": "Input Broker",
"description": "Input device handling (buttons, encoders, touchscreens). Routes button presses to appropriate modules like Canned Messages."
},
{
"id": "MESHTASTIC_EXCLUDE_MQTT",
"name": "MQTT",
"description": "MQTT client for cloud integration. Publishes mesh messages to MQTT brokers and subscribes to receive cloud messages. Enables IoT integration and remote monitoring."
},
{
"id": "MESHTASTIC_EXCLUDE_NEIGHBORINFO",
"name": "Neighbor Info",
"description": "Broadcasts information about directly-reachable neighbor nodes including signal strength (SNR). Helps build mesh topology maps and track network health."
},
{
"id": "MESHTASTIC_EXCLUDE_PAXCOUNTER",
"name": "Pax Counter",
"description": "Counts nearby WiFi and Bluetooth devices for crowd density estimation. Useful for people-counting in public spaces."
},
{
"id": "MESHTASTIC_EXCLUDE_PKI",
"name": "PKI",
"description": "Public Key Infrastructure for enhanced security and key verification between nodes."
},
{
"id": "MESHTASTIC_EXCLUDE_POWERMON",
"name": "Power Monitor",
"description": "Battery and power monitoring hardware support (INA260, INA219, etc.). Tracks voltage, current, and power consumption."
},
{
"id": "MESHTASTIC_EXCLUDE_POWER_FSM",
"name": "Power FSM",
"description": "Power management finite state machine. Handles sleep modes, power state transitions, and battery optimization."
},
{
"id": "MESHTASTIC_EXCLUDE_POWER_TELEMETRY",
"name": "Power Telemetry",
"description": "Broadcasts battery voltage, current, and power consumption data to the mesh. Different from Power Monitor which is the hardware interface."
},
{
"id": "MESHTASTIC_EXCLUDE_POWERSTRESS",
"name": "Power Stress",
"description": "Power consumption testing tool. Stresses the device to measure battery life under various transmission patterns. For development/testing only."
},
{
"id": "MESHTASTIC_EXCLUDE_RANGETEST",
"name": "Range Test",
"description": "Mesh range and signal quality testing. Sends periodic packets and logs signal strength (RSSI), SNR, and packet loss statistics to a file."
},
{
"id": "MESHTASTIC_EXCLUDE_REMOTEHARDWARE",
"name": "Remote Hardware",
"description": "Remote GPIO control over the mesh. Read/write digital pins, read ADC values, and control hardware on remote nodes."
},
{
"id": "MESHTASTIC_EXCLUDE_SCREEN",
"name": "Screen",
"description": "OLED/E-Ink display support. Shows messages, node info, and status on screen. Disabling saves power but removes visual feedback."
},
{
"id": "MESHTASTIC_EXCLUDE_SERIAL",
"name": "Serial",
"description": "Serial port communication for sensors and external devices. Can relay serial data over the mesh and supports NMEA GPS bridging."
},
{
"id": "MESHTASTIC_EXCLUDE_STOREFORWARD",
"name": "Store & Forward",
"description": "Message store-and-forward server for offline nodes. Router devices can cache messages and replay them when distant nodes reconnect. Requires PSRAM."
},
{
"id": "MESHTASTIC_EXCLUDE_TEXTMESSAGE",
"name": "Text Messaging",
"description": "Send and receive text messages between nodes. Displays messages on OLED screens and forwards to connected apps. **Important:** Disabling prevents sending/receiving but the node still relays messages for others."
},
{
"id": "MESHTASTIC_EXCLUDE_TRACEROUTE",
"name": "Traceroute",
"description": "Network path tracing tool. Shows the route packets take through the mesh, including all intermediate hops and hop limits."
},
{
"id": "MESHTASTIC_EXCLUDE_TZ",
"name": "Timezone",
"description": "Timezone database support for local time display. Allows devices to show correct local time based on GPS position."
},
{
"id": "MESHTASTIC_EXCLUDE_WAYPOINT",
"name": "Waypoint",
"description": "Share and display waypoints (points of interest) on the mesh. Shows waypoints on screen and in apps for navigation and location marking."
},
{
"id": "MESHTASTIC_EXCLUDE_WEBSERVER",
"name": "Web Server",
"description": "Built-in web interface for device configuration. Automatically excluded if WiFi is disabled."
},
{
"id": "MESHTASTIC_EXCLUDE_WIFI",
"name": "WiFi",
"description": "WiFi connectivity for network access, web server, and MQTT. Disabling saves power but removes WiFi features including the web interface."
}
]
}

View File

@@ -5,8 +5,8 @@
"type": "module",
"scripts": {
"generate:versions": "node scripts/generate-versions.js",
"dev": "bun run generate:versions && bun run scan:options && vite",
"build": "bun run generate:versions && bun run scan:options && tsc && vite build",
"dev": "bun run generate:versions && vite",
"build": "bun run generate:versions && tsc && vite build",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
"deploy": "npx convex deploy --cmd 'bun run build' && wrangler deploy"

View File

@@ -0,0 +1,62 @@
interface ModuleCardProps {
name: string;
description: string;
selected: boolean;
onClick: () => void;
}
export function ModuleCard({
name,
description,
selected,
onClick,
}: ModuleCardProps) {
return (
<button
type="button"
onClick={onClick}
className={`
w-full text-left p-4 rounded-lg border-2 transition-all
${
selected
? "border-blue-500 bg-blue-500/10"
: "border-slate-700 bg-slate-900/50 hover:border-slate-600"
}
`}
>
<div className="flex items-start gap-3">
<div className="mt-1">
<div
className={`
w-5 h-5 rounded border-2 flex items-center justify-center
${selected ? "border-blue-500 bg-blue-500" : "border-slate-500"}
`}
>
{selected && (
<svg
className="w-3 h-3 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<title>Checkmark</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M5 13l4 4L19 7"
/>
</svg>
)}
</div>
</div>
<div className="flex-1 min-w-0">
<h4 className="font-semibold text-sm mb-1">{name}</h4>
<p className="text-xs text-slate-400 leading-relaxed">
{description}
</p>
</div>
</div>
</button>
);
}

View File

@@ -5,8 +5,10 @@ import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { api } from "../../convex/_generated/api";
import modulesData from "../../convex/modules.json";
import { TARGETS } from "../constants/targets";
import { VERSIONS } from "../constants/versions";
import { ModuleCard } from "./ModuleCard";
interface ProfileEditorProps {
initialData?: any;
@@ -26,10 +28,7 @@ export default function ProfileEditor({
defaultValues: initialData || {
name: "",
targets: [],
config: {
MESHTASTIC_EXCLUDE_MQTT: false,
MESHTASTIC_EXCLUDE_AUDIO: false,
},
config: {},
version: VERSIONS[0],
},
});
@@ -99,18 +98,22 @@ export default function ProfileEditor({
>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium mb-2">Profile Name</label>
<label htmlFor="name" className="block text-sm font-medium mb-2">
Profile Name
</label>
<Input
id="name"
{...register("name")}
className="bg-slate-950 border-slate-800"
placeholder="e.g. Solar Repeater"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">
<label htmlFor="version" className="block text-sm font-medium mb-2">
Firmware Version
</label>
<select
id="version"
{...register("version")}
className="w-full h-10 px-3 rounded-md border border-slate-800 bg-slate-950 text-sm focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 focus:ring-offset-slate-950"
>
@@ -125,8 +128,6 @@ export default function ProfileEditor({
<div>
<label className="block text-sm font-medium mb-2">Targets</label>
{/* ... existing target UI */}
<div className="space-y-4">
{/* Category Pills */}
<div className="flex flex-wrap gap-2">
@@ -173,6 +174,11 @@ export default function ProfileEditor({
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
>
{item.name}
{item.architecture && (
<span className="ml-2 text-xs text-slate-500">
({item.architecture})
</span>
)}
</label>
</div>
))}
@@ -181,36 +187,43 @@ export default function ProfileEditor({
</div>
</div>
<div>
<label className="block text-sm font-medium mb-2">
Configuration Flags
</label>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Checkbox
id="no_mqtt"
checked={watch("config.MESHTASTIC_EXCLUDE_MQTT")}
onCheckedChange={(checked) =>
setValue("config.MESHTASTIC_EXCLUDE_MQTT", checked)
}
/>
<label htmlFor="no_mqtt">Exclude MQTT</label>
<div className="space-y-6">
<div>
<div className="mb-4">
<h3 className="text-lg font-medium">Modules</h3>
<p className="text-sm text-slate-400">
Select the modules to include in your build.
</p>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="no_audio"
checked={watch("config.MESHTASTIC_EXCLUDE_AUDIO")}
onCheckedChange={(checked) =>
setValue("config.MESHTASTIC_EXCLUDE_AUDIO", checked)
}
/>
<label htmlFor="no_audio">Exclude Audio</label>
<div className="flex flex-col gap-2">
{modulesData.modules.map((module) => {
// Inverted logic:
// config[id] === false -> Explicitly Included
// config[id] === true or undefined -> Excluded
const configValue = watch(`config.${module.id}`);
const isIncluded = configValue === false;
return (
<ModuleCard
key={module.id}
name={module.name}
description={module.description}
selected={isIncluded}
onClick={() => {
// Toggle:
// If currently included (true), we want to exclude (set config to true)
// If currently excluded (false), we want to include (set config to false)
setValue(`config.${module.id}`, !!isIncluded);
}}
/>
);
})}
</div>
</div>
</div>
<div className="flex gap-2 justify-end">
<Button type="button" variant="ghost" onClick={onCancel}>
<div className="flex justify-end gap-4 pt-4">
<Button type="button" variant="outline" onClick={onCancel}>
Cancel
</Button>
<Button type="submit">Save Profile</Button>

View File

@@ -3,6 +3,7 @@ import hardwareList from "../../vendor/web-flasher/public/data/hardware-list.jso
export interface TargetMetadata {
name: string;
category: string;
architecture?: string;
}
export const TARGETS: Record<string, TargetMetadata> = {};
@@ -17,6 +18,7 @@ sortedHardware.forEach((hw) => {
TARGETS[hw.platformioTarget] = {
name: hw.displayName || hw.platformioTarget,
category: hw.tags?.[0] || "Other",
architecture: hw.architecture,
};
}
});