From 7e425c4625bfc846578a91f4ef60d25062c34614 Mon Sep 17 00:00:00 2001 From: joeyjoe02 <308000940+joeyjoe02@users.noreply.github.com> Date: Mon, 12 Oct 2026 11:17:16 +0800 Subject: [PATCH 1/2] feat(lib): dual connections for the D1301 (Sleep A30) The D1301's firmware accepts the same dual connections commands as the other multipoint models (0b01 device list, 0b84 enable, 0b81/0b82/0b83 disconnect/connect/forget) and dual connections works once enabled, but it neither reports the enabled flag (the state update packet is byte-identical with it on or off) nor keeps it after the earbuds fully power off. The common dual connections module is added to the D1301, and the D1301 adds: - the device list is always requested on connect, since the D1301 answers it even while dual connections is disabled - enabled is assumed when two devices are connected, otherwise off - the last value set through openscq30 is saved and, if it was on, sent again on connect The value is saved per device model in a new device_model_setting table (migration 1), for settings a device accepts but cannot report back or does not keep. SoundcoreDeviceBuilder::state_sender is added so a module can update state while the device is being built, and TestSoundcoreDevice::new_with_database so tests can reconnect with the same database. Tested on a Sleep A30 Special (firmware 01.07): enabling dual connections lets a second host connect alongside the first, and after disabling it behind openscq30's back, reconnecting sends 0b84 01 again and the earbuds acknowledge it. Co-Authored-By: Claude Opus 5.5 --- lib/src/devices/soundcore/common/device.rs | 35 ++++- lib/src/devices/soundcore/d1301.rs | 141 +++++++++++++++++- lib/src/devices/soundcore/d1301/modules.rs | 1 + .../d1301/modules/dual_connections.rs | 130 ++++++++++++++++ lib/src/devices/soundcore/d1301/state.rs | 10 +- lib/src/storage/device_model_setting.rs | 84 +++++++++++ lib/src/storage/migration.rs | 2 +- lib/src/storage/migrations/1.sql | 9 ++ lib/src/storage/mod.rs | 10 ++ 9 files changed, 417 insertions(+), 5 deletions(-) create mode 100644 lib/src/devices/soundcore/d1301/modules/dual_connections.rs create mode 100644 lib/src/storage/device_model_setting.rs create mode 100644 lib/src/storage/migrations/1.sql diff --git a/lib/src/devices/soundcore/common/device.rs b/lib/src/devices/soundcore/common/device.rs index cc963faad..be6b0a47b 100644 --- a/lib/src/devices/soundcore/common/device.rs +++ b/lib/src/devices/soundcore/common/device.rs @@ -267,6 +267,10 @@ where self.change_notify.clone() } + pub fn state_sender(&self) -> &watch::Sender { + &self.state_sender + } + pub fn sound_modes(&mut self, available_sound_modes: AvailableSoundModes) where StateType: Has, @@ -836,13 +840,42 @@ pub mod test_utils { config: SoundcoreDeviceConfig, delays: HashMap, ) -> Self + where + StateType: Clone + Send + Sync + 'static, + SoundcoreDeviceRegistry: BuildDevice, + { + let database = Arc::new(OpenSCQ30Database::new_in_memory().await.unwrap()); + Self::new_with_database( + constructor, + device_model, + packet_responses, + config, + delays, + database, + ) + .await + } + + /// Allows sharing a database between multiple connections, for testing things that are + /// persisted across reconnects. + pub async fn new_with_database( + constructor: fn( + Arc, + Arc, + DeviceModel, + ) -> SoundcoreDeviceRegistry, + device_model: DeviceModel, + packet_responses: HashMap, + config: SoundcoreDeviceConfig, + delays: HashMap, + database: Arc, + ) -> Self where StateType: Clone + Send + Sync + 'static, SoundcoreDeviceRegistry: BuildDevice, { let (inbound_sender, inbound_receiver) = mpsc::channel(100); let (outbound_sender, mut outbound_receiver) = mpsc::channel(100); - let database = Arc::new(OpenSCQ30Database::new_in_memory().await.unwrap()); let registry = constructor( Arc::new(MockRfcommBackend::new(inbound_receiver, outbound_sender)), diff --git a/lib/src/devices/soundcore/d1301.rs b/lib/src/devices/soundcore/d1301.rs index 6da40ede0..c2d295916 100644 --- a/lib/src/devices/soundcore/d1301.rs +++ b/lib/src/devices/soundcore/d1301.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use crate::{ devices::soundcore::{ common::{ + self, macros::soundcore_device, modules::{ button_configuration::{ @@ -11,7 +12,7 @@ use crate::{ dual_battery_level::DualBatteryLevelConfiguration, }, packet::{ - inbound::TryToPacket, + inbound::{DualConnectionsDevicePacket, TryToPacket}, outbound::{RequestState, ToPacket}, }, structures::button_configuration::{ @@ -43,10 +44,15 @@ soundcore_device!( .send_with_response(&packets::outbound::request_alarms()) .await? .try_to_packet()?; + // Unlike other models, the D1301 answers this even while dual connections is disabled, and the + // state update packet has no enabled flag, so always ask. + let dual_connections_devices = + common::modules::dual_connections::take_dual_connection_devices(&packet_io).await?; Ok(state::D1301State::new( state_update_packet, auto_stop_timer_packet.0, alarms_packet.0, + modules::dual_connections::initial_dual_connections(dual_connections_devices), )) }, async |builder| { @@ -60,6 +66,8 @@ soundcore_device!( builder.d1301_alarms(); + builder.d1301_dual_connections().await; + // misc builder.d1301_auto_stop_timer(); builder.d1301_auto_switch_once_asleep(); @@ -96,6 +104,10 @@ soundcore_device!( packets::inbound::AlarmsPacket::COMMAND, packets::inbound::AlarmsPacket(Vec::new()).to_packet(), ), + ( + DualConnectionsDevicePacket::COMMAND, + DualConnectionsDevicePacket::demo().to_packet(), + ), ]) }, ); @@ -337,4 +349,131 @@ mod tests { device.assert_setting_values(vec![(SettingId::Alarm1Volume, 51.into())]); } + + mod dual_connections { + use std::{sync::Arc, time::Duration}; + + use macaddr::MacAddr6; + + use super::*; + use crate::{ + devices::soundcore::common::structures::DualConnectionsDevice, + storage::OpenSCQ30Database, + }; + + fn packet_responses( + dual_connections_devices: Vec, + ) -> HashMap { + HashMap::from([ + ( + RequestState::COMMAND, + D1301StateUpdatePacket::default().to_packet(), + ), + ( + packets::inbound::AutoStopTimerPacket::COMMAND, + packets::inbound::AutoStopTimerPacket(structures::AutoStopTimer::default()) + .to_packet(), + ), + ( + packets::inbound::AlarmsPacket::COMMAND, + packets::inbound::AlarmsPacket(Vec::new()).to_packet(), + ), + ( + DualConnectionsDevicePacket::COMMAND, + DualConnectionsDevicePacket { + total_packets: 1, + current_packet_index: 1, + devices: dual_connections_devices, + } + .to_packet(), + ), + // captured from a real device: echoes the value followed by a 0 status byte + ( + packet::Command([0x0b, 0x84]), + packet::Inbound::new(packet::Command([0x0b, 0x84]), vec![1, 0]), + ), + ]) + } + + fn host(last_octet: u8, is_connected: bool) -> DualConnectionsDevice { + DualConnectionsDevice { + is_connected, + mac_address: MacAddr6::new(0, 0, 0, 0, 0, last_octet), + name: format!("Device {last_octet}"), + } + } + + async fn connect( + database: Arc, + dual_connections_devices: Vec, + ) -> TestSoundcoreDevice { + TestSoundcoreDevice::new_with_database( + super::super::device_registry, + DeviceModel::SoundcoreD1301, + packet_responses(dual_connections_devices), + SoundcoreDeviceConfig::default(), + HashMap::::new(), + database, + ) + .await + } + + async fn database() -> Arc { + Arc::new(OpenSCQ30Database::new_in_memory().await.unwrap()) + } + + #[tokio::test(start_paused = true)] + async fn is_off_when_unknown() { + let device = connect(database().await, vec![host(1, true), host(2, false)]).await; + device.assert_setting_values([(SettingId::DualConnections, false.into())]); + } + + #[tokio::test(start_paused = true)] + async fn is_on_when_two_devices_are_connected() { + let device = connect(database().await, vec![host(1, true), host(2, true)]).await; + device.assert_setting_values([(SettingId::DualConnections, true.into())]); + } + + #[tokio::test(start_paused = true)] + async fn enabling_sends_packet() { + let mut device = connect(database().await, vec![host(1, true)]).await; + device + .assert_set_settings_response( + vec![(SettingId::DualConnections, true.into())], + vec![packet::outbound::set_dual_connections_enabled(true)], + ) + .await; + device.assert_setting_values([(SettingId::DualConnections, true.into())]); + } + + #[tokio::test(start_paused = true)] + async fn enabled_is_reapplied_on_reconnect() { + let database = database().await; + let mut device = connect(database.clone(), vec![host(1, true)]).await; + device + .set_settings(vec![(SettingId::DualConnections, true.into())]) + .await; + drop(device); + + // simulate the earbuds having been powered off, which disables dual connections + let device = connect(database, vec![host(1, true)]).await; + device.assert_setting_values([(SettingId::DualConnections, true.into())]); + } + + #[tokio::test(start_paused = true)] + async fn disabled_is_not_reapplied_on_reconnect() { + let database = database().await; + let mut device = connect(database.clone(), vec![host(1, true)]).await; + device + .set_settings(vec![(SettingId::DualConnections, true.into())]) + .await; + device + .set_settings(vec![(SettingId::DualConnections, false.into())]) + .await; + drop(device); + + let device = connect(database, vec![host(1, true)]).await; + device.assert_setting_values([(SettingId::DualConnections, false.into())]); + } + } } diff --git a/lib/src/devices/soundcore/d1301/modules.rs b/lib/src/devices/soundcore/d1301/modules.rs index 94c09d884..8f64bcc30 100644 --- a/lib/src/devices/soundcore/d1301/modules.rs +++ b/lib/src/devices/soundcore/d1301/modules.rs @@ -3,6 +3,7 @@ use crate::devices::soundcore::{common::device::SoundcoreDeviceBuilder, d1301::s mod alarms; mod auto_stop_timer; mod auto_switch_once_asleep; +pub mod dual_connections; mod flags; mod listening_mode; diff --git a/lib/src/devices/soundcore/d1301/modules/dual_connections.rs b/lib/src/devices/soundcore/d1301/modules/dual_connections.rs new file mode 100644 index 000000000..221c64d18 --- /dev/null +++ b/lib/src/devices/soundcore/d1301/modules/dual_connections.rs @@ -0,0 +1,130 @@ +//! The D1301 supports the standard dual connections commands (0x0b family), but unlike other models +//! it neither reports whether dual connections is enabled nor remembers it after powering off. To +//! make up for that, the last value set through openscq30 is stored and reapplied on connect. + +use std::sync::{Arc, Mutex}; + +use async_trait::async_trait; +use openscq30_lib_has::Has; +use tokio::sync::watch; + +use crate::{ + api::{device, settings::SettingId}, + devices::{ + DeviceModel, + soundcore::{ + common::{ + device::SoundcoreDeviceBuilder, + modules::ModuleCollection, + packet, + state_modifier::StateModifier, + structures::{DualConnections, DualConnectionsDevice}, + }, + d1301::state::D1301State, + }, + }, + storage::OpenSCQ30Database, +}; + +/// The D1301 does not report whether dual connections is enabled, but if more than one device is +/// connected, it must be. +pub fn initial_dual_connections(devices: Vec) -> DualConnections { + DualConnections { + is_enabled: devices.iter().filter(|device| device.is_connected).count() > 1, + devices, + } +} + +impl SoundcoreDeviceBuilder { + pub async fn d1301_dual_connections(&mut self) { + let database = self.database(); + let device_model = self.device_model(); + + let should_enable = match database + .fetch_device_model_setting(device_model, SettingId::DualConnections) + .await + { + Ok(value) => value.and_then(|value| value.as_bool()) == Some(true), + Err(err) => { + tracing::warn!("failed to fetch saved dual connections setting: {err:?}"); + false + } + }; + let is_enabled = Has::::get(&*self.state_sender().borrow()).is_enabled; + if should_enable && !is_enabled { + match self + .packet_io_controller() + .send_with_response(&packet::outbound::set_dual_connections_enabled(true)) + .await + { + Ok(_) => self.state_sender().send_modify(|state| { + Has::::get_mut(state).is_enabled = true; + }), + Err(err) => tracing::warn!("failed to reapply dual connections: {err:?}"), + } + } + + self.dual_connections(); + let saved = Has::::get(&*self.state_sender().borrow()).is_enabled; + self.module_collection() + .add_d1301_dual_connections_persistence(database, device_model, saved); + } +} + +impl ModuleCollection +where + T: Has + Clone + Send + Sync, +{ + /// Must be added after the dual connections module so that only values that were successfully + /// sent to the device get saved. + fn add_d1301_dual_connections_persistence( + &mut self, + database: Arc, + device_model: DeviceModel, + saved: bool, + ) { + self.state_modifiers + .push(Box::new(PersistDualConnectionsStateModifier { + database, + device_model, + saved: Mutex::new(saved), + })); + } +} + +struct PersistDualConnectionsStateModifier { + database: Arc, + device_model: DeviceModel, + saved: Mutex, +} + +#[async_trait] +impl StateModifier for PersistDualConnectionsStateModifier +where + StateT: Has + Send + Sync, +{ + async fn move_to_state( + &self, + state_sender: &watch::Sender, + _target_state: &StateT, + ) -> device::Result<()> { + let is_enabled = state_sender.borrow().get().is_enabled; + if *self.saved.lock().unwrap() == is_enabled { + return Ok(()); + } + // Failing to save shouldn't fail setting the value on the device, which already happened. + match self + .database + .upsert_device_model_setting( + self.device_model, + SettingId::DualConnections, + is_enabled.into(), + ) + .await + { + Ok(()) => *self.saved.lock().unwrap() = is_enabled, + Err(err) => tracing::warn!("failed to save dual connections setting: {err:?}"), + } + Ok(()) + } +} diff --git a/lib/src/devices/soundcore/d1301/state.rs b/lib/src/devices/soundcore/d1301/state.rs index d6d48a3ef..def9c6c7b 100644 --- a/lib/src/devices/soundcore/d1301/state.rs +++ b/lib/src/devices/soundcore/d1301/state.rs @@ -5,8 +5,9 @@ use crate::devices::soundcore::{ modules::reset_button_configuration::ResetButtonConfigurationPending, state::Update, structures::{ - DualBatteryLevel, DualFirmwareVersion, FirmwareVersion, LowBatteryPrompt, SerialNumber, - TwsStatus, button_configuration::ButtonStatusCollection, + DualBatteryLevel, DualConnections, DualFirmwareVersion, FirmwareVersion, + LowBatteryPrompt, SerialNumber, TwsStatus, + button_configuration::ButtonStatusCollection, }, }, d1301, @@ -37,6 +38,9 @@ pub struct D1301State { auto_stop_timer: d1301::structures::AutoStopTimer, alarms: Vec, + // Not part of the state update packet: the D1301 accepts the dual connections commands but never + // reports whether the feature is enabled, so this is tracked separately. See d1301/modules/dual_connections.rs + dual_connections: DualConnections, } impl D1301State { @@ -44,6 +48,7 @@ impl D1301State { state_update_packet: D1301StateUpdatePacket, auto_stop_timer: d1301::structures::AutoStopTimer, alarms: Vec, + dual_connections: DualConnections, ) -> Self { Self { tws_status: state_update_packet.tws_status, @@ -67,6 +72,7 @@ impl D1301State { button_reset_pending: ResetButtonConfigurationPending::default(), auto_stop_timer, alarms, + dual_connections, } } } diff --git a/lib/src/storage/device_model_setting.rs b/lib/src/storage/device_model_setting.rs new file mode 100644 index 000000000..d78c8af27 --- /dev/null +++ b/lib/src/storage/device_model_setting.rs @@ -0,0 +1,84 @@ +use rusqlite::{Connection, OptionalExtension}; + +use crate::{api::settings::SettingId, devices::DeviceModel}; + +use super::{Error, type_conversions::SqliteDeviceModel}; + +pub fn fetch( + connection: &Connection, + model: DeviceModel, + setting_id: SettingId, +) -> Result, Error> { + let setting_id: &'static str = setting_id.into(); + let json: Option = connection + .prepare_cached( + r#"SELECT value FROM device_model_setting WHERE device_model = ?1 AND setting_id = ?2"#, + )? + .query_row((SqliteDeviceModel(model), setting_id), |row| row.get(0)) + .optional()?; + json.map(|json| serde_json::from_str(&json).map_err(Error::from)) + .transpose() +} + +pub fn upsert( + connection: &Connection, + model: DeviceModel, + setting_id: SettingId, + value: serde_json::Value, +) -> Result<(), Error> { + let setting_id: &'static str = setting_id.into(); + let json = serde_json::to_string(&value).map_err(Error::from)?; + connection.execute( + r#"INSERT INTO device_model_setting (device_model, setting_id, value) + VALUES (?1, ?2, json(?3)) + ON CONFLICT(device_model, setting_id) DO UPDATE SET + value = excluded.value + "#, + (SqliteDeviceModel(model), setting_id, json), + )?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::{api::settings::SettingId, devices::DeviceModel, storage::OpenSCQ30Database}; + + #[tokio::test] + async fn missing_setting_is_none() { + let database = OpenSCQ30Database::new_in_memory().await.unwrap(); + let value = database + .fetch_device_model_setting(DeviceModel::SoundcoreD1301, SettingId::DualConnections) + .await + .unwrap(); + assert_eq!(value, None); + } + + #[tokio::test] + async fn upsert_overwrites_and_is_per_model() { + let database = OpenSCQ30Database::new_in_memory().await.unwrap(); + for value in [true, false] { + database + .upsert_device_model_setting( + DeviceModel::SoundcoreD1301, + SettingId::DualConnections, + value.into(), + ) + .await + .unwrap(); + } + assert_eq!( + database + .fetch_device_model_setting(DeviceModel::SoundcoreD1301, SettingId::DualConnections) + .await + .unwrap(), + Some(false.into()), + ); + assert_eq!( + database + .fetch_device_model_setting(DeviceModel::SoundcoreA3040, SettingId::DualConnections) + .await + .unwrap(), + None, + ); + } +} diff --git a/lib/src/storage/migration.rs b/lib/src/storage/migration.rs index fd8ef91b9..efcc0df5c 100644 --- a/lib/src/storage/migration.rs +++ b/lib/src/storage/migration.rs @@ -11,7 +11,7 @@ macro_rules! migration_file { }; } -pub const MIGRATIONS: &[Migration] = &[migration_file!("0.sql")]; +pub const MIGRATIONS: &[Migration] = &[migration_file!("0.sql"), migration_file!("1.sql")]; #[instrument(skip(connection, migrations))] pub fn migrate( diff --git a/lib/src/storage/migrations/1.sql b/lib/src/storage/migrations/1.sql new file mode 100644 index 000000000..619d38c58 --- /dev/null +++ b/lib/src/storage/migrations/1.sql @@ -0,0 +1,9 @@ +-- Settings that a device accepts but cannot report back or does not persist itself, +-- so openscq30 remembers the last value and reapplies it on connect. +CREATE TABLE device_model_setting ( + device_model TEXT NOT NULL, + setting_id TEXT NOT NULL, + value TEXT NOT NULL CHECK(json_valid(value)), + created_at INTEGER NOT NULL DEFAULT (strftime('%s')) +) STRICT; +CREATE UNIQUE INDEX idx_device_model_setting_device_model_setting_id ON device_model_setting (device_model, setting_id); diff --git a/lib/src/storage/mod.rs b/lib/src/storage/mod.rs index d7471a2ce..da3010c11 100644 --- a/lib/src/storage/mod.rs +++ b/lib/src/storage/mod.rs @@ -1,3 +1,4 @@ +mod device_model_setting; mod equalizer_profile; mod migration; mod paired_device; @@ -249,4 +250,13 @@ commands!( name: Vec<(String, Vec)>, ) -> Result<()>; equalizer_profile::delete => fn delete_equalizer_profile(model: DeviceModel, name: String) -> Result<()>; + device_model_setting::fetch => fn fetch_device_model_setting( + model: DeviceModel, + setting_id: SettingId, + ) -> Result>; + device_model_setting::upsert => fn upsert_device_model_setting( + model: DeviceModel, + setting_id: SettingId, + value: serde_json::Value, + ) -> Result<()>; ); From 275eba6593045f7492be7251e507889ec7a09e76 Mon Sep 17 00:00:00 2001 From: joeyjoe02 <308000940+joeyjoe02@users.noreply.github.com> Date: Mon, 12 Oct 2026 11:27:51 +0800 Subject: [PATCH 2/2] fix(lib): save d1301 dual connections when enabled while already on With two devices connected the D1301's dual connections is already shown as enabled, so setting it to on was not a change from the state and was never saved, and it was not reapplied after the earbuds were powered off. Compare against the saved value instead of the state. Co-Authored-By: Claude Opus 5.5 --- lib/src/devices/soundcore/d1301.rs | 14 ++++++++++ .../d1301/modules/dual_connections.rs | 26 +++++++++++-------- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/lib/src/devices/soundcore/d1301.rs b/lib/src/devices/soundcore/d1301.rs index c2d295916..b7f5eb447 100644 --- a/lib/src/devices/soundcore/d1301.rs +++ b/lib/src/devices/soundcore/d1301.rs @@ -460,6 +460,20 @@ mod tests { device.assert_setting_values([(SettingId::DualConnections, true.into())]); } + #[tokio::test(start_paused = true)] + async fn enabling_while_already_on_is_reapplied_on_reconnect() { + let database = database().await; + // two connected devices: already shown as enabled, but nothing has been saved yet + let mut device = connect(database.clone(), vec![host(1, true), host(2, true)]).await; + device + .set_settings(vec![(SettingId::DualConnections, true.into())]) + .await; + drop(device); + + let device = connect(database, vec![host(1, true)]).await; + device.assert_setting_values([(SettingId::DualConnections, true.into())]); + } + #[tokio::test(start_paused = true)] async fn disabled_is_not_reapplied_on_reconnect() { let database = database().await; diff --git a/lib/src/devices/soundcore/d1301/modules/dual_connections.rs b/lib/src/devices/soundcore/d1301/modules/dual_connections.rs index 221c64d18..74bfa2ea0 100644 --- a/lib/src/devices/soundcore/d1301/modules/dual_connections.rs +++ b/lib/src/devices/soundcore/d1301/modules/dual_connections.rs @@ -40,18 +40,18 @@ impl SoundcoreDeviceBuilder { let database = self.database(); let device_model = self.device_model(); - let should_enable = match database + let saved = match database .fetch_device_model_setting(device_model, SettingId::DualConnections) .await { - Ok(value) => value.and_then(|value| value.as_bool()) == Some(true), + Ok(value) => value.and_then(|value| value.as_bool()), Err(err) => { tracing::warn!("failed to fetch saved dual connections setting: {err:?}"); - false + None } }; let is_enabled = Has::::get(&*self.state_sender().borrow()).is_enabled; - if should_enable && !is_enabled { + if saved == Some(true) && !is_enabled { match self .packet_io_controller() .send_with_response(&packet::outbound::set_dual_connections_enabled(true)) @@ -65,7 +65,6 @@ impl SoundcoreDeviceBuilder { } self.dual_connections(); - let saved = Has::::get(&*self.state_sender().borrow()).is_enabled; self.module_collection() .add_d1301_dual_connections_persistence(database, device_model, saved); } @@ -81,7 +80,7 @@ where &mut self, database: Arc, device_model: DeviceModel, - saved: bool, + saved: Option, ) { self.state_modifiers .push(Box::new(PersistDualConnectionsStateModifier { @@ -95,7 +94,9 @@ where struct PersistDualConnectionsStateModifier { database: Arc, device_model: DeviceModel, - saved: Mutex, + /// Compared against the database's value rather than the device's state, since the state can + /// already be enabled (two devices connected) without that ever having been saved. + saved: Mutex>, } #[async_trait] @@ -106,10 +107,13 @@ where async fn move_to_state( &self, state_sender: &watch::Sender, - _target_state: &StateT, + target_state: &StateT, ) -> device::Result<()> { - let is_enabled = state_sender.borrow().get().is_enabled; - if *self.saved.lock().unwrap() == is_enabled { + let is_enabled = target_state.get().is_enabled; + // Only save what the dual connections module actually applied to the device. + if state_sender.borrow().get().is_enabled != is_enabled + || *self.saved.lock().unwrap() == Some(is_enabled) + { return Ok(()); } // Failing to save shouldn't fail setting the value on the device, which already happened. @@ -122,7 +126,7 @@ where ) .await { - Ok(()) => *self.saved.lock().unwrap() = is_enabled, + Ok(()) => *self.saved.lock().unwrap() = Some(is_enabled), Err(err) => tracing::warn!("failed to save dual connections setting: {err:?}"), } Ok(())