Buttplug sex toy control library
at dev 22 kB view raw
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*/