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
@@ -23,6 +23,31 @@ export interface MeshStatus {
|
||||
receive_block_headers?: boolean
|
||||
/** Operator-configured LoRa region (e.g. "EU_868"), for the Device tab. */
|
||||
region?: string | null
|
||||
/** Persisted config: LoRa region the driver programs on fresh radios. */
|
||||
lora_region?: string | null
|
||||
/** Persisted config: firmware pin (meshcore|meshtastic|reticulum), null = auto-detect. */
|
||||
device_kind?: string | null
|
||||
/** USB identity per detected port (for the setup modal's board image). */
|
||||
detected_device_info?: Array<{
|
||||
path: string
|
||||
vid?: string | null
|
||||
pid?: string | null
|
||||
product?: string | null
|
||||
manufacturer?: string | null
|
||||
}>
|
||||
}
|
||||
|
||||
/** Params accepted by mesh.configure (superset of the status fields). */
|
||||
export interface MeshConfigureParams {
|
||||
enabled?: boolean
|
||||
device_path?: string
|
||||
channel_name?: string
|
||||
broadcast_identity?: boolean
|
||||
advert_name?: string
|
||||
announce_block_headers?: boolean
|
||||
receive_block_headers?: boolean
|
||||
lora_region?: string
|
||||
device_kind?: string
|
||||
}
|
||||
|
||||
export interface MeshPeer {
|
||||
@@ -234,6 +259,33 @@ export const useMeshStore = defineStore('mesh', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Global device-detection (for the app-wide "device detected" modal) ──
|
||||
// Ports the user dismissed the setup modal for, persisted per browser.
|
||||
const DETECT_DISMISS_KEY = 'archipelago.mesh.detect-dismissed.v1'
|
||||
const dismissedDetectedPaths = ref<Set<string>>(new Set(
|
||||
JSON.parse(localStorage.getItem(DETECT_DISMISS_KEY) || '[]') as string[]
|
||||
))
|
||||
const undismissedDetectedDevices = computed(() => {
|
||||
const s = status.value
|
||||
if (!s || s.device_connected || s.enabled) return []
|
||||
return (s.detected_devices || []).filter(p => !dismissedDetectedPaths.value.has(p))
|
||||
})
|
||||
function dismissDetectedDevice(path: string) {
|
||||
dismissedDetectedPaths.value.add(path)
|
||||
dismissedDetectedPaths.value = new Set(dismissedDetectedPaths.value)
|
||||
localStorage.setItem(DETECT_DISMISS_KEY, JSON.stringify([...dismissedDetectedPaths.value]))
|
||||
}
|
||||
let globalDetectTimer: ReturnType<typeof setInterval> | null = null
|
||||
/** App-wide light poll so the detected-device modal works on every page
|
||||
* (the Mesh view's own 5s poll takes over while it is mounted). */
|
||||
function startGlobalDetection(intervalMs = 15000) {
|
||||
if (globalDetectTimer) return
|
||||
void fetchStatus()
|
||||
globalDetectTimer = setInterval(() => {
|
||||
if (document.visibilityState === 'visible') void fetchStatus()
|
||||
}, intervalMs)
|
||||
}
|
||||
|
||||
async function fetchPeers() {
|
||||
try {
|
||||
const res = await rpcClient.call<{ peers: MeshPeer[]; count: number }>({
|
||||
@@ -455,12 +507,12 @@ export const useMeshStore = defineStore('mesh', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function configure(config: Partial<MeshStatus>) {
|
||||
async function configure(config: MeshConfigureParams) {
|
||||
try {
|
||||
error.value = null
|
||||
await rpcClient.call<{ configured: boolean }>({
|
||||
method: 'mesh.configure',
|
||||
params: config,
|
||||
params: config as Record<string, unknown>,
|
||||
})
|
||||
await fetchStatus()
|
||||
} catch (err: unknown) {
|
||||
@@ -851,6 +903,9 @@ export const useMeshStore = defineStore('mesh', () => {
|
||||
blockHeaders,
|
||||
latestBlockHeight,
|
||||
fetchStatus,
|
||||
undismissedDetectedDevices,
|
||||
dismissDetectedDevice,
|
||||
startGlobalDetection,
|
||||
fetchPeers,
|
||||
fetchMessages,
|
||||
sendMessage,
|
||||
|
||||
Reference in New Issue
Block a user