Buttplug sex toy control library
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}