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:
co-authored by
Claude Fable 5
parent
c014c4b3ee
commit
7b36b5cc34
@@ -1309,7 +1309,7 @@ fn derive_channel_psk(channel_name: &str) -> Vec<u8> {
|
||||
/// Map a Meshtastic `RegionCode` name (as set in `mesh-config.json`, e.g.
|
||||
/// "EU_868", "US", "ANZ") to its protobuf enum value. Case-insensitive.
|
||||
/// Returns `None` for an unrecognised name so we never write a bogus region.
|
||||
fn region_name_to_code(name: &str) -> Option<u32> {
|
||||
pub(crate) fn region_name_to_code(name: &str) -> Option<u32> {
|
||||
Some(match name.trim().to_uppercase().as_str() {
|
||||
"UNSET" => 0,
|
||||
"US" => 1,
|
||||
|
||||
@@ -401,6 +401,12 @@ pub struct MeshConfig {
|
||||
pub reticulum_tcp: Option<types::ReticulumTcpConfig>,
|
||||
}
|
||||
|
||||
/// True when `name` is a LoRa region the Meshtastic driver can program
|
||||
/// (the canonical region list for the mesh settings UI).
|
||||
pub fn meshtastic_region_is_valid(name: &str) -> bool {
|
||||
meshtastic::region_name_to_code(name).is_some()
|
||||
}
|
||||
|
||||
fn default_assistant_backend() -> String {
|
||||
"claude".to_string()
|
||||
}
|
||||
@@ -545,6 +551,12 @@ pub async fn detect_devices() -> Vec<String> {
|
||||
serial::detect_serial_devices().await
|
||||
}
|
||||
|
||||
/// Detected serial ports with USB identity metadata (for the device-detected
|
||||
/// setup modal's hardware graphic).
|
||||
pub async fn detect_devices_info() -> Vec<serial::DetectedDeviceInfo> {
|
||||
serial::detect_serial_devices_info().await
|
||||
}
|
||||
|
||||
// ─── MeshService ────────────────────────────────────────────────────────
|
||||
|
||||
/// Top-level mesh networking service.
|
||||
|
||||
@@ -509,6 +509,83 @@ pub async fn detect_serial_devices() -> Vec<String> {
|
||||
devices
|
||||
}
|
||||
|
||||
/// USB identity of a detected serial port, read from sysfs — lets the UI
|
||||
/// show the actual board (native-USB boards like T-Deck/RAK4631 report their
|
||||
/// name in `product`; bridge chips like CP2102/CH340 only identify the chip,
|
||||
/// so vid:pid is the fallback signal).
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct DetectedDeviceInfo {
|
||||
pub path: String,
|
||||
pub vid: Option<String>,
|
||||
pub pid: Option<String>,
|
||||
pub product: Option<String>,
|
||||
pub manufacturer: Option<String>,
|
||||
}
|
||||
|
||||
/// Like `detect_serial_devices`, but with USB metadata per port.
|
||||
pub async fn detect_serial_devices_info() -> Vec<DetectedDeviceInfo> {
|
||||
let mut out = Vec::new();
|
||||
for path in detect_serial_devices().await {
|
||||
let usb = usb_info_for_tty(&path).await;
|
||||
out.push(DetectedDeviceInfo {
|
||||
path,
|
||||
vid: usb.0,
|
||||
pid: usb.1,
|
||||
product: usb.2,
|
||||
manufacturer: usb.3,
|
||||
});
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Resolve a tty path (following the /dev/mesh-radio symlink) to its USB
|
||||
/// device sysfs node and read idVendor/idProduct/product/manufacturer.
|
||||
/// Best-effort: any miss returns None fields (e.g. non-USB UARTs).
|
||||
async fn usb_info_for_tty(
|
||||
path: &str,
|
||||
) -> (
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
) {
|
||||
let resolved = tokio::fs::canonicalize(path)
|
||||
.await
|
||||
.unwrap_or_else(|_| std::path::PathBuf::from(path));
|
||||
let Some(name) = resolved.file_name().and_then(|n| n.to_str()) else {
|
||||
return (None, None, None, None);
|
||||
};
|
||||
// /sys/class/tty/<name>/device -> .../usbN/N-M/N-M:1.0/(ttyUSBx|tty). Walk
|
||||
// up from the device link until a directory with idVendor appears.
|
||||
let mut dir = std::path::PathBuf::from(format!("/sys/class/tty/{name}/device"));
|
||||
for _ in 0..6 {
|
||||
if tokio::fs::metadata(dir.join("idVendor")).await.is_ok() {
|
||||
let read = |f: &str| {
|
||||
let p = dir.join(f);
|
||||
async move {
|
||||
tokio::fs::read_to_string(p)
|
||||
.await
|
||||
.ok()
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
}
|
||||
};
|
||||
return (
|
||||
read("idVendor").await,
|
||||
read("idProduct").await,
|
||||
read("product").await,
|
||||
read("manufacturer").await,
|
||||
);
|
||||
}
|
||||
dir.push("..");
|
||||
let Ok(canon) = tokio::fs::canonicalize(&dir).await else {
|
||||
break;
|
||||
};
|
||||
dir = canon;
|
||||
}
|
||||
(None, None, None, None)
|
||||
}
|
||||
|
||||
/// Try to open and handshake with each detected serial device.
|
||||
/// Returns the first device that responds as Meshcore.
|
||||
pub async fn probe_for_meshcore(paths: &[String]) -> Option<(String, DeviceInfo)> {
|
||||
|
||||
Reference in New Issue
Block a user