feat(mesh-ui): region-recommended RNode plan applies from the setup modal too
Demo images / Build & push demo images (push) Successful in 4m4s

The device-detected modal's region selector now drives real RNode
settings instead of a "managed by the daemon config" shrug: choosing a
region shows its concrete plan (frequency/bw/SF/CR/power) and Apply &
Connect writes it through mesh.rnode-config-apply — the same
radio-confirmed round-trip as the Device panel, best-effort so a plan
failure never aborts the connect. RNODE_REGION_PLANS moves to
utils/loraRegions (single source shared by panel + modal).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-06 10:31:01 -04:00
co-authored by Claude Fable 5
parent f394c02055
commit 4773b32a76
3 changed files with 43 additions and 17 deletions
@@ -256,8 +256,11 @@
<p v-if="effectiveKind === 'meshcore' && rfPreset" class="text-[11px] text-sky-300/80 mt-1">
MeshCore RF params for {{ selectedRegion?.code }} are applied to the radio automatically on connect.
</p>
<p v-if="effectiveKind === 'reticulum'" class="text-[11px] text-sky-300/80 mt-1">
RNode radio parameters (frequency / bandwidth / SF / CR) are managed by the Reticulum daemon's interface config on this node.
<p v-if="effectiveKind === 'reticulum' && form.region && RNODE_REGION_PLANS[form.region]" class="text-[11px] text-sky-300/80 mt-1">
RNode plan for {{ form.region }}: {{ (RNODE_REGION_PLANS[form.region].frequency / 1e6).toFixed(4) }} MHz, {{ RNODE_REGION_PLANS[form.region].bandwidth / 1000 }} kHz, SF{{ RNODE_REGION_PLANS[form.region].spreading_factor }}, CR4/{{ RNODE_REGION_PLANS[form.region].coding_rate }}, {{ RNODE_REGION_PLANS[form.region].txpower }} dBm applied on connect, and the radio confirms it.
</p>
<p v-else-if="effectiveKind === 'reticulum'" class="text-[11px] text-sky-300/80 mt-1">
Pick a region to apply its recommended RNode RF plan on connect editable any time in Mesh Device settings.
</p>
</div>
@@ -302,7 +305,7 @@ import { useRouter } from 'vue-router'
import BaseModal from '@/components/BaseModal.vue'
import { useMeshStore, type MeshDeviceProbe, type MeshConfigureParams, type FlashFirmwareFamily, type FlashBoard, type FlashJobStatus } from '@/stores/mesh'
import { useAppStore } from '@/stores/app'
import { LORA_REGIONS, regionByCode, suggestRegionFromLatLon, meshcorePlanFor } from '@/utils/loraRegions'
import { LORA_REGIONS, regionByCode, suggestRegionFromLatLon, meshcorePlanFor, RNODE_REGION_PLANS } from '@/utils/loraRegions'
import { resolveMeshDeviceImage } from '@/utils/meshDeviceImages'
const mesh = useMeshStore()
@@ -501,6 +504,18 @@ async function applySetup() {
}
}
await mesh.configure(params)
// RNode radios: the region's recommended RF plan is applied through the
// Reticulum daemon's persisted settings (mesh.rnode-config-apply), the
// same round-trip the Device panel uses — the radio confirms the values
// itself after the daemon restarts with them. Best-effort here: a
// failure must not abort the connect the user just asked for.
if (effectiveKind.value === 'reticulum' && form.value.region) {
const plan = RNODE_REGION_PLANS[form.value.region]
if (plan) {
mesh.suppressDeviceDetect()
void mesh.applyRnodeConfig({ enabled: true, port: null, ...plan }).catch(() => {})
}
}
mesh.dismissDetectedDevice(path)
void router.push('/dashboard/mesh')
} catch (e) {
+24
View File
@@ -126,3 +126,27 @@ export const MESHCORE_RF_PRESETS: MeshcoreRfPreset[] = [
{ id: 'us_anz_915', label: 'US / Canada / ANZ — 915.0 MHz, 250 kHz, SF10, CR4/5', freqMhz: 915.0, bwKhz: 250, sf: 10, cr: 5 },
{ id: 'eu_433', label: 'Europe (433 MHz) — 433.65 MHz, 250 kHz, SF11, CR4/5', freqMhz: 433.65, bwKhz: 250, sf: 11, cr: 5 },
]
/** Recommended Reticulum RNode RF plan per region. Applied via
* mesh.rnode-config-apply (the daemon restarts the radio with these and the
* radio confirms them back). EU868 is the operator-validated Portugal plan:
* 869.4625 MHz sits in the 10%-duty 869.4869.65 sub-band clear of the
* default community channel, with the EU airtime locks written in. Others
* follow RNS community conventions with the region's legal power cap. */
export interface RnodeRegionPlan {
frequency: number
bandwidth: number
spreading_factor: number
coding_rate: number
txpower: number
airtime_limit_short: number | null
airtime_limit_long: number | null
}
export const RNODE_REGION_PLANS: Record<string, RnodeRegionPlan> = {
EU868: { frequency: 869462500, bandwidth: 125000, spreading_factor: 8, coding_rate: 5, txpower: 14, airtime_limit_short: 25, airtime_limit_long: 10 },
US915: { frequency: 914875000, bandwidth: 125000, spreading_factor: 8, coding_rate: 5, txpower: 17, airtime_limit_short: null, airtime_limit_long: null },
AU915: { frequency: 916800000, bandwidth: 125000, spreading_factor: 8, coding_rate: 5, txpower: 17, airtime_limit_short: null, airtime_limit_long: null },
ANZ: { frequency: 916800000, bandwidth: 125000, spreading_factor: 8, coding_rate: 5, txpower: 17, airtime_limit_short: null, airtime_limit_long: null },
AS923: { frequency: 923200000, bandwidth: 125000, spreading_factor: 8, coding_rate: 5, txpower: 13, airtime_limit_short: null, airtime_limit_long: null },
IN865: { frequency: 866000000, bandwidth: 125000, spreading_factor: 8, coding_rate: 5, txpower: 17, airtime_limit_short: null, airtime_limit_long: null },
}
+1 -14
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useMeshStore } from '@/stores/mesh'
import { LORA_REGIONS, regionByCode, meshcorePlanFor, MESHCORE_RF_PRESETS } from '@/utils/loraRegions'
import { LORA_REGIONS, regionByCode, meshcorePlanFor, MESHCORE_RF_PRESETS, RNODE_REGION_PLANS } from '@/utils/loraRegions'
const mesh = useMeshStore()
@@ -28,19 +28,6 @@ async function handleReboot() {
}
// ── RNode (Reticulum) RF settings — full round-trip with device read-back ──
// Recommended plans per region for Reticulum RNode radios. EU868 is the
// operator-validated Portugal plan (869.4625 MHz keeps clear of the default
// community channel while staying in the 10%-duty 869.4869.65 sub-band;
// airtime locks match EU duty-cycle law). Others use the RNS community
// conventions for the band with the region's legal power cap.
const RNODE_REGION_PLANS: Record<string, { frequency: number; bandwidth: number; spreading_factor: number; coding_rate: number; txpower: number; airtime_limit_short: number | null; airtime_limit_long: number | null }> = {
EU868: { frequency: 869462500, bandwidth: 125000, spreading_factor: 8, coding_rate: 5, txpower: 14, airtime_limit_short: 25, airtime_limit_long: 10 },
US915: { frequency: 914875000, bandwidth: 125000, spreading_factor: 8, coding_rate: 5, txpower: 17, airtime_limit_short: null, airtime_limit_long: null },
AU915: { frequency: 916800000, bandwidth: 125000, spreading_factor: 8, coding_rate: 5, txpower: 17, airtime_limit_short: null, airtime_limit_long: null },
ANZ: { frequency: 916800000, bandwidth: 125000, spreading_factor: 8, coding_rate: 5, txpower: 17, airtime_limit_short: null, airtime_limit_long: null },
AS923: { frequency: 923200000, bandwidth: 125000, spreading_factor: 8, coding_rate: 5, txpower: 13, airtime_limit_short: null, airtime_limit_long: null },
IN865: { frequency: 866000000, bandwidth: 125000, spreading_factor: 8, coding_rate: 5, txpower: 17, airtime_limit_short: null, airtime_limit_long: null },
}
const rnodeForm = ref({
enabled: true,