From b268380a574f3b96b4ab29c09329717f7e343aa8 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sat, 11 Apr 2020 13:55:20 -0700 Subject: [PATCH] feat: Update parsing of device config file, add user config loading Update the device configuration format to match the new array based config file, and add the ability to merge user configurations for adding local serial ports. Fixes #64 --- .../buttplug-user-device-config-schema.json | 66 ++++++++ buttplug/src/device/configuration_manager.rs | 160 ++++++++++++++---- 2 files changed, 196 insertions(+), 30 deletions(-) create mode 100644 buttplug/dependencies/buttplug-device-config/buttplug-user-device-config-schema.json diff --git a/buttplug/dependencies/buttplug-device-config/buttplug-user-device-config-schema.json b/buttplug/dependencies/buttplug-device-config/buttplug-user-device-config-schema.json new file mode 100644 index 00000000..66dddd3f --- /dev/null +++ b/buttplug/dependencies/buttplug-device-config/buttplug-user-device-config-schema.json @@ -0,0 +1,66 @@ +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "title": "Buttplug User Device Config Schema", + "version": 2, + "description": "JSON format for Buttplug User Created Device Config Files.", + "components": { + "uuid": { + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" + }, + "serial-definition": { + "type": "array", + "items": { + "type": "object", + "properties": { + "port": { + "type": "string" + }, + "baud-rate": { + "type": "integer" + }, + "data-bits": { + "type": "integer" + }, + "parity": { + "type": "string" + }, + "stop-bits": { + "type": "integer" + } + }, + "required": [ + "port", + "baud-rate", + "data-bits", + "parity", + "stop-bits" + ], + "additionalProperties": false + }, + "minItems": 1 + } + }, + "type": "object", + "properties": { + "protocols": { + "type": "object", + "patternProperties": { + "^.*$": { + "type": "object", + "properties": { + "serial": { + "$ref": "#/components/serial-definition" + } + } + } + }, + "additionalProperties": false + }, + "additionalProperties": false + }, + "required": [ + "protocols" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/buttplug/src/device/configuration_manager.rs b/buttplug/src/device/configuration_manager.rs index 806e12f4..35f43e38 100644 --- a/buttplug/src/device/configuration_manager.rs +++ b/buttplug/src/device/configuration_manager.rs @@ -11,7 +11,7 @@ use super::protocol::{self, ButtplugProtocolCreator}; use crate::{ core::{ errors::{ButtplugDeviceError, ButtplugError}, - messages::MessageAttributesMap + messages::MessageAttributesMap, }, device::Endpoint, }; @@ -20,14 +20,17 @@ use serde::Deserialize; use std::collections::{HashMap, HashSet}; use uuid::Uuid; // TODO Use parking_lot? We don't really need extra speed for this though. -use std::sync::{Arc, RwLock}; use serde_json::Value; +use std::sync::{Arc, RwLock}; use valico::json_schema; +use std::mem; static DEVICE_CONFIGURATION_JSON: &str = include_str!("../../dependencies/buttplug-device-config/buttplug-device-config.json"); static DEVICE_CONFIGURATION_JSON_SCHEMA: &str = include_str!("../../dependencies/buttplug-device-config/buttplug-device-config-schema.json"); +static USER_DEVICE_CONFIGURATION_JSON_SCHEMA: &str = + include_str!("../../dependencies/buttplug-device-config/buttplug-user-device-config-schema.json"); static DEVICE_EXTERNAL_CONFIGURATION_JSON: Lazy>>> = Lazy::new(|| Arc::new(RwLock::new(None))); static DEVICE_USER_CONFIGURATION_JSON: Lazy>>> = @@ -43,9 +46,14 @@ pub fn set_user_device_config(config: Option<&'static str>) { *c = config.clone(); } +fn clear_user_device_config() { + let mut c = DEVICE_USER_CONFIGURATION_JSON.write().unwrap(); + *c = None; +} + // Note: There's a ton of extra structs in here just to deserialize the json // file. Just leave them and build extras (for instance, -// DeviceProtocolConfiguraation) if needed elsewhere in the codebase. It's not +// DeviceProtocolConfiguration) if needed elsewhere in the codebase. It's not // gonna hurt anything and making a ton of serde attributes is just going to get // confusing (see the messages impl). @@ -97,14 +105,12 @@ impl BluetoothLESpecifier { #[derive(Deserialize, Debug, Clone, Copy)] pub struct XInputSpecifier { - exists: bool + exists: bool, } impl Default for XInputSpecifier { fn default() -> Self { - Self { - exists: true - } + Self { exists: true } } } @@ -131,13 +137,12 @@ pub struct SerialSpecifier { #[serde(rename = "stop-bits")] stop_bits: u8, parity: char, - #[serde(default)] - ports: HashSet, + port: String, } impl PartialEq for SerialSpecifier { fn eq(&self, other: &Self) -> bool { - self.ports.intersection(&other.ports).count() > 0 + self.port == other.port } } @@ -170,41 +175,76 @@ pub struct ProtocolDefinition { // Can't get serde flatten specifiers into a String/DeviceSpecifier map, so // they're kept separate here, and we return them in get_specifiers(). Feels // very clumsy, but we really don't do this a bunch during a session. - pub usb: Option, + pub usb: Option>, pub btle: Option, - pub serial: Option, - pub hid: Option, + pub serial: Option>, + pub hid: Option>, pub xinput: Option, pub defaults: Option, pub configurations: Vec, } +#[derive(Deserialize, Debug, Clone)] +pub struct UserProtocolDefinition { + // Right now, we only allow users to specify serial ports through this + // interface. It will contain more additions in the future. + pub serial: Option>, +} + + fn option_some_eq(a: &Option, b: &T) -> bool where T: PartialEq, { - match &a { - Some(a) => a == b, - _ => false, - } + a.as_ref().map_or(false, |x| x == b) +} + +fn option_some_eq_vec(a_opt: &Option>, b: &T) -> bool +where + T: PartialEq, +{ + a_opt.as_ref().map_or(false, |a_vec| a_vec.contains(b)) } impl PartialEq for ProtocolDefinition { fn eq(&self, other: &DeviceSpecifier) -> bool { // TODO This seems like a really gross way to do this? match other { - DeviceSpecifier::USB(other_usb) => option_some_eq(&self.usb, other_usb), - DeviceSpecifier::Serial(other_serial) => option_some_eq(&self.serial, other_serial), + DeviceSpecifier::USB(other_usb) => option_some_eq_vec(&self.usb, other_usb), + DeviceSpecifier::Serial(other_serial) => option_some_eq_vec(&self.serial, other_serial), DeviceSpecifier::BluetoothLE(other_btle) => option_some_eq(&self.btle, other_btle), - DeviceSpecifier::HID(other_hid) => option_some_eq(&self.hid, other_hid), - DeviceSpecifier::XInput(other_xinput) => option_some_eq(&self.xinput, other_xinput) + DeviceSpecifier::HID(other_hid) => option_some_eq_vec(&self.hid, other_hid), + DeviceSpecifier::XInput(other_xinput) => option_some_eq(&self.xinput, other_xinput), } } } #[derive(Deserialize, Debug)] pub struct ProtocolConfiguration { - protocols: HashMap, + pub(self) protocols: HashMap, +} + + +#[derive(Deserialize, Debug)] +pub struct UserProtocolConfiguration { + pub protocols: HashMap, +} + +impl ProtocolConfiguration { + pub fn merge_user_config(&mut self, other: UserProtocolConfiguration) { + // For now, we're only merging serial info in. + for (protocol, conf) in other.protocols { + if self.protocols.contains_key(&protocol) { + let our_serial_conf_option = &mut self.protocols.get_mut(&protocol).unwrap().serial; + let mut other_serial_conf = conf.serial; + if let Some(ref mut our_serial_config) = our_serial_conf_option { + our_serial_config.extend(other_serial_conf.unwrap()); + } else { + mem::swap(our_serial_conf_option, &mut other_serial_conf); + } + } + } + } } #[derive(Clone, Debug)] @@ -261,8 +301,8 @@ pub type ProtocolConstructor = Box Box>; pub struct DeviceConfigurationManager { - config: ProtocolConfiguration, - protocols: HashMap, + pub(self) config: ProtocolConfiguration, + pub(self) protocols: HashMap, } unsafe impl Send for DeviceConfigurationManager {} @@ -272,30 +312,58 @@ impl DeviceConfigurationManager { pub fn new() -> Self { let external_config_guard = DEVICE_EXTERNAL_CONFIGURATION_JSON.clone(); let external_config = external_config_guard.read().unwrap(); - let config: ProtocolConfiguration; + let mut config: ProtocolConfiguration; // TODO We should already load the JSON into the file statics, and just // clone it out of our statics as needed. - let configuration_schema: Value = serde_json::from_str(DEVICE_CONFIGURATION_JSON_SCHEMA).unwrap(); + let configuration_schema: Value = + serde_json::from_str(DEVICE_CONFIGURATION_JSON_SCHEMA).unwrap(); let mut scope = json_schema::Scope::new(); - let schema = scope.compile_and_return(configuration_schema.clone(), false).unwrap(); + let schema = scope + .compile_and_return(configuration_schema.clone(), false) + .unwrap(); if let Some(cfg) = *external_config { let config_check = serde_json::from_str(cfg).unwrap(); let state = schema.validate(&config_check); if !state.is_valid() { - panic!("Built-in configuration schema is invalid! Aborting! {:?}", state); + panic!( + "External configuration schema is invalid! Aborting! {:?}", + state + ); } config = serde_json::from_str(cfg).unwrap(); } else { let config_check = serde_json::from_str(DEVICE_CONFIGURATION_JSON).unwrap(); let state = schema.validate(&config_check); if !state.is_valid() { - panic!("Built-in configuration schema is invalid! Aborting! {:?}", state); + panic!( + "Built-in configuration schema is invalid! Aborting! {:?}", + state + ); } config = serde_json::from_str(DEVICE_CONFIGURATION_JSON).unwrap(); } // TODO actually load user configuration and merge into maps + let user_config_guard = DEVICE_USER_CONFIGURATION_JSON.clone(); + let user_config_str = user_config_guard.read().unwrap(); + if let Some(user_cfg) = *user_config_str { + let user_configuration_schema: Value = + serde_json::from_str(USER_DEVICE_CONFIGURATION_JSON_SCHEMA).unwrap(); + let mut user_scope = json_schema::Scope::new(); + let user_schema = user_scope + .compile_and_return(user_configuration_schema.clone(), false) + .unwrap(); + let config_check = serde_json::from_str(user_cfg).unwrap(); + let state = user_schema.validate(&config_check); + if !state.is_valid() { + panic!( + "User configuration schema is invalid! Aborting! {:?}", + state + ); + } + config.merge_user_config(serde_json::from_str(user_cfg).unwrap()); + } // Do not try to use HashMap::new() here. We need the explicit typing, // otherwise we'll just get an anonymous closure type during insert that @@ -346,7 +414,7 @@ impl DeviceConfigurationManager { mod test { use super::{ BluetoothLESpecifier, DeviceConfigurationManager, DeviceProtocolConfiguration, - DeviceSpecifier, + DeviceSpecifier, set_user_device_config, clear_user_device_config }; use crate::core::messages::ButtplugDeviceMessageType; @@ -398,4 +466,36 @@ mod test { 2 ); } + + #[test] + fn test_user_config_loading() { + let _ = env_logger::builder().is_test(true).try_init(); + let mut config = DeviceConfigurationManager::new(); + assert!(config.config.protocols.contains_key("erostek-et312")); + assert!(config.config.protocols.get("erostek-et312").unwrap().serial.as_ref().is_some()); + assert_eq!(config.config.protocols.get("erostek-et312").unwrap().serial.as_ref().unwrap().len(), 1); + set_user_device_config(Some(r#" + { + "protocols": { + "erostek-et312": { + "serial": [ + { + "port": "COM1", + "baud-rate": 19200, + "data-bits": 8, + "parity": "N", + "stop-bits": 1 + } + ] + } + } + } + "#)); + config = DeviceConfigurationManager::new(); + assert!(config.config.protocols.contains_key("erostek-et312")); + assert!(config.config.protocols.get("erostek-et312").unwrap().serial.as_ref().is_some()); + assert_eq!(config.config.protocols.get("erostek-et312").unwrap().serial.as_ref().unwrap().len(), 2); + assert!(config.config.protocols.get("erostek-et312").unwrap().serial.as_ref().unwrap().iter().any(|x| x.port == "COM1")); + clear_user_device_config(); + } }