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
8//! Implementations of communication protocols for hardware supported by Buttplug
9
10use buttplug_core::{
11 errors::ButtplugDeviceError,
12 message::{InputData, InputReadingV4, InputType, OutputCommand},
13};
14use buttplug_server_device_config::{
15 Endpoint,
16 ProtocolCommunicationSpecifier,
17 ServerDeviceDefinition,
18 UserDeviceIdentifier,
19};
20use dashmap::DashMap;
21
22use super::hardware::HardwareWriteCmd;
23use crate::{
24 device::{
25 hardware::{Hardware, HardwareCommand, HardwareReadCmd},
26 protocol_impl::get_default_protocol_map,
27 },
28 message::{
29 ButtplugServerDeviceMessage,
30 checked_output_cmd::CheckedOutputCmdV4,
31 spec_enums::ButtplugDeviceCommandMessageUnionV4,
32 },
33};
34use async_trait::async_trait;
35use futures::{
36 StreamExt,
37 future::{self, BoxFuture, FutureExt},
38};
39use std::{collections::HashMap, sync::Arc};
40use std::{pin::Pin, time::Duration};
41use uuid::Uuid;
42
43/// Strategy for situations where hardware needs to get updates every so often in order to keep
44/// things alive. Currently this applies to iOS backgrounding with bluetooth devices, as well as
45/// some protocols like Satisfyer and Mysteryvibe that need constant command refreshing, but since
46/// we never know which of our hundreds of supported devices someone might connect, we need context
47/// as to which keepalive strategy to use.
48///
49/// When choosing a keepalive strategy for a protocol:
50///
51/// - If the protocol has a command that essentially does nothing to the actuators, set up
52/// RepeatPacketStrategy to use that. This is useful for devices that have info commands (like
53/// Lovense), ping commands (like The Handy), sensor commands that aren't yet subscribed to output
54/// notifications, etc...
55/// - If a protocol needs specific timing or keepalives, regardless of the OS/hardware manager being
56/// used, like Satisfyer or Mysteryvibe, use RepeatLastPacketStrategyWithTiming.
57/// - For many devices with only scalar actuators, RepeatLastPacketStrategy should work. You just
58/// need to make sure the protocol doesn't have a packet counter or something else that will trip
59/// if the same packet is replayed multiple times.
60#[derive(Debug)]
61pub enum ProtocolKeepaliveStrategy {
62 /// Repeat a specific packet, such as a ping or a no-op. Only do this when the hardware manager
63 /// requires it (currently only iOS bluetooth during backgrounding).
64 HardwareRequiredRepeatPacketStrategy(HardwareWriteCmd),
65 /// Repeat whatever the last packet sent was, and send Stop commands until first packet sent. Uses
66 /// a default timing, suitable for most protocols that don't need constant device updates outside
67 /// of OS requirements. Only do this when the hardware manager requires it (currently only iOS
68 /// bluetooth during backgrounding).
69 HardwareRequiredRepeatLastPacketStrategy,
70 /// Repeat whatever the last packet sent was, and send Stop commands until first packet sent. Do
71 /// this regardless of whether or not the hardware manager requires it. Useful for hardware that
72 /// requires keepalives, like Satisfyer, Mysteryvibe, Leten, etc...
73 RepeatLastPacketStrategyWithTiming(Duration),
74}
75
76pub trait ProtocolIdentifierFactory: Send + Sync {
77 fn identifier(&self) -> &str;
78 fn create(&self) -> Box<dyn ProtocolIdentifier>;
79}
80
81pub enum ProtocolValueCommandPrefilterStrategy {
82 /// Drop repeated ValueCmd/ValueWithParameterCmd messages
83 DropRepeats,
84 /// No filter, send all value messages for processing
85 None,
86}
87
88fn print_type_of<T>(_: &T) -> &'static str {
89 std::any::type_name::<T>()
90}
91
92pub struct ProtocolSpecializer {
93 specifiers: Vec<ProtocolCommunicationSpecifier>,
94 identifier: Box<dyn ProtocolIdentifier>,
95}
96
97impl ProtocolSpecializer {
98 pub fn new(
99 specifiers: Vec<ProtocolCommunicationSpecifier>,
100 identifier: Box<dyn ProtocolIdentifier>,
101 ) -> Self {
102 Self {
103 specifiers,
104 identifier,
105 }
106 }
107
108 pub fn specifiers(&self) -> &Vec<ProtocolCommunicationSpecifier> {
109 &self.specifiers
110 }
111
112 pub fn identify(self) -> Box<dyn ProtocolIdentifier> {
113 self.identifier
114 }
115}
116
117#[async_trait]
118pub trait ProtocolIdentifier: Sync + Send {
119 async fn identify(
120 &mut self,
121 hardware: Arc<Hardware>,
122 specifier: ProtocolCommunicationSpecifier,
123 ) -> Result<(UserDeviceIdentifier, Box<dyn ProtocolInitializer>), ButtplugDeviceError>;
124}
125
126#[async_trait]
127pub trait ProtocolInitializer: Sync + Send {
128 async fn initialize(
129 &mut self,
130 hardware: Arc<Hardware>,
131 device_definition: &ServerDeviceDefinition,
132 ) -> Result<Arc<dyn ProtocolHandler>, ButtplugDeviceError>;
133}
134
135pub struct GenericProtocolIdentifier {
136 handler: Option<Arc<dyn ProtocolHandler>>,
137 protocol_identifier: String,
138}
139
140impl GenericProtocolIdentifier {
141 pub fn new(handler: Arc<dyn ProtocolHandler>, protocol_identifier: &str) -> Self {
142 Self {
143 handler: Some(handler),
144 protocol_identifier: protocol_identifier.to_owned(),
145 }
146 }
147}
148
149#[async_trait]
150impl ProtocolIdentifier for GenericProtocolIdentifier {
151 async fn identify(
152 &mut self,
153 hardware: Arc<Hardware>,
154 _: ProtocolCommunicationSpecifier,
155 ) -> Result<(UserDeviceIdentifier, Box<dyn ProtocolInitializer>), ButtplugDeviceError> {
156 let device_identifier = UserDeviceIdentifier::new(
157 hardware.address(),
158 &self.protocol_identifier,
159 &Some(hardware.name().to_owned()),
160 );
161 Ok((
162 device_identifier,
163 Box::new(GenericProtocolInitializer::new(
164 self.handler.take().unwrap(),
165 )),
166 ))
167 }
168}
169
170pub struct GenericProtocolInitializer {
171 handler: Option<Arc<dyn ProtocolHandler>>,
172}
173
174impl GenericProtocolInitializer {
175 pub fn new(handler: Arc<dyn ProtocolHandler>) -> Self {
176 Self {
177 handler: Some(handler),
178 }
179 }
180}
181
182#[async_trait]
183impl ProtocolInitializer for GenericProtocolInitializer {
184 async fn initialize(
185 &mut self,
186 _: Arc<Hardware>,
187 _: &ServerDeviceDefinition,
188 ) -> Result<Arc<dyn ProtocolHandler>, ButtplugDeviceError> {
189 Ok(self.handler.take().unwrap())
190 }
191}
192
193pub trait ProtocolHandler: Sync + Send {
194 fn keepalive_strategy(&self) -> ProtocolKeepaliveStrategy {
195 ProtocolKeepaliveStrategy::HardwareRequiredRepeatLastPacketStrategy
196 }
197
198 fn handle_message(
199 &self,
200 message: &ButtplugDeviceCommandMessageUnionV4,
201 ) -> Result<Vec<HardwareCommand>, ButtplugDeviceError> {
202 self.command_unimplemented(print_type_of(&message))
203 }
204
205 // Allow here since this changes between debug/release
206 #[allow(unused_variables)]
207 fn command_unimplemented(
208 &self,
209 command: &str,
210 ) -> Result<Vec<HardwareCommand>, ButtplugDeviceError> {
211 #[cfg(debug_assertions)]
212 unimplemented!("Command not implemented for this protocol");
213 #[cfg(not(debug_assertions))]
214 Err(ButtplugDeviceError::UnhandledCommand(format!(
215 "Command not implemented for this protocol: {}",
216 command
217 )))
218 }
219
220 // The default scalar handler assumes that most devices require discrete commands per feature. If
221 // a protocol has commands that combine multiple features, either with matched or unmatched
222 // actuators, they should just implement their own version of this method.
223 fn handle_output_cmd(
224 &self,
225 cmd: &CheckedOutputCmdV4,
226 ) -> Result<Vec<HardwareCommand>, ButtplugDeviceError> {
227 let output_command = cmd.output_command();
228 match output_command {
229 OutputCommand::Constrict(x) => self.handle_output_constrict_cmd(
230 cmd.feature_index(),
231 cmd.feature_id(),
232 x.value()
233 .try_into()
234 .map_err(|_| ButtplugDeviceError::DeviceCommandSignError)?,
235 ),
236 OutputCommand::Spray(x) => self.handle_output_spray_cmd(
237 cmd.feature_index(),
238 cmd.feature_id(),
239 x.value()
240 .try_into()
241 .map_err(|_| ButtplugDeviceError::DeviceCommandSignError)?,
242 ),
243 OutputCommand::Oscillate(x) => self.handle_output_oscillate_cmd(
244 cmd.feature_index(),
245 cmd.feature_id(),
246 x.value()
247 .try_into()
248 .map_err(|_| ButtplugDeviceError::DeviceCommandSignError)?,
249 ),
250 OutputCommand::Rotate(x) => {
251 self.handle_output_rotate_cmd(cmd.feature_index(), cmd.feature_id(), x.value())
252 }
253 OutputCommand::Vibrate(x) => self.handle_output_vibrate_cmd(
254 cmd.feature_index(),
255 cmd.feature_id(),
256 x.value()
257 .try_into()
258 .map_err(|_| ButtplugDeviceError::DeviceCommandSignError)?,
259 ),
260 OutputCommand::Position(x) => self.handle_output_position_cmd(
261 cmd.feature_index(),
262 cmd.feature_id(),
263 x.value()
264 .try_into()
265 .map_err(|_| ButtplugDeviceError::DeviceCommandSignError)?,
266 ),
267 OutputCommand::Temperature(x) => self.handle_output_temperature_cmd(
268 cmd.feature_index(),
269 cmd.feature_id(),
270 x.value()
271 ),
272 OutputCommand::Led(x) => self.handle_output_led_cmd(
273 cmd.feature_index(),
274 cmd.feature_id(),
275 x.value()
276 .try_into()
277 .map_err(|_| ButtplugDeviceError::DeviceCommandSignError)?,
278 ),
279 OutputCommand::PositionWithDuration(x) => self.handle_position_with_duration_cmd(
280 cmd.feature_index(),
281 cmd.feature_id(),
282 x.position(),
283 x.duration(),
284 ),
285 }
286 }
287
288 fn handle_output_vibrate_cmd(
289 &self,
290 _feature_index: u32,
291 _feature_id: Uuid,
292 _speed: u32,
293 ) -> Result<Vec<HardwareCommand>, ButtplugDeviceError> {
294 self.command_unimplemented("OutputCmd (Vibrate Actuator)")
295 }
296
297 fn handle_output_rotate_cmd(
298 &self,
299 _feature_index: u32,
300 _feature_id: Uuid,
301 _speed: i32,
302 ) -> Result<Vec<HardwareCommand>, ButtplugDeviceError> {
303 self.command_unimplemented("OutputCmd (Rotate Actuator)")
304 }
305
306 fn handle_output_oscillate_cmd(
307 &self,
308 _feature_index: u32,
309 _feature_id: Uuid,
310 _speed: u32,
311 ) -> Result<Vec<HardwareCommand>, ButtplugDeviceError> {
312 self.command_unimplemented("OutputCmd (Oscillate Actuator)")
313 }
314
315 fn handle_output_spray_cmd(
316 &self,
317 _feature_index: u32,
318 _feature_id: Uuid,
319 _level: u32,
320 ) -> Result<Vec<HardwareCommand>, ButtplugDeviceError> {
321 self.command_unimplemented("OutputCmd (Spray Actuator)")
322 }
323
324 fn handle_output_constrict_cmd(
325 &self,
326 _feature_index: u32,
327 _feature_id: Uuid,
328 _level: u32,
329 ) -> Result<Vec<HardwareCommand>, ButtplugDeviceError> {
330 self.command_unimplemented("OutputCmd (Constrict Actuator)")
331 }
332
333 fn handle_output_temperature_cmd(
334 &self,
335 _feature_index: u32,
336 _feature_id: Uuid,
337 _level: i32,
338 ) -> Result<Vec<HardwareCommand>, ButtplugDeviceError> {
339 self.command_unimplemented("OutputCmd (Temperature Actuator)")
340 }
341
342 fn handle_output_led_cmd(
343 &self,
344 _feature_index: u32,
345 _feature_id: Uuid,
346 _level: u32,
347 ) -> Result<Vec<HardwareCommand>, ButtplugDeviceError> {
348 self.command_unimplemented("OutputCmd (Led Actuator)")
349 }
350
351 fn handle_output_position_cmd(
352 &self,
353 _feature_index: u32,
354 _feature_id: Uuid,
355 _position: u32,
356 ) -> Result<Vec<HardwareCommand>, ButtplugDeviceError> {
357 self.command_unimplemented("OutputCmd (Position Actuator)")
358 }
359
360 fn handle_position_with_duration_cmd(
361 &self,
362 _feature_index: u32,
363 _feature_id: Uuid,
364 _position: u32,
365 _duration: u32,
366 ) -> Result<Vec<HardwareCommand>, ButtplugDeviceError> {
367 self.command_unimplemented("OutputCmd (Position w/ Duration Actuator)")
368 }
369
370 fn handle_input_subscribe_cmd(
371 &self,
372 _device_index: u32,
373 _device: Arc<Hardware>,
374 _feature_index: u32,
375 _feature_id: Uuid,
376 _sensor_type: InputType,
377 ) -> BoxFuture<'_, Result<(), ButtplugDeviceError>> {
378 future::ready(Err(ButtplugDeviceError::UnhandledCommand(
379 "Command not implemented for this protocol: InputCmd (Subscribe)".to_string(),
380 )))
381 .boxed()
382 }
383
384 fn handle_input_unsubscribe_cmd(
385 &self,
386 _device: Arc<Hardware>,
387 _feature_index: u32,
388 _feature_id: Uuid,
389 _sensor_type: InputType,
390 ) -> BoxFuture<'_, Result<(), ButtplugDeviceError>> {
391 future::ready(Err(ButtplugDeviceError::UnhandledCommand(
392 "Command not implemented for this protocol: InputCmd (Unsubscribe)".to_string(),
393 )))
394 .boxed()
395 }
396
397 fn handle_input_read_cmd(
398 &self,
399 device_index: u32,
400 device: Arc<Hardware>,
401 feature_index: u32,
402 feature_id: Uuid,
403 sensor_type: InputType,
404 ) -> BoxFuture<'_, Result<InputReadingV4, ButtplugDeviceError>> {
405 match sensor_type {
406 InputType::Battery => {
407 self.handle_battery_level_cmd(device_index, device, feature_index, feature_id)
408 }
409 _ => future::ready(Err(ButtplugDeviceError::UnhandledCommand(
410 "Command not implemented for this protocol: InputCmd (Read)".to_string(),
411 )))
412 .boxed(),
413 }
414 }
415
416 // Handle Battery Level returns a SensorReading, as we'll always need to do a sensor index
417 // conversion on it.
418 fn handle_battery_level_cmd(
419 &self,
420 device_index: u32,
421 device: Arc<Hardware>,
422 feature_index: u32,
423 feature_id: Uuid,
424 ) -> BoxFuture<'_, Result<InputReadingV4, ButtplugDeviceError>> {
425 // If we have a standardized BLE Battery endpoint, handle that above the
426 // protocol, as it'll always be the same.
427 if device.endpoints().contains(&Endpoint::RxBLEBattery) {
428 debug!("Trying to get battery reading.");
429 let msg = HardwareReadCmd::new(feature_id, Endpoint::RxBLEBattery, 1, 0);
430 let fut = device.read_value(&msg);
431 async move {
432 let hw_msg = fut.await?;
433 let battery_level = hw_msg.data()[0] as i32;
434 let battery_reading = InputReadingV4::new(
435 device_index,
436 feature_index,
437 buttplug_core::message::InputTypeData::Battery(InputData::new(battery_level as u8)),
438 );
439 debug!("Got battery reading: {}", battery_level);
440 Ok(battery_reading)
441 }
442 .boxed()
443 } else {
444 future::ready(Err(ButtplugDeviceError::UnhandledCommand(
445 "Command not implemented for this protocol: SensorReadCmd".to_string(),
446 )))
447 .boxed()
448 }
449 }
450
451 fn handle_rssi_level_cmd(
452 &self,
453 _device: Arc<Hardware>,
454 _feature_index: u32,
455 _feature_id: Uuid,
456 ) -> BoxFuture<'_, Result<(), ButtplugDeviceError>> {
457 future::ready(Err(ButtplugDeviceError::UnhandledCommand(
458 "Command not implemented for this protocol: SensorReadCmd".to_string(),
459 )))
460 .boxed()
461 }
462
463 fn event_stream(
464 &self,
465 ) -> Pin<Box<dyn tokio_stream::Stream<Item = ButtplugServerDeviceMessage> + Send>> {
466 tokio_stream::empty().boxed()
467 }
468}
469
470#[macro_export]
471macro_rules! generic_protocol_setup {
472 ( $protocol_name:ident, $protocol_identifier:tt) => {
473 paste::paste! {
474 pub mod setup {
475 use std::sync::Arc;
476 use $crate::device::protocol::{
477 GenericProtocolIdentifier, ProtocolIdentifier, ProtocolIdentifierFactory,
478 };
479 #[derive(Default)]
480 pub struct [< $protocol_name IdentifierFactory >] {}
481
482 impl ProtocolIdentifierFactory for [< $protocol_name IdentifierFactory >] {
483 fn identifier(&self) -> &str {
484 $protocol_identifier
485 }
486
487 fn create(&self) -> Box<dyn ProtocolIdentifier> {
488 Box::new(GenericProtocolIdentifier::new(
489 Arc::new(super::$protocol_name::default()),
490 self.identifier(),
491 ))
492 }
493 }
494 }
495 }
496 };
497}
498
499#[macro_export]
500macro_rules! generic_protocol_initializer_setup {
501 ( $protocol_name:ident, $protocol_identifier:tt) => {
502 paste::paste! {
503 pub mod setup {
504 use $crate::device::protocol::{ProtocolIdentifier, ProtocolIdentifierFactory};
505 #[derive(Default)]
506 pub struct [< $protocol_name IdentifierFactory >] {}
507
508 impl ProtocolIdentifierFactory for [< $protocol_name IdentifierFactory >] {
509 fn identifier(&self) -> &str {
510 $protocol_identifier
511 }
512
513 fn create(&self) -> Box<dyn ProtocolIdentifier> {
514 Box::new(super::[< $protocol_name Identifier >]::default())
515 }
516 }
517 }
518
519 #[derive(Default)]
520 pub struct [< $protocol_name Identifier >] {}
521
522 #[async_trait]
523 impl ProtocolIdentifier for [< $protocol_name Identifier >] {
524 async fn identify(
525 &mut self,
526 hardware: Arc<Hardware>,
527 _: ProtocolCommunicationSpecifier,
528 ) -> Result<(UserDeviceIdentifier, Box<dyn ProtocolInitializer>), ButtplugDeviceError> {
529 Ok((UserDeviceIdentifier::new(hardware.address(), $protocol_identifier, &Some(hardware.name().to_owned())), Box::new([< $protocol_name Initializer >]::default())))
530 }
531 }
532 }
533 };
534}
535
536pub use generic_protocol_initializer_setup;
537pub use generic_protocol_setup;
538
539pub struct ProtocolManager {
540 // Map of protocol names to their respective protocol instance factories
541 protocol_map: HashMap<String, Arc<dyn ProtocolIdentifierFactory>>,
542}
543
544impl Default for ProtocolManager {
545 fn default() -> Self {
546 Self {
547 protocol_map: get_default_protocol_map(),
548 }
549 }
550}
551
552impl ProtocolManager {
553 pub fn protocol_specializers(
554 &self,
555 specifier: &ProtocolCommunicationSpecifier,
556 base_communication_specifiers: &HashMap<String, Vec<ProtocolCommunicationSpecifier>>,
557 user_communication_specifiers: &DashMap<String, Vec<ProtocolCommunicationSpecifier>>,
558 ) -> Vec<ProtocolSpecializer> {
559 debug!(
560 "Looking for protocol that matches specifier: {:?}",
561 specifier
562 );
563 let mut specializers = vec![];
564 let mut update_specializer_map =
565 |name: &str, specifiers: &Vec<ProtocolCommunicationSpecifier>| {
566 if specifiers.contains(specifier) {
567 info!(
568 "Found protocol {:?} for user specifier {:?}.",
569 name, specifier
570 );
571 if self.protocol_map.contains_key(name) {
572 specializers.push(ProtocolSpecializer::new(
573 specifiers.clone(),
574 self
575 .protocol_map
576 .get(name)
577 .expect("already checked existence")
578 .create(),
579 ));
580 } else {
581 warn!(
582 "No protocol implementation for {:?} found for specifier {:?}.",
583 name, specifier
584 );
585 }
586 }
587 };
588 // Loop through both maps, as chaining between DashMap and HashMap gets kinda gross.
589 for spec in user_communication_specifiers.iter() {
590 update_specializer_map(spec.key(), spec.value());
591 }
592 for (name, specifiers) in base_communication_specifiers.iter() {
593 update_specializer_map(name, specifiers);
594 }
595 specializers
596 }
597}
598
599/*
600#[cfg(test)]
601mod test {
602 use super::*;
603 use crate::{
604 core::message::{OutputType, FeatureType},
605 server::message::server_device_feature::{ServerDeviceFeature, ServerDeviceFeatureOutput},
606 };
607 use std::{
608 collections::{HashMap, HashSet},
609 ops::RangeInclusive,
610 };
611
612 fn create_unit_test_dcm() -> DeviceConfigurationManager {
613 let mut builder = DeviceConfigurationManagerBuilder::default();
614 let specifiers = ProtocolCommunicationSpecifier::BluetoothLE(BluetoothLESpecifier::new(
615 HashSet::from(["LVS-*".to_owned(), "LovenseDummyTestName".to_owned()]),
616 vec![],
617 HashSet::new(),
618 HashMap::new(),
619 ));
620 let mut feature_actuator = HashMap::new();
621 feature_actuator.insert(
622 OutputType::Vibrate,
623 ServerDeviceFeatureOutput::new(&RangeInclusive::new(0, 20), &RangeInclusive::new(0, 20)),
624 );
625 builder
626 .communication_specifier("lovense", &[specifiers])
627 .protocol_features(
628 &BaseDeviceIdentifier::new("lovense", &Some("P".to_owned())),
629 &BaseDeviceDefinition::new(
630 "Lovense Edge",
631 &uuid::Uuid::new_v4(),
632 &None,
633 &vec![
634 ServerDeviceFeature::new(
635 "Edge Vibration 1",
636 &uuid::Uuid::new_v4(),
637 &None,
638 FeatureType::Vibrate,
639 &Some(feature_actuator.clone()),
640 &None,
641 ),
642 ServerDeviceFeature::new(
643 "Edge Vibration 2",
644 &uuid::Uuid::new_v4(),
645 &None,
646 FeatureType::Vibrate,
647 &Some(feature_actuator.clone()),
648 &None,
649 ),
650 ],
651 &None
652 ),
653 )
654 .finish()
655 .unwrap()
656 }
657
658 #[test]
659 fn test_config_equals() {
660 let config = create_unit_test_dcm();
661 let spec = ProtocolCommunicationSpecifier::BluetoothLE(BluetoothLESpecifier::new_from_device(
662 "LVS-Something",
663 &HashMap::new(),
664 &[],
665 ));
666 assert!(!config.protocol_specializers(&spec).is_empty());
667 }
668
669 #[test]
670 fn test_config_wildcard_equals() {
671 let config = create_unit_test_dcm();
672 let spec = ProtocolCommunicationSpecifier::BluetoothLE(BluetoothLESpecifier::new_from_device(
673 "LVS-Whatever",
674 &HashMap::new(),
675 &[],
676 ));
677 assert!(!config.protocol_specializers(&spec).is_empty());
678 }
679 /*
680 #[test]
681 fn test_specific_device_config_creation() {
682 let dcm = create_unit_test_dcm(false);
683 let spec = ProtocolCommunicationSpecifier::BluetoothLE(BluetoothLESpecifier::new_from_device(
684 "LVS-Whatever",
685 &HashMap::new(),
686 &[],
687 ));
688 assert!(!dcm.protocol_specializers(&spec).is_empty());
689 let config: ProtocolDeviceAttributes = dcm
690 .device_definition(
691 &UserDeviceIdentifier::new("Whatever", "lovense", &Some("P".to_owned())),
692 &[],
693 )
694 .expect("Should be found")
695 .into();
696 // Make sure we got the right name
697 assert_eq!(config.name(), "Lovense Edge");
698 // Make sure we overwrote the default of 1
699 assert_eq!(
700 config
701 .message_attributes()
702 .scalar_cmd()
703 .as_ref()
704 .expect("Test, assuming infallible")
705 .get(0)
706 .expect("Test, assuming infallible")
707 .step_count(),
708 20
709 );
710 }
711 */
712}
713*/