From 999a1005319955cb983fd870629201726f5aa5c1 Mon Sep 17 00:00:00 2001 From: archipelago Date: Tue, 21 Jul 2026 06:53:10 -0400 Subject: [PATCH] feat(mesh): RF-params settings UI + expose lora_radio_params in mesh.status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Device panel gets MeshCore RF fields (frequency MHz, bandwidth kHz, SF, CR) wired to mesh.configure's validated lora_radio_params; shown only for MeshCore radios, all-empty leaves the radio's flashed settings untouched. Human units in the UI, firmware field units on the wire. Per operator direction the panel's static stat rows duplicating editable fields (name, region, channel) are dropped — the editable control is the display. Stale 'adjust via a MeshCore client' hints updated: the node programs the radio now. Co-Authored-By: Claude Fable 5 --- core/archipelago/src/api/rpc/mesh/status.rs | 4 + neode-ui/src/stores/mesh.ts | 3 + neode-ui/src/views/mesh/MeshDevicePanel.vue | 88 +++++++++++++++++---- 3 files changed, 81 insertions(+), 14 deletions(-) diff --git a/core/archipelago/src/api/rpc/mesh/status.rs b/core/archipelago/src/api/rpc/mesh/status.rs index fd8c2813..74332a69 100644 --- a/core/archipelago/src/api/rpc/mesh/status.rs +++ b/core/archipelago/src/api/rpc/mesh/status.rs @@ -41,6 +41,10 @@ impl RpcHandler { // 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( + "lora_radio_params".into(), + serde_json::to_value(config.lora_radio_params).unwrap_or_default(), + ); obj.insert( "device_kind".into(), config diff --git a/neode-ui/src/stores/mesh.ts b/neode-ui/src/stores/mesh.ts index f8cfc670..65e6c023 100644 --- a/neode-ui/src/stores/mesh.ts +++ b/neode-ui/src/stores/mesh.ts @@ -48,6 +48,9 @@ export interface MeshConfigureParams { receive_block_headers?: boolean lora_region?: string device_kind?: string + /** MeshCore LoRa PHY params in firmware field units (freq_khz = MHz×1000, + * bw_hz = kHz×1000). null clears; omitted leaves unchanged. */ + lora_radio_params?: { freq_khz: number; bw_hz: number; sf: number; cr: number } | null } export interface MeshPeer { diff --git a/neode-ui/src/views/mesh/MeshDevicePanel.vue b/neode-ui/src/views/mesh/MeshDevicePanel.vue index 2d7da3e6..5364dedd 100644 --- a/neode-ui/src/views/mesh/MeshDevicePanel.vue +++ b/neode-ui/src/views/mesh/MeshDevicePanel.vue @@ -27,6 +27,12 @@ const form = ref({ channel: 'archipelago', name: '', broadcastIdentity: true, + // MeshCore LoRa PHY params (human units; converted to firmware units on + // save). All four empty = leave the radio's flashed settings untouched. + rfFreqMhz: '', + rfBwKhz: '', + rfSf: '', + rfCr: '', }) const saving = ref(false) const saveError = ref(null) @@ -43,6 +49,16 @@ watch( form.value.deviceKind = s.device_kind ?? 'auto' form.value.channel = s.channel_name || 'archipelago' form.value.name = s.self_advert_name ?? '' + const rp = (s as Record).lora_radio_params as + | { freq_khz: number; bw_hz: number; sf: number; cr: number } + | null + | undefined + if (rp) { + form.value.rfFreqMhz = String(rp.freq_khz / 1000) + form.value.rfBwKhz = String(rp.bw_hz / 1000) + form.value.rfSf = String(rp.sf) + form.value.rfCr = String(rp.cr) + } }, { immediate: true }, ) @@ -62,12 +78,31 @@ async function saveSettings() { saveError.value = null saveDone.value = false try { + // MeshCore RF params: send only when all four are filled (partial input + // is a user error surfaced by the API's range validation; all-empty + // means "leave the radio's flashed settings alone"). + const rf = [form.value.rfFreqMhz, form.value.rfBwKhz, form.value.rfSf, form.value.rfCr] + const rfAll = rf.every((v) => String(v).trim() !== '') + const rfAny = rf.some((v) => String(v).trim() !== '') + if (rfAny && !rfAll) { + throw new Error('Fill in all four RF fields (frequency, bandwidth, SF, CR) or leave all empty') + } await mesh.configure({ lora_region: form.value.region, device_kind: form.value.deviceKind, channel_name: form.value.channel.trim() || 'archipelago', ...(form.value.name.trim() ? { advert_name: form.value.name.trim() } : {}), broadcast_identity: form.value.broadcastIdentity, + ...(rfAll + ? { + lora_radio_params: { + freq_khz: Math.round(parseFloat(form.value.rfFreqMhz) * 1000), + bw_hz: Math.round(parseFloat(form.value.rfBwKhz) * 1000), + sf: parseInt(form.value.rfSf, 10), + cr: parseInt(form.value.rfCr, 10), + }, + } + : {}), }) saveDone.value = true setTimeout(() => { saveDone.value = false }, 3000) @@ -93,18 +128,6 @@ async function saveSettings() { Node ID {{ mesh.status.self_node_id != null ? `!${mesh.status.self_node_id.toString(16).padStart(8, '0')}` : '—' }} -
- Name - {{ mesh.status.self_advert_name ?? '—' }} -
-
- Region (radio) - {{ mesh.status.region ?? 'Not set' }} -
-
- Channel - {{ mesh.status.channel_name }} -
Type {{ deviceType === 'unknown' ? '—' : deviceType }} @@ -128,10 +151,10 @@ async function saveSettings() { Applied to fresh (region-unset) Meshtastic radios; a radio that already has a region keeps it.

- MeshCore community plan for {{ selectedRegion?.code }}: {{ meshcorePlan.freqMhz }} MHz, {{ meshcorePlan.bwKhz }} kHz, SF{{ meshcorePlan.sf }}, CR4/{{ meshcorePlan.cr }} — the radio's flashed RF settings apply; adjust via a MeshCore client if they differ. + MeshCore community plan for {{ selectedRegion?.code }}: {{ meshcorePlan.freqMhz }} MHz, {{ meshcorePlan.bwKhz }} kHz, SF{{ meshcorePlan.sf }}, CR4/{{ meshcorePlan.cr }} — set the RF fields below to program the radio.

- MeshCore radios keep their flashed RF settings — verify the radio matches your region's band{{ selectedRegion ? ` (${selectedRegion.band} MHz)` : '' }}. + Program the radio's RF settings with the fields below — every radio on your mesh must match{{ selectedRegion ? ` (${selectedRegion.band} MHz band)` : '' }}.

RNode RF parameters are managed by the Reticulum daemon's interface config on this node. @@ -158,6 +181,43 @@ async function saveSettings() {

+ + +
+
MeshCore RF parameters
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+

+ Saved settings program the radio on its next connect (it reboots once to apply). Leave all four empty to keep the radio's own settings. +

+