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>
103 lines
5.6 KiB
TypeScript
103 lines
5.6 KiB
TypeScript
/**
|
||
* 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: '902–928', dutyCyclePct: 100, maxPowerDbm: 30 },
|
||
{ code: 'EU_868', label: 'Europe (868 MHz)', band: '869.4–869.65', dutyCyclePct: 10, maxPowerDbm: 27 },
|
||
{ code: 'EU_433', label: 'Europe (433 MHz)', band: '433–434', dutyCyclePct: 10, maxPowerDbm: 10 },
|
||
{ code: 'ANZ', label: 'Australia / New Zealand (915 MHz)', band: '915–928', dutyCyclePct: 100, maxPowerDbm: 30 },
|
||
{ code: 'ANZ_433', label: 'Australia / New Zealand (433 MHz)', band: '433.05–434.79', dutyCyclePct: 100, maxPowerDbm: 14 },
|
||
{ code: 'NZ_865', label: 'New Zealand (865 MHz)', band: '864–868', dutyCyclePct: 100, maxPowerDbm: 36 },
|
||
{ code: 'CN', label: 'China (470 MHz)', band: '470–510', dutyCyclePct: 100, maxPowerDbm: 19 },
|
||
{ code: 'JP', label: 'Japan (920 MHz)', band: '920.5–923.5', dutyCyclePct: 100, maxPowerDbm: 13 },
|
||
{ code: 'KR', label: 'South Korea (920 MHz)', band: '920–923', dutyCyclePct: 100, maxPowerDbm: 23 },
|
||
{ code: 'TW', label: 'Taiwan (920 MHz)', band: '920–925', dutyCyclePct: 100, maxPowerDbm: 27 },
|
||
{ code: 'RU', label: 'Russia (868 MHz)', band: '868.7–869.2', dutyCyclePct: 100, maxPowerDbm: 20 },
|
||
{ code: 'IN', label: 'India (865 MHz)', band: '865–867', dutyCyclePct: 100, maxPowerDbm: 30 },
|
||
{ code: 'TH', label: 'Thailand (920 MHz)', band: '920–925', dutyCyclePct: 10, maxPowerDbm: 27 },
|
||
{ code: 'UA_868', label: 'Ukraine (868 MHz)', band: '868–868.6', dutyCyclePct: 1, maxPowerDbm: 14 },
|
||
{ code: 'UA_433', label: 'Ukraine (433 MHz)', band: '433–434.7', dutyCyclePct: 10, maxPowerDbm: 10 },
|
||
{ code: 'MY_919', label: 'Malaysia (919 MHz)', band: '919–924', dutyCyclePct: 100, maxPowerDbm: 27 },
|
||
{ code: 'MY_433', label: 'Malaysia (433 MHz)', band: '433–435', dutyCyclePct: 100, maxPowerDbm: 20 },
|
||
{ code: 'SG_923', label: 'Singapore (923 MHz)', band: '917–925', dutyCyclePct: 100, maxPowerDbm: 20 },
|
||
{ code: 'PH_915', label: 'Philippines (915 MHz)', band: '915–918', dutyCyclePct: 100, maxPowerDbm: 24 },
|
||
{ code: 'PH_868', label: 'Philippines (868 MHz)', band: '868–869.4', dutyCyclePct: 100, maxPowerDbm: 14 },
|
||
{ code: 'PH_433', label: 'Philippines (433 MHz)', band: '433–434.7', dutyCyclePct: 100, maxPowerDbm: 10 },
|
||
{ code: 'LORA_24', label: 'Worldwide (2.4 GHz, SX1280 only)', band: '2400–2483.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()]
|
||
}
|