From 59e7dd455bae92a846be7ad68778ad23046cc3a7 Mon Sep 17 00:00:00 2001 From: Uub <88688681+DatUub@users.noreply.github.com> Date: Sat, 11 Apr 2026 18:55:47 -0400 Subject: [PATCH] Add Utimi protocol handler (benben lightspot BLE SDK) Adds support for Utimi BLE toys using the benben lightspot protocol. Command format: A0 03 [vib] [thrust] [m2] [m3] [m4] AA on service 0000ffa0 / characteristic 0000ffa1. Similar to JoyHub but uses 5 motor slots (8 bytes) instead of 4 (7 bytes). Protocol reverse-engineered from the Utimi Android APK (com.benben.lightspot.bluetooth SDK). --- .../src/device/protocol_impl/mod.rs | 2 + .../src/device/protocol_impl/utimi.rs | 108 ++++++++++++++++++ .../device-config-v4/protocols/utimi.yml | 31 +++++ 3 files changed, 141 insertions(+) create mode 100644 crates/buttplug_server/src/device/protocol_impl/utimi.rs create mode 100644 crates/buttplug_server_device_config/device-config-v4/protocols/utimi.yml diff --git a/crates/buttplug_server/src/device/protocol_impl/mod.rs b/crates/buttplug_server/src/device/protocol_impl/mod.rs index 49d85aa5..34df24ce 100644 --- a/crates/buttplug_server/src/device/protocol_impl/mod.rs +++ b/crates/buttplug_server/src/device/protocol_impl/mod.rs @@ -112,6 +112,7 @@ pub mod thehandy_v3; pub mod tryfun; pub mod tryfun_blackhole; pub mod tryfun_meta2; +pub mod utimi; pub mod vibcrafter; pub mod vibratissimo; pub mod vorze_sa; @@ -538,6 +539,7 @@ pub fn get_default_protocol_map() -> HashMap, + _def: &ServerDeviceDefinition, + ) -> Result, ButtplugDeviceError> { + Ok(Arc::new(Utimi::default())) + } +} + +pub struct Utimi { + // Benben lightspot protocol supports up to 5 motor slots. + // Utimi devices use slot 0 (vibrate) and slot 1 (thrust/oscillate). + last_cmds: [AtomicU8; 5], +} + +impl Default for Utimi { + fn default() -> Self { + Self { + last_cmds: [const { AtomicU8::new(0) }; 5], + } + } +} + +impl Utimi { + fn send_command( + &self, + index: u32, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.last_cmds[index as usize].store(speed as u8, Ordering::Relaxed); + Ok(vec![ + HardwareWriteCmd::new( + &[UTIMI_PROTOCOL_UUID], + Endpoint::Tx, + vec![ + 0xa0, + 0x03, + self.last_cmds[0].load(Ordering::Relaxed), + self.last_cmds[1].load(Ordering::Relaxed), + self.last_cmds[2].load(Ordering::Relaxed), + self.last_cmds[3].load(Ordering::Relaxed), + self.last_cmds[4].load(Ordering::Relaxed), + 0xaa, + ], + false, + ) + .into(), + ]) + } +} + +impl ProtocolHandler for Utimi { + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.send_command(feature_index, speed) + } + + fn handle_output_oscillate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.send_command(feature_index, speed) + } +} diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/utimi.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/utimi.yml new file mode 100644 index 00000000..58985f6d --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/utimi.yml @@ -0,0 +1,31 @@ +--- +defaults: + name: Utimi Device + features: + - id: a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d + output: + vibrate: + value: + - 0 + - 255 + index: 0 + - id: b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e + output: + oscillate: + value: + - 0 + - 255 + index: 1 + id: c3d4e5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f +configurations: +- identifier: + - Utimi + name: Utimi Prostate Massager + id: d4e5f6a7-b8c9-4d0e-1f2a-3b4c5d6e7f80 +communication: +- btle: + names: + - Utimi* + services: + 0000ffa0-0000-1000-8000-00805f9b34fb: + tx: 0000ffa1-0000-1000-8000-00805f9b34fb