feat(mesh): device-detected setup modal with board images + full radio settings

Plugging in a LoRa radio now pops a global two-step setup modal on any
page: step 1 shows the ACTUAL detected board (25 board SVGs vendored
from the Meshtastic web-flasher, GPL-3.0; matched by USB product string
on native-USB boards, vid:pid heuristics for bridge chips, animated
fallback otherwise); step 2 configures with Archipelago presets — the
LoRa region pre-selected from the node's location (new lat/lon→region
mapping), channel 'archipelago', node name, and an advanced firmware
pin. Options adapt per protocol: region+channel program Meshtastic;
MeshCore shows its community frequency plan (firmware owns RF); RNode
defers to the Reticulum daemon config.

Backend: mesh.configure now accepts lora_region (validated against the
driver's region table) and device_kind (probe pin); mesh.status returns
the persisted values plus per-port USB identity (sysfs vid/pid/product)
for board identification. The Mesh Device tab gains a full settings
form (region with duty-cycle/legality hints, firmware pin, channel,
name, identity broadcast) replacing the read-only panel; the old
Mesh-page-only onboarding modal is superseded by the global flow.

Mock backend: stateful mesh config so the dev UI demos the full
detect→configure round-trip (disable mesh → modal fires with Heltec V3).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-07-14 08:40:18 -04:00
co-authored by Claude Fable 5
parent c014c4b3ee
commit 7b36b5cc34
38 changed files with 5210 additions and 71 deletions
@@ -145,6 +145,32 @@ impl RpcHandler {
{
config.receive_block_headers = receive;
}
// LoRa region (Meshtastic): validated against the driver's region
// table so a typo can't be persisted and silently ignored on connect.
// Empty string clears the setting (radio keeps/uses its own region).
if let Some(region) = params.get("lora_region").and_then(|v| v.as_str()) {
let trimmed = region.trim();
if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("unset") {
config.lora_region = None;
} else if mesh::meshtastic_region_is_valid(trimmed) {
config.lora_region = Some(trimmed.to_uppercase());
} else {
anyhow::bail!("Unknown LoRa region: {trimmed}");
}
}
// Firmware pin: probe only the named firmware on the port ("auto"/""
// clears the pin and restores strict-probe auto-detect).
if let Some(kind) = params.get("device_kind").and_then(|v| v.as_str()) {
config.device_kind = match kind.trim().to_lowercase().as_str() {
"" | "auto" => None,
"meshcore" => Some(mesh::types::DeviceType::Meshcore),
"meshtastic" => Some(mesh::types::DeviceType::Meshtastic),
"reticulum" | "rnode" => Some(mesh::types::DeviceType::Reticulum),
other => anyhow::bail!(
"Unknown device_kind: {other} (expected auto|meshcore|meshtastic|reticulum)"
),
};
}
mesh::save_config(&self.config.data_dir, &config).await?;
@@ -161,6 +187,8 @@ impl RpcHandler {
"device_path": config.device_path,
"announce_block_headers": config.announce_block_headers,
"receive_block_headers": config.receive_block_headers,
"lora_region": config.lora_region,
"device_kind": config.device_kind.map(|k| k.to_string()),
}))
}
}
@@ -37,6 +37,24 @@ impl RpcHandler {
"receive_block_headers".into(),
config.receive_block_headers.into(),
);
// Persisted config values the settings UI edits (distinct from the
// live radio-reported `region`): the configured LoRa region and
// the firmware pin ("meshcore"|"meshtastic"|"reticulum"|null=auto).
obj.insert("lora_region".into(), config.lora_region.clone().into());
obj.insert(
"device_kind".into(),
config
.device_kind
.map(|k| k.to_string().to_lowercase())
.into(),
);
// USB identity per detected port so the setup modal can show the
// actual board (product string on native-USB boards, vid:pid as
// the fallback for bridge chips).
obj.insert(
"detected_device_info".into(),
serde_json::to_value(mesh::detect_devices_info().await).unwrap_or_default(),
);
// Raw serial-device presence, in BOTH branches. MeshStatus has no
// such field, so while the service was running the UI couldn't
// tell "no radio plugged in" from "radio present but the session