fix: per-cycle control slot to close reconnect race, idempotent battery icon

pull/3/head
lambda 7 days ago
parent 9ec7b78e98
commit d194d2a226
No known key found for this signature in database
GPG Key ID: CF45A8EE158BBCB6

@ -46,15 +46,17 @@ const CONTROL_POLL_INTERVAL: Duration = Duration::from_millis(50);
/// Реальный источник данных через btleplug. /// Реальный источник данных через btleplug.
pub(crate) struct BleSource { pub(crate) struct BleSource {
adapter: Adapter, adapter: Adapter,
/// Last control result (set by the notification router, awaited by `request_control`). /// Current cycle's control-result slot. Each `stream()` swaps in a fresh slot so a
control: Arc<Mutex<Option<ControlResult>>>, /// 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<Arc<Mutex<Option<ControlResult>>>>,
} }
impl BleSource { impl BleSource {
pub(crate) fn new(adapter: Adapter) -> Self { pub(crate) fn new(adapter: Adapter) -> Self {
Self { Self {
adapter, 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 // This keeps the control-result flow concurrent with the telemetry consumer so that
// `request_control` can await confirmation while telemetry is still being consumed. // `request_control` can await confirmation while telemetry is still being consumed.
let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<Telemetry>(); let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<Telemetry>();
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::<()>(); let (cancel_tx, cancel_rx) = oneshot::channel::<()>();
tokio::spawn(async move { tokio::spawn(async move {
@ -191,7 +196,8 @@ impl SensorSource for BleSource {
.ok_or_else(|| BleError::DeviceNotFound(cp_uuid.to_string()))?; .ok_or_else(|| BleError::DeviceNotFound(cp_uuid.to_string()))?;
// Reset and send Request Control (opcode 0x00). // 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); let (_, payload) = encode_command(Command::FtmsRequestControl);
peripheral peripheral
.write(&ch, &payload, WriteType::WithoutResponse) .write(&ch, &payload, WriteType::WithoutResponse)
@ -199,9 +205,9 @@ impl SensorSource for BleSource {
.map_err(BleError::WriteFailed)?; .map_err(BleError::WriteFailed)?;
// Wait for the Control Point Response (routed by the stream's notification task). // 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 { loop {
let current = *self.control.lock().unwrap(); let current = *slot.lock().unwrap();
if let Some(r) = current { if let Some(r) = current {
return r; return r;
} }

@ -142,7 +142,10 @@ once("devices-loaded", (event) => {
listen("battery", (event) => { listen("battery", (event) => {
const { device_id, percent } = event.payload; const { device_id, percent } = event.payload;
const card = deviceCards.get(device_id); 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) => { listen("telemetry", (event) => {

Loading…
Cancel
Save