feat(ui): MeshCore RF frequency-plan presets by country + Custom entry
All checks were successful
Demo images / Build & push demo images (push) Successful in 2m47s
All checks were successful
Demo images / Build & push demo images (push) Successful in 2m47s
The RF section shipped as four bare fields; restore a prepopulated plan list (dropdown) with Custom unlocking free entry, per operator direction. Preset labels carry the full values, so the number fields render only in Custom mode — no duplicated display. Presets limited to verifiable sources: the repo's MeshCore community plans (EU 868/433), the MeshCore firmware's own build defaults (platformio.ini arduino_base 869.618/62.5/ SF8 CR5; companion example 915.0/250/SF10/CR5), and the operator-attested Portugal plan (869.618/62.5/SF8/CR8). Stored params are matched back to a named preset on load; hand-editing flips the dropdown to Custom. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
a71a18bffd
commit
918b0275a9
@ -100,3 +100,29 @@ export function meshcorePlanFor(code: string | null | undefined): MeshcorePlan |
|
||||
if (!code) return undefined
|
||||
return MESHCORE_PLANS[code.toUpperCase()]
|
||||
}
|
||||
|
||||
/** A named MeshCore RF preset for the Device-panel dropdown. */
|
||||
export interface MeshcoreRfPreset {
|
||||
id: string
|
||||
label: string
|
||||
freqMhz: number
|
||||
bwKhz: number
|
||||
sf: number
|
||||
cr: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepopulated MeshCore RF plans by country/region for the Device panel's
|
||||
* preset dropdown ("Custom" in the UI unlocks free entry). Values are only
|
||||
* added here when verifiable: the MeshCore community plans above, the
|
||||
* MeshCore firmware's own build defaults (platformio.ini `arduino_base`:
|
||||
* 869.618/62.5/SF8, CR default 5; companion example: 915.0/250/SF10/CR5),
|
||||
* and operator-attested deployment plans.
|
||||
*/
|
||||
export const MESHCORE_RF_PRESETS: MeshcoreRfPreset[] = [
|
||||
{ id: 'eu_868', label: 'Europe / UK — 869.525 MHz, 250 kHz, SF11, CR4/5', freqMhz: 869.525, bwKhz: 250, sf: 11, cr: 5 },
|
||||
{ id: 'pt', label: 'Portugal — 869.618 MHz, 62.5 kHz, SF8, CR4/8', freqMhz: 869.618, bwKhz: 62.5, sf: 8, cr: 8 },
|
||||
{ id: 'fw_default', label: 'MeshCore firmware default — 869.618 MHz, 62.5 kHz, SF8, CR4/5', freqMhz: 869.618, bwKhz: 62.5, sf: 8, cr: 5 },
|
||||
{ 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 },
|
||||
]
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useMeshStore } from '@/stores/mesh'
|
||||
import { LORA_REGIONS, regionByCode, meshcorePlanFor } from '@/utils/loraRegions'
|
||||
import { LORA_REGIONS, regionByCode, meshcorePlanFor, MESHCORE_RF_PRESETS } from '@/utils/loraRegions'
|
||||
|
||||
const mesh = useMeshStore()
|
||||
|
||||
@ -54,15 +54,49 @@ watch(
|
||||
| null
|
||||
| undefined
|
||||
if (rp) {
|
||||
applyingPreset = true
|
||||
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)
|
||||
// Show the matching named preset if the stored values are one; else Custom.
|
||||
const match = MESHCORE_RF_PRESETS.find(
|
||||
(p) =>
|
||||
Math.round(p.freqMhz * 1000) === rp.freq_khz &&
|
||||
Math.round(p.bwKhz * 1000) === rp.bw_hz &&
|
||||
p.sf === rp.sf &&
|
||||
p.cr === rp.cr,
|
||||
)
|
||||
rfPreset.value = match ? match.id : 'custom'
|
||||
applyingPreset = false
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// RF preset dropdown: picking a named plan fills the four fields; hand-editing
|
||||
// any field flips the dropdown to Custom so it never misrepresents the values.
|
||||
const rfPreset = ref('')
|
||||
let applyingPreset = false
|
||||
function selectRfPreset(id: string) {
|
||||
rfPreset.value = id
|
||||
if (id === 'custom' || id === '') return
|
||||
const p = MESHCORE_RF_PRESETS.find((x) => x.id === id)
|
||||
if (!p) return
|
||||
applyingPreset = true
|
||||
form.value.rfFreqMhz = String(p.freqMhz)
|
||||
form.value.rfBwKhz = String(p.bwKhz)
|
||||
form.value.rfSf = String(p.sf)
|
||||
form.value.rfCr = String(p.cr)
|
||||
applyingPreset = false
|
||||
}
|
||||
watch(
|
||||
() => [form.value.rfFreqMhz, form.value.rfBwKhz, form.value.rfSf, form.value.rfCr],
|
||||
() => {
|
||||
if (!applyingPreset && rfPreset.value && rfPreset.value !== 'custom') rfPreset.value = 'custom'
|
||||
},
|
||||
)
|
||||
|
||||
const selectedRegion = computed(() => regionByCode(form.value.region))
|
||||
const deviceType = computed(() => mesh.status?.device_type ?? 'unknown')
|
||||
// Firmware whose options apply: explicit pin wins, else the connected type.
|
||||
@ -78,14 +112,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')
|
||||
// MeshCore RF params: a named preset sends its plan; Custom sends the
|
||||
// four fields (all required — partial input is an error); "Keep the
|
||||
// radio's current settings" omits the key so the config is untouched.
|
||||
let rfParams: { freq_khz: number; bw_hz: number; sf: number; cr: number } | undefined
|
||||
if (rfPreset.value && rfPreset.value !== 'custom') {
|
||||
const p = MESHCORE_RF_PRESETS.find((x) => x.id === rfPreset.value)
|
||||
if (p) {
|
||||
rfParams = {
|
||||
freq_khz: Math.round(p.freqMhz * 1000),
|
||||
bw_hz: Math.round(p.bwKhz * 1000),
|
||||
sf: p.sf,
|
||||
cr: p.cr,
|
||||
}
|
||||
}
|
||||
} else if (rfPreset.value === 'custom') {
|
||||
const rf = [form.value.rfFreqMhz, form.value.rfBwKhz, form.value.rfSf, form.value.rfCr]
|
||||
if (!rf.every((v) => String(v).trim() !== '')) {
|
||||
throw new Error('Fill in all four RF fields (frequency, bandwidth, SF, CR)')
|
||||
}
|
||||
rfParams = {
|
||||
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),
|
||||
}
|
||||
}
|
||||
await mesh.configure({
|
||||
lora_region: form.value.region,
|
||||
@ -93,16 +144,7 @@ async function saveSettings() {
|
||||
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),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(rfParams ? { lora_radio_params: rfParams } : {}),
|
||||
})
|
||||
saveDone.value = true
|
||||
setTimeout(() => { saveDone.value = false }, 3000)
|
||||
@ -187,7 +229,15 @@ async function saveSettings() {
|
||||
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 class="mb-3">
|
||||
<label class="block text-xs text-white/60 mb-1">Frequency plan</label>
|
||||
<select :value="rfPreset" 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" @change="selectRfPreset(($event.target as HTMLSelectElement).value)">
|
||||
<option value="">Keep the radio's current settings</option>
|
||||
<option v-for="p in MESHCORE_RF_PRESETS" :key="p.id" :value="p.id">{{ p.label }}</option>
|
||||
<option value="custom">Custom…</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="rfPreset === 'custom'" 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" />
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user