Skip to content

Commit 23280af

Browse files
committed
feat: Implement Linear movement for Lovense Solace Pro
is chonk. so chonk. 100ms timing sucks. But it works. Sorta.
1 parent dab0176 commit 23280af

File tree

3 files changed

+111
-18
lines changed

3 files changed

+111
-18
lines changed

buttplug/buttplug-device-config/build-config/buttplug-device-config-v3.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,19 @@
758758
]
759759
}
760760
},
761+
{
762+
"feature-type": "Position",
763+
"description": "Stroker Position Based Movement",
764+
"actuator": {
765+
"step-range": [
766+
0,
767+
100
768+
],
769+
"messages": [
770+
"LinearCmd"
771+
]
772+
}
773+
},
761774
{
762775
"feature-type": "Battery",
763776
"description": "Battery Level",

buttplug/buttplug-device-config/device-config-v3/buttplug-device-config-v3.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -422,14 +422,22 @@ protocols:
422422
- 20
423423
messages:
424424
- ScalarCmd
425+
- feature-type: Position
426+
description: Stroker Position Based Movement
427+
actuator:
428+
step-range:
429+
- 0
430+
- 100
431+
messages:
432+
- LinearCmd
425433
- feature-type: Battery
426434
description: Battery Level
427435
sensor:
428436
value-range:
429437
- - 0
430438
- 100
431439
messages:
432-
- SensorReadCmd
440+
- SensorReadCmd
433441
communication:
434442
- btle:
435443
names:

buttplug/src/server/device/protocol/lovense.rs

Lines changed: 89 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@ use crate::{
1515
hardware::{Hardware, HardwareCommand, HardwareEvent, HardwareSubscribeCmd, HardwareWriteCmd},
1616
protocol::{ProtocolHandler, ProtocolIdentifier, ProtocolInitializer},
1717
},
18-
util::sleep,
18+
util::{sleep, async_manager}
1919
};
2020
use async_trait::async_trait;
2121
use futures::{future::BoxFuture, FutureExt};
2222
use regex::Regex;
2323
use std::{
2424
sync::{
25-
atomic::{AtomicBool, Ordering},
25+
atomic::{AtomicBool, AtomicU32, AtomicU8, Ordering},
2626
Arc,
2727
},
2828
time::Duration,
@@ -141,13 +141,12 @@ impl LovenseInitializer {
141141
impl ProtocolInitializer for LovenseInitializer {
142142
async fn initialize(
143143
&mut self,
144-
_: Arc<Hardware>,
144+
hardware: Arc<Hardware>,
145145
device_definition: &UserDeviceDefinition,
146146
) -> Result<Arc<dyn ProtocolHandler>, ButtplugDeviceError> {
147-
let mut protocol = Lovense::default();
148-
protocol.device_type = self.device_type.clone();
147+
let device_type = self.device_type.clone();
149148

150-
protocol.vibrator_count = device_definition
149+
let vibrator_count = device_definition
151150
.features()
152151
.iter()
153152
.filter(|x| [FeatureType::Vibrate, FeatureType::Oscillate].contains(x.feature_type()))
@@ -159,31 +158,49 @@ impl ProtocolInitializer for LovenseInitializer {
159158
.filter(|x| x.actuator().is_some())
160159
.count();
161160

161+
162162
// This might need better tuning if other complex Lovenses are released
163163
// Currently this only applies to the Flexer/Lapis/Solace
164-
if (protocol.vibrator_count == 2 && actuator_count > 2)
165-
|| protocol.vibrator_count > 2
166-
|| protocol.device_type == "H"
167-
{
168-
protocol.use_mply = true;
169-
}
164+
let use_mply = (vibrator_count == 2 && actuator_count > 2)
165+
|| vibrator_count > 2
166+
|| device_type == "H";
170167

171168
debug!(
172169
"Device type {} initialized with {} vibrators {} using Mply",
173-
protocol.device_type,
174-
protocol.vibrator_count,
175-
if protocol.use_mply { "" } else { "not " }
170+
device_type,
171+
vibrator_count,
172+
if use_mply { "" } else { "not " }
176173
);
177-
Ok(Arc::new(protocol))
174+
175+
Ok(Arc::new(Lovense::new(hardware, &device_type, vibrator_count, use_mply)))
178176
}
179177
}
180178

181-
#[derive(Default)]
182179
pub struct Lovense {
183180
rotation_direction: Arc<AtomicBool>,
184181
vibrator_count: usize,
185182
use_mply: bool,
186183
device_type: String,
184+
// Pairing of position: u8, duration: u32
185+
linear_info: Arc<(AtomicU8, AtomicU32)>,
186+
}
187+
188+
impl Lovense {
189+
pub fn new(hardware: Arc<Hardware>, device_type: &str, vibrator_count: usize, use_mply: bool) -> Self {
190+
191+
let linear_info = Arc::new((AtomicU8::new(0), AtomicU32::new(0)));
192+
if device_type == "BA" {
193+
async_manager::spawn(update_linear_movement(hardware.clone(), linear_info.clone()));
194+
}
195+
196+
Self {
197+
rotation_direction: Arc::new(AtomicBool::new(false)),
198+
vibrator_count,
199+
use_mply,
200+
device_type: device_type.to_owned(),
201+
linear_info,
202+
}
203+
}
187204
}
188205

189206
impl ProtocolHandler for Lovense {
@@ -385,4 +402,59 @@ impl ProtocolHandler for Lovense {
385402
}
386403
.boxed()
387404
}
405+
406+
fn handle_linear_cmd(
407+
&self,
408+
message: message::LinearCmdV4,
409+
) -> Result<Vec<HardwareCommand>, ButtplugDeviceError> {
410+
let vector = message.vectors().first().expect("Already checked for vector subcommand");
411+
self.linear_info.0.store((vector.position() * 100f64) as u8, Ordering::SeqCst);
412+
self.linear_info.1.store(vector.duration(), Ordering::SeqCst);
413+
Ok(vec!())
414+
}
388415
}
416+
417+
async fn update_linear_movement(device: Arc<Hardware>, linear_info: Arc<(AtomicU8, AtomicU32)>) {
418+
let mut last_goal_position = 0i32;
419+
let mut current_move_amount = 0i32;
420+
let mut current_position = 0i32;
421+
loop {
422+
// See if we've updated our goal position
423+
let goal_position = linear_info.0.load(Ordering::Relaxed) as i32;
424+
// If we have and it's not the same, recalculate based on current status.
425+
if last_goal_position != goal_position {
426+
last_goal_position = goal_position;
427+
// We move every 100ms, so divide the movement into that many chunks.
428+
// If we're moving so fast it'd be under our 100ms boundary, just move in 1 step.
429+
let move_steps = (linear_info.1.load(Ordering::Relaxed) / 100).max(1);
430+
current_move_amount = (goal_position as i32 - current_position) as i32 / move_steps as i32;
431+
}
432+
433+
// If we aren't going anywhere, just pause then restart
434+
if current_position == last_goal_position {
435+
sleep(Duration::from_millis(100)).await;
436+
continue;
437+
}
438+
439+
// Update our position, make sure we don't overshoot
440+
current_position += current_move_amount;
441+
if current_move_amount < 0 {
442+
if current_position < last_goal_position {
443+
current_position = last_goal_position;
444+
}
445+
} else {
446+
if current_position > last_goal_position {
447+
current_position = last_goal_position;
448+
}
449+
}
450+
451+
let lovense_cmd = format!("FSetSite:{};", current_position);
452+
info!("{}", lovense_cmd);
453+
454+
let hardware_cmd = HardwareWriteCmd::new(Endpoint::Tx, lovense_cmd.into_bytes(), false);
455+
if device.write_value(&hardware_cmd).await.is_err() {
456+
return;
457+
}
458+
sleep(Duration::from_millis(100)).await;
459+
}
460+
}

0 commit comments

Comments
 (0)