From d194d2a226a56933485dd207cb94f3f1d7b4f423 Mon Sep 17 00:00:00 2001 From: lambda Date: Mon, 26 Oct 2026 18:30:47 +0300 Subject: [PATCH] fix: per-cycle control slot to close reconnect race, idempotent battery icon --- src-tauri/src/ble/source.rs | 20 +++++++++++++------- src/main.js | 5 ++++- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src-tauri/src/ble/source.rs b/src-tauri/src/ble/source.rs index 09a36fd..a86b613 100644 --- a/src-tauri/src/ble/source.rs +++ b/src-tauri/src/ble/source.rs @@ -46,15 +46,17 @@ const CONTROL_POLL_INTERVAL: Duration = Duration::from_millis(50); /// Реальный источник данных через btleplug. pub(crate) struct BleSource { adapter: Adapter, - /// Last control result (set by the notification router, awaited by `request_control`). - control: Arc>>, + /// Current cycle's control-result slot. Each `stream()` swaps in a fresh slot so a + /// still-winding-down router from the previous cycle writes to a discarded slot, + /// never the one the new cycle's `request_control` polls. + control: Mutex>>>, } impl BleSource { pub(crate) fn new(adapter: Adapter) -> Self { Self { adapter, - control: Arc::new(Mutex::new(None)), + control: Mutex::new(Arc::new(Mutex::new(None))), } } @@ -130,7 +132,10 @@ impl SensorSource for BleSource { // This keeps the control-result flow concurrent with the telemetry consumer so that // `request_control` can await confirmation while telemetry is still being consumed. let (tx, rx) = tokio::sync::mpsc::unbounded_channel::(); - let control = self.control.clone(); + // Swap in a fresh control slot for this cycle; the previous cycle's router holds + // the old slot and can only write there, so it cannot race this cycle's poll. + let control = Arc::new(Mutex::new(None)); + *self.control.lock().unwrap() = control.clone(); let (cancel_tx, cancel_rx) = oneshot::channel::<()>(); tokio::spawn(async move { @@ -191,7 +196,8 @@ impl SensorSource for BleSource { .ok_or_else(|| BleError::DeviceNotFound(cp_uuid.to_string()))?; // Reset and send Request Control (opcode 0x00). - *self.control.lock().unwrap() = None; + let slot = self.control.lock().unwrap().clone(); + *slot.lock().unwrap() = None; let (_, payload) = encode_command(Command::FtmsRequestControl); peripheral .write(&ch, &payload, WriteType::WithoutResponse) @@ -199,9 +205,9 @@ impl SensorSource for BleSource { .map_err(BleError::WriteFailed)?; // Wait for the Control Point Response (routed by the stream's notification task). - let result = tokio::time::timeout(CONTROL_CONFIRM_TIMEOUT, async { + let result = tokio::time::timeout(CONTROL_CONFIRM_TIMEOUT, async move { loop { - let current = *self.control.lock().unwrap(); + let current = *slot.lock().unwrap(); if let Some(r) = current { return r; } diff --git a/src/main.js b/src/main.js index fa15a92..7eb5121 100644 --- a/src/main.js +++ b/src/main.js @@ -142,7 +142,10 @@ once("devices-loaded", (event) => { listen("battery", (event) => { const { device_id, percent } = event.payload; const card = deviceCards.get(device_id); - if (card) card.querySelector(".device__head").append(batteryIcon(percent)); + if (!card) return; + const head = card.querySelector(".device__head"); + head.querySelector(".battery")?.remove(); + head.append(batteryIcon(percent)); }); listen("telemetry", (event) => {