Buttplug sex toy control library
at master 316 lines 11 kB view raw
1// Buttplug Rust Source Code File - See https://buttplug.io for more info. 2// 3// Copyright 2016-2024 Nonpolynomial Labs LLC. All rights reserved. 4// 5// Licensed under the BSD 3-Clause license. See LICENSE file in the project root 6// for full license information. 7 8use crate::{ 9 core::{ 10 errors::ButtplugDeviceError, 11 message::{self, ActuatorType, Endpoint, FeatureType, SensorReadingV4}, 12 }, 13use crate::{ 14 device::{ 15 configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, 16 hardware::{Hardware, HardwareCommand, HardwareReadCmd, HardwareWriteCmd}, 17 protocol::{ 18 generic_protocol_initializer_setup, 19 ProtocolHandler, 20 ProtocolIdentifier, 21 ProtocolInitializer, 22 }, 23 }, 24 message::checked_sensor_read_cmd::CheckedSensorReadCmdV4, 25 }, 26}; 27use async_trait::async_trait; 28use futures::future::{BoxFuture, FutureExt}; 29use std::sync::{ 30 atomic::{AtomicBool, Ordering}, 31 Arc, 32}; 33 34generic_protocol_initializer_setup!(LovenseConnectService, "lovense-connect-service"); 35 36#[derive(Default)] 37pub struct LovenseConnectServiceInitializer {} 38 39#[async_trait] 40impl ProtocolInitializer for LovenseConnectServiceInitializer { 41 async fn initialize( 42 &mut self, 43 hardware: Arc<Hardware>, 44 device_definition: &UserDeviceDefinition, 45 ) -> Result<Arc<dyn ProtocolHandler>, ButtplugDeviceError> { 46 let mut protocol = LovenseConnectService::new(hardware.address()); 47 48 protocol.vibrator_count = device_definition 49 .features() 50 .iter() 51 .filter(|x| *x.feature_type() == FeatureType::Vibrate) 52 .count(); 53 protocol.thusting_count = device_definition 54 .features() 55 .iter() 56 .filter(|x| *x.feature_type() == FeatureType::Oscillate) 57 .count(); 58 59 // The Ridge and Gravity both oscillate, but the Ridge only oscillates but takes 60 // the vibrate command... The Gravity has a vibe as well, and uses a Thrusting 61 // command for that oscillator. 62 if protocol.vibrator_count == 0 && protocol.thusting_count != 0 { 63 protocol.vibrator_count = protocol.thusting_count; 64 protocol.thusting_count = 0; 65 } 66 67 if hardware.name() == "Solace" { 68 // Just hardcoding this weird exception until we can control depth 69 let lovense_cmd = format!("Depth?v={}&t={}", 3, hardware.address()) 70 .as_bytes() 71 .to_vec(); 72 73 hardware 74 .write_value(&HardwareWriteCmd::new(Endpoint::Tx, lovense_cmd, false)) 75 .await?; 76 77 protocol.vibrator_count = 0; 78 protocol.thusting_count = 1; 79 } 80 81 Ok(Arc::new(protocol)) 82 } 83} 84 85#[derive(Default)] 86pub struct LovenseConnectService { 87 address: String, 88 rotation_direction: Arc<AtomicBool>, 89 vibrator_count: usize, 90 thusting_count: usize, 91} 92 93impl LovenseConnectService { 94 pub fn new(address: &str) -> Self { 95 Self { 96 address: address.to_owned(), 97 ..Default::default() 98 } 99 } 100} 101 102impl ProtocolHandler for LovenseConnectService { 103 fn handle_value_cmd( 104 &self, 105 cmds: &[Option<(ActuatorType, i32)>], 106 ) -> Result<Vec<HardwareCommand>, ButtplugDeviceError> { 107 let mut hardware_cmds = vec![]; 108 109 // Handle vibration commands, these will be by far the most common. Fucking machine oscillation 110 // uses lovense vibrate commands internally too, so we can include them here. 111 let vibrate_cmds: Vec<&(ActuatorType, i32)> = cmds 112 .iter() 113 .filter(|x| { 114 if let Some(val) = x { 115 if self.thusting_count == 0 { 116 [ActuatorType::Vibrate, ActuatorType::Oscillate].contains(&val.0) 117 } else { 118 [ActuatorType::Vibrate].contains(&val.0) 119 } 120 } else { 121 false 122 } 123 }) 124 .map(|x| x.as_ref().expect("Already verified is some")) 125 .collect(); 126 127 if !vibrate_cmds.is_empty() { 128 // Lovense is the same situation as the Lovehoney Desire, where commands 129 // are different if we're addressing all motors or seperate motors. 130 // Difference here being that there's Lovense variants with different 131 // numbers of motors. 132 // 133 // Neat way of checking if everything is the same via 134 // https://sts10.github.io/2019/06/06/is-all-equal-function.html. 135 // 136 // Just make sure we're not matching on None, 'cause if that's the case 137 // we ain't got shit to do. 138 if self.vibrator_count == vibrate_cmds.len() 139 && (self.vibrator_count == 1 || vibrate_cmds.windows(2).all(|w| w[0].1 == w[1].1)) 140 { 141 let lovense_cmd = format!("Vibrate?v={}&t={}", vibrate_cmds[0].1, self.address) 142 .as_bytes() 143 .to_vec(); 144 hardware_cmds.push(HardwareWriteCmd::new(Endpoint::Tx, lovense_cmd, false).into()); 145 } else { 146 for (i, cmd) in cmds.iter().enumerate() { 147 if let Some((actuator, speed)) = cmd { 148 if self.thusting_count == 0 149 && ![ActuatorType::Vibrate, ActuatorType::Oscillate].contains(actuator) 150 { 151 continue; 152 } 153 if self.thusting_count != 0 && ![ActuatorType::Vibrate].contains(actuator) { 154 continue; 155 } 156 let lovense_cmd = format!("Vibrate{}?v={}&t={}", i + 1, speed, self.address) 157 .as_bytes() 158 .to_vec(); 159 hardware_cmds.push(HardwareWriteCmd::new(Endpoint::Tx, lovense_cmd, false).into()); 160 } 161 } 162 } 163 } 164 165 // Handle constriction commands. 166 let thrusting_cmds: Vec<&(ActuatorType, i32)> = cmds 167 .iter() 168 .filter(|x| { 169 if let Some(val) = x { 170 [ActuatorType::Oscillate].contains(&val.0) 171 } else { 172 false 173 } 174 }) 175 .map(|x| x.as_ref().expect("Already verified is some")) 176 .collect(); 177 if self.thusting_count != 0 && !thrusting_cmds.is_empty() { 178 let lovense_cmd = format!("Thrusting?v={}&t={}", thrusting_cmds[0].1, self.address) 179 .as_bytes() 180 .to_vec(); 181 182 hardware_cmds.push(HardwareWriteCmd::new(Endpoint::Tx, lovense_cmd, false).into()); 183 } 184 185 // Handle constriction commands. 186 let constrict_cmds: Vec<&(ActuatorType, i32)> = cmds 187 .iter() 188 .filter(|x| { 189 if let Some(val) = x { 190 val.0 == ActuatorType::Constrict 191 } else { 192 false 193 } 194 }) 195 .map(|x| x.as_ref().expect("Already verified is some")) 196 .collect(); 197 198 if !constrict_cmds.is_empty() { 199 // Only the max has a constriction system, and there's only one, so just parse the first command. 200 /* ~ Sutekh 201 * - Implemented constriction. 202 * - Kept things consistent with the lovense handle_scalar_cmd() method. 203 * - Using AirAuto method. 204 * - Changed step count in device config file to 3. 205 */ 206 let lovense_cmd = format!("AirAuto?v={}&t={}", constrict_cmds[0].1, self.address) 207 .as_bytes() 208 .to_vec(); 209 210 hardware_cmds.push(HardwareWriteCmd::new(Endpoint::Tx, lovense_cmd, false).into()); 211 } 212 213 // Handle "rotation" commands: Currently just applicable as the Flexer's Fingering command 214 let rotation_cmds: Vec<&(ActuatorType, i32)> = cmds 215 .iter() 216 .filter(|x| { 217 if let Some(val) = x { 218 val.0 == ActuatorType::Rotate 219 } else { 220 false 221 } 222 }) 223 .map(|x| x.as_ref().expect("Already verified is some")) 224 .collect(); 225 226 if !rotation_cmds.is_empty() { 227 let lovense_cmd = format!("Fingering?v={}&t={}", rotation_cmds[0].1, self.address) 228 .as_bytes() 229 .to_vec(); 230 231 hardware_cmds.push(HardwareWriteCmd::new(Endpoint::Tx, lovense_cmd, false).into()); 232 } 233 234 Ok(hardware_cmds) 235 236 /* Note from Sutekh: 237 * I removed the code below to keep the handle_scalar_cmd methods for lovense toys somewhat consistent. 238 * The patch above is almost the same as the "Lovense" ProtocolHandler implementation. 239 * I have changed the commands to the Lovense Connect API format. 240 * During my testing of the Lovense Connect app's API it seems that even though Constriction has a step range of 0-5. It only responds to values 1-3. 241 */ 242 243 /* 244 // Lovense is the same situation as the Lovehoney Desire, where commands 245 // are different if we're addressing all motors or seperate motors. 246 // Difference here being that there's Lovense variants with different 247 @@ -77,26 +220,27 @@ 248 // Just make sure we're not matching on None, 'cause if that's the case 249 // we ain't got shit to do. 250 let mut msg_vec = vec![]; 251 if cmds[0].is_some() && (cmds.len() == 1 || cmds.windows(2).all(|w| w[0] == w[1])) { 252 let lovense_cmd = format!( 253 "Vibrate?v={}&t={}", 254 cmds[0].expect("Already checked existence").1, 255 self.address 256 ) 257 .as_bytes() 258 .to_vec(); 259 msg_vec.push(HardwareWriteCmd::new(Endpoint::Tx, lovense_cmd, false).into()); 260 } else { 261 for (i, cmd) in cmds.iter().enumerate() { 262 if let Some((_, speed)) = cmd { 263 let lovense_cmd = format!("Vibrate{}?v={}&t={}", i + 1, speed, self.address) 264 .as_bytes() 265 .to_vec(); 266 msg_vec.push(HardwareWriteCmd::new(Endpoint::Tx, lovense_cmd, false).into()); 267 } 268 } 269 } 270 Ok(msg_vec) 271 */ 272 } 273 274 fn handle_rotate_cmd( 275 &self, 276 cmds: &[Option<(u32, bool)>], 277 ) -> Result<Vec<HardwareCommand>, ButtplugDeviceError> { 278 let mut hardware_cmds = vec![]; 279 if let Some(Some((speed, clockwise))) = cmds.first() { 280 let lovense_cmd = format!("/Rotate?v={}&t={}", speed, self.address) 281 .as_bytes() 282 .to_vec(); 283 hardware_cmds.push(HardwareWriteCmd::new(Endpoint::Tx, lovense_cmd, false).into()); 284 let dir = self.rotation_direction.load(Ordering::Relaxed); 285 // TODO Should we store speed and direction as an option for rotation caching? This is weird. 286 if dir != *clockwise { 287 self.rotation_direction.store(*clockwise, Ordering::Relaxed); 288 hardware_cmds 289 .push(HardwareWriteCmd::new(Endpoint::Tx, b"RotateChange?".to_vec(), false).into()); 290 } 291 } 292 Ok(hardware_cmds) 293 } 294 295 fn handle_battery_level_cmd( 296 &self, 297 device: Arc<Hardware>, 298 msg: CheckedSensorReadCmdV4, 299 ) -> BoxFuture<Result<SensorReadingV4, ButtplugDeviceError>> { 300 async move { 301 // This is a dummy read. We just store the battery level in the device 302 // implementation and it's the only thing read will return. 303 let reading = device 304 .read_value(&HardwareReadCmd::new(Endpoint::Rx, 0, 0)) 305 .await?; 306 debug!("Battery level: {}", reading.data()[0]); 307 Ok(message::SensorReadingV4::new( 308 msg.device_index(), 309 msg.feature_index(), 310 msg.input_type(), 311 vec![reading.data()[0] as i32], 312 )) 313 } 314 .boxed() 315 } 316}