Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 103 additions & 17 deletions crates/buttplug_server/src/device/protocol_impl/lelo_harmony.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ use std::sync::Arc;
use uuid::{Uuid, uuid};

const LELO_HARMONY_PROTOCOL_UUID: Uuid = uuid!("220e180a-e6d5-4fd1-963e-43a6f990b717");
const LELO_HARMONY_F1SV3_VARIANT: &str = "f1sv3";
generic_protocol_initializer_setup!(LeloHarmony, "lelo-harmony");

#[derive(Default)]
Expand All @@ -43,7 +44,7 @@ impl ProtocolInitializer for LeloHarmonyInitializer {
async fn initialize(
&mut self,
hardware: Arc<Hardware>,
_: &ServerDeviceDefinition,
def: &ServerDeviceDefinition,
) -> Result<Arc<dyn ProtocolHandler>, ButtplugDeviceError> {
// The Lelo Harmony has a very specific pairing flow:
// * First the device is turned on in BLE mode (long press)
Expand Down Expand Up @@ -72,6 +73,13 @@ impl ProtocolInitializer for LeloHarmonyInitializer {
)
} else if !n.is_empty() && n[0] == 1u8 && n[1..].iter().all(|b| *b == 0u8) {
debug!("Lelo Harmony is authorised!");
if def
.protocol_variant()
.as_deref()
.is_some_and(|variant| variant == LELO_HARMONY_F1SV3_VARIANT)
{
return Ok(Arc::new(LeloHarmony::new(Endpoint::Tx, true, true)));
}
return Ok(Arc::new(LeloHarmony::default()));
} else {
debug!("Lelo Harmony gave us a password: {:?}", n);
Expand Down Expand Up @@ -109,33 +117,64 @@ impl ProtocolInitializer for LeloHarmonyInitializer {
}
}

#[derive(Default)]
pub struct LeloHarmony {}
pub struct LeloHarmony {
output_endpoint: Endpoint,
use_zero_pattern_for_stop: bool,
write_with_response: bool,
}

impl Default for LeloHarmony {
fn default() -> Self {
Self::new(Endpoint::Tx, false, false)
}
}

impl LeloHarmony {
pub(super) fn f1sv3() -> Self {
Self::new(Endpoint::TxVibrate, true, true)
}

fn new(
output_endpoint: Endpoint,
use_zero_pattern_for_stop: bool,
write_with_response: bool,
) -> Self {
Self {
output_endpoint,
use_zero_pattern_for_stop,
write_with_response,
}
}

fn handle_input_cmd(
&self,
feature_index: u32,
feature_id: Uuid,
speed: u32,
) -> Result<Vec<HardwareCommand>, ButtplugDeviceError> {
let pattern = if self.use_zero_pattern_for_stop && speed == 0 {
0x00
} else {
0x08
};
let data = vec![
0x0a,
0x12,
feature_index as u8 + 1,
pattern,
0x00,
0x00,
0x00,
0x00,
speed as u8,
0x00,
];
Ok(vec![
HardwareWriteCmd::new(
&[feature_id],
Endpoint::Tx,
vec![
0x0a,
0x12,
feature_index as u8 + 1,
0x08,
0x00,
0x00,
0x00,
0x00,
speed as u8,
0x00,
],
false,
self.output_endpoint,
data,
self.write_with_response,
)
.into(),
])
Expand All @@ -161,3 +200,50 @@ impl ProtocolHandler for LeloHarmony {
self.handle_input_cmd(feature_index, feature_id, speed)
}
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn uses_configured_output_endpoint_for_vibration() {
let handler = LeloHarmony::new(Endpoint::TxVibrate, false, false);
let commands = handler
.handle_output_vibrate_cmd(1, uuid!("00000000-0000-0000-0000-000000000001"), 50)
.expect("Command should build");

assert_eq!(commands.len(), 1);
match &commands[0] {
HardwareCommand::Write(cmd) => {
assert_eq!(cmd.endpoint(), Endpoint::TxVibrate);
assert_eq!(
cmd.data(),
&[0x0a, 0x12, 0x02, 0x08, 0x00, 0x00, 0x00, 0x00, 0x32, 0x00]
);
assert!(!cmd.write_with_response());
}
_ => panic!("Expected write command"),
}
}

#[test]
fn can_use_zero_pattern_for_stop() {
let handler = LeloHarmony::new(Endpoint::TxVibrate, true, true);
let commands = handler
.handle_output_vibrate_cmd(0, uuid!("00000000-0000-0000-0000-000000000001"), 0)
.expect("Command should build");

assert_eq!(commands.len(), 1);
match &commands[0] {
HardwareCommand::Write(cmd) => {
assert_eq!(cmd.endpoint(), Endpoint::TxVibrate);
assert_eq!(
cmd.data(),
&[0x0a, 0x12, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
);
assert!(cmd.write_with_response());
}
_ => panic!("Expected write command"),
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ impl ProtocolInitializer for LeloF1sV2Initializer {
} else if n.eq(&authed) {
debug!("Lelo F1s V2 is authorised!");
if use_harmony {
return Ok(Arc::new(LeloHarmony::default()));
return Ok(Arc::new(LeloHarmony::f1sv3()));
} else {
return Ok(Arc::new(LeloF1s::new(true)));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"version": {
"major": 5,
"minor": 5
"minor": 6
},
"protocols": {
"activejoy": {
Expand Down Expand Up @@ -11818,8 +11818,7 @@
"btle": {
"names": [
"F1SV2A",
"F1SV2X",
"F1SV3"
"F1SV2X"
],
"services": {
"0000fff0-0000-1000-8000-00805f9b34fb": {
Expand All @@ -11841,13 +11840,6 @@
"F1SV2X"
],
"name": "Lelo F1s V2"
},
{
"id": "36adf7ce-98bf-4fad-b916-b44d20a5d9e1",
"identifier": [
"F1SV3"
],
"name": "Lelo F1s V3"
}
],
"defaults": {
Expand Down Expand Up @@ -11900,6 +11892,7 @@
"Switch",
"SURFER2",
"F2",
"F1SV3",
"Boomerang"
],
"services": {
Expand Down Expand Up @@ -12111,6 +12104,40 @@
],
"name": "Lelo F2S"
},
{
"features": [
{
"id": "90bd67a5-4601-4c49-97bb-0845ab7011ba",
"index": 0,
"output": {
"vibrate": {
"value": [
0,
100
]
}
}
},
{
"id": "05fc758b-a3fe-4156-b3ae-9cdcb9ae95c6",
"index": 1,
"output": {
"vibrate": {
"value": [
0,
100
]
}
}
}
],
"id": "36adf7ce-98bf-4fad-b916-b44d20a5d9e1",
"identifier": [
"F1SV3"
],
"name": "Lelo F1s V3",
"protocol_variant": "f1sv3"
},
{
"features": [
{
Expand Down Expand Up @@ -24462,4 +24489,4 @@
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,11 @@ configurations:
- F1SV2X
name: Lelo F1s V2
id: 64505ced-309b-4a32-93a8-13ee55e2da2c
- identifier:
- F1SV3
name: Lelo F1s V3
id: 36adf7ce-98bf-4fad-b916-b44d20a5d9e1
communication:
- btle:
names:
- F1SV2A
- F1SV2X
- F1SV3
services:
0000fff0-0000-1000-8000-00805f9b34fb:
tx: 0000fff1-0000-1000-8000-00805f9b34fb
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,26 @@ configurations:
- F2
name: Lelo F2S
id: d8f3c009-6712-4a2e-98f6-a75170fa4112
- identifier:
- F1SV3
name: Lelo F1s V3
protocol_variant: f1sv3
features:
- id: 90bd67a5-4601-4c49-97bb-0845ab7011ba
output:
vibrate:
value:
- 0
- 100
index: 0
- id: 05fc758b-a3fe-4156-b3ae-9cdcb9ae95c6
output:
vibrate:
value:
- 0
- 100
index: 1
id: 36adf7ce-98bf-4fad-b916-b44d20a5d9e1
- identifier:
- Tiani Twist
name: Lelo Tiani Twist
Expand Down Expand Up @@ -177,6 +197,7 @@ communication:
- Switch
- SURFER2
- F2
- F1SV3
- Boomerang
services:
0000fff0-0000-1000-8000-00805f9b34fb:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
version:
major: 5
minor: 5
minor: 6
10 changes: 10 additions & 0 deletions crates/buttplug_tests/tests/test_device_protocols.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ async fn load_test_case(test_file: &str) -> DeviceTestCase {
#[test_case("test_lelo_f1sv1.yaml" ; "Lelo F1s V1 Protocol")]
#[test_case("test_lelo_f1sv2.yaml" ; "Lelo F1s V2 Protocol")]
#[test_case("test_lelo_idawave.yaml" ; "Lelo Harmony Protocol - Ida Wave")]
#[test_case("test_lelo_f1sv3_harmony.yaml" ; "Lelo Harmony Protocol - F1s V3")]
#[test_case("test_lelo_tianiharmony.yaml" ; "Lelo Harmony Protocol - Tiani Harmony")]
#[test_case("test_leten_protocol.yaml" ; "Leten Protocol")]
#[test_case("test_loob_protocol.yaml" ; "Joyroid Loob Protocol")]
Expand Down Expand Up @@ -192,6 +193,7 @@ async fn test_device_protocols_embedded_v4(test_file: &str) {
#[test_case("test_lelo_f1sv1.yaml" ; "Lelo F1s V1 Protocol")]
#[test_case("test_lelo_f1sv2.yaml" ; "Lelo F1s V2 Protocol")]
#[test_case("test_lelo_idawave.yaml" ; "Lelo Harmony Protocol - Ida Wave")]
#[test_case("test_lelo_f1sv3_harmony.yaml" ; "Lelo Harmony Protocol - F1s V3")]
#[test_case("test_lelo_tianiharmony.yaml" ; "Lelo Harmony Protocol - Tiani Harmony")]
#[test_case("test_leten_protocol.yaml" ; "Leten Protocol")]
#[test_case("test_loob_protocol.yaml" ; "Joyroid Loob Protocol")]
Expand Down Expand Up @@ -318,6 +320,7 @@ async fn test_device_protocols_json_v4(test_file: &str) {
#[test_case("test_lelo_f1sv1.yaml" ; "Lelo F1s V1 Protocol")]
#[test_case("test_lelo_f1sv2.yaml" ; "Lelo F1s V2 Protocol")]
#[test_case("test_lelo_idawave.yaml" ; "Lelo Harmony Protocol - Ida Wave")]
#[test_case("test_lelo_f1sv3_harmony.yaml" ; "Lelo Harmony Protocol - F1s V3")]
#[test_case("test_lelo_tianiharmony.yaml" ; "Lelo Harmony Protocol - Tiani Harmony")]
#[test_case("test_leten_protocol.yaml" ; "Leten Protocol")]
#[test_case("test_loob_protocol.yaml" ; "Joyroid Loob Protocol")]
Expand Down Expand Up @@ -445,6 +448,7 @@ async fn test_device_protocols_embedded_v3(test_file: &str) {
#[test_case("test_lelo_f1sv1.yaml" ; "Lelo F1s V1 Protocol")]
#[test_case("test_lelo_f1sv2.yaml" ; "Lelo F1s V2 Protocol")]
#[test_case("test_lelo_idawave.yaml" ; "Lelo Harmony Protocol - Ida Wave")]
#[test_case("test_lelo_f1sv3_harmony.yaml" ; "Lelo Harmony Protocol - F1s V3")]
#[test_case("test_lelo_tianiharmony.yaml" ; "Lelo Harmony Protocol - Tiani Harmony")]
#[test_case("test_leten_protocol.yaml" ; "Leten Protocol")]
#[test_case("test_loob_protocol.yaml" ; "Joyroid Loob Protocol")]
Expand Down Expand Up @@ -567,6 +571,7 @@ async fn test_device_protocols_json_v3(test_file: &str) {
//#[test_case("test_lelo_f1sv1.yaml" ; "Lelo F1s V1 Protocol")]
//#[test_case("test_lelo_f1sv2.yaml" ; "Lelo F1s V2 Protocol")]
#[test_case("test_lelo_idawave.yaml" ; "Lelo Harmony Protocol - Ida Wave")]
#[test_case("test_lelo_f1sv3_harmony.yaml" ; "Lelo Harmony Protocol - F1s V3")]
#[test_case("test_lelo_tianiharmony.yaml" ; "Lelo Harmony Protocol - Tiani Harmony")]
#[test_case("test_leten_protocol.yaml" ; "Leten Protocol")]
//#[test_case("test_longlosttouch_protocol.yaml" ; "LongLostTouch Protocol")]
Expand Down Expand Up @@ -688,6 +693,7 @@ async fn test_device_protocols_embedded_v2(test_file: &str) {
//#[test_case("test_lelo_f1sv1.yaml" ; "Lelo F1s V1 Protocol")]
//#[test_case("test_lelo_f1sv2.yaml" ; "Lelo F1s V2 Protocol")]
#[test_case("test_lelo_idawave.yaml" ; "Lelo Harmony Protocol - Ida Wave")]
#[test_case("test_lelo_f1sv3_harmony.yaml" ; "Lelo Harmony Protocol - F1s V3")]
#[test_case("test_lelo_tianiharmony.yaml" ; "Lelo Harmony Protocol - Tiani Harmony")]
#[test_case("test_leten_protocol.yaml" ; "Leten Protocol")]
//#[test_case("test_longlosttouch_protocol.yaml" ; "LongLostTouch Protocol")]
Expand Down Expand Up @@ -809,6 +815,7 @@ async fn test_device_protocols_json_v2(test_file: &str) {
//#[test_case("test_lelo_f1sv1.yaml" ; "Lelo F1s V1 Protocol")]
//#[test_case("test_lelo_f1sv2.yaml" ; "Lelo F1s V2 Protocol")]
#[test_case("test_lelo_idawave.yaml" ; "Lelo Harmony Protocol - Ida Wave")]
#[test_case("test_lelo_f1sv3_harmony.yaml" ; "Lelo Harmony Protocol - F1s V3")]
#[test_case("test_lelo_tianiharmony.yaml" ; "Lelo Harmony Protocol - Tiani Harmony")]
#[test_case("test_leten_protocol.yaml" ; "Leten Protocol")]
//#[test_case("test_longlosttouch_protocol.yaml" ; "LongLostTouch Protocol")]
Expand Down Expand Up @@ -929,6 +936,7 @@ async fn test_device_protocols_embedded_v1(test_file: &str) {
//#[test_case("test_lelo_f1sv1.yaml" ; "Lelo F1s V1 Protocol")]
//#[test_case("test_lelo_f1sv2.yaml" ; "Lelo F1s V2 Protocol")]
#[test_case("test_lelo_idawave.yaml" ; "Lelo Harmony Protocol - Ida Wave")]
#[test_case("test_lelo_f1sv3_harmony.yaml" ; "Lelo Harmony Protocol - F1s V3")]
#[test_case("test_lelo_tianiharmony.yaml" ; "Lelo Harmony Protocol - Tiani Harmony")]
#[test_case("test_leten_protocol.yaml" ; "Leten Protocol")]
//#[test_case("test_longlosttouch_protocol.yaml" ; "LongLostTouch Protocol")]
Expand Down Expand Up @@ -1042,6 +1050,7 @@ async fn test_device_protocols_json_v1(test_file: &str) {
#[test_case("test_hismith_wildolo.yaml" ; "Hismith Protocol - Wildolo")]
#[test_case("test_itoys_protocol.yaml" ; "iToys Protocol")]
#[test_case("test_lelo_idawave.yaml" ; "Lelo Harmony Protocol - Ida Wave")]
//#[test_case("test_lelo_f1sv3_harmony.yaml" ; "Lelo Harmony Protocol - F1s V3")]
//#[test_case("test_lelo_tianiharmony.yaml" ; "Lelo Harmony Protocol - Tiani Harmony")]
#[test_case("test_leten_protocol.yaml" ; "Leten Protocol")]
// loob excluded: top-level Linear
Expand Down Expand Up @@ -1113,6 +1122,7 @@ async fn test_device_protocols_embedded_v0(test_file: &str) {
#[test_case("test_hismith_wildolo.yaml" ; "Hismith Protocol - Wildolo")]
#[test_case("test_itoys_protocol.yaml" ; "iToys Protocol")]
#[test_case("test_lelo_idawave.yaml" ; "Lelo Harmony Protocol - Ida Wave")]
//#[test_case("test_lelo_f1sv3_harmony.yaml" ; "Lelo Harmony Protocol - F1s V3")]
//#[test_case("test_lelo_tianiharmony.yaml" ; "Lelo Harmony Protocol - Tiani Harmony")]
#[test_case("test_leten_protocol.yaml" ; "Leten Protocol")]
#[test_case("test_lovense_battery_non_default.yaml" ; "Lovense Protocol - Lovense Battery (Non-Default Devices)")]
Expand Down
Loading