diff --git a/crates/buttplug_server/src/device/protocol_impl/lelo_harmony.rs b/crates/buttplug_server/src/device/protocol_impl/lelo_harmony.rs index 01172e230..9d6ad3021 100644 --- a/crates/buttplug_server/src/device/protocol_impl/lelo_harmony.rs +++ b/crates/buttplug_server/src/device/protocol_impl/lelo_harmony.rs @@ -7,32 +7,30 @@ use crate::device::{ hardware::{ - Hardware, - HardwareCommand, - HardwareEvent, - HardwareSubscribeCmd, - HardwareUnsubscribeCmd, + Hardware, HardwareCommand, HardwareEvent, HardwareSubscribeCmd, HardwareUnsubscribeCmd, HardwareWriteCmd, }, protocol::{ - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - generic_protocol_initializer_setup, + ProtocolHandler, ProtocolIdentifier, ProtocolInitializer, generic_protocol_initializer_setup, }, }; use async_trait::async_trait; use buttplug_core::errors::ButtplugDeviceError; +use buttplug_core::util::async_manager; use buttplug_server_device_config::Endpoint; use buttplug_server_device_config::{ - ProtocolCommunicationSpecifier, - ServerDeviceDefinition, - UserDeviceIdentifier, + ProtocolCommunicationSpecifier, ServerDeviceDefinition, UserDeviceIdentifier, +}; +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, + time::Duration, }; -use std::sync::Arc; use uuid::{Uuid, uuid}; const LELO_HARMONY_PROTOCOL_UUID: Uuid = uuid!("220e180a-e6d5-4fd1-963e-43a6f990b717"); +const LELO_HARMONY_F1SV3_VARIANT: &str = "f1sv3"; +const LELO_F1SV3_DEFAULT_IDLE_STOP_TIMEOUT_MS: u32 = 800; generic_protocol_initializer_setup!(LeloHarmony, "lelo-harmony"); #[derive(Default)] @@ -43,7 +41,7 @@ impl ProtocolInitializer for LeloHarmonyInitializer { async fn initialize( &mut self, hardware: Arc, - _: &ServerDeviceDefinition, + def: &ServerDeviceDefinition, ) -> Result, ButtplugDeviceError> { // The Lelo Harmony has a very specific pairing flow: // * First the device is turned on in BLE mode (long press) @@ -72,6 +70,13 @@ impl ProtocolInitializer for LeloHarmonyInitializer { ) } else if !n.is_empty() && n[0] == 1u8 && n[1..].iter().all(|b| *b == 0u8) { debug!("Lelo Harmony is authorised!"); + if def + .protocol_variant() + .as_deref() + .is_some_and(|variant| variant == LELO_HARMONY_F1SV3_VARIANT) + { + return Ok(Arc::new(LeloHarmony::f1sv3_harmony(hardware.clone(), def))); + } return Ok(Arc::new(LeloHarmony::default())); } else { debug!("Lelo Harmony gave us a password: {:?}", n); @@ -109,35 +114,181 @@ impl ProtocolInitializer for LeloHarmonyInitializer { } } +pub struct LeloHarmony { + output_endpoint: Endpoint, + use_zero_pattern_for_stop: bool, + write_with_response: bool, + idle_stop_timeout: Option, + hardware: Option>, + state: Arc>>, +} + #[derive(Default)] -pub struct LeloHarmony {} +struct MotorState { + has_seen_nonzero: bool, + generation: u64, +} + +impl Default for LeloHarmony { + fn default() -> Self { + Self::new(Endpoint::Tx, false, false, None, None) + } +} impl LeloHarmony { + pub(super) fn f1sv3(hardware: Arc, def: &ServerDeviceDefinition) -> Self { + Self::new( + Endpoint::TxVibrate, + true, + true, + Self::idle_stop_timeout(def), + Some(hardware), + ) + } + + fn f1sv3_harmony(hardware: Arc, def: &ServerDeviceDefinition) -> Self { + Self::new( + Endpoint::Tx, + true, + true, + Self::idle_stop_timeout(def), + Some(hardware), + ) + } + + fn idle_stop_timeout(def: &ServerDeviceDefinition) -> Option { + def.vibrate_smoothing_enabled().then(|| { + Duration::from_millis( + def + .vibrate_smoothing_idle_stop_ms() + .unwrap_or(LELO_F1SV3_DEFAULT_IDLE_STOP_TIMEOUT_MS) as u64, + ) + }) + } + + fn new( + output_endpoint: Endpoint, + use_zero_pattern_for_stop: bool, + write_with_response: bool, + idle_stop_timeout: Option, + hardware: Option>, + ) -> Self { + Self { + output_endpoint, + use_zero_pattern_for_stop, + write_with_response, + idle_stop_timeout, + hardware, + state: Arc::new(Mutex::new(HashMap::new())), + } + } + + fn command_for_speed( + &self, + feature_id: Uuid, + feature_index: u32, + speed: u32, + ) -> HardwareWriteCmd { + let pattern = if self.use_zero_pattern_for_stop && speed == 0 { + 0x00 + } else { + 0x08 + }; + HardwareWriteCmd::new( + &[feature_id], + self.output_endpoint, + vec![ + 0x0a, + 0x12, + feature_index as u8 + 1, + pattern, + 0x00, + 0x00, + 0x00, + 0x00, + speed as u8, + 0x00, + ], + self.write_with_response, + ) + } + + fn maybe_defer_stop( + &self, + feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result { + if speed != 0 { + if self.idle_stop_timeout.is_some() { + let mut state = self.state.lock().map_err(|_| { + ButtplugDeviceError::ProtocolSpecificError( + "LeloHarmony".to_owned(), + "Lelo Harmony motor state lock failed".to_owned(), + ) + })?; + let motor_state = state.entry(feature_index).or_default(); + motor_state.has_seen_nonzero = true; + motor_state.generation += 1; + } + return Ok(false); + } + + let Some(idle_stop_timeout) = self.idle_stop_timeout else { + return Ok(false); + }; + let Some(hardware) = self.hardware.clone() else { + return Ok(false); + }; + + let mut state = self.state.lock().map_err(|_| { + ButtplugDeviceError::ProtocolSpecificError( + "LeloHarmony".to_owned(), + "Lelo Harmony motor state lock failed".to_owned(), + ) + })?; + let motor_state = state.entry(feature_index).or_default(); + if !motor_state.has_seen_nonzero { + return Ok(true); + } + + motor_state.generation += 1; + let generation = motor_state.generation; + let stop_cmd = self.command_for_speed(feature_id, feature_index, 0); + + let state = self.state.clone(); + buttplug_core::spawn!("LeloHarmonyF1sV3DelayedStop", async move { + async_manager::sleep(idle_stop_timeout).await; + let should_stop = state + .lock() + .ok() + .and_then(|state| { + state + .get(&feature_index) + .map(|state| state.generation == generation) + }) + .unwrap_or(false); + if should_stop { + let _ = hardware.write_value(&stop_cmd).await; + } + }); + + Ok(true) + } + fn handle_input_cmd( &self, feature_index: u32, feature_id: Uuid, speed: u32, ) -> Result, ButtplugDeviceError> { + if self.maybe_defer_stop(feature_index, feature_id, speed)? { + return Ok(vec![]); + } Ok(vec![ - HardwareWriteCmd::new( - &[feature_id], - Endpoint::Tx, - vec![ - 0x0a, - 0x12, - feature_index as u8 + 1, - 0x08, - 0x00, - 0x00, - 0x00, - 0x00, - speed as u8, - 0x00, - ], - false, - ) - .into(), + self + .command_for_speed(feature_id, feature_index, speed) + .into(), ]) } } @@ -161,3 +312,50 @@ impl ProtocolHandler for LeloHarmony { self.handle_input_cmd(feature_index, feature_id, speed) } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn uses_configured_output_endpoint_for_vibration() { + let handler = LeloHarmony::new(Endpoint::TxVibrate, false, false, None, None); + let commands = handler + .handle_output_vibrate_cmd(1, uuid!("00000000-0000-0000-0000-000000000001"), 50) + .expect("Command should build"); + + assert_eq!(commands.len(), 1); + match &commands[0] { + HardwareCommand::Write(cmd) => { + assert_eq!(cmd.endpoint(), Endpoint::TxVibrate); + assert_eq!( + cmd.data(), + &[0x0a, 0x12, 0x02, 0x08, 0x00, 0x00, 0x00, 0x00, 0x32, 0x00] + ); + assert!(!cmd.write_with_response()); + } + _ => panic!("Expected write command"), + } + } + + #[test] + fn can_use_zero_pattern_for_stop() { + let handler = LeloHarmony::new(Endpoint::TxVibrate, true, true, None, None); + let commands = handler + .handle_output_vibrate_cmd(0, uuid!("00000000-0000-0000-0000-000000000001"), 0) + .expect("Command should build"); + + assert_eq!(commands.len(), 1); + match &commands[0] { + HardwareCommand::Write(cmd) => { + assert_eq!(cmd.endpoint(), Endpoint::TxVibrate); + assert_eq!( + cmd.data(), + &[0x0a, 0x12, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + ); + assert!(cmd.write_with_response()); + } + _ => panic!("Expected write command"), + } + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/lelof1sv2.rs b/crates/buttplug_server/src/device/protocol_impl/lelof1sv2.rs index 329f7147b..c190a14f5 100644 --- a/crates/buttplug_server/src/device/protocol_impl/lelof1sv2.rs +++ b/crates/buttplug_server/src/device/protocol_impl/lelof1sv2.rs @@ -8,26 +8,17 @@ use super::{lelo_harmony::LeloHarmony, lelof1s::LeloF1s}; use crate::device::{ hardware::{ - Hardware, - HardwareEvent, - HardwareSubscribeCmd, - HardwareUnsubscribeCmd, - HardwareWriteCmd, + Hardware, HardwareEvent, HardwareSubscribeCmd, HardwareUnsubscribeCmd, HardwareWriteCmd, }, protocol::{ - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - generic_protocol_initializer_setup, + ProtocolHandler, ProtocolIdentifier, ProtocolInitializer, generic_protocol_initializer_setup, }, }; use async_trait::async_trait; use buttplug_core::errors::ButtplugDeviceError; use buttplug_server_device_config::Endpoint; use buttplug_server_device_config::{ - ProtocolCommunicationSpecifier, - ServerDeviceDefinition, - UserDeviceIdentifier, + ProtocolCommunicationSpecifier, ServerDeviceDefinition, UserDeviceIdentifier, }; use std::sync::Arc; use uuid::{Uuid, uuid}; @@ -43,7 +34,7 @@ impl ProtocolInitializer for LeloF1sV2Initializer { async fn initialize( &mut self, hardware: Arc, - _: &ServerDeviceDefinition, + def: &ServerDeviceDefinition, ) -> Result, ButtplugDeviceError> { let use_harmony = !hardware.endpoints().contains(&Endpoint::Whitelist); let sec_endpoint = if use_harmony { @@ -82,7 +73,7 @@ impl ProtocolInitializer for LeloF1sV2Initializer { } else if n.eq(&authed) { debug!("Lelo F1s V2 is authorised!"); if use_harmony { - return Ok(Arc::new(LeloHarmony::default())); + return Ok(Arc::new(LeloHarmony::f1sv3(hardware.clone(), def))); } else { return Ok(Arc::new(LeloF1s::new(true))); } diff --git a/crates/buttplug_server/src/device/protocol_impl/lelof1sv3.rs b/crates/buttplug_server/src/device/protocol_impl/lelof1sv3.rs new file mode 100644 index 000000000..f4c0121e8 --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/lelof1sv3.rs @@ -0,0 +1,89 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2026 Nonpolynomial Labs LLC. All rights reserved. +// +// Licensed under the BSD 3-Clause license. See LICENSE file in the project root +// for full license information. + +use super::lelo_harmony::LeloHarmony; +use crate::device::{ + hardware::{ + Hardware, HardwareEvent, HardwareSubscribeCmd, HardwareUnsubscribeCmd, HardwareWriteCmd, + }, + protocol::{ + ProtocolHandler, ProtocolIdentifier, ProtocolInitializer, generic_protocol_initializer_setup, + }, +}; +use async_trait::async_trait; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::{ + Endpoint, ProtocolCommunicationSpecifier, ServerDeviceDefinition, UserDeviceIdentifier, +}; +use std::sync::Arc; +use uuid::{Uuid, uuid}; + +const LELO_F1S_V3_PROTOCOL_UUID: Uuid = uuid!("f786e955-8295-4ac6-af47-852e4487a1f4"); +generic_protocol_initializer_setup!(LeloF1sV3, "lelo-f1sv3"); + +#[derive(Default)] +pub struct LeloF1sV3Initializer {} + +#[async_trait] +impl ProtocolInitializer for LeloF1sV3Initializer { + async fn initialize( + &mut self, + hardware: Arc, + def: &ServerDeviceDefinition, + ) -> Result, ButtplugDeviceError> { + let mut event_receiver = hardware.event_stream(); + hardware + .subscribe(&HardwareSubscribeCmd::new( + LELO_F1S_V3_PROTOCOL_UUID, + Endpoint::Generic0, + )) + .await?; + let noauth: Vec = vec![0; 8]; + let authed: Vec = vec![1, 0, 0, 0, 0, 0, 0, 0]; + + loop { + let event = event_receiver.recv().await; + if let Ok(HardwareEvent::Notification(_, _, n)) = event { + if n.eq(&noauth) { + info!( + "Lelo F1s V3 isn't authorised: Tap the device's power button to complete connection." + ) + } else if n.eq(&authed) { + debug!("Lelo F1s V3 is authorised!"); + return Ok(Arc::new(LeloHarmony::f1sv3(hardware.clone(), def))); + } else { + debug!("Lelo F1s V3 gave us a password: {:?}", n); + hardware + .unsubscribe(&HardwareUnsubscribeCmd::new( + LELO_F1S_V3_PROTOCOL_UUID, + Endpoint::Generic0, + )) + .await?; + hardware + .write_value(&HardwareWriteCmd::new( + &[LELO_F1S_V3_PROTOCOL_UUID], + Endpoint::Generic0, + n, + true, + )) + .await?; + hardware + .subscribe(&HardwareSubscribeCmd::new( + LELO_F1S_V3_PROTOCOL_UUID, + Endpoint::Generic0, + )) + .await?; + } + } else { + return Err(ButtplugDeviceError::ProtocolSpecificError( + "LeloF1sV3".to_owned(), + "Lelo F1s V3 didn't provided valid security handshake".to_owned(), + )); + } + } + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/mod.rs b/crates/buttplug_server/src/device/protocol_impl/mod.rs index d1a54d2ae..472e3f4f5 100644 --- a/crates/buttplug_server/src/device/protocol_impl/mod.rs +++ b/crates/buttplug_server/src/device/protocol_impl/mod.rs @@ -56,6 +56,7 @@ pub mod kizuna; pub mod lelo_harmony; pub mod lelof1s; pub mod lelof1sv2; +pub mod lelof1sv3; pub mod leten; pub mod libo_elle; pub mod libo_shark; @@ -288,6 +289,10 @@ pub fn get_default_protocol_map() -> HashMap, + #[serde(default, skip_serializing_if = "is_false")] + vibrate_smoothing_enabled: bool, + #[getset(get_copy = "pub")] + #[serde(default, skip_serializing_if = "Option::is_none")] + vibrate_smoothing_idle_stop_ms: Option, +} + +fn is_false(value: &bool) -> bool { + !*value } impl From<&ServerDeviceDefinition> for ConfigUserDeviceCustomization { @@ -96,6 +105,8 @@ impl From<&ServerDeviceDefinition> for ConfigUserDeviceCustomization { deny: value.deny(), index: value.index(), message_gap_ms: value.message_gap_ms(), + vibrate_smoothing_enabled: value.vibrate_smoothing_enabled(), + vibrate_smoothing_idle_stop_ms: value.vibrate_smoothing_idle_stop_ms(), } } } @@ -123,6 +134,8 @@ impl ConfigUserDeviceDefinition { let mut builder = ServerDeviceDefinitionBuilder::from_base(base, self.id, false); builder.display_name(&self.user_config.display_name); builder.message_gap_ms(self.user_config.message_gap_ms); + builder.vibrate_smoothing_enabled(self.user_config.vibrate_smoothing_enabled); + builder.vibrate_smoothing_idle_stop_ms(self.user_config.vibrate_smoothing_idle_stop_ms); self.user_config.allow.then(|| builder.allow(true)); self.user_config.deny.then(|| builder.deny(true)); builder.index(self.user_config.index); diff --git a/crates/buttplug_server_device_config/src/device_definitions.rs b/crates/buttplug_server_device_config/src/device_definitions.rs index 57557c2d9..07bd9d560 100644 --- a/crates/buttplug_server_device_config/src/device_definitions.rs +++ b/crates/buttplug_server_device_config/src/device_definitions.rs @@ -25,6 +25,10 @@ pub struct ServerDeviceDefinition { protocol_variant: Option, #[getset(get_copy = "pub")] message_gap_ms: Option, + #[getset(get_copy = "pub")] + vibrate_smoothing_enabled: bool, + #[getset(get_copy = "pub")] + vibrate_smoothing_idle_stop_ms: Option, #[getset(get = "pub")] display_name: Option, #[getset(get_copy = "pub")] @@ -55,6 +59,8 @@ impl ServerDeviceDefinitionBuilder { base_id: None, protocol_variant: None, message_gap_ms: None, + vibrate_smoothing_enabled: false, + vibrate_smoothing_idle_stop_ms: None, display_name: None, allow: false, deny: false, @@ -113,6 +119,16 @@ impl ServerDeviceDefinitionBuilder { self } + pub fn vibrate_smoothing_enabled(&mut self, enabled: bool) -> &mut Self { + self.def.vibrate_smoothing_enabled = enabled; + self + } + + pub fn vibrate_smoothing_idle_stop_ms(&mut self, idle_stop_ms: Option) -> &mut Self { + self.def.vibrate_smoothing_idle_stop_ms = idle_stop_ms; + self + } + pub fn allow(&mut self, allow: bool) -> &mut Self { self.def.allow = allow; self diff --git a/crates/buttplug_tests/tests/test_device_protocols.rs b/crates/buttplug_tests/tests/test_device_protocols.rs index 3d22c116b..6cfe03305 100644 --- a/crates/buttplug_tests/tests/test_device_protocols.rs +++ b/crates/buttplug_tests/tests/test_device_protocols.rs @@ -64,6 +64,7 @@ async fn load_test_case(test_file: &str) -> DeviceTestCase { #[test_case("test_kiiroo_spot.yaml" ; "Kiiroo Spot Protocol")] #[test_case("test_lelo_f1sv1.yaml" ; "Lelo F1s V1 Protocol")] #[test_case("test_lelo_f1sv2.yaml" ; "Lelo F1s V2 Protocol")] +#[test_case("test_lelo_f1sv3.yaml" ; "Lelo F1s V3 Protocol")] #[test_case("test_lelo_idawave.yaml" ; "Lelo Harmony Protocol - Ida Wave")] #[test_case("test_lelo_tianiharmony.yaml" ; "Lelo Harmony Protocol - Tiani Harmony")] #[test_case("test_leten_protocol.yaml" ; "Leten Protocol")] @@ -195,6 +196,7 @@ async fn test_device_protocols_embedded_v4(test_file: &str) { #[test_case("test_kiiroo_spot.yaml" ; "Kiiroo Spot Protocol")] #[test_case("test_lelo_f1sv1.yaml" ; "Lelo F1s V1 Protocol")] #[test_case("test_lelo_f1sv2.yaml" ; "Lelo F1s V2 Protocol")] +#[test_case("test_lelo_f1sv3.yaml" ; "Lelo F1s V3 Protocol")] #[test_case("test_lelo_idawave.yaml" ; "Lelo Harmony Protocol - Ida Wave")] #[test_case("test_lelo_tianiharmony.yaml" ; "Lelo Harmony Protocol - Tiani Harmony")] #[test_case("test_leten_protocol.yaml" ; "Leten Protocol")] @@ -325,6 +327,7 @@ async fn test_device_protocols_json_v4(test_file: &str) { #[test_case("test_kiiroo_spot.yaml" ; "Kiiroo Spot Protocol")] #[test_case("test_lelo_f1sv1.yaml" ; "Lelo F1s V1 Protocol")] #[test_case("test_lelo_f1sv2.yaml" ; "Lelo F1s V2 Protocol")] +#[test_case("test_lelo_f1sv3.yaml" ; "Lelo F1s V3 Protocol")] #[test_case("test_lelo_idawave.yaml" ; "Lelo Harmony Protocol - Ida Wave")] #[test_case("test_lelo_tianiharmony.yaml" ; "Lelo Harmony Protocol - Tiani Harmony")] #[test_case("test_leten_protocol.yaml" ; "Leten Protocol")] @@ -456,6 +459,7 @@ async fn test_device_protocols_embedded_v3(test_file: &str) { #[test_case("test_kiiroo_spot.yaml" ; "Kiiroo Spot Protocol")] #[test_case("test_lelo_f1sv1.yaml" ; "Lelo F1s V1 Protocol")] #[test_case("test_lelo_f1sv2.yaml" ; "Lelo F1s V2 Protocol")] +#[test_case("test_lelo_f1sv3.yaml" ; "Lelo F1s V3 Protocol")] #[test_case("test_lelo_idawave.yaml" ; "Lelo Harmony Protocol - Ida Wave")] #[test_case("test_lelo_tianiharmony.yaml" ; "Lelo Harmony Protocol - Tiani Harmony")] #[test_case("test_leten_protocol.yaml" ; "Leten Protocol")] diff --git a/crates/buttplug_tests/tests/util/device_test/device_test_case/test_lelo_f1sv3.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_lelo_f1sv3.yaml new file mode 100644 index 000000000..98d0f19f7 --- /dev/null +++ b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_lelo_f1sv3.yaml @@ -0,0 +1,87 @@ +devices: + - identifier: + name: "F1SV3" + expected_name: "Lelo F1s V3" +device_init: + - !Commands + device_index: 0 + commands: + - !Subscribe + endpoint: generic0 + - !Events + device_index: 0 + events: + - !Notifications + - endpoint: generic0 + data: [0,0,0,0,0,0,0,0] + - !Events + device_index: 0 + events: + - !Notifications + - endpoint: generic0 + data: [1,2,3,4,5,6,8] + - !Commands + device_index: 0 + commands: + - !Unsubscribe + endpoint: generic0 + - !Write + endpoint: generic0 + data: [1,2,3,4,5,6,8] + write_with_response: true + - !Subscribe + endpoint: generic0 + - !Events + device_index: 0 + events: + - !Notifications + - endpoint: generic0 + data: [0x01, 0, 0, 0, 0, 0, 0, 0] +device_commands: + - !Messages + device_index: 0 + messages: + - !Vibrate + - Index: 0 + Speed: 0.5 + - !Commands + device_index: 0 + commands: + - !Write + endpoint: txvibrate + data: [0x0a, 0x12, 0x01, 0x08, 0x00, 0x00, 0x00, 0x00, 0x32, 0x00] + write_with_response: true + - !Messages + device_index: 0 + messages: + - !Vibrate + - Index: 0 + Speed: 0.75 + - Index: 1 + Speed: 0.5 + - !Commands + device_index: 0 + commands: + - !Write + endpoint: txvibrate + data: [0x0a, 0x12, 0x01, 0x08, 0x00, 0x00, 0x00, 0x00, 0x4b, 0x00] + write_with_response: true + - !Write + endpoint: txvibrate + data: [0x0a, 0x12, 0x02, 0x08, 0x00, 0x00, 0x00, 0x00, 0x32, 0x00] + write_with_response: true + - !Messages + device_index: 0 + messages: + - !Stop + - !Commands + device_index: 0 + commands: + - !Write + endpoint: txvibrate + data: [0x0a, 0x12, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + write_with_response: true + - !Write + endpoint: txvibrate + data: [0x0a, 0x12, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + write_with_response: true