fix: bulletproof mesh serial connection — PrivateDevices, auto-detect fallback, backoff

Root cause: systemd PrivateDevices=yes hid /dev/ttyUSB* from the service,
preventing .198 from connecting to its Heltec V3 after the security hardening.

Changes:
- Set PrivateDevices=no in systemd service (serial access needs physical devices;
  other hardening layers remain: NoNewPrivileges, ProtectSystem, RestrictNamespaces)
- Add SupplementaryGroups=dialout for explicit serial permissions
- Add fallback auto-detect when configured serial path fails to open
- Add exponential backoff on reconnect (5s→60s cap) to reduce log spam
- Add pre-open device existence check with actionable error messages
- Add udev rule (99-mesh-radio.rules) for stable /dev/mesh-radio symlink
- Add /dev/mesh-radio to serial candidate list (checked first)
- Add Connect button per detected device in Mesh UI
- Deploy udev rule to both servers and ISO build
- Fix FEDI_HASH unbound variable in deploy script
- Fix deploy binary step to handle hung service stop gracefully

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-18 10:50:13 +00:00
parent 12b5fb2d1b
commit e4089287a3
7 changed files with 157 additions and 24 deletions

View File

@@ -16,6 +16,7 @@ const messageText = ref('')
const sendError = ref('')
const broadcasting = ref(false)
const configuring = ref(false)
const connectingDevice = ref<string | null>(null)
const chatScrollEl = ref<HTMLElement | null>(null)
let pollInterval: ReturnType<typeof setInterval> | null = null
@@ -302,6 +303,15 @@ async function handleToggleEnabled() {
} finally { configuring.value = false }
}
async function handleConnectDevice(devicePath: string) {
connectingDevice.value = devicePath
try {
await mesh.configure({ enabled: true, device_path: devicePath } as Partial<import('@/stores/mesh').MeshStatus>)
} finally {
connectingDevice.value = null
}
}
function signalBars(rssi: number | null): number {
if (rssi === null) return 0
if (rssi > -60) return 4
@@ -397,6 +407,14 @@ function truncatePubkey(hex: string | null): string {
<div v-for="dev in mesh.status.detected_devices" :key="dev" class="mesh-device-row">
<div class="mesh-device-indicator" />
<span class="mesh-device-path">{{ dev }}</span>
<button
v-if="!mesh.status?.device_connected"
class="glass-button mesh-connect-btn"
:disabled="connectingDevice !== null"
@click="handleConnectDevice(dev)"
>
{{ connectingDevice === dev ? 'Connecting...' : 'Connect' }}
</button>
</div>
</div>
</div>
@@ -951,6 +969,13 @@ function truncatePubkey(hex: string | null): string {
font-family: monospace;
font-size: 0.8rem;
color: rgba(255, 255, 255, 0.7);
flex: 1;
}
.mesh-connect-btn {
padding: 3px 12px;
font-size: 0.75rem;
flex-shrink: 0;
}
/* ─── Off-grid banner ─── */