Merge main into archy-hwconfig — reconcile probe/dedup/name work

Both sides independently fixed the serial-alias dedup and the ESP32
boot-reset races; kept the branch's defer-to-auto-detect for unpinned
preferred paths (single probe pass per cycle) on top of main's
advert-name threading, Reticulum name propagation and radio-first
routing. Modal keeps main's 'Set Recommended' naming + probe progress
bar alongside the branch's in-app firmware flasher step.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-07-28 19:03:19 -04:00
co-authored by Claude Fable 5
173 changed files with 10967 additions and 1925 deletions
@@ -1,7 +1,7 @@
<template>
<BaseModal
:show="show"
:title="step === 1 ? 'Mesh Radio Detected' : step === 2 ? 'Apply Archipelago Settings' : 'Flash Firmware'"
:title="step === 1 ? 'Mesh Radio Detected' : step === 2 ? 'Set Recommended' : 'Flash Firmware'"
max-width="max-w-lg"
content-class="max-h-[90vh] overflow-y-auto"
@close="dismiss"
@@ -35,9 +35,17 @@
<!-- What's currently flashed / configured on it -->
<div class="mt-4 text-left rounded-xl bg-white/[0.05] border border-white/10 p-3">
<div v-if="probing" class="flex items-center gap-2 text-white/60 text-sm py-1">
<span class="inline-block w-3.5 h-3.5 rounded-full border-2 border-orange-300/70 border-t-transparent animate-spin"></span>
Reading what's on the radio…
<div v-if="probing" class="py-1">
<div class="flex items-center justify-between text-white/60 text-sm mb-1.5">
<span>{{ probeStage }}</span>
<span class="text-white/40 text-xs tabular-nums">{{ Math.round(probeProgress) }}%</span>
</div>
<div class="h-1.5 rounded-full bg-white/10 overflow-hidden">
<div
class="h-full rounded-full bg-orange-400/80 transition-[width] duration-500 ease-linear"
:style="{ width: probeProgress + '%' }"
></div>
</div>
</div>
<template v-else-if="probe">
<div class="flex items-center gap-2">
@@ -94,7 +102,7 @@
:disabled="!!connecting"
@click="step = 2"
>
Set Up with Archipelago Settings
Set Recommended
</button>
</div>
<p class="text-white/40 text-[11px] mt-3">
@@ -209,8 +217,8 @@
<!-- Step 2: our latest parameters, shown before anything is written -->
<div v-else>
<p class="text-white/60 text-xs mb-3">
These are the latest Archipelago settings nothing is written to the
radio until you confirm.
These are the recommended Archipelago settings — nothing is written to
the radio until you confirm.
</p>
<!-- Summary of what will be applied -->
@@ -308,6 +316,30 @@ const probing = ref(false)
const probe = ref<MeshDeviceProbe | null>(null)
const probeError = ref('')
// Time-driven probe progress: the probe RPC is a single opaque call that can
// take ~5-30s (boot settle + up to three firmware handshakes), so the bar
// advances on a clock toward 92% and snaps to 100% when the result lands.
const probeProgress = ref(0)
const probeStage = ref('Waiting for the radio to boot…')
let probeTicker: ReturnType<typeof setInterval> | null = null
function startProbeProgress() {
stopProbeProgress()
probeProgress.value = 0
probeStage.value = 'Waiting for the radio to boot…'
const startedAt = Date.now()
probeTicker = setInterval(() => {
const elapsed = (Date.now() - startedAt) / 1000
// ~92% at 30s, decelerating — never looks stuck, never lies "done".
probeProgress.value = Math.min(92, 100 * (1 - Math.exp(-elapsed / 11)))
if (elapsed >= 4) probeStage.value = 'Detecting firmware…'
if (elapsed >= 18) probeStage.value = 'Still checking (radios can be slow to answer)…'
}, 400)
}
function stopProbeProgress(done = false) {
if (probeTicker) { clearInterval(probeTicker); probeTicker = null }
if (done) probeProgress.value = 100
}
const devicePath = computed(() => mesh.undismissedDetectedDevices[0] ?? '')
const show = computed(() => !!devicePath.value)
const imageFailed = ref(false)
@@ -380,6 +412,7 @@ watch([show, devicePath], async ([visible]) => {
probe.value = null
probeError.value = ''
probing.value = true
startProbeProgress()
const path = devicePath.value
try {
const res = await mesh.probeDevice(path)
@@ -389,6 +422,7 @@ watch([show, devicePath], async ([visible]) => {
probeError.value = e instanceof Error ? e.message : String(e)
}
} finally {
stopProbeProgress(true)
if (devicePath.value === path) probing.value = false
}
}, { immediate: false })