archy/neode-ui/src/utils/loraRegions.ts
archipelago 7b36b5cc34 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>
2026-07-14 08:40:18 -04:00

103 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* LoRa regional frequency plans — the canonical region list the backend's
* Meshtastic driver can program (core/.../mesh/meshtastic.rs region table),
* annotated with band / duty-cycle / power constraints from the Meshtastic
* firmware `regions[]` table so the UI can surface legality hints.
*
* Also used to derive suggested radio parameters for MeshCore and RNode
* presets (those firmwares take raw frequency/BW/SF/CR, not a region enum).
*/
export interface LoraRegion {
/** Wire value for mesh.configure { lora_region } */
code: string
label: string
/** Frequency range, MHz (display) */
band: string
/** Regional duty-cycle cap, percent (100 = none) */
dutyCyclePct: number
/** Max TX power, dBm */
maxPowerDbm: number
}
export const LORA_REGIONS: LoraRegion[] = [
{ code: 'US', label: 'United States / Americas (915 MHz)', band: '902928', dutyCyclePct: 100, maxPowerDbm: 30 },
{ code: 'EU_868', label: 'Europe (868 MHz)', band: '869.4869.65', dutyCyclePct: 10, maxPowerDbm: 27 },
{ code: 'EU_433', label: 'Europe (433 MHz)', band: '433434', dutyCyclePct: 10, maxPowerDbm: 10 },
{ code: 'ANZ', label: 'Australia / New Zealand (915 MHz)', band: '915928', dutyCyclePct: 100, maxPowerDbm: 30 },
{ code: 'ANZ_433', label: 'Australia / New Zealand (433 MHz)', band: '433.05434.79', dutyCyclePct: 100, maxPowerDbm: 14 },
{ code: 'NZ_865', label: 'New Zealand (865 MHz)', band: '864868', dutyCyclePct: 100, maxPowerDbm: 36 },
{ code: 'CN', label: 'China (470 MHz)', band: '470510', dutyCyclePct: 100, maxPowerDbm: 19 },
{ code: 'JP', label: 'Japan (920 MHz)', band: '920.5923.5', dutyCyclePct: 100, maxPowerDbm: 13 },
{ code: 'KR', label: 'South Korea (920 MHz)', band: '920923', dutyCyclePct: 100, maxPowerDbm: 23 },
{ code: 'TW', label: 'Taiwan (920 MHz)', band: '920925', dutyCyclePct: 100, maxPowerDbm: 27 },
{ code: 'RU', label: 'Russia (868 MHz)', band: '868.7869.2', dutyCyclePct: 100, maxPowerDbm: 20 },
{ code: 'IN', label: 'India (865 MHz)', band: '865867', dutyCyclePct: 100, maxPowerDbm: 30 },
{ code: 'TH', label: 'Thailand (920 MHz)', band: '920925', dutyCyclePct: 10, maxPowerDbm: 27 },
{ code: 'UA_868', label: 'Ukraine (868 MHz)', band: '868868.6', dutyCyclePct: 1, maxPowerDbm: 14 },
{ code: 'UA_433', label: 'Ukraine (433 MHz)', band: '433434.7', dutyCyclePct: 10, maxPowerDbm: 10 },
{ code: 'MY_919', label: 'Malaysia (919 MHz)', band: '919924', dutyCyclePct: 100, maxPowerDbm: 27 },
{ code: 'MY_433', label: 'Malaysia (433 MHz)', band: '433435', dutyCyclePct: 100, maxPowerDbm: 20 },
{ code: 'SG_923', label: 'Singapore (923 MHz)', band: '917925', dutyCyclePct: 100, maxPowerDbm: 20 },
{ code: 'PH_915', label: 'Philippines (915 MHz)', band: '915918', dutyCyclePct: 100, maxPowerDbm: 24 },
{ code: 'PH_868', label: 'Philippines (868 MHz)', band: '868869.4', dutyCyclePct: 100, maxPowerDbm: 14 },
{ code: 'PH_433', label: 'Philippines (433 MHz)', band: '433434.7', dutyCyclePct: 100, maxPowerDbm: 10 },
{ code: 'LORA_24', label: 'Worldwide (2.4 GHz, SX1280 only)', band: '24002483.5', dutyCyclePct: 100, maxPowerDbm: 10 },
]
export function regionByCode(code: string | null | undefined): LoraRegion | undefined {
if (!code) return undefined
return LORA_REGIONS.find(r => r.code === code.toUpperCase())
}
/**
* Coarse lat/lon → LoRa region suggestion. Bounding boxes, most-specific
* first — good enough to preselect the dropdown; the user confirms.
*/
export function suggestRegionFromLatLon(lat: number, lon: number): LoraRegion | undefined {
const boxes: Array<{ code: string; latMin: number; latMax: number; lonMin: number; lonMax: number }> = [
{ code: 'JP', latMin: 24, latMax: 46, lonMin: 123, lonMax: 146 },
{ code: 'KR', latMin: 33, latMax: 39, lonMin: 124, lonMax: 132 },
{ code: 'TW', latMin: 21.5, latMax: 25.5, lonMin: 119.5, lonMax: 122.5 },
{ code: 'PH_915', latMin: 4, latMax: 21, lonMin: 116, lonMax: 127 },
{ code: 'SG_923', latMin: 1, latMax: 1.6, lonMin: 103.5, lonMax: 104.2 },
{ code: 'MY_919', latMin: 0.8, latMax: 7.5, lonMin: 99, lonMax: 119.5 },
{ code: 'TH', latMin: 5.5, latMax: 20.5, lonMin: 97, lonMax: 106 },
{ code: 'IN', latMin: 6, latMax: 36, lonMin: 68, lonMax: 97.5 },
{ code: 'CN', latMin: 18, latMax: 54, lonMin: 73, lonMax: 135 },
{ code: 'UA_868', latMin: 44, latMax: 52.5, lonMin: 22, lonMax: 40.5 },
{ code: 'RU', latMin: 41, latMax: 82, lonMin: 27, lonMax: 180 },
{ code: 'ANZ', latMin: -48, latMax: -9, lonMin: 112, lonMax: 180 },
{ code: 'EU_868', latMin: 34, latMax: 72, lonMin: -25, lonMax: 45 },
// Americas
{ code: 'US', latMin: -56, latMax: 72, lonMin: -170, lonMax: -30 },
]
for (const b of boxes) {
if (lat >= b.latMin && lat <= b.latMax && lon >= b.lonMin && lon <= b.lonMax) {
return regionByCode(b.code)
}
}
return undefined
}
/** Community MeshCore radio plan for a region, where one is well established.
* MeshCore has no region enum — the firmware takes raw freq/BW/SF/CR — so
* these are display-level suggestions (EU is the verified community default;
* other regions show the legal band and defer to the local community plan). */
export interface MeshcorePlan {
freqMhz: number
bwKhz: number
sf: number
cr: number
}
export const MESHCORE_PLANS: Record<string, MeshcorePlan> = {
EU_868: { freqMhz: 869.525, bwKhz: 250, sf: 11, cr: 5 },
EU_433: { freqMhz: 433.65, bwKhz: 250, sf: 11, cr: 5 },
}
export function meshcorePlanFor(code: string | null | undefined): MeshcorePlan | undefined {
if (!code) return undefined
return MESHCORE_PLANS[code.toUpperCase()]
}