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::device::{
9 hardware::{Hardware, HardwareCommand, HardwareWriteCmd},
10 protocol::{ProtocolHandler, ProtocolKeepaliveStrategy},
11};
12use buttplug_core::{
13 errors::ButtplugDeviceError,
14 message::InputReadingV4,
15 util::{async_manager, sleep},
16};
17use buttplug_server_device_config::Endpoint;
18use futures::future::BoxFuture;
19use std::{
20 sync::{
21 Arc,
22 atomic::{AtomicU32, Ordering},
23 },
24 time::Duration,
25};
26use uuid::{Uuid, uuid};
27
28const LOVENSE_STROKER_PROTOCOL_UUID: Uuid = uuid!("a97fc354-5561-459a-bc62-110d7c2868ac");
29
30pub struct LovenseStroker {
31 linear_info: Arc<(AtomicU32, AtomicU32)>,
32}
33
34impl LovenseStroker {
35 pub fn new(hardware: Arc<Hardware>) -> Self {
36 let linear_info = Arc::new((AtomicU32::new(0), AtomicU32::new(0)));
37 async_manager::spawn(update_linear_movement(
38 hardware.clone(),
39 linear_info.clone(),
40 ));
41 Self { linear_info }
42 }
43}
44
45impl ProtocolHandler for LovenseStroker {
46 fn keepalive_strategy(&self) -> ProtocolKeepaliveStrategy {
47 super::keepalive_strategy()
48 }
49
50 fn handle_position_with_duration_cmd(
51 &self,
52 _feature_index: u32,
53 _feature_id: Uuid,
54 position: u32,
55 duration: u32,
56 ) -> Result<Vec<HardwareCommand>, ButtplugDeviceError> {
57 self.linear_info.0.store(position, Ordering::Relaxed);
58 self.linear_info.1.store(duration, Ordering::Relaxed);
59 Ok(vec![])
60 }
61
62 fn handle_battery_level_cmd(
63 &self,
64 device_index: u32,
65 device: Arc<Hardware>,
66 feature_index: u32,
67 feature_id: Uuid,
68 ) -> BoxFuture<'static, Result<InputReadingV4, ButtplugDeviceError>> {
69 super::handle_battery_level_cmd(device_index, device, feature_index, feature_id)
70 }
71}
72
73async fn update_linear_movement(device: Arc<Hardware>, linear_info: Arc<(AtomicU32, AtomicU32)>) {
74 let mut last_goal_position = 0i32;
75 let mut current_move_amount = 0i32;
76 let mut current_position = 0i32;
77 loop {
78 // See if we've updated our goal position
79 let goal_position = linear_info.0.load(Ordering::Relaxed) as i32;
80 // If we have and it's not the same, recalculate based on current status.
81 if last_goal_position != goal_position {
82 last_goal_position = goal_position;
83 // We move every 100ms, so divide the movement into that many chunks.
84 // If we're moving so fast it'd be under our 100ms boundary, just move in 1 step.
85 let move_steps = (linear_info.1.load(Ordering::Relaxed) / 100).max(1);
86 current_move_amount = (goal_position - current_position) / move_steps as i32;
87 }
88
89 // If we aren't going anywhere, just pause then restart
90 if current_position == last_goal_position {
91 sleep(Duration::from_millis(100)).await;
92 continue;
93 }
94
95 // Update our position, make sure we don't overshoot
96 current_position += current_move_amount;
97 if current_move_amount < 0 {
98 if current_position < last_goal_position {
99 current_position = last_goal_position;
100 }
101 } else if current_position > last_goal_position {
102 current_position = last_goal_position;
103 }
104
105 let lovense_cmd = format!("FSetSite:{current_position};");
106
107 let hardware_cmd: HardwareWriteCmd = HardwareWriteCmd::new(
108 &[LOVENSE_STROKER_PROTOCOL_UUID],
109 Endpoint::Tx,
110 lovense_cmd.into_bytes(),
111 false,
112 );
113 if device.write_value(&hardware_cmd).await.is_err() {
114 return;
115 }
116 sleep(Duration::from_millis(100)).await;
117 }
118}