feat(mesh): RF-params settings UI + expose lora_radio_params in mesh.status
Some checks failed
Demo images / Build & push demo images (push) Has been cancelled
Some checks failed
Demo images / Build & push demo images (push) Has been cancelled
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 <noreply@anthropic.com>
This commit is contained in:
parent
a5810d4e51
commit
999a100531
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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<string | null>(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<string, unknown>).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() {
|
||||
<span class="mesh-stat-label">Node ID</span>
|
||||
<span class="mesh-stat-value">{{ mesh.status.self_node_id != null ? `!${mesh.status.self_node_id.toString(16).padStart(8, '0')}` : '—' }}</span>
|
||||
</div>
|
||||
<div class="mesh-stat">
|
||||
<span class="mesh-stat-label">Name</span>
|
||||
<span class="mesh-stat-value">{{ mesh.status.self_advert_name ?? '—' }}</span>
|
||||
</div>
|
||||
<div class="mesh-stat">
|
||||
<span class="mesh-stat-label">Region (radio)</span>
|
||||
<span class="mesh-stat-value">{{ mesh.status.region ?? 'Not set' }}</span>
|
||||
</div>
|
||||
<div class="mesh-stat">
|
||||
<span class="mesh-stat-label">Channel</span>
|
||||
<span class="mesh-stat-value">{{ mesh.status.channel_name }}</span>
|
||||
</div>
|
||||
<div class="mesh-stat">
|
||||
<span class="mesh-stat-label">Type</span>
|
||||
<span class="mesh-stat-value">{{ deviceType === 'unknown' ? '—' : deviceType }}</span>
|
||||
@ -128,10 +151,10 @@ async function saveSettings() {
|
||||
Applied to fresh (region-unset) Meshtastic radios; a radio that already has a region keeps it.
|
||||
</p>
|
||||
<p v-else-if="effectiveKind === 'meshcore' && meshcorePlan" class="text-[11px] text-sky-300/80 mt-1">
|
||||
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.
|
||||
</p>
|
||||
<p v-else-if="effectiveKind === 'meshcore'" class="text-[11px] text-sky-300/80 mt-1">
|
||||
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)` : '' }}.
|
||||
</p>
|
||||
<p v-else-if="effectiveKind === 'reticulum'" class="text-[11px] text-sky-300/80 mt-1">
|
||||
RNode RF parameters are managed by the Reticulum daemon's interface config on this node.
|
||||
@ -158,6 +181,43 @@ async function saveSettings() {
|
||||
<input v-model="form.name" maxlength="24" placeholder="node name" class="w-full rounded-lg bg-white/[0.06] border border-white/10 text-white px-3 py-2 text-sm focus:outline-none focus:border-orange-400/60" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MeshCore LoRa PHY params — every radio on the local mesh must match
|
||||
or it hears RF energy but decodes nothing. Empty = leave the radio's
|
||||
flashed settings untouched. -->
|
||||
<div v-if="effectiveKind === 'meshcore'" class="mt-4">
|
||||
<h5 class="text-xs font-semibold text-white/80 mb-2">MeshCore RF parameters</h5>
|
||||
<div class="grid gap-3 grid-cols-2 sm:grid-cols-4">
|
||||
<div>
|
||||
<label class="block text-xs text-white/60 mb-1">Frequency (MHz)</label>
|
||||
<input v-model="form.rfFreqMhz" inputmode="decimal" placeholder="869.618" class="w-full rounded-lg bg-white/[0.06] border border-white/10 text-white px-3 py-2 text-sm focus:outline-none focus:border-orange-400/60" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-white/60 mb-1">Bandwidth (kHz)</label>
|
||||
<select v-model="form.rfBwKhz" class="w-full rounded-lg bg-white/[0.06] border border-white/10 text-white px-3 py-2 text-sm focus:outline-none focus:border-orange-400/60">
|
||||
<option value="">—</option>
|
||||
<option v-for="bw in ['62.5', '125', '250', '500']" :key="bw" :value="bw">{{ bw }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-white/60 mb-1">Spreading factor</label>
|
||||
<select v-model="form.rfSf" class="w-full rounded-lg bg-white/[0.06] border border-white/10 text-white px-3 py-2 text-sm focus:outline-none focus:border-orange-400/60">
|
||||
<option value="">—</option>
|
||||
<option v-for="sf in [5, 6, 7, 8, 9, 10, 11, 12]" :key="sf" :value="String(sf)">SF {{ sf }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-white/60 mb-1">Coding rate</label>
|
||||
<select v-model="form.rfCr" class="w-full rounded-lg bg-white/[0.06] border border-white/10 text-white px-3 py-2 text-sm focus:outline-none focus:border-orange-400/60">
|
||||
<option value="">—</option>
|
||||
<option v-for="cr in [5, 6, 7, 8]" :key="cr" :value="String(cr)">4/{{ cr }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-[11px] text-white/40 mt-1">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
<label class="flex items-center gap-2 mt-3 text-sm text-white/80 cursor-pointer">
|
||||
<input v-model="form.broadcastIdentity" type="checkbox" class="h-4 w-4 accent-orange-500" />
|
||||
Periodically broadcast this node's identity on the mesh
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user