diff --git a/crates/buttplug_client/src/device/device.rs b/crates/buttplug_client/src/device/device.rs index 9fc5798ff..b13f139ff 100644 --- a/crates/buttplug_client/src/device/device.rs +++ b/crates/buttplug_client/src/device/device.rs @@ -152,15 +152,8 @@ impl ButtplugClientDevice { fn filter_device_outputs(&self, actuator_type: OutputType) -> Vec { self .device_features - .iter() - .filter(|x| { - if let Some(output) = x.1.feature().output() { - output.contains(actuator_type) - } else { - false - } - }) - .map(|(_, x)| x) + .values() + .filter(|x| x.feature().contains_output(actuator_type)) .cloned() .collect() } @@ -204,13 +197,10 @@ impl ButtplugClientDevice { } pub fn input_available(&self, input_type: InputType) -> bool { - self.device_features.iter().any(|x| { - x.1 - .feature() - .input() - .as_ref() - .is_some_and(|x| x.contains(input_type)) - }) + self + .device_features + .values() + .any(|x| x.feature().contains_input(input_type)) } fn input_feature( @@ -219,14 +209,8 @@ impl ButtplugClientDevice { ) -> Result<&ClientDeviceFeature, ButtplugClientError> { let inputs: Vec<_> = self .device_features - .iter() - .filter(|x| { - x.1 - .feature() - .input() - .as_ref() - .is_some_and(|x| x.contains(input_type)) - }) + .values() + .filter(|x| x.feature().contains_input(input_type)) .collect(); let input_count = inputs.len(); if input_count > 1 { @@ -238,7 +222,7 @@ impl ButtplugClientDevice { ButtplugDeviceError::DeviceNoInputError(input_type).into(), )) } else { - Ok(inputs[0].1) + Ok(inputs[0]) } } diff --git a/crates/buttplug_client/src/device/feature.rs b/crates/buttplug_client/src/device/feature.rs index b805143a0..3278e8861 100644 --- a/crates/buttplug_client/src/device/feature.rs +++ b/crates/buttplug_client/src/device/feature.rs @@ -76,7 +76,7 @@ impl ClientDeviceFeature { ClientDeviceCommandValue::Percent(f) => self.convert_float_value(feature_output, *f)?, ClientDeviceCommandValue::Steps(i) => *i, }; - if feature_output.step_limit().contains(&value) { + if feature_output.step_limit().contains(value) { Ok(value) } else { Err(ButtplugClientError::ButtplugOutputCommandConversionError( @@ -118,23 +118,12 @@ impl ClientDeviceFeature { ) -> Result { let output_type: OutputType = client_cmd.into(); // First off, make sure we support this output. - let output = self - .feature - .output() - .as_ref() - .ok_or(ButtplugClientError::ButtplugOutputCommandConversionError( - format!( - "Device feature does not support output type {}", - output_type - ), - ))? - .get(output_type) - .ok_or(ButtplugClientError::ButtplugOutputCommandConversionError( - format!( - "Device feature does not support output type {}", - output_type - ), - ))?; + let output = self.feature.get_output_limits(output_type).ok_or( + ButtplugClientError::ButtplugOutputCommandConversionError(format!( + "Device feature does not support output type {}", + output_type + )), + )?; let output_cmd = match client_cmd { ClientDeviceOutputCommand::Vibrate(v) => { @@ -186,9 +175,8 @@ impl ClientDeviceFeature { } pub fn run_input_subscribe(&self, sensor_type: InputType) -> ButtplugClientResultFuture { - if let Some(sensor_map) = self.feature.input() - && let Some(sensor) = sensor_map.get(sensor_type) - && sensor.command().contains(&InputCommandType::Subscribe) + if let Some(sensor) = self.feature.get_input(sensor_type) + && sensor.command().contains(InputCommandType::Subscribe) { let msg = InputCmdV4::new( self.device_index, @@ -206,9 +194,8 @@ impl ClientDeviceFeature { } pub fn run_input_unsubscribe(&self, sensor_type: InputType) -> ButtplugClientResultFuture { - if let Some(sensor_map) = self.feature.input() - && let Some(sensor) = sensor_map.get(sensor_type) - && sensor.command().contains(&InputCommandType::Subscribe) + if let Some(sensor) = self.feature.get_input(sensor_type) + && sensor.command().contains(InputCommandType::Subscribe) { let msg = InputCmdV4::new( self.device_index, @@ -229,9 +216,8 @@ impl ClientDeviceFeature { &self, sensor_type: InputType, ) -> ButtplugClientResultFuture { - if let Some(sensor_map) = self.feature.input() - && let Some(sensor) = sensor_map.get(sensor_type) - && sensor.command().contains(&InputCommandType::Read) + if let Some(sensor) = self.feature.get_input(sensor_type) + && sensor.command().contains(InputCommandType::Read) { let msg = InputCmdV4::new( self.device_index, @@ -271,14 +257,7 @@ impl ClientDeviceFeature { } pub fn battery(&self) -> ButtplugClientResultFuture { - if self - .feature() - .input() - .as_ref() - .ok_or(false) - .unwrap() - .contains(InputType::Battery) - { + if self.feature().contains_input(InputType::Battery) { let send_fut = self.run_input_read(InputType::Battery); Box::pin(async move { let data = send_fut.await?; @@ -298,14 +277,7 @@ impl ClientDeviceFeature { } pub fn rssi(&self) -> ButtplugClientResultFuture { - if self - .feature() - .input() - .as_ref() - .ok_or(false) - .unwrap() - .contains(InputType::Rssi) - { + if self.feature().contains_input(InputType::Rssi) { let send_fut = self.run_input_read(InputType::Rssi); Box::pin(async move { let data = send_fut.await?; diff --git a/crates/buttplug_core/Cargo.toml b/crates/buttplug_core/Cargo.toml index d43e55229..22e2b8859 100644 --- a/crates/buttplug_core/Cargo.toml +++ b/crates/buttplug_core/Cargo.toml @@ -55,4 +55,5 @@ enum_dispatch = "0.3" tracing = "0.1.44" wasm-bindgen-futures = { version = "0.4.64", optional = true } wasmtimer = { version = "0.4.3", optional = true } - +smallvec = { version = "1.15.1", features = ["serde", "const_generics"] } +enumflags2 = "0.7.12" diff --git a/crates/buttplug_core/src/message/device_feature.rs b/crates/buttplug_core/src/message/device_feature.rs index 94658f45b..10a7208f4 100644 --- a/crates/buttplug_core/src/message/device_feature.rs +++ b/crates/buttplug_core/src/message/device_feature.rs @@ -5,61 +5,16 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{message::InputCommandType, util::range_serialize::*}; -use derive_builder::Builder; +use crate::{ + message::InputCommandType, + util::{ + range::RangeInclusive, + small_vec_enum_map::{SmallVecEnumMap, VariantKey}, + }, +}; +use enumflags2::BitFlags; use getset::{CopyGetters, Getters, MutGetters, Setters}; -use serde::{Deserialize, Serialize, Serializer, ser::SerializeSeq}; -use std::{collections::HashSet, hash::Hash, ops::RangeInclusive}; - -#[derive( - Debug, Display, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, EnumIter, EnumString, -)] -pub enum OutputType { - Unknown, - #[serde(alias = "vibrate")] - Vibrate, - #[serde(alias = "rotate")] - Rotate, - #[serde(alias = "oscillate")] - Oscillate, - #[serde(alias = "constrict")] - Constrict, - #[serde(alias = "temperature")] - Temperature, - #[serde(alias = "led")] - Led, - // For instances where we specify a position to move to ASAP. Usually servos, probably for the - // OSR-2/SR-6. - #[serde(alias = "position")] - Position, - #[serde(alias = "hw_position_with_duration")] - HwPositionWithDuration, - // Lube shooters - #[serde(alias = "spray")] - Spray, - // Things we might add in the future - // Inflate, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, Hash, EnumIter)] -pub enum InputType { - Unknown, - #[serde(alias = "battery")] - Battery, - #[serde(alias = "rssi")] - Rssi, - #[serde(alias = "button")] - Button, - #[serde(alias = "pressure")] - Pressure, - #[serde(alias = "depth")] - Depth, - #[serde(alias = "position")] - Position, - // Temperature, - // Accelerometer, - // Gyro, -} +use serde::{Deserialize, Serialize}; // This will look almost exactly like ServerDeviceFeature. However, it will only contain // information we want the client to know, i.e. step counts versus specific step ranges. This is @@ -69,6 +24,11 @@ pub enum InputType { // For many messages, client and server configurations may be exactly the same. If they are not, // then we denote this by prefixing the type with Client/Server. Server attributes will usually be // hosted in the server/device/configuration module. +// +// SERIALIZATION NOTE: This type and its children use PascalCase field names because they are part +// of the wire protocol (DeviceAdded/DeviceList messages). Internal server-side types +// (ServerDeviceFeature and friends) use snake_case. Never use DeviceFeature for internal +// storage or config files. #[derive( Clone, Debug, Default, Getters, MutGetters, CopyGetters, Setters, Serialize, Deserialize, )] @@ -82,21 +42,18 @@ pub struct DeviceFeature { #[getset(get = "pub", get_mut = "pub(super)")] #[serde(default, rename = "FeatureDescription")] description: String, - // TODO Maybe make this its own object instead of a HashMap? - #[getset(get = "pub")] - #[serde(skip_serializing_if = "Option::is_none")] - output: Option, - #[getset(get = "pub")] - #[serde(skip_serializing_if = "Option::is_none")] - input: Option, + #[serde(skip_serializing_if = "SmallVecEnumMap::is_empty", default)] + output: SmallVecEnumMap, + #[serde(skip_serializing_if = "SmallVecEnumMap::is_empty", default)] + input: SmallVecEnumMap, } impl DeviceFeature { pub fn new( index: u32, description: &str, - output: &Option, - input: &Option, + output: &SmallVecEnumMap, + input: &SmallVecEnumMap, ) -> Self { Self { feature_index: index, @@ -105,20 +62,29 @@ impl DeviceFeature { input: input.clone(), } } -} -fn range_sequence_serialize( - range_vec: &Vec>, - serializer: S, -) -> Result -where - S: Serializer, -{ - let mut seq = serializer.serialize_seq(Some(range_vec.len()))?; - for range in range_vec { - seq.serialize_element(&vec![*range.start(), *range.end()])?; + pub fn contains_output(&self, output_type: OutputType) -> bool { + self.output.contains_key(&output_type) + } + + pub fn contains_input(&self, input_type: InputType) -> bool { + self.input.contains_key(&input_type) + } + + pub fn get_output(&self, output_type: OutputType) -> Option<&DeviceFeatureOutput> { + self.output.find_by_key(&output_type) + } + + pub fn get_output_limits( + &self, + output_type: OutputType, + ) -> Option<&dyn DeviceFeatureOutputLimits> { + self.output.find_by_key(&output_type).map(|o| o.as_limits()) + } + + pub fn get_input(&self, input_type: InputType) -> Option<&DeviceFeatureInput> { + self.input.find_by_key(&input_type) } - seq.end() } pub trait DeviceFeatureOutputLimits { @@ -130,28 +96,21 @@ pub trait DeviceFeatureOutputLimits { #[serde(rename_all = "PascalCase")] pub struct DeviceFeatureOutputValueProperties { #[getset(get = "pub")] - #[serde(serialize_with = "range_serialize")] value: RangeInclusive, } impl DeviceFeatureOutputValueProperties { - pub fn new(value: &RangeInclusive) -> Self { - DeviceFeatureOutputValueProperties { - value: value.clone(), - } - } - - pub fn step_count(&self) -> u32 { - *self.value.end() as u32 + pub fn new(value: RangeInclusive) -> Self { + DeviceFeatureOutputValueProperties { value } } } impl DeviceFeatureOutputLimits for DeviceFeatureOutputValueProperties { fn step_count(&self) -> u32 { - self.step_count() + self.value.end() as u32 } fn step_limit(&self) -> RangeInclusive { - self.value.clone() + self.value } } @@ -159,115 +118,90 @@ impl DeviceFeatureOutputLimits for DeviceFeatureOutputValueProperties { #[serde(rename_all = "PascalCase")] pub struct DeviceFeatureOutputHwPositionWithDurationProperties { #[getset(get = "pub")] - #[serde(serialize_with = "range_serialize")] value: RangeInclusive, #[getset(get = "pub")] - #[serde(serialize_with = "range_serialize")] duration: RangeInclusive, } impl DeviceFeatureOutputHwPositionWithDurationProperties { - pub fn new(position: &RangeInclusive, duration: &RangeInclusive) -> Self { + pub fn new(position: RangeInclusive, duration: RangeInclusive) -> Self { DeviceFeatureOutputHwPositionWithDurationProperties { - value: position.clone(), - duration: duration.clone(), + value: position, + duration, } } - - pub fn step_count(&self) -> u32 { - *self.value.end() as u32 - } } impl DeviceFeatureOutputLimits for DeviceFeatureOutputHwPositionWithDurationProperties { fn step_count(&self) -> u32 { - self.step_count() + self.value.end() as u32 } fn step_limit(&self) -> RangeInclusive { - self.value.clone() + self.value } } -#[derive(Clone, Debug, Getters, Setters, Default, Serialize, Deserialize, Builder)] -#[builder(setter(strip_option), default)] -#[getset(get = "pub")] -#[serde(rename_all = "PascalCase")] -pub struct DeviceFeatureOutput { - #[serde(skip_serializing_if = "Option::is_none")] - vibrate: Option, - #[serde(skip_serializing_if = "Option::is_none")] - rotate: Option, - #[serde(skip_serializing_if = "Option::is_none")] - oscillate: Option, - #[serde(skip_serializing_if = "Option::is_none")] - constrict: Option, - #[serde(skip_serializing_if = "Option::is_none")] - temperature: Option, - #[serde(skip_serializing_if = "Option::is_none")] - led: Option, - #[serde(skip_serializing_if = "Option::is_none")] - position: Option, - #[serde(skip_serializing_if = "Option::is_none")] - hw_position_with_duration: Option, - #[serde(skip_serializing_if = "Option::is_none")] - spray: Option, -} - -/// Helper macro to cast an Option to Option<&dyn DeviceFeatureOutputLimits> -macro_rules! as_output_limits { - ($opt:expr) => { - $opt.as_ref().map(|x| x as &dyn DeviceFeatureOutputLimits) - }; +// OutputType is auto-generated as the discriminant enum of DeviceFeatureOutput. +// Adding or renaming a DeviceFeatureOutput variant automatically updates OutputType. +#[derive(Clone, Debug, Serialize, Deserialize, EnumDiscriminants)] +#[strum_discriminants(name(OutputType))] +#[strum_discriminants(vis(pub))] +#[strum_discriminants(derive(Display, Hash, EnumIter, EnumString, Serialize, Deserialize))] +pub enum DeviceFeatureOutput { + Vibrate(DeviceFeatureOutputValueProperties), + Rotate(DeviceFeatureOutputValueProperties), + Oscillate(DeviceFeatureOutputValueProperties), + Constrict(DeviceFeatureOutputValueProperties), + Temperature(DeviceFeatureOutputValueProperties), + Led(DeviceFeatureOutputValueProperties), + Position(DeviceFeatureOutputValueProperties), + HwPositionWithDuration(DeviceFeatureOutputHwPositionWithDurationProperties), + Spray(DeviceFeatureOutputValueProperties), } impl DeviceFeatureOutput { - pub fn contains(&self, output_type: OutputType) -> bool { - match output_type { - OutputType::Constrict => self.constrict.is_some(), - OutputType::Temperature => self.temperature.is_some(), - OutputType::Led => self.led.is_some(), - OutputType::Oscillate => self.oscillate.is_some(), - OutputType::Position => self.position.is_some(), - OutputType::HwPositionWithDuration => self.hw_position_with_duration.is_some(), - OutputType::Rotate => self.rotate.is_some(), - OutputType::Spray => self.spray.is_some(), - OutputType::Unknown => false, - OutputType::Vibrate => self.vibrate.is_some(), - } + pub fn output_type(&self) -> OutputType { + OutputType::from(self) } - pub fn get(&self, output_type: OutputType) -> Option<&dyn DeviceFeatureOutputLimits> { - match output_type { - OutputType::Constrict => as_output_limits!(self.constrict()), - OutputType::Temperature => as_output_limits!(self.temperature()), - OutputType::Led => as_output_limits!(self.led()), - OutputType::Oscillate => as_output_limits!(self.oscillate()), - OutputType::Position => as_output_limits!(self.position()), - OutputType::HwPositionWithDuration => as_output_limits!(self.hw_position_with_duration()), - OutputType::Rotate => as_output_limits!(self.rotate()), - OutputType::Spray => as_output_limits!(self.spray()), - OutputType::Unknown => None, - OutputType::Vibrate => as_output_limits!(self.vibrate()), + fn as_limits(&self) -> &dyn DeviceFeatureOutputLimits { + match self { + DeviceFeatureOutput::Vibrate(v) => v, + DeviceFeatureOutput::Rotate(v) => v, + DeviceFeatureOutput::Oscillate(v) => v, + DeviceFeatureOutput::Constrict(v) => v, + DeviceFeatureOutput::Temperature(v) => v, + DeviceFeatureOutput::Led(v) => v, + DeviceFeatureOutput::Position(v) => v, + DeviceFeatureOutput::HwPositionWithDuration(v) => v, + DeviceFeatureOutput::Spray(v) => v, } } } +impl VariantKey for DeviceFeatureOutput { + type Key = OutputType; + fn variant_key(&self) -> OutputType { + self.output_type() + } +} + #[derive( Clone, Debug, Default, PartialEq, Eq, Getters, MutGetters, Setters, Serialize, Deserialize, )] #[serde(rename_all = "PascalCase")] pub struct DeviceFeatureInputProperties { #[getset(get = "pub", get_mut = "pub(super)")] - #[serde(serialize_with = "range_sequence_serialize")] value: Vec>, #[getset(get = "pub")] - command: HashSet, + #[serde(with = "crate::util::serializers::bitflags_seq")] + command: BitFlags, } impl DeviceFeatureInputProperties { pub fn new( value: &Vec>, - sensor_commands: &HashSet, + sensor_commands: &BitFlags, ) -> Self { Self { value: value.clone(), @@ -276,47 +210,40 @@ impl DeviceFeatureInputProperties { } } -#[derive(Clone, Debug, Getters, Setters, Default, Serialize, Deserialize, Builder)] -#[builder(setter(strip_option), default)] -#[getset(get = "pub")] -#[serde(rename_all = "PascalCase")] -pub struct DeviceFeatureInput { - #[serde(skip_serializing_if = "Option::is_none")] - battery: Option, - #[serde(skip_serializing_if = "Option::is_none")] - rssi: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pressure: Option, - #[serde(skip_serializing_if = "Option::is_none")] - button: Option, - #[serde(skip_serializing_if = "Option::is_none")] - depth: Option, - #[serde(skip_serializing_if = "Option::is_none")] - position: Option, +// InputType is auto-generated as the discriminant enum of DeviceFeatureInput. +#[derive(Clone, Debug, Serialize, Deserialize, EnumDiscriminants)] +#[strum_discriminants(name(InputType))] +#[strum_discriminants(vis(pub))] +#[strum_discriminants(derive(Display, Hash, EnumIter, EnumString, Serialize, Deserialize))] +pub enum DeviceFeatureInput { + Battery(DeviceFeatureInputProperties), + Rssi(DeviceFeatureInputProperties), + Button(DeviceFeatureInputProperties), + Pressure(DeviceFeatureInputProperties), + Depth(DeviceFeatureInputProperties), + Position(DeviceFeatureInputProperties), } impl DeviceFeatureInput { - pub fn contains(&self, input_type: InputType) -> bool { - match input_type { - InputType::Battery => self.battery.is_some(), - InputType::Rssi => self.rssi.is_some(), - InputType::Pressure => self.pressure.is_some(), - InputType::Button => self.button.is_some(), - InputType::Depth => self.depth.is_some(), - InputType::Position => self.position.is_some(), - InputType::Unknown => false, - } + pub fn input_type(&self) -> InputType { + InputType::from(self) } - pub fn get(&self, input_type: InputType) -> &Option { - match input_type { - InputType::Battery => self.battery(), - InputType::Rssi => self.rssi(), - InputType::Pressure => self.pressure(), - InputType::Button => self.button(), - InputType::Depth => self.depth(), - InputType::Position => self.position(), - InputType::Unknown => &None, + pub fn command(&self) -> &BitFlags { + match self { + DeviceFeatureInput::Battery(p) => p.command(), + DeviceFeatureInput::Rssi(p) => p.command(), + DeviceFeatureInput::Button(p) => p.command(), + DeviceFeatureInput::Pressure(p) => p.command(), + DeviceFeatureInput::Depth(p) => p.command(), + DeviceFeatureInput::Position(p) => p.command(), } } } + +impl VariantKey for DeviceFeatureInput { + type Key = InputType; + fn variant_key(&self) -> InputType { + self.input_type() + } +} diff --git a/crates/buttplug_core/src/message/v4/input_cmd.rs b/crates/buttplug_core/src/message/v4/input_cmd.rs index 1b5042354..f79237d01 100644 --- a/crates/buttplug_core/src/message/v4/input_cmd.rs +++ b/crates/buttplug_core/src/message/v4/input_cmd.rs @@ -12,9 +12,12 @@ use crate::message::{ ButtplugMessageValidator, InputType, }; +use enumflags2::bitflags; use getset::CopyGetters; use serde::{Deserialize, Serialize}; +#[bitflags] +#[repr(u8)] #[derive(Debug, Display, PartialEq, Eq, Clone, Serialize, Deserialize, Hash, Copy)] pub enum InputCommandType { #[serde(alias = "read")] diff --git a/crates/buttplug_core/src/util/mod.rs b/crates/buttplug_core/src/util/mod.rs index 63cdc8040..e5e34b284 100644 --- a/crates/buttplug_core/src/util/mod.rs +++ b/crates/buttplug_core/src/util/mod.rs @@ -10,5 +10,12 @@ pub mod async_manager; pub mod json; -pub mod range_serialize; +pub mod range; pub mod stream; +pub mod small_vec_enum_map; +pub mod serializers; + +#[cfg(not(feature = "wasm"))] +pub use tokio::time::sleep; +#[cfg(feature = "wasm")] +pub use wasmtimer::tokio::sleep; diff --git a/crates/buttplug_core/src/util/range.rs b/crates/buttplug_core/src/util/range.rs new file mode 100644 index 000000000..83da2fc4d --- /dev/null +++ b/crates/buttplug_core/src/util/range.rs @@ -0,0 +1,48 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2026 Nonpolynomial Labs LLC. All rights reserved. +// +// Licensed under the BSD 3-Clause license. See LICENSE file in the project root +// for full license information. + +use std::fmt; + +use serde::{Deserialize, Serialize}; + +/// A range bounded inclusively below and above (`start..=end`). +/// Use this instead of `std::ops::RangeInclusive` when directly iterating over the range is not required. +/// It uses less memory and doesn't need special serialization. +#[derive(Clone, Copy, Hash, Eq, PartialEq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct RangeInclusive([T; 2]); + +impl RangeInclusive { + pub fn new(start: T, end: T) -> Self { + Self([start, end]) + } + + pub fn start(&self) -> T { + self.0[0] + } + + pub fn end(&self) -> T { + self.0[1] + } + + pub fn is_empty(&self) -> bool { + self.0[0] > self.0[1] + } + + pub fn contains(&self, value: T) -> bool { + value >= self.0[0] && value <= self.0[1] + } +} + +impl fmt::Debug for RangeInclusive +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "[{:?}..={:?}]", self.0[0], self.0[1]) + } +} diff --git a/crates/buttplug_core/src/util/range_serialize.rs b/crates/buttplug_core/src/util/range_serialize.rs deleted file mode 100644 index 573fc55d2..000000000 --- a/crates/buttplug_core/src/util/range_serialize.rs +++ /dev/null @@ -1,45 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2026 Nonpolynomial Labs LLC. All rights reserved. -// -// Licensed under the BSD 3-Clause license. See LICENSE file in the project root -// for full license information. - -use std::ops::RangeInclusive; - -use serde::{Serialize, Serializer}; - -pub fn option_range_serialize( - range: &Option>, - serializer: S, -) -> Result -where - S: Serializer, - T: Serialize + Copy, -{ - if let Some(r) = range { - range_serialize(r, serializer) - } else { - core::option::Option::None::.serialize(serializer) - } -} - -pub fn range_serialize(range: &RangeInclusive, serializer: S) -> Result -where - S: Serializer, - T: Serialize + Copy, -{ - [*range.start(), *range.end()].serialize(serializer) -} - -pub fn range_sequence_serialize( - range_vec: &Vec>, - serializer: S, -) -> Result -where - S: Serializer, - T: Serialize + Copy, -{ - let arrays: Vec<[T; 2]> = range_vec.iter().map(|r| [*r.start(), *r.end()]).collect(); - arrays.serialize(serializer) -} diff --git a/crates/buttplug_core/src/util/serializers.rs b/crates/buttplug_core/src/util/serializers.rs new file mode 100644 index 000000000..fcb11ebc5 --- /dev/null +++ b/crates/buttplug_core/src/util/serializers.rs @@ -0,0 +1,62 @@ +//! Reusable serde helper modules for types that need custom serialization. + +/// Serializes / deserializes `BitFlags` as a sequence of variant name strings, +/// matching the wire format that `HashSet` produced previously. +/// +/// Usage: `#[serde(with = "crate::util::serializers::bitflags_seq")]` +pub mod bitflags_seq { + use enumflags2::{BitFlag, BitFlags}; + use serde::{ + Deserialize, + Deserializer, + Serialize, + Serializer, + de::{SeqAccess, Visitor}, + ser::SerializeSeq, + }; + use std::{fmt, marker::PhantomData}; + + pub fn serialize(flags: &BitFlags, serializer: S) -> Result + where + S: Serializer, + T: BitFlag + Serialize, + { + let mut seq = serializer.serialize_seq(None)?; + for flag in flags.iter() { + seq.serialize_element(&flag)?; + } + seq.end() + } + + pub fn deserialize<'de, D, T>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + T: BitFlag + Deserialize<'de>, + { + struct FlagsVisitor(PhantomData); + + impl<'de, T> Visitor<'de> for FlagsVisitor + where + T: BitFlag + Deserialize<'de>, + { + type Value = BitFlags; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a sequence of flag variant names") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let mut flags = BitFlags::empty(); + while let Some(flag) = seq.next_element::()? { + flags |= flag; + } + Ok(flags) + } + } + + deserializer.deserialize_seq(FlagsVisitor(PhantomData)) + } +} diff --git a/crates/buttplug_core/src/util/small_vec_enum_map.rs b/crates/buttplug_core/src/util/small_vec_enum_map.rs new file mode 100644 index 000000000..597975922 --- /dev/null +++ b/crates/buttplug_core/src/util/small_vec_enum_map.rs @@ -0,0 +1,446 @@ +//! A compact map-like collection for enum types with newtype variants. +//! +//! [`SmallVecEnumMap`] stores enum values in a [`SmallVec`] and serializes/deserializes +//! them as a JSON object, where each key is the (snake_case) variant name and each value is +//! the inner payload. This preserves an existing `{"vibrate": {...}, "rotate": {...}}` wire +//! format while avoiding the memory overhead of a struct with one `Option` field per variant. +//! +//! With `N = 1`, no heap allocation is needed when a single variant is present — the common case. +//! +//! # Constraints +//! `V` must be an enum where every active variant is a newtype variant (single unnamed field). +//! Unit, tuple, and struct variants are not supported and will return a serde error at runtime. + +use core::fmt; +use serde::{ + Deserialize, + Deserializer, + Serialize, + Serializer, + de::{MapAccess, Visitor}, + ser::SerializeMap, +}; +use smallvec::SmallVec; +use std::ops::{Deref, DerefMut}; + +const UNSUPPORTED: &str = "SmallVecEnumMap only supports enums where every active variant is a \ + newtype variant (single unnamed field). Unit, tuple, and struct \ + variants are not supported. Check that your enum definition matches \ + these constraints or see the small_vec_enum_map module docs for \ + details."; + +/// A [`SmallVec`]-backed collection that round-trips through a JSON object. +/// +/// See the [module docs](self) for usage and constraints. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SmallVecEnumMap(pub SmallVec<[V; N]>); + +impl Deref for SmallVecEnumMap { + type Target = SmallVec<[V; N]>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for SmallVecEnumMap { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl From> for SmallVecEnumMap { + fn from(inner: SmallVec<[V; N]>) -> Self { + SmallVecEnumMap(inner) + } +} + +impl From> for SmallVecEnumMap { + fn from(vec: Vec) -> Self { + SmallVecEnumMap(SmallVec::from_vec(vec)) + } +} + +impl FromIterator for SmallVecEnumMap { + fn from_iter>(iter: I) -> Self { + SmallVecEnumMap(SmallVec::from_iter(iter)) + } +} + +impl Default for SmallVecEnumMap { + fn default() -> Self { + SmallVecEnumMap(SmallVec::new()) + } +} + +impl SmallVecEnumMap { + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +/// Associates an enum type with a plain discriminant key that can be compared +/// without inspecting variant payloads or matching on string names. +/// +/// Implement this on an enum to enable [`SmallVecEnumMap::find_by_key`] and +/// [`SmallVecEnumMap::contains_key`]. +pub trait VariantKey { + type Key: PartialEq; + fn variant_key(&self) -> Self::Key; +} + +impl SmallVecEnumMap +where + V: VariantKey, +{ + /// Returns a reference to the element whose variant key equals `key`, if any. + pub fn find_by_key(&self, key: &V::Key) -> Option<&V> { + self.0.iter().find(|v| &v.variant_key() == key) + } + + /// Returns `true` if an element with the given variant key is present. + pub fn contains_key(&self, key: &V::Key) -> bool { + self.0.iter().any(|v| &v.variant_key() == key) + } +} + +// --------------------------------------------------------------------------- +// Serialization +// --------------------------------------------------------------------------- + +impl Serialize for SmallVecEnumMap +where + V: Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map = serializer.serialize_map(Some(self.0.len()))?; + for v in &self.0 { + // Route each element through MapEntrySerializer, which intercepts the + // serialize_newtype_variant call that serde generates for enum variants + // and writes it directly as a map key-value pair. + v.serialize(MapEntrySerializer { map: &mut map })?; + } + map.end() + } +} + +/// A [`Serializer`] adapter that writes a single enum newtype variant into an +/// outer [`SerializeMap`] as a key-value entry. +/// +/// Only `serialize_newtype_variant` is meaningful here. All other methods return +/// an error. Serde has no `forward_to_error!` macro for serializers (unlike +/// `forward_to_deserialize_any!` for deserializers), so the full trait surface +/// must be spelled out below. +struct MapEntrySerializer<'a, M: SerializeMap> { + map: &'a mut M, +} + +/// Generates `Serializer` stub methods that unconditionally return [`UNSUPPORTED`]. +/// +/// Used for every path in [`MapEntrySerializer`] except `serialize_newtype_variant`. +/// Methods with generic type parameters or compound return types are written out +/// explicitly below since they can't fit this pattern cleanly. +macro_rules! reject_serialize { + ($(fn $name:ident($($arg:tt)*);)*) => {$( + fn $name(self, $($arg)*) -> Result { + Err(serde::ser::Error::custom(UNSUPPORTED)) + } + )*}; +} + +impl<'a, M> Serializer for MapEntrySerializer<'a, M> +where + M: SerializeMap, +{ + type Ok = (); + type Error = M::Error; + type SerializeSeq = serde::ser::Impossible<(), M::Error>; + type SerializeTuple = serde::ser::Impossible<(), M::Error>; + type SerializeTupleStruct = serde::ser::Impossible<(), M::Error>; + type SerializeTupleVariant = serde::ser::Impossible<(), M::Error>; + type SerializeMap = serde::ser::Impossible<(), M::Error>; + type SerializeStruct = serde::ser::Impossible<(), M::Error>; + type SerializeStructVariant = serde::ser::Impossible<(), M::Error>; + + // Scalar and unit methods — all unsupported. + reject_serialize! { + fn serialize_bool(_v: bool); + fn serialize_i8(_v: i8); + fn serialize_i16(_v: i16); + fn serialize_i32(_v: i32); + fn serialize_i64(_v: i64); + fn serialize_u8(_v: u8); + fn serialize_u16(_v: u16); + fn serialize_u32(_v: u32); + fn serialize_u64(_v: u64); + fn serialize_f32(_v: f32); + fn serialize_f64(_v: f64); + fn serialize_char(_v: char); + fn serialize_str(_v: &str); + fn serialize_bytes(_v: &[u8]); + fn serialize_none(); + fn serialize_unit(); + fn serialize_unit_struct(_name: &'static str); + fn serialize_unit_variant(_name: &'static str, _idx: u32, _variant: &'static str); + } + + // Generic methods — can't go in the macro due to type parameter bounds. + fn serialize_some(self, _value: &T) -> Result { + Err(serde::ser::Error::custom(UNSUPPORTED)) + } + fn serialize_newtype_struct( + self, + _name: &'static str, + _value: &T, + ) -> Result { + Err(serde::ser::Error::custom(UNSUPPORTED)) + } + + /// The only supported path: writes the variant name as the map key and the + /// inner value as the map value. + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + value: &T, + ) -> Result { + self.map.serialize_entry(variant, value) + } + + // Compound-return-type methods — can't go in the macro due to distinct return types. + fn serialize_seq(self, _len: Option) -> Result { + Err(serde::ser::Error::custom(UNSUPPORTED)) + } + fn serialize_tuple(self, _len: usize) -> Result { + Err(serde::ser::Error::custom(UNSUPPORTED)) + } + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Err(serde::ser::Error::custom(UNSUPPORTED)) + } + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(serde::ser::Error::custom(UNSUPPORTED)) + } + fn serialize_map(self, _len: Option) -> Result { + Err(serde::ser::Error::custom(UNSUPPORTED)) + } + fn serialize_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Err(serde::ser::Error::custom(UNSUPPORTED)) + } + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(serde::ser::Error::custom(UNSUPPORTED)) + } +} + +// --------------------------------------------------------------------------- +// Deserialization +// +// Pipeline for a single map entry (key string → enum variant): +// +// SmallVecEnumMapVisitor::visit_map +// └─ EnumEntrySeed (carries the key string into value deserialization) +// └─ EnumEntryDeserializer (wraps the value deserializer; only accepts deserialize_enum) +// └─ EnumAccess (presents the key as the variant discriminant) +// └─ VariantAccess (delegates inner-value deserialization to the original deserializer) +// --------------------------------------------------------------------------- + +impl<'de, V, const N: usize> Deserialize<'de> for SmallVecEnumMap +where + V: Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct SmallVecEnumMapVisitor(std::marker::PhantomData<[V; N]>); + + impl<'de, V, const N: usize> Visitor<'de> for SmallVecEnumMapVisitor + where + V: Deserialize<'de>, + { + type Value = SmallVecEnumMap; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a map that can be converted into a SmallVecEnumMap") + } + + fn visit_map(self, mut map: A) -> Result + where + A: MapAccess<'de>, + { + let mut smallvec = SmallVec::new(); + while let Some(key) = map.next_key::()? { + let value = map.next_value_seed(EnumEntrySeed { + key, + _phantom: std::marker::PhantomData, + })?; + smallvec.push(value); + } + Ok(SmallVecEnumMap(smallvec)) + } + } + + deserializer.deserialize_map(SmallVecEnumMapVisitor(std::marker::PhantomData)) + } +} + +/// Carries a map key string into the value deserialization step so it can be +/// used as the enum variant discriminant. +struct EnumEntrySeed { + key: String, + _phantom: std::marker::PhantomData, +} + +impl<'de, V> serde::de::DeserializeSeed<'de> for EnumEntrySeed +where + V: Deserialize<'de>, +{ + type Value = V; + + fn deserialize(self, deserializer: D) -> Result + where + D: Deserializer<'de>, + { + V::deserialize(EnumEntryDeserializer { + key: self.key, + value_deserializer: deserializer, + }) + } +} + +/// A [`Deserializer`] that only supports `deserialize_enum`. +/// +/// Pairs the already-decoded key string with the not-yet-decoded value deserializer +/// so serde can reconstruct the enum variant. +struct EnumEntryDeserializer { + key: String, + value_deserializer: D, +} + +impl<'de, D> Deserializer<'de> for EnumEntryDeserializer +where + D: Deserializer<'de>, +{ + type Error = D::Error; + + fn deserialize_any(self, _visitor: V) -> Result + where + V: Visitor<'de>, + { + Err(serde::de::Error::custom(UNSUPPORTED)) + } + + fn deserialize_enum( + self, + _name: &'static str, + _variants: &'static [&'static str], + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + visitor.visit_enum(EnumAccess { + key: self.key, + value_deserializer: self.value_deserializer, + }) + } + + serde::forward_to_deserialize_any! { + bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string + bytes byte_buf option unit unit_struct newtype_struct seq tuple + tuple_struct map struct identifier ignored_any + } +} + +/// Presents the map key as the enum variant discriminant to serde's enum visitor. +struct EnumAccess { + key: String, + value_deserializer: D, +} + +impl<'de, D> serde::de::EnumAccess<'de> for EnumAccess +where + D: Deserializer<'de>, +{ + type Error = D::Error; + type Variant = VariantAccess; + + fn variant_seed(self, seed: V) -> Result<(V::Value, Self::Variant), Self::Error> + where + V: serde::de::DeserializeSeed<'de>, + { + let variant = seed.deserialize(serde::de::value::StringDeserializer::new(self.key))?; + Ok(( + variant, + VariantAccess { + value_deserializer: self.value_deserializer, + }, + )) + } +} + +/// Delegates deserialization of the variant's inner value to the original deserializer. +/// +/// Only newtype variants are supported; all other variant forms return an error. +struct VariantAccess { + value_deserializer: D, +} + +impl<'de, D> serde::de::VariantAccess<'de> for VariantAccess +where + D: Deserializer<'de>, +{ + type Error = D::Error; + + fn unit_variant(self) -> Result<(), Self::Error> { + Err(serde::de::Error::custom(UNSUPPORTED)) + } + + fn newtype_variant_seed(self, seed: T) -> Result + where + T: serde::de::DeserializeSeed<'de>, + { + seed.deserialize(self.value_deserializer) + } + + fn tuple_variant(self, _len: usize, _visitor: V) -> Result + where + V: Visitor<'de>, + { + Err(serde::de::Error::custom(UNSUPPORTED)) + } + + fn struct_variant( + self, + _fields: &'static [&'static str], + _visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + Err(serde::de::Error::custom(UNSUPPORTED)) + } +} diff --git a/crates/buttplug_server/src/device/device_handle.rs b/crates/buttplug_server/src/device/device_handle.rs index 841ec7b2b..9c3e31c3a 100644 --- a/crates/buttplug_server/src/device/device_handle.rs +++ b/crates/buttplug_server/src/device/device_handle.rs @@ -23,7 +23,6 @@ use buttplug_core::{ DeviceMessageInfoV4, InputCommandType, InputType, - OutputType, OutputValue, StopCmdV4, }, @@ -32,6 +31,7 @@ use buttplug_core::{ use buttplug_server_device_config::{ DeviceConfigurationManager, ServerDeviceDefinition, + ServerDeviceFeatureOutput, UserDeviceIdentifier, }; use dashmap::DashMap; @@ -180,8 +180,8 @@ impl DeviceHandle { .definition .features() .values() + .filter(|x| x.has_output() || x.has_input()) .map(|x| (x.index(), x.as_device_feature().expect("Infallible"))) - .filter(|(_, x)| x.output().as_ref().is_some() || x.input().as_ref().is_some()) .collect::>(), ) } @@ -302,20 +302,22 @@ impl DeviceHandle { } if msg.inputs() { self.definition.features().iter().for_each(|(i, f)| { - if let Some(inputs) = f.input() { - if inputs.can_subscribe() { - fut_vec.push( - self.parse_message(ButtplugDeviceCommandMessageUnionV4::InputCmd( - CheckedInputCmdV4::new( - 1, - self.definition.index(), - *i, - InputType::Unknown, - InputCommandType::Unsubscribe, - f.id(), - ), - )), - ); + if f.can_subscribe() { + for input in f.input.iter() { + if input.can_subscribe() { + fut_vec.push( + self.parse_message(ButtplugDeviceCommandMessageUnionV4::InputCmd( + CheckedInputCmdV4::new( + 1, + self.definition.index(), + *i, + input.input_type(), + InputCommandType::Unsubscribe, + f.id(), + ), + )), + ); + } } } }); @@ -517,48 +519,45 @@ pub(super) async fn build_device_handle( // Generate stop commands for this device let mut stop_commands: Vec = vec![]; for feature in definition.features().values() { - if let Some(output_map) = feature.output() { - for actuator_type in output_map.output_types() { - let mut stop_cmd = |actuator_cmd| { - stop_commands.push( - CheckedOutputCmdV4::new(1, 0, feature.index(), feature.id(), actuator_cmd).into(), - ); - }; - - // Break out of these if one is found, we only need 1 stop message per output. - match actuator_type { - OutputType::Constrict => { - stop_cmd(message::OutputCommand::Constrict(OutputValue::new(0))); - break; - } - OutputType::Temperature => { - stop_cmd(message::OutputCommand::Temperature(OutputValue::new(0))); - break; - } - OutputType::Spray => { - stop_cmd(message::OutputCommand::Spray(OutputValue::new(0))); - break; - } - OutputType::Led => { - stop_cmd(message::OutputCommand::Led(OutputValue::new(0))); - break; - } - OutputType::Oscillate => { - stop_cmd(message::OutputCommand::Oscillate(OutputValue::new(0))); - break; - } - OutputType::Rotate => { - stop_cmd(message::OutputCommand::Rotate(OutputValue::new(0))); - break; - } - OutputType::Vibrate => { - stop_cmd(message::OutputCommand::Vibrate(OutputValue::new(0))); - break; - } - _ => { - // There's not much we can do about position or position w/ duration, so just continue on - continue; - } + for output in feature.output.iter() { + let mut stop_cmd = |actuator_cmd| { + stop_commands + .push(CheckedOutputCmdV4::new(1, 0, feature.index(), feature.id(), actuator_cmd).into()); + }; + + // Break out of these if one is found, we only need 1 stop message per output. + match output { + ServerDeviceFeatureOutput::Constrict(_) => { + stop_cmd(message::OutputCommand::Constrict(OutputValue::new(0))); + break; + } + ServerDeviceFeatureOutput::Temperature(_) => { + stop_cmd(message::OutputCommand::Temperature(OutputValue::new(0))); + break; + } + ServerDeviceFeatureOutput::Spray(_) => { + stop_cmd(message::OutputCommand::Spray(OutputValue::new(0))); + break; + } + ServerDeviceFeatureOutput::Led(_) => { + stop_cmd(message::OutputCommand::Led(OutputValue::new(0))); + break; + } + ServerDeviceFeatureOutput::Oscillate(_) => { + stop_cmd(message::OutputCommand::Oscillate(OutputValue::new(0))); + break; + } + ServerDeviceFeatureOutput::Rotate(_) => { + stop_cmd(message::OutputCommand::Rotate(OutputValue::new(0))); + break; + } + ServerDeviceFeatureOutput::Vibrate(_) => { + stop_cmd(message::OutputCommand::Vibrate(OutputValue::new(0))); + break; + } + _ => { + // There's not much we can do about position or position w/ duration, so just continue on + continue; } } } diff --git a/crates/buttplug_server/src/device/protocol_impl/galaku.rs b/crates/buttplug_server/src/device/protocol_impl/galaku.rs index e99dc7734..8afb0deea 100644 --- a/crates/buttplug_server/src/device/protocol_impl/galaku.rs +++ b/crates/buttplug_server/src/device/protocol_impl/galaku.rs @@ -118,12 +118,7 @@ impl ProtocolInitializer for GalakuInitializer { if hardware.name() == "AC695X_1(BLE)" { protocol.is_caiping_pump_device = true; } - for _ in 0..def - .features() - .values() - .filter(|f| f.output().is_some()) - .count() - { + for _ in 0..def.features().values().filter(|f| f.has_output()).count() { protocol.speeds.push(AtomicU8::new(0)); } Ok(Arc::new(protocol)) diff --git a/crates/buttplug_server/src/device/protocol_impl/hismith_mini.rs b/crates/buttplug_server/src/device/protocol_impl/hismith_mini.rs index 3358c4608..13cb440df 100644 --- a/crates/buttplug_server/src/device/protocol_impl/hismith_mini.rs +++ b/crates/buttplug_server/src/device/protocol_impl/hismith_mini.rs @@ -85,21 +85,13 @@ impl ProtocolInitializer for HismithMiniInitializer { dual_vibe: device_definition .features() .values() - .filter(|x| { - x.output() - .as_ref() - .is_some_and(|x| x.contains(OutputType::Vibrate)) - }) + .filter(|x| x.contains_output(OutputType::Vibrate)) .count() >= 2, second_constrict: device_definition .features() .values() - .position(|x| { - x.output() - .as_ref() - .is_some_and(|x| x.contains(OutputType::Constrict)) - }) + .position(|x| x.contains_output(OutputType::Constrict)) .unwrap_or(0) == 1, })) diff --git a/crates/buttplug_server/src/device/protocol_impl/honeyplaybox.rs b/crates/buttplug_server/src/device/protocol_impl/honeyplaybox.rs index 7a829ae52..791fb5dfb 100644 --- a/crates/buttplug_server/src/device/protocol_impl/honeyplaybox.rs +++ b/crates/buttplug_server/src/device/protocol_impl/honeyplaybox.rs @@ -64,8 +64,8 @@ impl ProtocolInitializer for HoneyPlayBoxInitializer { ) -> Result, ButtplugDeviceError> { let feature_count = device_definition .features() - .iter() - .filter(|x| x.1.output().is_some()) + .values() + .filter(|x| x.has_output()) .count(); let mut event_receiver = hardware.event_stream(); diff --git a/crates/buttplug_server/src/device/protocol_impl/lovehoney_desire.rs b/crates/buttplug_server/src/device/protocol_impl/lovehoney_desire.rs index 3150bf3f6..ff77cb455 100644 --- a/crates/buttplug_server/src/device/protocol_impl/lovehoney_desire.rs +++ b/crates/buttplug_server/src/device/protocol_impl/lovehoney_desire.rs @@ -45,11 +45,7 @@ impl ProtocolInitializer for LovehoneyDesireInitializer { def: &ServerDeviceDefinition, ) -> Result, ButtplugDeviceError> { Ok(Arc::new(LovehoneyDesire::new( - def - .features() - .values() - .filter(|x| x.output().is_some()) - .count() as u8, + def.features().values().filter(|x| x.has_output()).count() as u8, ))) } } diff --git a/crates/buttplug_server/src/device/protocol_impl/lovense/mod.rs b/crates/buttplug_server/src/device/protocol_impl/lovense/mod.rs index 03a086277..71b5dc751 100644 --- a/crates/buttplug_server/src/device/protocol_impl/lovense/mod.rs +++ b/crates/buttplug_server/src/device/protocol_impl/lovense/mod.rs @@ -173,37 +173,27 @@ impl ProtocolInitializer for LovenseInitializer { .features() .values() .filter(|x| { - x.output() - .as_ref() - .is_some_and(|x| x.contains(OutputType::Vibrate) || x.contains(OutputType::Oscillate)) + x.contains_output(OutputType::Vibrate) || x.contains_output(OutputType::Oscillate) }) .count(); let output_count = device_definition .features() .values() - .filter(|x| x.output().is_some()) + .filter(|x| x.has_output()) .count(); let vibrator_rotator = output_count == 2 && device_definition .features() .values() - .filter(|x| { - x.output() - .as_ref() - .is_some_and(|x| x.contains(OutputType::Vibrate)) - }) + .filter(|x| x.contains_output(OutputType::Vibrate)) .count() == 1 && device_definition .features() .values() - .filter(|x| { - x.output() - .as_ref() - .is_some_and(|x| x.contains(OutputType::Rotate)) - }) + .filter(|x| x.contains_output(OutputType::Rotate)) .count() == 1; @@ -211,21 +201,13 @@ impl ProtocolInitializer for LovenseInitializer { && device_definition .features() .values() - .filter(|x| { - x.output() - .as_ref() - .is_some_and(|x| x.contains(OutputType::Vibrate)) - }) + .filter(|x| x.contains_output(OutputType::Vibrate)) .count() == 1 && device_definition .features() .values() - .filter(|x| { - x.output() - .as_ref() - .is_some_and(|x| x.contains(OutputType::Constrict)) - }) + .filter(|x| x.contains_output(OutputType::Constrict)) .count() == 1; diff --git a/crates/buttplug_server/src/device/protocol_impl/lovense_connect_service.rs b/crates/buttplug_server/src/device/protocol_impl/lovense_connect_service.rs index 3dc39cb4d..432827c49 100644 --- a/crates/buttplug_server/src/device/protocol_impl/lovense_connect_service.rs +++ b/crates/buttplug_server/src/device/protocol_impl/lovense_connect_service.rs @@ -81,25 +81,13 @@ impl ProtocolInitializer for LovenseConnectServiceInitializer { protocol.vibrator_count = device_definition .features() - .iter() - .filter(|x| { - if let Some(o) = x.1.output() { - o.contains(OutputType::Vibrate) - } else { - false - } - }) + .values() + .filter(|x| x.contains_output(OutputType::Vibrate)) .count(); protocol.thusting_count = device_definition .features() - .iter() - .filter(|x| { - if let Some(o) = x.1.output() { - o.contains(OutputType::Oscillate) - } else { - false - } - }) + .values() + .filter(|x| x.contains_output(OutputType::Oscillate)) .count(); // The Ridge and Gravity both oscillate, but the Ridge only oscillates but takes diff --git a/crates/buttplug_server/src/device/protocol_impl/magic_motion_v4.rs b/crates/buttplug_server/src/device/protocol_impl/magic_motion_v4.rs index 8b0515449..28db25337 100644 --- a/crates/buttplug_server/src/device/protocol_impl/magic_motion_v4.rs +++ b/crates/buttplug_server/src/device/protocol_impl/magic_motion_v4.rs @@ -41,11 +41,7 @@ impl ProtocolInitializer for MagicMotionV4Initializer { def: &ServerDeviceDefinition, ) -> Result, ButtplugDeviceError> { Ok(Arc::new(MagicMotionV4::new( - def - .features() - .values() - .filter(|x| x.output().is_some()) - .count() as u8, + def.features().values().filter(|x| x.has_output()).count() as u8, ))) } } diff --git a/crates/buttplug_server/src/device/protocol_impl/monsterpub.rs b/crates/buttplug_server/src/device/protocol_impl/monsterpub.rs index e6aa7bb0a..92fa2dec1 100644 --- a/crates/buttplug_server/src/device/protocol_impl/monsterpub.rs +++ b/crates/buttplug_server/src/device/protocol_impl/monsterpub.rs @@ -134,11 +134,7 @@ impl ProtocolInitializer for MonsterPubInitializer { )) .await?; } - let output_count = def - .features() - .values() - .filter(|x| x.output().is_some()) - .count(); + let output_count = def.features().values().filter(|x| x.has_output()).count(); Ok(Arc::new(MonsterPub::new( if hardware.endpoints().contains(&Endpoint::TxVibrate) { diff --git a/crates/buttplug_server/src/device/protocol_impl/mysteryvibe.rs b/crates/buttplug_server/src/device/protocol_impl/mysteryvibe.rs index e928f074a..9d789eb4a 100644 --- a/crates/buttplug_server/src/device/protocol_impl/mysteryvibe.rs +++ b/crates/buttplug_server/src/device/protocol_impl/mysteryvibe.rs @@ -53,11 +53,7 @@ impl ProtocolInitializer for MysteryVibeInitializer { true, ); hardware.write_value(&msg).await?; - let vibrator_count = def - .features() - .values() - .filter(|x| x.output().is_some()) - .count(); + let vibrator_count = def.features().values().filter(|x| x.has_output()).count(); Ok(Arc::new(MysteryVibe::new(vibrator_count as u8))) } } diff --git a/crates/buttplug_server/src/device/protocol_impl/mysteryvibe_v2.rs b/crates/buttplug_server/src/device/protocol_impl/mysteryvibe_v2.rs index 040fb9f12..19ae74c5d 100644 --- a/crates/buttplug_server/src/device/protocol_impl/mysteryvibe_v2.rs +++ b/crates/buttplug_server/src/device/protocol_impl/mysteryvibe_v2.rs @@ -49,11 +49,7 @@ impl ProtocolInitializer for MysteryVibeV2Initializer { true, ); hardware.write_value(&msg).await?; - let vibrator_count = def - .features() - .values() - .filter(|x| x.output().is_some()) - .count(); + let vibrator_count = def.features().values().filter(|x| x.has_output()).count(); Ok(Arc::new(MysteryVibe::new(vibrator_count as u8))) } } diff --git a/crates/buttplug_server/src/device/protocol_impl/satisfyer.rs b/crates/buttplug_server/src/device/protocol_impl/satisfyer.rs index ebb5904ee..f22b179b4 100644 --- a/crates/buttplug_server/src/device/protocol_impl/satisfyer.rs +++ b/crates/buttplug_server/src/device/protocol_impl/satisfyer.rs @@ -120,7 +120,7 @@ impl ProtocolInitializer for SatisfyerInitializer { let feature_count = device_definition .features() .values() - .filter(|x| x.output().is_some()) + .filter(|x| x.has_output()) .count(); Ok(Arc::new(Satisfyer::new(feature_count))) diff --git a/crates/buttplug_server/src/device/protocol_impl/sensee_v2.rs b/crates/buttplug_server/src/device/protocol_impl/sensee_v2.rs index 9f24d88bb..58751cca0 100644 --- a/crates/buttplug_server/src/device/protocol_impl/sensee_v2.rs +++ b/crates/buttplug_server/src/device/protocol_impl/sensee_v2.rs @@ -62,12 +62,10 @@ impl ProtocolInitializer for SenseeV2Initializer { 0x65 }; - let feature_map = |output_type| { + let feature_map = |output_type: OutputType| { let mut map = HashMap::new(); device_definition.features().iter().for_each(|(i, x)| { - if let Some(output_map) = x.output() - && output_map.contains(output_type) - { + if x.contains_output(output_type) { map.insert(*i, AtomicU8::new(0)); } }); diff --git a/crates/buttplug_server/src/device/protocol_impl/sexverse_v1.rs b/crates/buttplug_server/src/device/protocol_impl/sexverse_v1.rs index 3115653a2..3763de4b9 100644 --- a/crates/buttplug_server/src/device/protocol_impl/sexverse_v1.rs +++ b/crates/buttplug_server/src/device/protocol_impl/sexverse_v1.rs @@ -43,10 +43,8 @@ impl ProtocolInitializer for SexverseV1Initializer { ) -> Result, ButtplugDeviceError> { let mut commands = vec![]; def.features().values().for_each(|x| { - if let Some(m) = x.output() { - for output in m.output_types() { - commands.push((output, AtomicU8::default())) - } + for output in x.output.iter() { + commands.push((output.output_type(), AtomicU8::default())) } }); Ok(Arc::new(SexverseV1::new(commands))) diff --git a/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_barney.rs b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_barney.rs index 083ad5d64..cbafd8401 100644 --- a/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_barney.rs +++ b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_barney.rs @@ -48,13 +48,7 @@ impl ProtocolInitializer for SvakomBarneyInitializer { let num_vibrators = def .features() .values() - .filter(|x| { - if let Some(output_map) = x.output() { - output_map.contains(OutputType::Vibrate) - } else { - false - } - }) + .filter(|x| x.contains_output(OutputType::Vibrate)) .count() as u8; Ok(Arc::new(SvakomBarney::new(num_vibrators))) } diff --git a/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_v4.rs b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_v4.rs index 6ec262ebe..0f047e217 100644 --- a/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_v4.rs +++ b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_v4.rs @@ -48,13 +48,7 @@ impl ProtocolInitializer for SvakomV4Initializer { let num_vibrators = def .features() .values() - .filter(|x| { - if let Some(output_map) = x.output() { - output_map.contains(OutputType::Vibrate) - } else { - false - } - }) + .filter(|x| x.contains_output(OutputType::Vibrate)) .count() as u8; Ok(Arc::new(SvakomV4::new(num_vibrators))) } diff --git a/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_v6.rs b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_v6.rs index fe65f0cab..88e2e3d41 100644 --- a/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_v6.rs +++ b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_v6.rs @@ -48,13 +48,7 @@ impl ProtocolInitializer for SvakomV6Initializer { let num_vibrators = def .features() .values() - .filter(|x| { - if let Some(output_map) = x.output() { - output_map.contains(OutputType::Vibrate) - } else { - false - } - }) + .filter(|x| x.contains_output(OutputType::Vibrate)) .count() as u8; Ok(Arc::new(SvakomV6::new(num_vibrators))) } diff --git a/crates/buttplug_server/src/device/protocol_impl/vibratissimo.rs b/crates/buttplug_server/src/device/protocol_impl/vibratissimo.rs index 2f534c957..cb4d228c0 100644 --- a/crates/buttplug_server/src/device/protocol_impl/vibratissimo.rs +++ b/crates/buttplug_server/src/device/protocol_impl/vibratissimo.rs @@ -5,8 +5,7 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use buttplug_core::errors::ButtplugDeviceError; -use buttplug_core::message::OutputType; +use buttplug_core::{errors::ButtplugDeviceError, message::OutputType}; use buttplug_server_device_config::Endpoint; use buttplug_server_device_config::{ ProtocolCommunicationSpecifier, @@ -81,11 +80,7 @@ impl ProtocolInitializer for VibratissimoInitializer { let num_vibrators: u8 = def .features() .values() - .filter(|x| { - x.output() - .as_ref() - .is_some_and(|x| x.contains(OutputType::Vibrate)) - }) + .filter(|x| x.contains_output(OutputType::Vibrate)) .count() as u8; Ok(Arc::new(Vibratissimo::new(num_vibrators))) } diff --git a/crates/buttplug_server/src/device/protocol_impl/wevibe.rs b/crates/buttplug_server/src/device/protocol_impl/wevibe.rs index 32375145d..3753c787d 100644 --- a/crates/buttplug_server/src/device/protocol_impl/wevibe.rs +++ b/crates/buttplug_server/src/device/protocol_impl/wevibe.rs @@ -15,8 +15,7 @@ use crate::device::{ }, }; use async_trait::async_trait; -use buttplug_core::errors::ButtplugDeviceError; -use buttplug_core::message::OutputType; +use buttplug_core::{errors::ButtplugDeviceError, message::OutputType}; use buttplug_server_device_config::Endpoint; use buttplug_server_device_config::{ ProtocolCommunicationSpecifier, @@ -60,11 +59,7 @@ impl ProtocolInitializer for WeVibeInitializer { let num_vibrators = def .features() .values() - .filter(|x| { - x.output() - .as_ref() - .is_some_and(|x| x.contains(OutputType::Vibrate)) - }) + .filter(|x| x.contains_output(OutputType::Vibrate)) .count() as u8; Ok(Arc::new(WeVibe::new(num_vibrators))) } diff --git a/crates/buttplug_server/src/device/protocol_impl/wevibe8bit.rs b/crates/buttplug_server/src/device/protocol_impl/wevibe8bit.rs index 60ac20b58..243455320 100644 --- a/crates/buttplug_server/src/device/protocol_impl/wevibe8bit.rs +++ b/crates/buttplug_server/src/device/protocol_impl/wevibe8bit.rs @@ -47,11 +47,7 @@ impl ProtocolInitializer for WeVibe8BitInitializer { let num_vibrators = def .features() .values() - .filter(|x| { - x.output() - .as_ref() - .is_some_and(|x| x.contains(OutputType::Vibrate)) - }) + .filter(|x| x.contains_output(OutputType::Vibrate)) .count() as u8; Ok(Arc::new(WeVibe8Bit::new(num_vibrators))) } diff --git a/crates/buttplug_server/src/device/protocol_impl/wevibe_chorus.rs b/crates/buttplug_server/src/device/protocol_impl/wevibe_chorus.rs index 8c39cf964..35f9ed0f3 100644 --- a/crates/buttplug_server/src/device/protocol_impl/wevibe_chorus.rs +++ b/crates/buttplug_server/src/device/protocol_impl/wevibe_chorus.rs @@ -47,11 +47,7 @@ impl ProtocolInitializer for WeVibeChorusInitializer { let num_vibrators = def .features() .values() - .filter(|x| { - x.output() - .as_ref() - .is_some_and(|x| x.contains(OutputType::Vibrate)) - }) + .filter(|x| x.contains_output(OutputType::Vibrate)) .count() as u8; Ok(Arc::new(WeVibeChorus::new(num_vibrators))) } diff --git a/crates/buttplug_server/src/message/v2/battery_level_cmd.rs b/crates/buttplug_server/src/message/v2/battery_level_cmd.rs index 6ea2186c6..c0b1d97cb 100644 --- a/crates/buttplug_server/src/message/v2/battery_level_cmd.rs +++ b/crates/buttplug_server/src/message/v2/battery_level_cmd.rs @@ -83,7 +83,7 @@ impl TryFromDeviceAttributes for CheckedInputCmdV4 { let feature_index = features .features() .iter() - .find(|(_, p)| p.input().as_ref().is_some_and(|x| x.battery().is_some())) + .find(|(_, p)| p.contains_input(InputType::Battery)) .expect("Already found matching battery feature, can unwrap this.") .0; diff --git a/crates/buttplug_server/src/message/v3/client_device_message_attributes.rs b/crates/buttplug_server/src/message/v3/client_device_message_attributes.rs index f09546ee0..7d600f0ee 100644 --- a/crates/buttplug_server/src/message/v3/client_device_message_attributes.rs +++ b/crates/buttplug_server/src/message/v3/client_device_message_attributes.rs @@ -11,7 +11,8 @@ use crate::message::{ }; use buttplug_core::message::{ DeviceFeature, - DeviceFeatureOutputValueProperties, + DeviceFeatureInput, + DeviceFeatureOutput, InputCommandType, InputType, OutputType, @@ -246,40 +247,24 @@ impl From> for ClientDeviceMessageAttributesV3 { .iter() .flat_map(|feature| { let mut actuator_vec = vec![]; - if let Some(output_map) = feature.output() { - let mut create_actuator = - |actuator_type, actuator: &DeviceFeatureOutputValueProperties| { - let attrs = ClientGenericDeviceMessageAttributesV3 { - feature_descriptor: feature.description().to_owned(), - actuator_type, - step_count: actuator.step_count(), - index: 0, - }; - actuator_vec.push(attrs) + for output_type in [ + OutputType::Constrict, + OutputType::Temperature, + OutputType::Led, + OutputType::Oscillate, + OutputType::Position, + OutputType::Rotate, + OutputType::Spray, + OutputType::Vibrate, + ] { + if let Some(limits) = feature.get_output_limits(output_type) { + let attrs = ClientGenericDeviceMessageAttributesV3 { + feature_descriptor: feature.description().to_owned(), + actuator_type: output_type, + step_count: limits.step_count(), + index: 0, }; - if let Some(x) = output_map.constrict().as_ref() { - create_actuator(OutputType::Constrict, x) - } - if let Some(x) = output_map.temperature().as_ref() { - create_actuator(OutputType::Temperature, x) - } - if let Some(x) = output_map.led().as_ref() { - create_actuator(OutputType::Led, x) - } - if let Some(x) = output_map.oscillate().as_ref() { - create_actuator(OutputType::Oscillate, x) - } - if let Some(x) = output_map.position().as_ref() { - create_actuator(OutputType::Position, x) - } - if let Some(x) = output_map.rotate().as_ref() { - create_actuator(OutputType::Rotate, x) - } - if let Some(x) = output_map.spray().as_ref() { - create_actuator(OutputType::Spray, x) - } - if let Some(x) = output_map.vibrate().as_ref() { - create_actuator(OutputType::Vibrate, x) + actuator_vec.push(attrs); } } actuator_vec @@ -292,15 +277,13 @@ impl From> for ClientDeviceMessageAttributesV3 { .iter() .flat_map(|feature| { let mut actuator_vec = vec![]; - if let Some(output_map) = feature.output() - && let Some(actuator) = output_map.rotate() - && *actuator.value().start() < 0 + if let Some(DeviceFeatureOutput::Rotate(actuator)) = feature.get_output(OutputType::Rotate) + && actuator.value().start() < 0 { - let actuator_type = OutputType::Rotate; let attrs = ClientGenericDeviceMessageAttributesV3 { feature_descriptor: feature.description().to_owned(), - actuator_type, - step_count: actuator.step_count(), + actuator_type: OutputType::Rotate, + step_count: actuator.value().end() as u32, index: 0, }; actuator_vec.push(attrs) @@ -313,14 +296,13 @@ impl From> for ClientDeviceMessageAttributesV3 { .iter() .flat_map(|feature| { let mut actuator_vec = vec![]; - if let Some(output_map) = feature.output() - && let Some(actuator) = output_map.hw_position_with_duration() + if let Some(DeviceFeatureOutput::HwPositionWithDuration(actuator)) = + feature.get_output(OutputType::HwPositionWithDuration) { - let actuator_type = OutputType::Position; let attrs = ClientGenericDeviceMessageAttributesV3 { feature_descriptor: feature.description().to_owned(), - actuator_type, - step_count: actuator.step_count(), + actuator_type: OutputType::Position, + step_count: actuator.value().end() as u32, index: 0, }; actuator_vec.push(attrs) @@ -334,16 +316,19 @@ impl From> for ClientDeviceMessageAttributesV3 { .iter() .map(|feature| { let mut sensor_vec = vec![]; - if let Some(sensor_map) = feature.input() { + if let Some(DeviceFeatureInput::Battery(battery)) = feature.get_input(InputType::Battery) + { // Only convert Battery backwards. Other sensors weren't really built for v3 and we // never recommended using them or implemented much for them. - if let Some(battery) = sensor_map.battery() - && battery.command().contains(&InputCommandType::Read) - { + if battery.command().contains(InputCommandType::Read) { sensor_vec.push(SensorDeviceMessageAttributesV3 { feature_descriptor: feature.description().to_owned(), sensor_type: InputType::Battery, - sensor_range: battery.value().clone(), + sensor_range: battery + .value() + .iter() + .map(|r| r.start()..=r.end()) + .collect(), feature: feature.clone(), index: 0, }); diff --git a/crates/buttplug_server/src/message/v3/sensor_read_cmd.rs b/crates/buttplug_server/src/message/v3/sensor_read_cmd.rs index 3d85b5b4e..ebfda689d 100644 --- a/crates/buttplug_server/src/message/v3/sensor_read_cmd.rs +++ b/crates/buttplug_server/src/message/v3/sensor_read_cmd.rs @@ -86,7 +86,7 @@ impl TryFromDeviceAttributes for CheckedInputCmdV4 { } else if let Some((feature_index, feature)) = features .features() .iter() - .find(|(_, p)| p.input().as_ref().is_some_and(|x| x.battery().is_some())) + .find(|(_, p)| p.contains_input(InputType::Battery)) { Ok(CheckedInputCmdV4::new( msg.id(), diff --git a/crates/buttplug_server/src/message/v3/server_device_message_attributes.rs b/crates/buttplug_server/src/message/v3/server_device_message_attributes.rs index 3632f2784..781585a69 100644 --- a/crates/buttplug_server/src/message/v3/server_device_message_attributes.rs +++ b/crates/buttplug_server/src/message/v3/server_device_message_attributes.rs @@ -7,7 +7,11 @@ use crate::message::v1::NullDeviceMessageAttributesV1; use buttplug_core::message::{InputType, OutputType}; -use buttplug_server_device_config::ServerDeviceFeature; +use buttplug_server_device_config::{ + ServerDeviceFeature, + ServerDeviceFeatureInput, + ServerDeviceFeatureOutput, +}; use getset::{Getters, MutGetters, Setters}; use std::ops::RangeInclusive; @@ -57,47 +61,25 @@ impl From> for ServerDeviceMessageAttributesV3 { let scalar_attrs: Vec = features .iter() .flat_map(|feature| { - let mut actuator_vec = vec![]; - if let Some(output_map) = feature.output() { - let mut create_attribute = |actuator_type, step_count| { - let actuator_type = actuator_type; - let attrs = ServerGenericDeviceMessageAttributesV3 { - feature_descriptor: feature.description().to_owned(), + feature + .output + .iter() + .filter(|output| !matches!(output, ServerDeviceFeatureOutput::HwPositionWithDuration(_))) + .map(|output| { + let actuator_type = output.output_type(); + let step_count = match output { + ServerDeviceFeatureOutput::Position(p) => p.value.step_count(), + _ => output.as_value_properties().unwrap().value.step_count(), + }; + ServerGenericDeviceMessageAttributesV3 { + feature_descriptor: feature.description.clone(), actuator_type, step_count, feature: feature.clone(), index: 0, - }; - actuator_vec.push(attrs) - }; - // TODO oh come on just make a fucking iterator here. At least, once we figure out the - // unifying trait we can use to make an iterator on this. - if let Some(attr) = output_map.constrict().as_ref() { - create_attribute(OutputType::Constrict, attr.value().step_count()) - } - if let Some(attr) = output_map.oscillate().as_ref() { - create_attribute(OutputType::Oscillate, attr.value().step_count()) - } - if let Some(attr) = output_map.position().as_ref() { - create_attribute(OutputType::Position, attr.value().step_count()) - } - if let Some(attr) = output_map.rotate().as_ref() { - create_attribute(OutputType::Rotate, attr.value().step_count()) - } - if let Some(attr) = output_map.temperature().as_ref() { - create_attribute(OutputType::Temperature, attr.value().step_count()) - } - if let Some(attr) = output_map.led().as_ref() { - create_attribute(OutputType::Led, attr.value().step_count()) - } - if let Some(attr) = output_map.vibrate().as_ref() { - create_attribute(OutputType::Vibrate, attr.value().step_count()) - } - if let Some(attr) = output_map.spray().as_ref() { - create_attribute(OutputType::Spray, attr.value().step_count()) - } - } - actuator_vec + } + }) + .collect::>() }) .collect(); @@ -107,20 +89,16 @@ impl From> for ServerDeviceMessageAttributesV3 { .iter() .flat_map(|feature| { let mut actuator_vec = vec![]; - if let Some(output_map) = feature.output() - && let Some(actuator) = output_map.rotate() - && *actuator.value().base().start() < 0 + if let Some(ServerDeviceFeatureOutput::Rotate(r)) = feature.get_output(OutputType::Rotate) + && r.value.base.start() < 0 { - let actuator_type = OutputType::Rotate; - let step_count = actuator.value().step_count(); - let attrs = ServerGenericDeviceMessageAttributesV3 { - feature_descriptor: feature.description().to_owned(), - actuator_type, - step_count, + actuator_vec.push(ServerGenericDeviceMessageAttributesV3 { + feature_descriptor: feature.description.clone(), + actuator_type: OutputType::Rotate, + step_count: r.value.step_count(), feature: feature.clone(), index: 0, - }; - actuator_vec.push(attrs) + }); } actuator_vec }) @@ -130,19 +108,16 @@ impl From> for ServerDeviceMessageAttributesV3 { .iter() .flat_map(|feature| { let mut actuator_vec = vec![]; - if let Some(output_map) = feature.output() - && let Some(actuator) = output_map.hw_position_with_duration() + if let Some(ServerDeviceFeatureOutput::HwPositionWithDuration(p)) = + feature.get_output(OutputType::HwPositionWithDuration) { - let actuator_type = OutputType::Position; - let step_count = actuator.value().step_count(); - let attrs = ServerGenericDeviceMessageAttributesV3 { - feature_descriptor: feature.description().to_owned(), - actuator_type, - step_count, + actuator_vec.push(ServerGenericDeviceMessageAttributesV3 { + feature_descriptor: feature.description.clone(), + actuator_type: OutputType::Position, + step_count: p.value.step_count(), feature: feature.clone(), index: 0, - }; - actuator_vec.push(attrs) + }); } actuator_vec }) @@ -153,15 +128,15 @@ impl From> for ServerDeviceMessageAttributesV3 { .iter() .map(|feature| { let mut sensor_vec = vec![]; - if let Some(sensor_map) = feature.input() - && let Some(battery) = sensor_map.battery() + if let Some(ServerDeviceFeatureInput::Battery(battery)) = + feature.get_input(InputType::Battery) { // Only convert Battery backwards. Other sensors weren't really built for v3 and we // never recommended using them or implemented much for them. sensor_vec.push(ServerSensorDeviceMessageAttributesV3 { - feature_descriptor: feature.description().to_owned(), + feature_descriptor: feature.description.clone(), sensor_type: InputType::Battery, - sensor_range: battery.value().clone(), + sensor_range: battery.value.iter().map(|r| r.start()..=r.end()).collect(), feature: feature.clone(), index: 0, }); diff --git a/crates/buttplug_server/src/message/v4/checked_input_cmd.rs b/crates/buttplug_server/src/message/v4/checked_input_cmd.rs index 82823223c..8408445e2 100644 --- a/crates/buttplug_server/src/message/v4/checked_input_cmd.rs +++ b/crates/buttplug_server/src/message/v4/checked_input_cmd.rs @@ -82,21 +82,15 @@ impl TryFromDeviceAttributes for CheckedInputCmdV4 { features: &crate::message::ServerDeviceAttributes, ) -> Result { if let Some(feature) = features.features().get(&msg.feature_index()) { - if let Some(sensor_map) = feature.input() { - if sensor_map.contains(msg.input_type()) { - Ok(CheckedInputCmdV4::new( - msg.id(), - msg.device_index(), - msg.feature_index(), - msg.input_type(), - msg.input_command(), - feature.id(), - )) - } else { - Err(ButtplugError::from( - ButtplugDeviceError::MessageNotSupported("InputCmd".to_string()), - )) - } + if feature.contains_input(msg.input_type()) { + Ok(CheckedInputCmdV4::new( + msg.id(), + msg.device_index(), + msg.feature_index(), + msg.input_type(), + msg.input_command(), + feature.id(), + )) } else { Err(ButtplugError::from( ButtplugDeviceError::MessageNotSupported("InputCmd".to_string()), diff --git a/crates/buttplug_server/src/message/v4/checked_output_cmd.rs b/crates/buttplug_server/src/message/v4/checked_output_cmd.rs index 5b2bccf76..a650d9d1c 100644 --- a/crates/buttplug_server/src/message/v4/checked_output_cmd.rs +++ b/crates/buttplug_server/src/message/v4/checked_output_cmd.rs @@ -119,41 +119,34 @@ impl TryFromDeviceAttributes for CheckedOutputCmdV4 { }; // Check to make sure the feature has an actuator that handles the data we've been passed - if let Some(output_map) = feature.output() { - let output_type = cmd.command().as_output_type(); - if output_map.is_disabled(output_type) { - return Err(ButtplugError::from( - ButtplugDeviceError::MessageNotSupported(format!( - "Output type {:?} is disabled for this device", - output_type - )), - )); - } - let value = cmd.command().value(); - let new_value = output_map - .calculate_from_value(output_type, value) - .map_err(|e| { - error!("{:?}", e); - ButtplugDeviceError::DeviceStepRangeError(0, value) - })?; - let mut new_command = cmd.command(); - new_command.set_value(new_value); - // We can't make a private trait impl to turn a ValueCmd into a CheckedValueCmd, and this - // is all about security, so we just copy. Silly, but it works for our needs in terms of - // making this a barrier. - Ok(Self { - id: cmd.id(), - feature_id: feature.id(), - device_index: cmd.device_index(), - feature_index: cmd.feature_index(), - output_command: new_command, - }) - } else { - Err(ButtplugError::from( - ButtplugDeviceError::MessageNotSupported( - ButtplugDeviceMessageNameV4::OutputCmd.to_string(), - ), - )) + let output_type = cmd.command().as_output_type(); + let output = feature.get_output(output_type).ok_or(ButtplugError::from( + ButtplugDeviceError::MessageNotSupported(ButtplugDeviceMessageNameV4::OutputCmd.to_string()), + ))?; + if output.is_disabled() { + return Err(ButtplugError::from( + ButtplugDeviceError::MessageNotSupported(format!( + "Output type {:?} is disabled for this device", + output_type + )), + )); } + let value = cmd.command().value(); + let new_value = output.calculate_from_value(value).map_err(|e| { + error!("{:?}", e); + ButtplugDeviceError::DeviceStepRangeError(0, value) + })?; + let mut new_command = cmd.command(); + new_command.set_value(new_value); + // We can't make a private trait impl to turn a ValueCmd into a CheckedValueCmd, and this + // is all about security, so we just copy. Silly, but it works for our needs in terms of + // making this a barrier. + Ok(Self { + id: cmd.id(), + feature_id: feature.id(), + device_index: cmd.device_index(), + feature_index: cmd.feature_index(), + output_command: new_command, + }) } } diff --git a/crates/buttplug_server/src/message/v4/checked_output_vec_cmd.rs b/crates/buttplug_server/src/message/v4/checked_output_vec_cmd.rs index 24e2a912a..733493129 100644 --- a/crates/buttplug_server/src/message/v4/checked_output_vec_cmd.rs +++ b/crates/buttplug_server/src/message/v4/checked_output_vec_cmd.rs @@ -27,6 +27,7 @@ use buttplug_core::{ OutputValue, }, }; +use buttplug_server_device_config::ServerDeviceFeatureOutput; use getset::{CopyGetters, Getters}; use super::checked_output_cmd::CheckedOutputCmdV4; @@ -90,12 +91,7 @@ impl TryFromDeviceAttributes for CheckedOutputVecCmdV4 let mut vibrate_features = features .features() .iter() - .filter(|(_, feature)| { - feature - .output() - .as_ref() - .is_some_and(|x| x.contains(OutputType::Vibrate)) - }) + .filter(|(_, feature)| feature.contains_output(OutputType::Vibrate)) .peekable(); // Check to make sure we have any vibrate attributes at all. @@ -110,11 +106,7 @@ impl TryFromDeviceAttributes for CheckedOutputVecCmdV4 for (index, feature) in vibrate_features { // if we've made it this far, we know we have actuators in a list let actuator = feature - .output() - .as_ref() - .unwrap() - .vibrate() - .as_ref() + .get_output(OutputType::Vibrate) .expect("Already confirmed we have vibrator for this feature"); // This doesn't need to run through a security check because we have to construct it to be // inherently secure anyways. @@ -124,7 +116,7 @@ impl TryFromDeviceAttributes for CheckedOutputVecCmdV4 *index, feature.id(), OutputCommand::Vibrate(OutputValue::new( - actuator.calculate_scaled_float(msg.speed()).map_err( + actuator.calculate_from_float(msg.speed()).map_err( |e: buttplug_server_device_config::ButtplugDeviceConfigError| { ButtplugMessageError::InvalidMessageContents(e.to_string()) }, @@ -178,17 +170,11 @@ impl TryFromDeviceAttributes for CheckedOutputVecCmdV4 { .find(|(_, f)| f.id() == feature.id()) .expect("Already checked existence") .0; - let actuator = feature - .output() - .as_ref() - .ok_or(ButtplugDeviceError::DeviceConfigurationError( - "Device configuration does not have Vibrate actuator available.".to_owned(), - ))? - .vibrate() - .as_ref() - .ok_or(ButtplugDeviceError::DeviceConfigurationError( + let actuator = feature.get_output(OutputType::Vibrate).ok_or( + ButtplugDeviceError::DeviceConfigurationError( "Device configuration does not have Vibrate actuator available.".to_owned(), - ))?; + ), + )?; cmds.push(CheckedOutputCmdV4::new( msg.id(), msg.device_index(), @@ -196,7 +182,7 @@ impl TryFromDeviceAttributes for CheckedOutputVecCmdV4 { feature.id(), OutputCommand::Vibrate(OutputValue::new( actuator - .calculate_scaled_float(vibrate_cmd.speed()) + .calculate_from_float(vibrate_cmd.speed()) .map_err(|e| ButtplugMessageError::InvalidMessageContents(e.to_string()))?, )), )) @@ -242,19 +228,16 @@ impl TryFromDeviceAttributes for CheckedOutputVecCmdV4 { .0; let output = feature .feature() - .output() - .as_ref() + .get_output(cmd.actuator_type()) .ok_or(ButtplugError::from( ButtplugDeviceError::MessageNotSupported("ScalarCmdV3".to_owned()), ))?; - let output_value = output - .calculate_from_float(cmd.actuator_type(), cmd.scalar()) - .map_err(|e| { - error!("{:?}", e); - ButtplugError::from(ButtplugDeviceError::MessageNotSupported( - "ScalarCmdV3".to_owned(), - )) - })?; + let output_value = output.calculate_from_float(cmd.scalar()).map_err(|e| { + error!("{:?}", e); + ButtplugError::from(ButtplugDeviceError::MessageNotSupported( + "ScalarCmdV3".to_owned(), + )) + })?; cmds.push(CheckedOutputCmdV4::new( msg.id(), msg.device_index(), @@ -296,21 +279,18 @@ impl TryFromDeviceAttributes for CheckedOutputVecCmdV4 { x.index(), ))? .feature(); - let actuator = f - .output() - .as_ref() - .ok_or(ButtplugError::from( - ButtplugDeviceError::DeviceFeatureMismatch( - "Device got LinearCmd command but has no actuators on Linear feature.".to_owned(), - ), - ))? - .hw_position_with_duration() - .as_ref() + let hw_pos = f + .get_output(OutputType::HwPositionWithDuration) .ok_or(ButtplugError::from( ButtplugDeviceError::DeviceFeatureMismatch( "Device got LinearCmd command but has no actuators on Linear feature.".to_owned(), ), ))?; + let actuator = if let ServerDeviceFeatureOutput::HwPositionWithDuration(p) = hw_pos { + p + } else { + unreachable!("get_output(HwPositionWithDuration) always returns HwPositionWithDuration") + }; cmds.push(CheckedOutputCmdV4::new( msg.device_index(), x.index(), @@ -369,25 +349,20 @@ impl TryFromDeviceAttributes for CheckedOutputVecCmdV4 { .find(|(_, f)| f.id() == feature.feature().id()) .expect("Already proved existence") .0; - let actuator = feature - .feature() - .output() - .as_ref() - .ok_or(ButtplugError::from( - ButtplugDeviceError::MessageNotSupported("RotateCmdV1".to_owned()), - ))? - .rotate() - .as_ref() - .ok_or(ButtplugError::from( - ButtplugDeviceError::MessageNotSupported("RotateCmdV1".to_owned()), - ))?; + let actuator = + feature + .feature() + .get_output(OutputType::Rotate) + .ok_or(ButtplugError::from( + ButtplugDeviceError::MessageNotSupported("RotateCmdV1".to_owned()), + ))?; cmds.push(CheckedOutputCmdV4::new( msg.id(), msg.device_index(), *idx, feature.feature.id(), OutputCommand::Rotate(OutputValue::new( - actuator.calculate_scaled_float(cmd.speed()).map_err(|_| { + actuator.calculate_from_float(cmd.speed()).map_err(|_| { ButtplugError::from(ButtplugMessageError::InvalidMessageContents( "Position should be 0.0 < x < 1.0".to_owned(), )) diff --git a/crates/buttplug_server_device_config/Cargo.toml b/crates/buttplug_server_device_config/Cargo.toml index 031b0e4df..b91c1106c 100644 --- a/crates/buttplug_server_device_config/Cargo.toml +++ b/crates/buttplug_server_device_config/Cargo.toml @@ -34,6 +34,7 @@ jsonschema = { version = "0.45.0", default-features = false } uuid = { version = "1.22.0", features = ["serde", "v4"] } strum_macros = "0.28.0" strum = "0.28.0" +enumflags2 = "0.7.12" [build-dependencies] serde_yaml = "0.9.34" diff --git a/crates/buttplug_server_device_config/src/device_config_file/feature.rs b/crates/buttplug_server_device_config/src/device_config_file/feature.rs index 5210a38c8..e9115c0f4 100644 --- a/crates/buttplug_server_device_config/src/device_config_file/feature.rs +++ b/crates/buttplug_server_device_config/src/device_config_file/feature.rs @@ -5,7 +5,13 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use std::ops::RangeInclusive; +use buttplug_core::{ + message::OutputType, + util::{ + range::RangeInclusive, + small_vec_enum_map::{SmallVecEnumMap, VariantKey}, + }, +}; use crate::{ ButtplugDeviceConfigError, @@ -17,7 +23,6 @@ use crate::{ ServerDeviceFeatureOutputPositionProperties, ServerDeviceFeatureOutputValueProperties, }; -use buttplug_core::util::range_serialize::option_range_serialize; use getset::{CopyGetters, Getters}; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -37,10 +42,7 @@ impl BaseFeatureSettings { #[derive(Serialize, Deserialize, Clone, Debug)] struct UserDeviceFeatureOutputValueProperties { - #[serde( - skip_serializing_if = "Option::is_none", - serialize_with = "option_range_serialize" - )] + #[serde(skip_serializing_if = "Option::is_none")] value: Option>, #[serde(default)] disabled: bool, @@ -51,9 +53,8 @@ impl UserDeviceFeatureOutputValueProperties { &self, base: &ServerDeviceFeatureOutputValueProperties, ) -> Result { - let range = RangeWithLimit::try_new(base.value().base(), &self.value)?; Ok(ServerDeviceFeatureOutputValueProperties::new( - &range, + RangeWithLimit::try_new(base.value.base, self.value)?, self.disabled, )) } @@ -62,18 +63,15 @@ impl UserDeviceFeatureOutputValueProperties { impl From<&ServerDeviceFeatureOutputValueProperties> for UserDeviceFeatureOutputValueProperties { fn from(value: &ServerDeviceFeatureOutputValueProperties) -> Self { Self { - value: value.value().user().clone(), - disabled: value.disabled(), + value: value.value.user, + disabled: value.disabled, } } } #[derive(Serialize, Deserialize, Clone, Debug)] struct UserDeviceFeatureOutputPositionProperties { - #[serde( - skip_serializing_if = "Option::is_none", - serialize_with = "option_range_serialize" - )] + #[serde(skip_serializing_if = "Option::is_none")] value: Option>, #[serde(default)] disabled: bool, @@ -86,9 +84,8 @@ impl UserDeviceFeatureOutputPositionProperties { &self, base: &ServerDeviceFeatureOutputPositionProperties, ) -> Result { - let value = RangeWithLimit::try_new(base.value().base(), &self.value)?; Ok(ServerDeviceFeatureOutputPositionProperties::new( - &value, + RangeWithLimit::try_new(base.value.base, self.value)?, self.disabled, self.reverse, )) @@ -100,24 +97,18 @@ impl From<&ServerDeviceFeatureOutputPositionProperties> { fn from(value: &ServerDeviceFeatureOutputPositionProperties) -> Self { Self { - value: value.value().user().clone(), - reverse: value.reverse_position(), - disabled: value.disabled(), + value: value.value.user, + reverse: value.reverse_position, + disabled: value.disabled, } } } #[derive(Serialize, Deserialize, Clone, Debug)] struct UserDeviceFeatureOutputHwPositionWithDurationProperties { - #[serde( - skip_serializing_if = "Option::is_none", - serialize_with = "option_range_serialize" - )] + #[serde(skip_serializing_if = "Option::is_none")] value: Option>, - #[serde( - skip_serializing_if = "Option::is_none", - serialize_with = "option_range_serialize" - )] + #[serde(skip_serializing_if = "Option::is_none")] duration: Option>, #[serde(default)] disabled: bool, @@ -131,12 +122,10 @@ impl UserDeviceFeatureOutputHwPositionWithDurationProperties { base: &ServerDeviceFeatureOutputHwPositionWithDurationProperties, ) -> Result { - let value = RangeWithLimit::try_new(base.value().base(), &self.value)?; - let duration = RangeWithLimit::try_new(base.duration().base(), &self.duration)?; Ok( ServerDeviceFeatureOutputHwPositionWithDurationProperties::new( - &value, - &duration, + RangeWithLimit::try_new(base.value.base, self.value)?, + RangeWithLimit::try_new(base.duration.base, self.duration)?, self.disabled, self.reverse, ), @@ -149,89 +138,105 @@ impl From<&ServerDeviceFeatureOutputHwPositionWithDurationProperties> { fn from(value: &ServerDeviceFeatureOutputHwPositionWithDurationProperties) -> Self { Self { - value: value.value().user().clone(), - duration: value.duration().user().clone(), - reverse: value.reverse_position(), - disabled: value.disabled(), + value: value.value.user, + duration: value.duration.user, + reverse: value.reverse_position, + disabled: value.disabled, } } } #[derive(Serialize, Deserialize, Clone, Debug)] -struct UserDeviceFeatureOutput { - #[serde(skip_serializing_if = "Option::is_none")] - vibrate: Option, - #[serde(skip_serializing_if = "Option::is_none")] - rotate: Option, - #[serde(skip_serializing_if = "Option::is_none")] - oscillate: Option, - #[serde(skip_serializing_if = "Option::is_none")] - constrict: Option, - #[serde(skip_serializing_if = "Option::is_none")] - temperature: Option, - #[serde(skip_serializing_if = "Option::is_none")] - led: Option, - #[serde(skip_serializing_if = "Option::is_none")] - position: Option, - #[serde(skip_serializing_if = "Option::is_none")] - hw_position_with_duration: Option, - #[serde(skip_serializing_if = "Option::is_none")] - spray: Option, -} - -/// Macro to apply user overrides to base output fields. -/// If base has the field, applies user override if present, otherwise keeps base value. -macro_rules! merge_output_field { - ($output:expr, $base:expr, $user:expr, $field:ident, $setter:ident) => { - if let Some(base_val) = $base.$field() { - if let Some(user_val) = &$user.$field { - $output.$setter(Some(user_val.with_base_properties(base_val)?)); - } else { - $output.$setter(Some(base_val.clone())); - } - } - }; +#[serde(rename_all = "snake_case")] +enum UserDeviceFeatureOutput { + Vibrate(UserDeviceFeatureOutputValueProperties), + Rotate(UserDeviceFeatureOutputValueProperties), + Oscillate(UserDeviceFeatureOutputValueProperties), + Constrict(UserDeviceFeatureOutputValueProperties), + Temperature(UserDeviceFeatureOutputValueProperties), + Led(UserDeviceFeatureOutputValueProperties), + Spray(UserDeviceFeatureOutputValueProperties), + Position(UserDeviceFeatureOutputPositionProperties), + HwPositionWithDuration(UserDeviceFeatureOutputHwPositionWithDurationProperties), } impl UserDeviceFeatureOutput { pub fn with_base_output( &self, - base_output: &ServerDeviceFeatureOutput, + base: &ServerDeviceFeatureOutput, ) -> Result { - let mut output = ServerDeviceFeatureOutput::default(); - - merge_output_field!(output, base_output, self, vibrate, set_vibrate); - merge_output_field!(output, base_output, self, rotate, set_rotate); - merge_output_field!(output, base_output, self, oscillate, set_oscillate); - merge_output_field!(output, base_output, self, constrict, set_constrict); - merge_output_field!(output, base_output, self, temperature, set_temperature); - merge_output_field!(output, base_output, self, led, set_led); - merge_output_field!(output, base_output, self, spray, set_spray); - merge_output_field!(output, base_output, self, position, set_position); - merge_output_field!( - output, - base_output, - self, - hw_position_with_duration, - set_hw_position_with_duration - ); + match (self, base) { + (Self::Vibrate(u), ServerDeviceFeatureOutput::Vibrate(b)) => Ok( + ServerDeviceFeatureOutput::Vibrate(u.with_base_properties(b)?), + ), + (Self::Rotate(u), ServerDeviceFeatureOutput::Rotate(b)) => Ok( + ServerDeviceFeatureOutput::Rotate(u.with_base_properties(b)?), + ), + (Self::Oscillate(u), ServerDeviceFeatureOutput::Oscillate(b)) => Ok( + ServerDeviceFeatureOutput::Oscillate(u.with_base_properties(b)?), + ), + (Self::Constrict(u), ServerDeviceFeatureOutput::Constrict(b)) => Ok( + ServerDeviceFeatureOutput::Constrict(u.with_base_properties(b)?), + ), + (Self::Temperature(u), ServerDeviceFeatureOutput::Temperature(b)) => Ok( + ServerDeviceFeatureOutput::Temperature(u.with_base_properties(b)?), + ), + (Self::Led(u), ServerDeviceFeatureOutput::Led(b)) => { + Ok(ServerDeviceFeatureOutput::Led(u.with_base_properties(b)?)) + } + (Self::Spray(u), ServerDeviceFeatureOutput::Spray(b)) => { + Ok(ServerDeviceFeatureOutput::Spray(u.with_base_properties(b)?)) + } + (Self::Position(u), ServerDeviceFeatureOutput::Position(b)) => Ok( + ServerDeviceFeatureOutput::Position(u.with_base_properties(b)?), + ), + (Self::HwPositionWithDuration(u), ServerDeviceFeatureOutput::HwPositionWithDuration(b)) => { + Ok(ServerDeviceFeatureOutput::HwPositionWithDuration( + u.with_base_properties(b)?, + )) + } + _ => Err(ButtplugDeviceConfigError::InvalidOutputTypeConversion( + format!( + "user output type {:?} does not match base output type {:?}", + self.variant_key(), + base.output_type() + ), + )), + } + } +} - Ok(output) +impl VariantKey for UserDeviceFeatureOutput { + type Key = OutputType; + fn variant_key(&self) -> OutputType { + match self { + Self::Vibrate(_) => OutputType::Vibrate, + Self::Rotate(_) => OutputType::Rotate, + Self::Oscillate(_) => OutputType::Oscillate, + Self::Constrict(_) => OutputType::Constrict, + Self::Temperature(_) => OutputType::Temperature, + Self::Led(_) => OutputType::Led, + Self::Spray(_) => OutputType::Spray, + Self::Position(_) => OutputType::Position, + Self::HwPositionWithDuration(_) => OutputType::HwPositionWithDuration, + } } } impl From<&ServerDeviceFeatureOutput> for UserDeviceFeatureOutput { fn from(value: &ServerDeviceFeatureOutput) -> Self { - Self { - vibrate: value.vibrate().as_ref().map(|x| x.into()), - rotate: value.rotate().as_ref().map(|x| x.into()), - oscillate: value.oscillate().as_ref().map(|x| x.into()), - constrict: value.constrict().as_ref().map(|x| x.into()), - temperature: value.temperature().as_ref().map(|x| x.into()), - led: value.led().as_ref().map(|x| x.into()), - position: value.position().as_ref().map(|x| x.into()), - hw_position_with_duration: value.hw_position_with_duration().as_ref().map(|x| x.into()), - spray: value.spray().as_ref().map(|x| x.into()), + match value { + ServerDeviceFeatureOutput::Vibrate(p) => Self::Vibrate(p.into()), + ServerDeviceFeatureOutput::Rotate(p) => Self::Rotate(p.into()), + ServerDeviceFeatureOutput::Oscillate(p) => Self::Oscillate(p.into()), + ServerDeviceFeatureOutput::Constrict(p) => Self::Constrict(p.into()), + ServerDeviceFeatureOutput::Temperature(p) => Self::Temperature(p.into()), + ServerDeviceFeatureOutput::Led(p) => Self::Led(p.into()), + ServerDeviceFeatureOutput::Spray(p) => Self::Spray(p.into()), + ServerDeviceFeatureOutput::Position(p) => Self::Position(p.into()), + ServerDeviceFeatureOutput::HwPositionWithDuration(p) => { + Self::HwPositionWithDuration(p.into()) + } } } } @@ -244,11 +249,11 @@ pub struct ConfigBaseDeviceFeature { #[serde(default)] description: String, #[getset(get = "pub")] - #[serde(skip_serializing_if = "Option::is_none")] - output: Option, + #[serde(skip_serializing_if = "SmallVecEnumMap::is_empty", default)] + output: SmallVecEnumMap, #[getset(get = "pub")] - #[serde(skip_serializing_if = "Option::is_none")] - input: Option, + #[serde(skip_serializing_if = "SmallVecEnumMap::is_empty", default)] + input: SmallVecEnumMap, #[getset(get_copy = "pub")] id: Uuid, #[getset(get = "pub")] @@ -270,15 +275,14 @@ impl From for ServerDeviceFeature { } } -#[derive(Clone, Debug, Default, Getters, Serialize, Deserialize, CopyGetters)] +#[derive(Clone, Debug, Default, Getters, CopyGetters, Serialize, Deserialize)] pub struct ConfigUserDeviceFeature { #[getset(get_copy = "pub")] id: Uuid, #[getset(get_copy = "pub")] base_id: Uuid, - #[getset(get = "pub")] - #[serde(rename = "output", skip_serializing_if = "Option::is_none")] - output: Option, + #[serde(skip_serializing_if = "SmallVecEnumMap::is_empty", default)] + output: SmallVecEnumMap, } impl ConfigUserDeviceFeature { @@ -286,24 +290,26 @@ impl ConfigUserDeviceFeature { &self, base_feature: &ServerDeviceFeature, ) -> Result { - let output = if let Some(o) = &self.output { - if let Some(base) = base_feature.output() { - Some(o.with_base_output(base)?) - } else { - None - } - } else { - // No user output override: inherit the base feature's output unchanged. - base_feature.output().clone() - }; + let output: SmallVecEnumMap = base_feature + .output + .iter() + .map(|base_out| { + let key = base_out.variant_key(); + if let Some(user_out) = self.output.find_by_key(&key) { + user_out.with_base_output(base_out) + } else { + Ok(base_out.clone()) + } + }) + .collect::, _>>()?; Ok(ServerDeviceFeature::new( base_feature.index(), - base_feature.description(), + &base_feature.description, self.id, Some(self.base_id), - base_feature.alt_protocol_index(), + base_feature.alt_protocol_index, &output, - base_feature.input(), + &base_feature.input, )) } } @@ -315,9 +321,9 @@ impl TryFrom<&ServerDeviceFeature> for ConfigUserDeviceFeature { Ok(Self { id: value.id(), base_id: value - .base_id() + .base_id .ok_or(ButtplugDeviceConfigError::MissingBaseId)?, - output: value.output().as_ref().map(|x| x.into()), + output: value.output.iter().map(Into::into).collect(), }) } } diff --git a/crates/buttplug_server_device_config/src/server_device_feature.rs b/crates/buttplug_server_device_config/src/server_device_feature.rs index 2e3651adb..4742db894 100644 --- a/crates/buttplug_server_device_config/src/server_device_feature.rs +++ b/crates/buttplug_server_device_config/src/server_device_feature.rs @@ -7,108 +7,67 @@ use crate::ButtplugDeviceConfigError; -use buttplug_core::message::{ - DeviceFeature, - DeviceFeatureInput, - DeviceFeatureInputBuilder, - DeviceFeatureInputProperties, - DeviceFeatureOutput, - DeviceFeatureOutputBuilder, - DeviceFeatureOutputHwPositionWithDurationProperties, - DeviceFeatureOutputValueProperties, - InputCommandType, - InputType, - OutputType, +use buttplug_core::{ + message::{ + DeviceFeature, + DeviceFeatureInput, + DeviceFeatureInputProperties, + DeviceFeatureOutput, + DeviceFeatureOutputHwPositionWithDurationProperties, + DeviceFeatureOutputValueProperties, + InputCommandType, + InputType, + OutputType, + }, + util::{ + range::RangeInclusive, + small_vec_enum_map::{SmallVecEnumMap, VariantKey}, + }, }; -use getset::{CopyGetters, Getters, Setters}; -use serde::{ - Deserialize, - Serialize, - Serializer, - de::{self, Deserializer, SeqAccess, Visitor}, - ser::SerializeSeq, -}; -use std::{collections::HashSet, fmt, ops::RangeInclusive}; -use uuid::Uuid; - -/// Serde helper module for serializing/deserializing Vec> as [[start, end], ...] -mod range_vec_serde { - use super::*; - - pub fn serialize(ranges: &Vec>, serializer: S) -> Result - where - S: Serializer, - { - let arrays: Vec<[i32; 2]> = ranges.iter().map(|r| [*r.start(), *r.end()]).collect(); - arrays.serialize(serializer) - } - - pub fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> - where - D: Deserializer<'de>, - { - struct RangeVecVisitor; +use enumflags2::BitFlags; +use getset::{CopyGetters, Getters}; +use serde::{Deserialize, Serialize}; - impl<'de> Visitor<'de> for RangeVecVisitor { - type Value = Vec>; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("an array of two-element arrays [[start, end], ...]") - } - - fn visit_seq(self, mut seq: A) -> Result - where - A: SeqAccess<'de>, - { - let mut ranges = Vec::new(); - while let Some([start, end]) = seq.next_element::<[i32; 2]>()? { - ranges.push(start..=end); - } - Ok(ranges) - } - } - - deserializer.deserialize_seq(RangeVecVisitor) - } -} +use strum_macros::{Display, EnumDiscriminants, EnumIter, EnumString}; +use uuid::Uuid; /// Holds a combination of ranges. Base range is defined in the base device config, user range is /// defined by the user later to be a sub-range of the base range. User range only stores in u32, /// ranges with negatives (i.e. rotate with direction) are considered to be symettric around 0, we /// let the system handle that conversion. -#[derive(Debug, Clone, Getters)] -#[getset(get = "pub")] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(transparent)] pub struct RangeWithLimit { - base: RangeInclusive, - internal_base: RangeInclusive, - user: Option>, + pub base: RangeInclusive, + #[serde(skip)] + pub user: Option>, } impl From> for RangeWithLimit { fn from(value: RangeInclusive) -> Self { - Self::new(&value) + Self::new(value) } } impl RangeWithLimit { - pub fn new(base: &RangeInclusive) -> Self { - Self { - base: base.clone(), - internal_base: RangeInclusive::new(0, *base.end() as u32), - user: None, - } + pub fn new(base: RangeInclusive) -> Self { + Self { base, user: None } } - pub fn new_with_user(base: &RangeInclusive, user: &Option>) -> Self { - Self { - base: base.clone(), - internal_base: RangeInclusive::new(0, *base.end() as u32), - user: user.clone(), + pub fn new_with_user(base: RangeInclusive, user: Option>) -> Self { + Self { base, user } + } + + /// Returns the effective u32 range for calculations + pub fn internal(&self) -> RangeInclusive { + match self.user { + Some(user_range) => user_range, + None => RangeInclusive::new(0, self.base.end() as u32), } } pub fn step_limit(&self) -> RangeInclusive { - if *self.base.start() < 0 { + if self.base.start() < 0 { RangeInclusive::new(-(self.step_count() as i32), self.step_count() as i32) } else { RangeInclusive::new(0, self.step_count() as i32) @@ -117,104 +76,50 @@ impl RangeWithLimit { pub fn step_count(&self) -> u32 { if let Some(user) = &self.user { - *user.end() - *user.start() + user.end() - user.start() } else { - *self.base.end() as u32 + self.base.end() as u32 } } pub fn try_new( - base: &RangeInclusive, - user: &Option>, + base: RangeInclusive, + user: Option>, ) -> Result { - let truncated_base = RangeInclusive::new(0, *base.end() as u32); + let truncated_base = RangeInclusive::new(0, base.end() as u32); if let Some(user) = user { if user.is_empty() { Err(ButtplugDeviceConfigError::InvalidUserRange) - } else if *user.start() < *truncated_base.start() - || *user.end() > *truncated_base.end() - || *user.start() > *truncated_base.end() - || *user.end() < *truncated_base.start() + } else if user.start() < truncated_base.start() + || user.end() > truncated_base.end() + || user.start() > truncated_base.end() + || user.end() < truncated_base.start() { Err(ButtplugDeviceConfigError::InvalidUserRange) } else { Ok(Self { - base: (*base).clone(), - internal_base: truncated_base, - user: Some((*user).clone()), + base, + user: Some(user), }) } } else if base.is_empty() { Err(ButtplugDeviceConfigError::BaseRangeRequired) } else { - Ok(Self { - base: (*base).clone(), - internal_base: truncated_base, - user: None, - }) - } - } -} - -impl Serialize for RangeWithLimit { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let mut seq = serializer.serialize_seq(Some(2))?; - seq.serialize_element(self.base.start())?; - seq.serialize_element(self.base.end())?; - seq.end() - } -} - -impl<'de> Deserialize<'de> for RangeWithLimit { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct RangeVisitor; - - impl<'de> Visitor<'de> for RangeVisitor { - type Value = RangeWithLimit; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a two-element array [start, end]") - } - - fn visit_seq(self, mut seq: A) -> Result - where - A: SeqAccess<'de>, - { - let start: i32 = seq - .next_element()? - .ok_or_else(|| de::Error::invalid_length(0, &self))?; - let end: i32 = seq - .next_element()? - .ok_or_else(|| de::Error::invalid_length(1, &self))?; - Ok(RangeWithLimit::new(&(start..=end))) - } + Ok(Self { base, user: None }) } - - deserializer.deserialize_seq(RangeVisitor) } } -#[derive(Debug, Clone, Getters, CopyGetters, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ServerDeviceFeatureOutputValueProperties { - #[getset(get = "pub")] - value: RangeWithLimit, - #[getset(get_copy = "pub")] + pub value: RangeWithLimit, #[serde(default, skip_serializing_if = "std::ops::Not::not")] - disabled: bool, + pub disabled: bool, } impl ServerDeviceFeatureOutputValueProperties { - pub fn new(value: &RangeWithLimit, disabled: bool) -> Self { - Self { - value: value.clone(), - disabled, - } + pub fn new(value: RangeWithLimit, disabled: bool) -> Self { + Self { value, disabled } } pub fn calculate_scaled_float(&self, value: f64) -> Result { @@ -229,14 +134,10 @@ impl ServerDeviceFeatureOutputValueProperties { // We'll get a number from 0-x here. We'll need to calculate it with in the range we have. We'll // consider negative ranges symmetric. pub fn calculate_scaled_value(&self, value: i32) -> Result { - let range = if let Some(user_range) = self.value.user() { - user_range - } else { - self.value.internal_base() - }; + let range = self.value.internal(); let current_value = value.unsigned_abs(); let mult = if value < 0 { -1 } else { 1 }; - if value != 0 && range.contains(&(range.start() + current_value)) { + if value != 0 && range.contains(range.start() + current_value) { Ok((range.start() + current_value) as i32 * mult) } else if value == 0 { Ok(0) @@ -251,26 +152,23 @@ impl ServerDeviceFeatureOutputValueProperties { impl From<&ServerDeviceFeatureOutputValueProperties> for DeviceFeatureOutputValueProperties { fn from(val: &ServerDeviceFeatureOutputValueProperties) -> Self { - DeviceFeatureOutputValueProperties::new(&val.value().step_limit()) + DeviceFeatureOutputValueProperties::new(val.value.step_limit()) } } #[derive(Debug, Clone, Getters, CopyGetters, Serialize, Deserialize)] pub struct ServerDeviceFeatureOutputPositionProperties { - #[getset(get = "pub")] - value: RangeWithLimit, - #[getset(get_copy = "pub")] + pub value: RangeWithLimit, #[serde(default, skip_serializing_if = "std::ops::Not::not")] - disabled: bool, - #[getset(get_copy = "pub")] + pub disabled: bool, #[serde(default, skip_serializing_if = "std::ops::Not::not")] - reverse_position: bool, + pub reverse_position: bool, } impl ServerDeviceFeatureOutputPositionProperties { - pub fn new(value: &RangeWithLimit, disabled: bool, reverse_position: bool) -> Self { + pub fn new(value: RangeWithLimit, disabled: bool, reverse_position: bool) -> Self { Self { - value: value.clone(), + value, disabled, reverse_position, } @@ -288,12 +186,8 @@ impl ServerDeviceFeatureOutputPositionProperties { // We'll get a number from 0-x here. We'll need to calculate it with in the range we have. pub fn calculate_scaled_value(&self, input: u32) -> Result { - let range = if let Some(user_range) = self.value.user() { - user_range - } else { - self.value.internal_base() - }; - if range.contains(&(range.start() + input)) { + let range = self.value.internal(); + if range.contains(range.start() + input) { if self.reverse_position { Ok(range.end() - input) } else { @@ -310,34 +204,30 @@ impl ServerDeviceFeatureOutputPositionProperties { impl From<&ServerDeviceFeatureOutputPositionProperties> for DeviceFeatureOutputValueProperties { fn from(val: &ServerDeviceFeatureOutputPositionProperties) -> Self { - DeviceFeatureOutputValueProperties::new(&val.value().step_limit()) + DeviceFeatureOutputValueProperties::new(val.value.step_limit()) } } -#[derive(Debug, Clone, Getters, CopyGetters, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ServerDeviceFeatureOutputHwPositionWithDurationProperties { - #[getset(get = "pub")] - value: RangeWithLimit, - #[getset(get = "pub")] - duration: RangeWithLimit, - #[getset(get_copy = "pub")] + pub value: RangeWithLimit, + pub duration: RangeWithLimit, #[serde(default, skip_serializing_if = "std::ops::Not::not")] - disabled: bool, - #[getset(get_copy = "pub")] + pub disabled: bool, #[serde(default, skip_serializing_if = "std::ops::Not::not")] - reverse_position: bool, + pub reverse_position: bool, } impl ServerDeviceFeatureOutputHwPositionWithDurationProperties { pub fn new( - value: &RangeWithLimit, - duration: &RangeWithLimit, + value: RangeWithLimit, + duration: RangeWithLimit, disabled: bool, reverse_position: bool, ) -> Self { Self { - value: value.clone(), - duration: duration.clone(), + value, + duration, disabled, reverse_position, } @@ -349,12 +239,8 @@ impl ServerDeviceFeatureOutputHwPositionWithDurationProperties { // We'll get a number from 0-x here. We'll need to calculate it with in the range we have. pub fn calculate_scaled_value(&self, input: u32) -> Result { - let range = if let Some(user_range) = self.value.user() { - user_range - } else { - self.value.internal_base() - }; - if input > 0 && range.contains(&(range.start() + input)) { + let range = self.value.internal(); + if input > 0 && range.contains(range.start() + input) { if self.reverse_position { Ok(range.end() - input) } else { @@ -376,252 +262,153 @@ impl From<&ServerDeviceFeatureOutputHwPositionWithDurationProperties> { fn from(val: &ServerDeviceFeatureOutputHwPositionWithDurationProperties) -> Self { DeviceFeatureOutputHwPositionWithDurationProperties::new( - &val.value().step_limit(), - &val.duration().step_limit(), + val.value.step_limit(), + val.duration.step_limit(), ) } } -#[derive(Clone, Debug, Getters, Setters, Default, Serialize, Deserialize)] -#[serde(default)] -#[getset(get = "pub", set = "pub")] -pub struct ServerDeviceFeatureOutput { - #[serde(skip_serializing_if = "Option::is_none")] - vibrate: Option, - #[serde(skip_serializing_if = "Option::is_none")] - rotate: Option, - #[serde(skip_serializing_if = "Option::is_none")] - oscillate: Option, - #[serde(skip_serializing_if = "Option::is_none")] - constrict: Option, - #[serde(skip_serializing_if = "Option::is_none")] - temperature: Option, - #[serde(skip_serializing_if = "Option::is_none")] - led: Option, - #[serde(skip_serializing_if = "Option::is_none")] - position: Option, - #[serde(skip_serializing_if = "Option::is_none")] - hw_position_with_duration: Option, - #[serde(skip_serializing_if = "Option::is_none")] - spray: Option, +// ServerOutputType is auto-generated as the discriminant enum of ServerDeviceFeatureOutput. +// Adding or renaming a ServerDeviceFeatureOutput variant automatically updates ServerOutputType. +#[derive(Clone, Debug, Serialize, Deserialize, EnumDiscriminants)] +#[serde(rename_all = "snake_case")] +#[strum_discriminants(name(ServerOutputType))] +#[strum_discriminants(vis(pub(crate)))] +#[strum_discriminants(derive(Display, Hash, EnumIter, EnumString, Serialize, Deserialize))] +#[strum_discriminants(serde(rename_all = "snake_case"))] +pub enum ServerDeviceFeatureOutput { + Vibrate(ServerDeviceFeatureOutputValueProperties), + Rotate(ServerDeviceFeatureOutputValueProperties), + Oscillate(ServerDeviceFeatureOutputValueProperties), + Constrict(ServerDeviceFeatureOutputValueProperties), + Temperature(ServerDeviceFeatureOutputValueProperties), + Led(ServerDeviceFeatureOutputValueProperties), + Spray(ServerDeviceFeatureOutputValueProperties), + Position(ServerDeviceFeatureOutputPositionProperties), + HwPositionWithDuration(ServerDeviceFeatureOutputHwPositionWithDurationProperties), } impl ServerDeviceFeatureOutput { - pub fn contains(&self, output_type: OutputType) -> bool { - match output_type { - OutputType::Constrict => self.constrict.is_some(), - OutputType::Temperature => self.temperature.is_some(), - OutputType::Led => self.led.is_some(), - OutputType::Oscillate => self.oscillate.is_some(), - OutputType::Position => self.position.is_some(), - OutputType::HwPositionWithDuration => self.hw_position_with_duration.is_some(), - OutputType::Rotate => self.rotate.is_some(), - OutputType::Spray => self.spray.is_some(), - OutputType::Unknown => false, - OutputType::Vibrate => self.vibrate.is_some(), + pub fn output_type(&self) -> OutputType { + OutputType::from(ServerOutputType::from(self)) + } + + pub fn is_disabled(&self) -> bool { + match self { + Self::Vibrate(p) + | Self::Rotate(p) + | Self::Oscillate(p) + | Self::Constrict(p) + | Self::Temperature(p) + | Self::Led(p) + | Self::Spray(p) => p.disabled, + Self::Position(p) => p.disabled, + Self::HwPositionWithDuration(p) => p.disabled, } } - pub fn output_types(&self) -> Vec { - [ - (self.vibrate.is_some(), OutputType::Vibrate), - (self.rotate.is_some(), OutputType::Rotate), - (self.oscillate.is_some(), OutputType::Oscillate), - (self.constrict.is_some(), OutputType::Constrict), - (self.temperature.is_some(), OutputType::Temperature), - (self.led.is_some(), OutputType::Led), - (self.position.is_some(), OutputType::Position), - ( - self.hw_position_with_duration.is_some(), - OutputType::HwPositionWithDuration, - ), - (self.spray.is_some(), OutputType::Spray), - ] - .into_iter() - .filter_map(|(present, ot)| present.then_some(ot)) - .collect() - } - - pub fn calculate_from_value( - &self, - output_type: OutputType, - value: i32, - ) -> Result { - // TODO just fucking do some trait implementations for calculation methods and clean this up for fuck sake. :c - match output_type { - OutputType::Constrict => self.constrict.as_ref().map_or( - Err(ButtplugDeviceConfigError::InvalidOutput(output_type)), - |x| x.calculate_scaled_value(value), - ), - OutputType::Temperature => self.temperature.as_ref().map_or( - Err(ButtplugDeviceConfigError::InvalidOutput(output_type)), - |x| x.calculate_scaled_value(value), - ), - OutputType::Led => self.led.as_ref().map_or( - Err(ButtplugDeviceConfigError::InvalidOutput(output_type)), - |x| x.calculate_scaled_value(value), - ), - OutputType::Oscillate => self.oscillate.as_ref().map_or( - Err(ButtplugDeviceConfigError::InvalidOutput(output_type)), - |x| x.calculate_scaled_value(value), - ), - OutputType::Position => self.position.as_ref().map_or( - Err(ButtplugDeviceConfigError::InvalidOutput(output_type)), - |x| x.calculate_scaled_value(value as u32).map(|x| x as i32), - ), - OutputType::HwPositionWithDuration => self.hw_position_with_duration.as_ref().map_or( - Err(ButtplugDeviceConfigError::InvalidOutput(output_type)), - |x| x.calculate_scaled_value(value as u32).map(|x| x as i32), - ), - OutputType::Rotate => self.rotate.as_ref().map_or( - Err(ButtplugDeviceConfigError::InvalidOutput(output_type)), - |x| x.calculate_scaled_value(value), - ), - OutputType::Spray => self.spray.as_ref().map_or( - Err(ButtplugDeviceConfigError::InvalidOutput(output_type)), - |x| x.calculate_scaled_value(value), - ), - OutputType::Unknown => Err(ButtplugDeviceConfigError::InvalidOutput(output_type)), - OutputType::Vibrate => self.vibrate.as_ref().map_or( - Err(ButtplugDeviceConfigError::InvalidOutput(output_type)), - |x| x.calculate_scaled_value(value), - ), + pub fn calculate_from_value(&self, value: i32) -> Result { + match self { + Self::Vibrate(p) + | Self::Rotate(p) + | Self::Oscillate(p) + | Self::Constrict(p) + | Self::Temperature(p) + | Self::Led(p) + | Self::Spray(p) => p.calculate_scaled_value(value), + Self::Position(p) => p.calculate_scaled_value(value as u32).map(|x| x as i32), + Self::HwPositionWithDuration(p) => p.calculate_scaled_value(value as u32).map(|x| x as i32), } } - pub fn is_disabled(&self, output_type: OutputType) -> bool { - match output_type { - OutputType::Vibrate => self.vibrate.as_ref().is_some_and(|x| x.disabled()), - OutputType::Rotate => self.rotate.as_ref().is_some_and(|x| x.disabled()), - OutputType::Oscillate => self.oscillate.as_ref().is_some_and(|x| x.disabled()), - OutputType::Constrict => self.constrict.as_ref().is_some_and(|x| x.disabled()), - OutputType::Temperature => self.temperature.as_ref().is_some_and(|x| x.disabled()), - OutputType::Led => self.led.as_ref().is_some_and(|x| x.disabled()), - OutputType::Position => self.position.as_ref().is_some_and(|x| x.disabled()), - OutputType::HwPositionWithDuration => self - .hw_position_with_duration - .as_ref() - .is_some_and(|x| x.disabled()), - OutputType::Spray => self.spray.as_ref().is_some_and(|x| x.disabled()), - OutputType::Unknown => false, + pub fn calculate_from_float(&self, value: f64) -> Result { + match self { + Self::Vibrate(p) + | Self::Rotate(p) + | Self::Oscillate(p) + | Self::Constrict(p) + | Self::Temperature(p) + | Self::Led(p) + | Self::Spray(p) => p.calculate_scaled_float(value), + Self::Position(p) => p.calculate_scaled_float(value), + Self::HwPositionWithDuration(p) => p.calculate_scaled_float(value).map(|x| x as i32), } } - pub fn calculate_from_float( - &self, - output_type: OutputType, - value: f64, - ) -> Result { - match output_type { - OutputType::Constrict => self.constrict.as_ref().map_or( - Err(ButtplugDeviceConfigError::InvalidOutput(output_type)), - |x| x.calculate_scaled_float(value), - ), - OutputType::Temperature => self.temperature.as_ref().map_or( - Err(ButtplugDeviceConfigError::InvalidOutput(output_type)), - |x| x.calculate_scaled_float(value), - ), - OutputType::Led => self.led.as_ref().map_or( - Err(ButtplugDeviceConfigError::InvalidOutput(output_type)), - |x| x.calculate_scaled_float(value), - ), - OutputType::Oscillate => self.oscillate.as_ref().map_or( - Err(ButtplugDeviceConfigError::InvalidOutput(output_type)), - |x| x.calculate_scaled_float(value), - ), - OutputType::Position => self.position.as_ref().map_or( - Err(ButtplugDeviceConfigError::InvalidOutput(output_type)), - |x| x.calculate_scaled_float(value), - ), - OutputType::HwPositionWithDuration => self.hw_position_with_duration.as_ref().map_or( - Err(ButtplugDeviceConfigError::InvalidOutput(output_type)), - |x| x.calculate_scaled_float(value).map(|x| x as i32), - ), - OutputType::Rotate => self.rotate.as_ref().map_or( - Err(ButtplugDeviceConfigError::InvalidOutput(output_type)), - |x| x.calculate_scaled_float(value), - ), - OutputType::Spray => self.spray.as_ref().map_or( - Err(ButtplugDeviceConfigError::InvalidOutput(output_type)), - |x| x.calculate_scaled_float(value), - ), - OutputType::Unknown => Err(ButtplugDeviceConfigError::InvalidOutput(output_type)), - OutputType::Vibrate => self.vibrate.as_ref().map_or( - Err(ButtplugDeviceConfigError::InvalidOutput(output_type)), - |x| x.calculate_scaled_float(value), - ), + /// Returns the value properties if this is one of the 7 simple value-type variants. + pub fn as_value_properties(&self) -> Option<&ServerDeviceFeatureOutputValueProperties> { + match self { + Self::Vibrate(p) + | Self::Rotate(p) + | Self::Oscillate(p) + | Self::Constrict(p) + | Self::Temperature(p) + | Self::Led(p) + | Self::Spray(p) => Some(p), + _ => None, } } } -impl From for DeviceFeatureOutput { - fn from(val: ServerDeviceFeatureOutput) -> Self { - let mut builder = DeviceFeatureOutputBuilder::default(); - val - .vibrate - .as_ref() - .filter(|x| !x.disabled()) - .map(|x| builder.vibrate(x.into())); - val - .rotate - .as_ref() - .filter(|x| !x.disabled()) - .map(|x| builder.rotate(x.into())); - val - .oscillate - .as_ref() - .filter(|x| !x.disabled()) - .map(|x| builder.oscillate(x.into())); - val - .constrict - .as_ref() - .filter(|x| !x.disabled()) - .map(|x| builder.constrict(x.into())); - val - .temperature - .as_ref() - .filter(|x| !x.disabled()) - .map(|x| builder.temperature(x.into())); - val - .led - .as_ref() - .filter(|x| !x.disabled()) - .map(|x| builder.led(x.into())); - val - .position - .as_ref() - .filter(|x| !x.disabled()) - .map(|x| builder.position(x.into())); - val - .hw_position_with_duration - .as_ref() - .filter(|x| !x.disabled()) - .map(|x| builder.hw_position_with_duration(x.into())); - val - .spray - .as_ref() - .filter(|x| !x.disabled()) - .map(|x| builder.spray(x.into())); - builder.build().expect("Infallible") +impl VariantKey for ServerDeviceFeatureOutput { + type Key = OutputType; + fn variant_key(&self) -> OutputType { + self.output_type() } } -#[derive(Clone, Debug, Getters, Serialize, Deserialize)] -#[getset(get = "pub")] +macro_rules! impl_output_type_conversions { + ($($variant:ident),+ $(,)?) => { + impl From for OutputType { + fn from(val: ServerOutputType) -> Self { + match val { + $(ServerOutputType::$variant => OutputType::$variant,)+ + } + } + } + + impl From for ServerOutputType { + fn from(val: OutputType) -> Self { + match val { + $(OutputType::$variant => ServerOutputType::$variant,)+ + } + } + } + + impl From<&ServerDeviceFeatureOutput> for DeviceFeatureOutput { + fn from(val: &ServerDeviceFeatureOutput) -> Self { + match val { + $(ServerDeviceFeatureOutput::$variant(p) => DeviceFeatureOutput::$variant(p.into()),)+ + } + } + } + }; +} + +impl_output_type_conversions![ + Vibrate, + Rotate, + Oscillate, + Constrict, + Temperature, + Led, + Spray, + Position, + HwPositionWithDuration, +]; + +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct ServerDeviceFeatureInputProperties { - #[serde(with = "range_vec_serde")] - value: Vec>, - command: HashSet, + pub value: Vec>, + #[serde(with = "buttplug_core::util::serializers::bitflags_seq")] + pub command: BitFlags, } impl ServerDeviceFeatureInputProperties { - pub fn new( - value: &Vec>, - sensor_commands: &HashSet, - ) -> Self { + pub fn new(value: &[RangeInclusive], sensor_commands: &BitFlags) -> Self { Self { - value: value.clone(), - command: sensor_commands.clone(), + value: value.to_vec(), + command: *sensor_commands, } } } @@ -632,97 +419,107 @@ impl From<&ServerDeviceFeatureInputProperties> for DeviceFeatureInputProperties } } -#[derive(Clone, Debug, Getters, Setters, Default, Serialize, Deserialize)] -#[serde(default)] -#[getset(get = "pub", set = "pub(crate)")] -pub struct ServerDeviceFeatureInput { - #[serde(skip_serializing_if = "Option::is_none")] - battery: Option, - #[serde(skip_serializing_if = "Option::is_none")] - rssi: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pressure: Option, - #[serde(skip_serializing_if = "Option::is_none")] - button: Option, - #[serde(skip_serializing_if = "Option::is_none")] - depth: Option, - #[serde(skip_serializing_if = "Option::is_none")] - position: Option, +// ServerInputType is auto-generated as the discriminant enum of ServerDeviceFeatureInput. +#[derive(Clone, Debug, Serialize, Deserialize, EnumDiscriminants)] +#[serde(rename_all = "snake_case")] +#[strum_discriminants(name(ServerInputType))] +#[strum_discriminants(vis(pub(crate)))] +#[strum_discriminants(derive(Display, Hash, EnumIter, EnumString, Serialize, Deserialize))] +#[strum_discriminants(serde(rename_all = "snake_case"))] +pub enum ServerDeviceFeatureInput { + Battery(ServerDeviceFeatureInputProperties), + Rssi(ServerDeviceFeatureInputProperties), + Button(ServerDeviceFeatureInputProperties), + Pressure(ServerDeviceFeatureInputProperties), + Depth(ServerDeviceFeatureInputProperties), + Position(ServerDeviceFeatureInputProperties), } impl ServerDeviceFeatureInput { - pub fn contains(&self, input_type: InputType) -> bool { - match input_type { - InputType::Battery => self.battery.is_some(), - InputType::Rssi => self.rssi.is_some(), - InputType::Pressure => self.pressure.is_some(), - InputType::Button => self.button.is_some(), - InputType::Depth => self.depth.is_some(), - InputType::Position => self.position.is_some(), - InputType::Unknown => false, + pub fn input_type(&self) -> InputType { + InputType::from(ServerInputType::from(self)) + } + + pub fn properties(&self) -> &ServerDeviceFeatureInputProperties { + match self { + Self::Battery(p) + | Self::Rssi(p) + | Self::Button(p) + | Self::Pressure(p) + | Self::Depth(p) + | Self::Position(p) => p, } } pub fn can_subscribe(&self) -> bool { - [ - &self.battery, - &self.rssi, - &self.pressure, - &self.button, - &self.depth, - &self.position, - ] - .iter() - .any(|input| { - input - .as_ref() - .map_or(false, |i| i.command.contains(&InputCommandType::Subscribe)) - }) + self + .properties() + .command + .contains(InputCommandType::Subscribe) } } -impl From for DeviceFeatureInput { - fn from(val: ServerDeviceFeatureInput) -> Self { - let mut builder = DeviceFeatureInputBuilder::default(); - val.battery.as_ref().map(|x| builder.battery(x.into())); - val.rssi.as_ref().map(|x| builder.rssi(x.into())); - val.pressure.as_ref().map(|x| builder.pressure(x.into())); - val.button.as_ref().map(|x| builder.button(x.into())); - val.depth.as_ref().map(|x| builder.depth(x.into())); - val.position.as_ref().map(|x| builder.position(x.into())); - builder.build().expect("Infallible") +impl VariantKey for ServerDeviceFeatureInput { + type Key = InputType; + fn variant_key(&self) -> InputType { + self.input_type() } } -#[derive(Clone, Debug, Getters, CopyGetters, Setters, Serialize, Deserialize)] +macro_rules! impl_input_type_conversions { + ($($variant:ident),+ $(,)?) => { + impl From for InputType { + fn from(val: ServerInputType) -> Self { + match val { + $(ServerInputType::$variant => InputType::$variant,)+ + } + } + } + + impl From for ServerInputType { + fn from(val: InputType) -> Self { + match val { + $(InputType::$variant => ServerInputType::$variant,)+ + } + } + } + + impl From<&ServerDeviceFeatureInput> for DeviceFeatureInput { + fn from(val: &ServerDeviceFeatureInput) -> Self { + match val { + $(ServerDeviceFeatureInput::$variant(p) => DeviceFeatureInput::$variant(p.into()),)+ + } + } + } + }; +} + +impl_input_type_conversions![Battery, Rssi, Button, Pressure, Depth, Position,]; + +#[derive(Clone, Debug, Getters, CopyGetters, Serialize, Deserialize)] #[serde(default)] pub struct ServerDeviceFeature { #[getset(get_copy = "pub")] #[serde(skip)] index: u32, - #[getset(get = "pub")] #[serde(default)] - description: String, - #[getset(get_copy = "pub")] + pub description: String, #[serde(skip)] - id: Uuid, #[getset(get_copy = "pub")] + id: Uuid, #[serde(skip_serializing_if = "Option::is_none")] - base_id: Option, - #[getset(get_copy = "pub")] - #[serde(skip_serializing_if = "Option::is_none")] - alt_protocol_index: Option, - #[getset(get = "pub", set = "pub")] - #[serde(skip_serializing_if = "Option::is_none")] - output: Option, - #[getset(get = "pub")] + pub base_id: Option, #[serde(skip_serializing_if = "Option::is_none")] - input: Option, + pub alt_protocol_index: Option, + #[serde(skip_serializing_if = "SmallVecEnumMap::is_empty", default)] + pub output: SmallVecEnumMap, + #[serde(skip_serializing_if = "SmallVecEnumMap::is_empty", default)] + pub input: SmallVecEnumMap, } impl PartialEq for ServerDeviceFeature { fn eq(&self, other: &Self) -> bool { - self.id() == other.id() + self.id == other.id } } @@ -737,8 +534,8 @@ impl Default for ServerDeviceFeature { id: Uuid::new_v4(), base_id: None, alt_protocol_index: None, - output: None, - input: None, + output: SmallVecEnumMap::default(), + input: SmallVecEnumMap::default(), } } } @@ -750,8 +547,8 @@ impl ServerDeviceFeature { id: Uuid, base_id: Option, alt_protocol_index: Option, - output: &Option, - input: &Option, + output: &SmallVecEnumMap, + input: &SmallVecEnumMap, ) -> Self { Self { index, @@ -764,6 +561,40 @@ impl ServerDeviceFeature { } } + // --- Output helpers --- + + pub fn has_output(&self) -> bool { + !self.output.is_empty() + } + + pub fn contains_output(&self, t: OutputType) -> bool { + self.output.contains_key(&t) + } + + pub fn get_output(&self, t: OutputType) -> Option<&ServerDeviceFeatureOutput> { + self.output.find_by_key(&t) + } + + // --- Input helpers --- + + pub fn has_input(&self) -> bool { + !self.input.is_empty() + } + + pub fn contains_input(&self, t: InputType) -> bool { + self.input.contains_key(&t) + } + + pub fn get_input(&self, t: InputType) -> Option<&ServerDeviceFeatureInput> { + self.input.find_by_key(&t) + } + + pub fn can_subscribe(&self) -> bool { + self.input.iter().any(|i| i.can_subscribe()) + } + + // --- Lifecycle --- + pub fn as_new_user_feature(&self) -> Self { let mut new_feature = self.clone(); new_feature.base_id = Some(self.id); @@ -772,11 +603,19 @@ impl ServerDeviceFeature { } pub fn as_device_feature(&self) -> Result { + let output: SmallVecEnumMap = self + .output + .iter() + .filter(|o| !o.is_disabled()) + .map(|o| o.into()) + .collect(); + let input: SmallVecEnumMap = + self.input.iter().map(|i| i.into()).collect(); Ok(DeviceFeature::new( self.index, - self.description(), - &self.output.as_ref().map(|x| x.clone().into()), - &self.input.as_ref().map(|x| x.clone().into()), + &self.description, + &output, + &input, )) } } diff --git a/crates/buttplug_tests/tests/util/device_test/client/client_v4/mod.rs b/crates/buttplug_tests/tests/util/device_test/client/client_v4/mod.rs index 477f0c015..bb24f2379 100644 --- a/crates/buttplug_tests/tests/util/device_test/client/client_v4/mod.rs +++ b/crates/buttplug_tests/tests/util/device_test/client/client_v4/mod.rs @@ -29,7 +29,6 @@ use super::super::{ TestCommand, filter_commands, }; -use buttplug_core::message::{DeviceFeatureOutput, DeviceFeatureOutputLimits}; use futures::StreamExt; use log::*; use std::{sync::Arc, time::Duration}; @@ -53,19 +52,13 @@ fn from_type_and_value(output_type: OutputType, value: f64) -> ClientDeviceOutpu fn get_scalar_index(device: &ButtplugClientDevice, index: u32) -> &u32 { let mut offset = 0; let mut iter = device.device_features().iter().filter(|f| { - let fo = f - .1 - .feature() - .output() - .clone() - .unwrap_or(DeviceFeatureOutput::default()); - fo.vibrate().is_some() - || fo.oscillate().is_some() - || fo.constrict().is_some() - || fo - .rotate() - .as_ref() - .is_some_and(|r| r.step_limit().start() >= &0) + let feature = f.1.feature(); + feature.contains_output(OutputType::Vibrate) + || feature.contains_output(OutputType::Oscillate) + || feature.contains_output(OutputType::Constrict) + || feature + .get_output_limits(OutputType::Rotate) + .is_some_and(|r| r.step_limit().start() >= 0) }); while let Some((idx, _)) = iter.next() { if offset >= index { @@ -96,15 +89,8 @@ async fn run_test_client_command(command: &TestClientCommand, device: &ButtplugC .map(|cmd| { let vibe_features: Vec<&ClientDeviceFeature> = device .device_features() - .iter() - .filter(|f| { - f.1 - .feature() - .output() - .as_ref() - .is_some_and(|x| x.contains(OutputType::Vibrate)) - }) - .map(|(_, x)| x) + .values() + .filter(|f| f.feature().contains_output(OutputType::Vibrate)) .collect(); let f = vibe_features[cmd.index() as usize].clone(); f.run_output(&from_type_and_value(OutputType::Vibrate, cmd.speed())) @@ -121,15 +107,8 @@ async fn run_test_client_command(command: &TestClientCommand, device: &ButtplugC .map(|cmd| { let rotate_features: Vec<&ClientDeviceFeature> = device .device_features() - .iter() - .filter(|f| { - f.1 - .feature() - .output() - .as_ref() - .is_some_and(|x| x.contains(OutputType::Rotate)) - }) - .map(|(_, x)| x) + .values() + .filter(|f| f.feature().contains_output(OutputType::Rotate)) .collect(); let f = rotate_features[cmd.index() as usize].clone(); f.run_output(&ClientDeviceOutputCommand::Rotate( @@ -146,18 +125,13 @@ async fn run_test_client_command(command: &TestClientCommand, device: &ButtplugC .iter() .map(|cmd| { let f = device.device_features()[&cmd.index()].clone(); + let step_count = f + .feature() + .get_output_limits(OutputType::HwPositionWithDuration) + .unwrap() + .step_count() as f64; f.run_output(&ClientDeviceOutputCommand::HwPositionWithDuration( - ((cmd.position() - * f - .feature() - .output() - .as_ref() - .unwrap() - .get(OutputType::HwPositionWithDuration) - .unwrap() - .step_count() as f64) - .ceil() as u32) - .into(), + ((cmd.position() * step_count).ceil() as u32).into(), cmd.duration(), )) }) diff --git a/examples/src/bin/device_control.rs b/examples/src/bin/device_control.rs index 436ec561a..894513b15 100644 --- a/examples/src/bin/device_control.rs +++ b/examples/src/bin/device_control.rs @@ -50,9 +50,7 @@ async fn main() -> anyhow::Result<()> { println!("{} supports these outputs:", device.name()); for output_type in OutputType::iter() { for feature in device.device_features().values() { - if let Some(output) = feature.feature().output() - && output.contains(output_type) - { + if feature.feature().contains_output(output_type) { println!("- {}", output_type); } } diff --git a/examples/src/bin/device_tester.rs b/examples/src/bin/device_tester.rs index f23bb0c53..7fbebb75b 100644 --- a/examples/src/bin/device_tester.rs +++ b/examples/src/bin/device_tester.rs @@ -92,8 +92,7 @@ async fn device_tester() { let exercise_device = |dev: ButtplugClientDevice| async move { let mut cmds = vec![]; dev.device_features().iter().for_each(|(_, feature)| { - let outs = feature.feature().output().clone().unwrap_or_default(); - if let Some(out) = outs.get(OutputType::Vibrate) { + if let Some(out) = feature.feature().get_output_limits(OutputType::Vibrate) { cmds.push(feature.run_output(&ClientDeviceOutputCommand::Vibrate( (out.step_count() as i32).into(), ))); @@ -103,9 +102,9 @@ async fn device_tester() { dev.index(), feature.feature_index() ); - } else if let Some(out) = outs.get(OutputType::Rotate) { + } else if let Some(out) = feature.feature().get_output_limits(OutputType::Rotate) { cmds.push(feature.run_output(&ClientDeviceOutputCommand::Rotate( - (*out.step_limit().end()).into(), + out.step_limit().end().into(), ))); println!( "{} ({}) should start rotating on feature {}!", @@ -113,7 +112,7 @@ async fn device_tester() { dev.index(), feature.feature_index() ); - } else if let Some(out) = outs.get(OutputType::Oscillate) { + } else if let Some(out) = feature.feature().get_output_limits(OutputType::Oscillate) { cmds.push(feature.run_output(&ClientDeviceOutputCommand::Oscillate( out.step_count().into(), ))); @@ -123,7 +122,7 @@ async fn device_tester() { dev.index(), feature.feature_index() ); - } else if let Some(out) = outs.get(OutputType::Constrict) { + } else if let Some(out) = feature.feature().get_output_limits(OutputType::Constrict) { cmds.push(feature.run_output(&ClientDeviceOutputCommand::Constrict( out.step_count().into(), ))); @@ -133,9 +132,9 @@ async fn device_tester() { dev.index(), feature.feature_index() ); - } else if let Some(out) = outs.get(OutputType::Temperature) { + } else if let Some(out) = feature.feature().get_output_limits(OutputType::Temperature) { cmds.push(feature.run_output(&ClientDeviceOutputCommand::Temperature( - (*out.step_limit().end()).into(), + out.step_limit().end().into(), ))); println!( "{} ({}) should start heating on feature {}!", @@ -161,8 +160,7 @@ async fn device_tester() { let mut cmds = vec![]; dev.device_features().iter().for_each(|(_, feature)| { - let outs = feature.feature().output().clone().unwrap_or_default(); - if outs.get(OutputType::Vibrate).is_some() { + if feature.feature().contains_output(OutputType::Vibrate) { cmds.push(feature.run_output(&ClientDeviceOutputCommand::Vibrate(0.into()))); println!( "{} ({}) should stop vibrating on feature {}!", @@ -170,7 +168,7 @@ async fn device_tester() { dev.index(), feature.feature_index() ); - } else if outs.get(OutputType::Rotate).is_some() { + } else if feature.feature().contains_output(OutputType::Rotate) { cmds.push(feature.run_output(&ClientDeviceOutputCommand::Rotate(0.into()))); println!( "{} ({}) should stop rotating on feature {}!", @@ -178,7 +176,7 @@ async fn device_tester() { dev.index(), feature.feature_index() ); - } else if outs.get(OutputType::Oscillate).is_some() { + } else if feature.feature().contains_output(OutputType::Oscillate) { cmds.push(feature.run_output(&ClientDeviceOutputCommand::Oscillate(0.into()))); println!( "{} ({}) should stop oscillating on feature {}!", @@ -186,7 +184,7 @@ async fn device_tester() { dev.index(), feature.feature_index() ); - } else if outs.get(OutputType::Constrict).is_some() { + } else if feature.feature().contains_output(OutputType::Constrict) { cmds.push(feature.run_output(&ClientDeviceOutputCommand::Constrict(0.into()))); println!( "{} ({}) should stop constricting on feature {}!", @@ -194,7 +192,7 @@ async fn device_tester() { dev.index(), feature.feature_index() ); - } else if outs.get(OutputType::Temperature).is_some() { + } else if feature.feature().contains_output(OutputType::Temperature) { cmds.push(feature.run_output(&ClientDeviceOutputCommand::Temperature(0.into()))); println!( "{} ({}) should stop heating on feature {}!", @@ -219,172 +217,161 @@ async fn device_tester() { // Exercise each feature for (_, feature) in dev.device_features() { - if let Some(out) = feature.feature().output() { - let outputs: Vec<&OutputType> = [ - OutputType::Constrict, - OutputType::Temperature, - OutputType::Led, - OutputType::Oscillate, - OutputType::Position, - OutputType::HwPositionWithDuration, - OutputType::Rotate, - OutputType::Spray, - OutputType::Unknown, - OutputType::Vibrate, - ] - .iter() - .filter(|o| out.contains(**o)) - .collect(); - - for otype in outputs { - let output = out.get(*otype).unwrap(); - match otype { - OutputType::Vibrate - | OutputType::Constrict - | OutputType::Oscillate - | OutputType::Temperature - | OutputType::Spray - | OutputType::Led - | OutputType::Position => { - set_level_and_wait(&dev, feature, &otype, 0.05).await; - set_level_and_wait(&dev, feature, &otype, 0.10).await; - set_level_and_wait(&dev, feature, &otype, 0.25).await; - set_level_and_wait(&dev, feature, &otype, 0.5).await; - set_level_and_wait(&dev, feature, &otype, 0.75).await; - set_level_and_wait(&dev, feature, &otype, 1.0).await; - set_level_and_wait(&dev, feature, &otype, 0.0).await; - } - OutputType::Unknown => { - error!( - "{} ({}) Can't test unknown feature {} ({}), output {:?}", - dev.name(), - dev.index(), - feature.feature().feature_index(), - feature.feature().description(), - otype - ); - } - OutputType::Rotate => { - if output.step_limit().start() >= &0 { - set_level_and_wait(&dev, feature, &otype, 0.25).await; - set_level_and_wait(&dev, feature, &otype, 0.5).await; - set_level_and_wait(&dev, feature, &otype, 0.75).await; - set_level_and_wait(&dev, feature, &otype, 1.0).await; - set_level_and_wait(&dev, feature, &otype, 0.0).await; - } else { - set_level_and_wait(&dev, feature, &otype, 0.25).await; - set_level_and_wait(&dev, feature, &otype, -0.25).await; - set_level_and_wait(&dev, feature, &otype, 0.5).await; - set_level_and_wait(&dev, feature, &otype, -0.5).await; - set_level_and_wait(&dev, feature, &otype, 0.75).await; - set_level_and_wait(&dev, feature, &otype, -0.75).await; - set_level_and_wait(&dev, feature, &otype, 1.0).await; - set_level_and_wait(&dev, feature, &otype, -1.0).await; - set_level_and_wait(&dev, feature, &otype, 0.0).await; + for output_type in [ + OutputType::Constrict, + OutputType::Temperature, + OutputType::Led, + OutputType::Oscillate, + OutputType::Position, + OutputType::HwPositionWithDuration, + OutputType::Rotate, + OutputType::Spray, + OutputType::Vibrate, + ] { + if !feature.feature().contains_output(output_type) { + continue; + } + match output_type { + OutputType::Vibrate + | OutputType::Constrict + | OutputType::Oscillate + | OutputType::Temperature + | OutputType::Spray + | OutputType::Led + | OutputType::Position => { + set_level_and_wait(&dev, feature, &output_type, 0.05).await; + set_level_and_wait(&dev, feature, &output_type, 0.10).await; + set_level_and_wait(&dev, feature, &output_type, 0.25).await; + set_level_and_wait(&dev, feature, &output_type, 0.5).await; + set_level_and_wait(&dev, feature, &output_type, 0.75).await; + set_level_and_wait(&dev, feature, &output_type, 1.0).await; + set_level_and_wait(&dev, feature, &output_type, 0.0).await; + } + OutputType::Rotate => { + if feature + .feature() + .get_output_limits(OutputType::Rotate) + .map(|l| l.step_limit().start() < 0) + .unwrap_or(false) + { + set_level_and_wait(&dev, feature, &output_type, 0.25).await; + set_level_and_wait(&dev, feature, &output_type, -0.25).await; + set_level_and_wait(&dev, feature, &output_type, 0.5).await; + set_level_and_wait(&dev, feature, &output_type, -0.5).await; + set_level_and_wait(&dev, feature, &output_type, 0.75).await; + set_level_and_wait(&dev, feature, &output_type, -0.75).await; + set_level_and_wait(&dev, feature, &output_type, 1.0).await; + set_level_and_wait(&dev, feature, &output_type, -1.0).await; + set_level_and_wait(&dev, feature, &output_type, 0.0).await; - set_level_and_wait(&dev, feature, &otype, 0.25).await; - set_level_and_wait(&dev, feature, &otype, 0.5).await; - set_level_and_wait(&dev, feature, &otype, 0.75).await; - set_level_and_wait(&dev, feature, &otype, 1.0).await; - set_level_and_wait(&dev, feature, &otype, -0.25).await; - set_level_and_wait(&dev, feature, &otype, -0.5).await; - set_level_and_wait(&dev, feature, &otype, -0.75).await; - set_level_and_wait(&dev, feature, &otype, -1.0).await; - set_level_and_wait(&dev, feature, &otype, 0.0).await; - } - } - OutputType::HwPositionWithDuration => { - feature - .run_output(&ClientDeviceOutputCommand::HwPositionWithDuration( - 0.0f64.into(), - 10, - )) - .await - .unwrap(); - println!( - "{} ({}) Testing feature {}: {}, output {:?} - {}% {}ms", - dev.name(), - dev.index(), - feature.feature().feature_index(), - feature.feature().description(), - "HwPositionWithDuration", - (0.0 * 100.0) as u8, - 10 - ); - sleep(Duration::from_secs(1)).await; - feature - .run_output(&ClientDeviceOutputCommand::HwPositionWithDuration( - 0.5f64.into(), - 1000, - )) - .await - .unwrap(); - println!( - "{} ({}) Testing feature {}: {}, output {:?} - {}% {}ms", - dev.name(), - dev.index(), - feature.feature().feature_index(), - feature.feature().description(), - "HwPositionWithDuration", - (0.0 * 100.0) as u8, - 1000 - ); - sleep(Duration::from_secs(1)).await; - feature - .run_output(&ClientDeviceOutputCommand::HwPositionWithDuration( - 0.0f64.into(), - 10, - )) - .await - .unwrap(); - println!( - "{} ({}) Testing feature {}: {}, output {:?} - {}% {}ms", - dev.name(), - dev.index(), - feature.feature().feature_index(), - feature.feature().description(), - "HwPositionWithDuration", - (0.0 * 100.0) as u8, - 10 - ); - sleep(Duration::from_secs(1)).await; - feature - .run_output(&ClientDeviceOutputCommand::HwPositionWithDuration( - 1.0f64.into(), - 500, - )) - .await - .unwrap(); - println!( - "{} ({}) Testing feature {}: {}, output {:?} - {}% {}ms", - dev.name(), - dev.index(), - feature.feature().feature_index(), - feature.feature().description(), - "HwPositionWithDuration", - (1.0 * 100.0) as u8, - 500 - ); - sleep(Duration::from_secs(1)).await; - feature - .run_output(&ClientDeviceOutputCommand::HwPositionWithDuration( - 0.0f64.into(), - 1500, - )) - .await - .unwrap(); - println!( - "{} ({}) Testing feature {}: {}, output {:?} - {}% {}ms", - dev.name(), - dev.index(), - feature.feature().feature_index(), - feature.feature().description(), - "HwPositionWithDuration", - (0.0 * 100.0) as u8, - 1500 - ); + set_level_and_wait(&dev, feature, &output_type, 0.25).await; + set_level_and_wait(&dev, feature, &output_type, 0.5).await; + set_level_and_wait(&dev, feature, &output_type, 0.75).await; + set_level_and_wait(&dev, feature, &output_type, 1.0).await; + set_level_and_wait(&dev, feature, &output_type, -0.25).await; + set_level_and_wait(&dev, feature, &output_type, -0.5).await; + set_level_and_wait(&dev, feature, &output_type, -0.75).await; + set_level_and_wait(&dev, feature, &output_type, -1.0).await; + set_level_and_wait(&dev, feature, &output_type, 0.0).await; + } else { + set_level_and_wait(&dev, feature, &output_type, 0.25).await; + set_level_and_wait(&dev, feature, &output_type, 0.5).await; + set_level_and_wait(&dev, feature, &output_type, 0.75).await; + set_level_and_wait(&dev, feature, &output_type, 1.0).await; + set_level_and_wait(&dev, feature, &output_type, 0.0).await; } } + OutputType::HwPositionWithDuration => { + feature + .run_output(&ClientDeviceOutputCommand::HwPositionWithDuration( + 0.0f64.into(), + 10, + )) + .await + .unwrap(); + println!( + "{} ({}) Testing feature {}: {}, output {:?} - {}% {}ms", + dev.name(), + dev.index(), + feature.feature().feature_index(), + feature.feature().description(), + "HwPositionWithDuration", + (0.0 * 100.0) as u8, + 10 + ); + sleep(Duration::from_secs(1)).await; + feature + .run_output(&ClientDeviceOutputCommand::HwPositionWithDuration( + 0.5f64.into(), + 1000, + )) + .await + .unwrap(); + println!( + "{} ({}) Testing feature {}: {}, output {:?} - {}% {}ms", + dev.name(), + dev.index(), + feature.feature().feature_index(), + feature.feature().description(), + "HwPositionWithDuration", + (0.0 * 100.0) as u8, + 1000 + ); + sleep(Duration::from_secs(1)).await; + feature + .run_output(&ClientDeviceOutputCommand::HwPositionWithDuration( + 0.0f64.into(), + 10, + )) + .await + .unwrap(); + println!( + "{} ({}) Testing feature {}: {}, output {:?} - {}% {}ms", + dev.name(), + dev.index(), + feature.feature().feature_index(), + feature.feature().description(), + "HwPositionWithDuration", + (0.0 * 100.0) as u8, + 10 + ); + sleep(Duration::from_secs(1)).await; + feature + .run_output(&ClientDeviceOutputCommand::HwPositionWithDuration( + 1.0f64.into(), + 500, + )) + .await + .unwrap(); + println!( + "{} ({}) Testing feature {}: {}, output {:?} - {}% {}ms", + dev.name(), + dev.index(), + feature.feature().feature_index(), + feature.feature().description(), + "HwPositionWithDuration", + (1.0 * 100.0) as u8, + 500 + ); + sleep(Duration::from_secs(1)).await; + feature + .run_output(&ClientDeviceOutputCommand::HwPositionWithDuration( + 0.0f64.into(), + 1500, + )) + .await + .unwrap(); + println!( + "{} ({}) Testing feature {}: {}, output {:?} - {}% {}ms", + dev.name(), + dev.index(), + feature.feature().feature_index(), + feature.feature().description(), + "HwPositionWithDuration", + (0.0 * 100.0) as u8, + 1500 + ); + } } } }