diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2d56bf6dd..8dc4038d4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -93,5 +93,4 @@ jobs: with: version: 'latest' - name: Build Dev - run: wasm-pack build --dev --no-default-features --features wasm - working-directory: ./buttplug \ No newline at end of file + run: wasm-pack build --dev crates/buttplug_server --no-default-features --features wasm diff --git a/.vscode/settings.json b/.vscode/settings.json index 22690c8ec..f00021f14 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -22,7 +22,7 @@ "!Battery mapping" ], "rust-analyzer.linkedProjects": [ - ".\\buttplug\\Cargo.toml" + "Cargo.toml" ], } \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 0e60e81c9..68a8de2b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,23 @@ [workspace] -resolver = "2" +resolver = "3" members = [ - "buttplug", - "buttplug_derive" + "crates/buttplug_client", + "crates/buttplug_client_in_process", + "crates/buttplug_core", + "crates/buttplug_derive", + "crates/buttplug_server", + "crates/buttplug_server_device_config", + "crates/buttplug_server_hwmgr_btleplug", + "crates/buttplug_server_hwmgr_hid", + "crates/buttplug_server_hwmgr_lovense_connect", + "crates/buttplug_server_hwmgr_lovense_dongle", + "crates/buttplug_server_hwmgr_serial", + "crates/buttplug_server_hwmgr_websocket", + "crates/buttplug_server_hwmgr_xinput", + "crates/buttplug_tests", + "crates/buttplug_transport_websocket_tungstenite", + "crates/examples", + "crates/intiface_engine", ] [profile.release] diff --git a/buttplug/Cargo.toml b/buttplug/Cargo.toml deleted file mode 100644 index 0e251635d..000000000 --- a/buttplug/Cargo.toml +++ /dev/null @@ -1,158 +0,0 @@ -[package] -name = "buttplug" -version = "9.0.8" -authors = ["Nonpolynomial Labs, LLC "] -description = "Buttplug Intimate Hardware Control Library" -license = "BSD-3-Clause" -homepage = "http://buttplug.io" -repository = "https://github.com/buttplugio/buttplug.git" -readme = "./README.md" -keywords = ["usb", "serial", "hardware", "bluetooth", "teledildonics"] -edition = "2021" -exclude = ["examples/**"] - -[lib] -name = "buttplug" -path = "src/lib.rs" -test = true -doctest = true -doc = true -crate-type = ["cdylib", "rlib"] - -# Only build docs on one platform (linux) -[package.metadata.docs.rs] -targets = [] -# Features to pass to Cargo (default: []) -features = ["default", "unstable"] - -[features] -# Basic features -default=["tokio-runtime", "jsonschema/resolve-file", "client", "server", "serialize-json", "websockets", "btleplug-manager", "xinput-manager", "serial-manager", "hid-manager", "lovense-dongle-manager", "lovense-connect-service-manager", "websocket-server-manager"] -client=[] -server=[] -serialize-json=[] -# Connectors -websockets=["serialize-json", "tokio-tungstenite", "rustls"] -# Device Communication Managers -xinput-manager=["server"] -btleplug-manager=["server", "btleplug"] -serial-manager=["server", "serialport"] -hid-manager=["server", "hidapi"] -lovense-dongle-manager=["server", "serialport", "hidapi"] -lovense-connect-service-manager=["server","reqwest"] -websocket-server-manager=["server", "websockets"] -# Runtime managers -tokio-runtime=[] -wasm-bindgen-runtime=[] -wasm = ["server", "wasm-bindgen-runtime", "serialize-json", "uuid/js"] -dummy-runtime=[] -# Compiler config -unstable=[] -allow-unstable-v4-connections=[] - -[dependencies] -buttplug_derive = "0.8.1" -# buttplug_derive = { path = "../buttplug_derive" } -futures = "0.3.31" -futures-util = "0.3.31" -async-trait = "0.1.88" -serde = { version = "1.0.219", features = ["derive"] } -serde_json = "1.0.140" -serde_repr = "0.1.20" -uuid = { version = "1.16.0", features = ["serde"] } -url = "2.5.4" -btleplug = { version = "0.11.8", optional = true } -# btleplug = { path = "../../btleplug", optional = true} -# btleplug = { git = 'https://github.com/deviceplug/btleplug', branch = 'master', optional = true } -strum_macros = "0.27.1" -strum = "0.27.1" -once_cell = "1.21.3" -paste = "1.0.15" -lazy_static = "1.5.0" -byteorder = "1.5.0" -thiserror = "2.0.12" -cfg-if = "1.0.0" -tracing = "0.1.41" -tracing-futures = "0.2.5" -tracing-subscriber = { version = "0.3.19", features = ["json"] } -dashmap = { version = "6.1.0", features = ["serde"] } -displaydoc = "0.2.5" -tokio = { version = "1.44.2", features = ["sync", "macros", "io-util"] } -async-stream = "0.3.6" -prost = "0.13.5" -tokio-util = "0.7.14" -reqwest = { version = "0.12.15", default-features = false, optional = true, features = ["rustls-tls"] } -serde-aux = "4.6.0" -getset = "0.1.5" -os_info = "3.10.0" -ahash = "0.8.11" -jsonschema = { version = "0.30.0", default-features = false } -derivative = "2.2.0" -tokio-stream = "0.1.17" -instant = "0.1.13" -regex = "1.11.1" -tokio-tungstenite = { version = "0.26.2", features = ["rustls-tls-webpki-roots", "url"], optional = true } -rustls = { version = "0.23.26", optional = true, default-features = false, features = ["ring"]} -aes = { version = "0.8.4" } -ecb = { version = "0.1.2", features = ["std"] } -sha2 = { version = "0.10.8", features = ["std"] } -# Used by several packages, but we need to bring in the JS feature for wasm. Pinned at 0.2 until dependencies update -rand = { version = "0.8" } -getrandom = { version = "0.2.11", features = ["js"] } - -[dev-dependencies] -serde_yaml = "0.9.34" -test-case = "3.3.1" -tokio = { version = "1.44.2", features = ["io-std", "rt"] } -tracing-log = { version = "0.2.0" } -tokio-test = "0.4.4" - -[build-dependencies] -prost-build = "0.13.5" - -[target.'cfg(target_os = "windows")'.dependencies] -rusty-xinput = "1.3.0" -windows = { version = "0.61.1", features = ["Devices_Bluetooth", "Foundation"] } -serialport = { version = "4.7.1", optional = true } -hidapi = { version = "2.6.3", default-features = false, features = ["windows-native"], optional = true } - -[target.'cfg(target_os = "linux")'.dependencies] -serialport = { version = "4.7.1", optional = true } -# Linux hidraw is needed here in order to work with the lovense dongle. libusb breaks it on linux. -# Other platforms are not affected by the feature changes. -hidapi = { version = "2.6.3", default-features = false, features = ["linux-static-hidraw"], optional = true } - -[target.'cfg(target_os = "macos")'.dependencies] -serialport = { version = "4.7.1", optional = true } -hidapi = { version = "2.6.3", default-features = false, features = ["macos-shared-device"], optional = true } - -[target.wasm32-unknown-unknown.dependencies] -wasm-bindgen = { version = "0.2.100", features = ["serde-serialize"] } -wasm-bindgen-futures = { version = "0.4.50" } -wasmtimer = { version = "0.4.1" } - -[dependencies.web-sys] -version = "0.3.77" -# path = "../../wasm-bindgen/crates/web-sys" -#git = "https://github.com/rustwasm/wasm-bindgen" -optional = true -features = [ - "Navigator", - "Bluetooth", - "BluetoothDevice", - "BluetoothLeScanFilterInit", - "BluetoothRemoteGattCharacteristic", - "BluetoothRemoteGattServer", - "BluetoothRemoteGattService", - "BinaryType", - "Blob", - "console", - "ErrorEvent", - "Event", - "FileReader", - "MessageEvent", - "ProgressEvent", - "RequestDeviceOptions", - "WebSocket", - "Window" -] diff --git a/buttplug/README.md b/buttplug/README.md index 0c2761ca7..f3622ca1b 100644 --- a/buttplug/README.md +++ b/buttplug/README.md @@ -102,7 +102,6 @@ The following crate features are available | --------- | ----------- | ----------- | | `client` | None | Buttplug client implementation (in-process connection only) | | `server` | None | Buttplug server implementation (in-process connection only) | -| `serialize-json` | None | Serde JSON serializer for Buttplug messages, needed for remote connectors | | `websockets` | `tokio-runtime` | Websocket connectors, used to connect remote clients (Clear/SSL)/servers (Clear Only) | | `btleplug-manager` | `server` | Bluetooth hardware support on Windows >=10, macOS, Linux, iOS, Android | | `lovense-dongle-manager` | `server` | Lovense USB Dongle support on Windows >=7, macOS, Linux | @@ -119,7 +118,6 @@ Default features are enough to build a full desktop system: - `tokio-runtime` - `client` - `server` -- `serialize-json` - `websocket` - `websocket-server-manager` - `btleplug-manager` (feature builds as noop on WASM) diff --git a/buttplug/buttplug-device-config/.gitignore b/buttplug/buttplug-device-config/.gitignore deleted file mode 100644 index 85e047b27..000000000 --- a/buttplug/buttplug-device-config/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -dist -node_modules -version \ No newline at end of file diff --git a/buttplug/buttplug-device-config/README.md b/buttplug/buttplug-device-config/README.md deleted file mode 100644 index 32bb262be..000000000 --- a/buttplug/buttplug-device-config/README.md +++ /dev/null @@ -1,51 +0,0 @@ -# Buttplug Device Configuration File - -[![Patreon donate button](https://img.shields.io/badge/patreon-donate-yellow.svg)](https://www.patreon.com/qdot) -[![Github donate button](https://img.shields.io/badge/github-donate-ff69b4.svg)](https://www.github.com/sponsors/qdot) -[![Discourse Forums](https://img.shields.io/discourse/status?label=buttplug.io%20forums&server=https%3A%2F%2Fdiscuss.buttplug.io)](https://discuss.buttplug.io) -[![Discord](https://img.shields.io/discord/353303527587708932.svg?logo=discord)](https://discord.buttplug.io) -[![Twitter](https://img.shields.io/twitter/follow/buttplugio.svg?style=social&logo=twitter)](https://twitter.com/buttplugio) - -[This file](buttplug-device-config.yml) contains information to bind -protocols to device identifiers for Buttplug libraries. This means -that we can take a set of connection information for something like a -Bluetooth LE device and say "if we see this, we should try to talk to -it in this certain way". - -This file is mostly useful for implementation of Buttplug -reference libraries, and is not part of the official Buttplug -Protocol. - -## License - -buttplug is BSD 3-Clause licensed. - - Copyright (c) 2017-2021, Nonpolynomial Labs LLC - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - - * Neither the name of the project nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE - FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - diff --git a/buttplug/buttplug-device-config/azure-pipelines.yml b/buttplug/buttplug-device-config/azure-pipelines.yml deleted file mode 100644 index b71f97c4a..000000000 --- a/buttplug/buttplug-device-config/azure-pipelines.yml +++ /dev/null @@ -1,44 +0,0 @@ -trigger: -- master - -variables: - buildNum: $[ counter() ] - -jobs: - - job: "Linux" - pool: - vmImage: 'ubuntu-16.04' - steps: - - task: NodeTool@0 - inputs: - versionSpec: '10.x' - displayName: 'Install Node.js' - - script: | - yarn - displayName: 'Install packages' - env: { "CI": "true" } - - script: | - yarn build - displayName: 'Build JSON version of device file' - env: { "CI": "true" } - - task: CopyFiles@2 - displayName: "Copy config files to staging" - inputs: - contents: "$(System.DefaultWorkingDirectory)/buttplug-device-config.*" - targetFolder: '$(Build.ArtifactStagingDirectory)' - - task: PublishPipelineArtifact@0 - displayName: "Publish artifacts" - inputs: - targetPath: '$(Build.ArtifactStagingDirectory)' - artifactName: 'config' - - task: GitHubRelease@0 - displayName: Upload device config release to Github if files changed - inputs: - gitHubConnection: "release" - repositoryName: "buttplugio/buttplug-device-config" - action: "create" - tagSource: "manual" - tag: "v$(buildNum)" - title: "Buttplug Device Config File Version $(buildNum)" - assets: "$(Build.ArtifactStagingDirectory)/*" - diff --git a/buttplug/buttplug-device-config/build-config/buttplug-device-config-v2.json b/buttplug/buttplug-device-config/build-config/buttplug-device-config-v2.json deleted file mode 100644 index 0ed32346b..000000000 --- a/buttplug/buttplug-device-config/build-config/buttplug-device-config-v2.json +++ /dev/null @@ -1,10104 +0,0 @@ -{ - "version": { - "major": 2, - "minor": 31 - }, - "protocols": { - "lovense": { - "btle": { - "names": [ - "LVS-*", - "LOVE-*" - ], - "manufacturer-data": [ - { - "company": 620, - "data": [ - 255, - 33 - ] - } - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff2-0000-1000-8000-00805f9b34fb", - "rx": "0000fff1-0000-1000-8000-00805f9b34fb" - }, - "6e400001-b5a3-f393-e0a9-e50e24dcca9e": { - "tx": "6e400002-b5a3-f393-e0a9-e50e24dcca9e", - "rx": "6e400003-b5a3-f393-e0a9-e50e24dcca9e" - }, - "50300001-0024-4bd4-bbd5-a6920e4c5653": { - "tx": "50300002-0024-4bd4-bbd5-a6920e4c5653", - "rx": "50300003-0024-4bd4-bbd5-a6920e4c5653" - }, - "57300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "57300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "57300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "5a300001-0024-4bd4-bbd5-a6920e4c5653": { - "tx": "5a300002-0024-4bd4-bbd5-a6920e4c5653", - "rx": "5a300003-0024-4bd4-bbd5-a6920e4c5653" - }, - "50300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "50300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "50300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "53300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "53300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "53300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "5a300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "5a300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "5a300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "4f300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "4f300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "4f300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "42300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "42300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "42300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "43300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "43300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "43300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "4c300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "4c300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "4c300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "4c410001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "4c410002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "4c410003-0023-4bd4-bbd5-a6920e4c5653" - }, - "56300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "56300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "56300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "58300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "58300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "58300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "52300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "52300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "52300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "46300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "46300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "46300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "50300011-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "50300012-0023-4bd4-bbd5-a6920e4c5653", - "rx": "50300013-0023-4bd4-bbd5-a6920e4c5653" - }, - "4a300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "4a300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "4a300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "45440001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "45440002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "45440003-0023-4bd4-bbd5-a6920e4c5653" - }, - "45420001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "45420002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "45420003-0023-4bd4-bbd5-a6920e4c5653" - }, - "54300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "54300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "54300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "45490001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "45490002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "45490003-0023-4bd4-bbd5-a6920e4c5653" - }, - "4e300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "4e300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "4e300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "45410001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "45410002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "45410003-0023-4bd4-bbd5-a6920e4c5653" - }, - "51300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "51300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "51300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "45460001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "45460002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "45460003-0023-4bd4-bbd5-a6920e4c5653" - }, - "454c0001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "454c0002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "454c0003-0023-4bd4-bbd5-a6920e4c5653" - }, - "55300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "55300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "55300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "53440001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "53440002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "53440003-0023-4bd4-bbd5-a6920e4c5653" - }, - "48300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "48300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "48300003-0023-4bd4-bbd5-a6920e4c5653" - } - } - }, - "defaults": { - "name": "Lovense Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Vibrate" - } - ], - "SensorReadCmd": [ - { - "FeatureDescriptor": "Battery Level", - "SensorType": "Battery", - "SensorRange": [ - [ - 0, - 100 - ] - ] - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "B" - ], - "name": "Lovense Max", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 20 - ], - "FeatureDescriptor": "Vibrator", - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 3 - ], - "FeatureDescriptor": "Air Pump", - "ActuatorType": "Constrict" - } - ] - } - }, - { - "identifier": [ - "P" - ], - "name": "Lovense Edge", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "A", - "C" - ], - "name": "Lovense Nora", - "messages": { - "RotateCmd": [ - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Rotate" - } - ] - } - }, - { - "identifier": [ - "L" - ], - "name": "Lovense Ambi" - }, - { - "identifier": [ - "S" - ], - "name": "Lovense Lush" - }, - { - "identifier": [ - "Z" - ], - "name": "Lovense Hush" - }, - { - "identifier": [ - "W" - ], - "name": "Lovense Domi" - }, - { - "identifier": [ - "O" - ], - "name": "Lovense Osci" - }, - { - "identifier": [ - "V" - ], - "name": "Lovense Mission" - }, - { - "identifier": [ - "X" - ], - "name": "Lovense Ferri" - }, - { - "identifier": [ - "R" - ], - "name": "Lovense Diamo" - }, - { - "identifier": [ - "ToyS" - ], - "name": "Loveai Dolp" - }, - { - "identifier": [ - "F" - ], - "name": "Lovense Sex Machine", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Oscillate", - "FeatureDescriptor": "Fucking Machine Oscillation Speed" - } - ] - } - }, - { - "identifier": [ - "FS" - ], - "name": "Lovense Mini Sex Machine", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Oscillate", - "FeatureDescriptor": "Fucking Machine Oscillation Speed" - } - ] - } - }, - { - "identifier": [ - "J" - ], - "name": "Lovense Dolce", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "ED" - ], - "name": "Lovense Gush" - }, - { - "identifier": [ - "EB" - ], - "name": "Lovense Hyphy", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "T" - ], - "name": "Lovense Calor" - }, - { - "identifier": [ - "EI" - ], - "name": "Lovense Flexer (Firmware update needed)" - }, - { - "identifier": [ - "EI-FW3" - ], - "name": "Lovense Flexer", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Vibrate", - "FeatureDescriptor": "Internal Vibe" - }, - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Vibrate", - "FeatureDescriptor": "External Vibe" - }, - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Rotate", - "FeatureDescriptor": "Finger motion" - } - ], - "SensorReadCmd": [ - { - "FeatureDescriptor": "Battery Level", - "SensorType": "Battery", - "SensorRange": [ - [ - 0, - 100 - ] - ] - } - ] - } - }, - { - "identifier": [ - "N" - ], - "name": "Lovense Gemini", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Vibrate" - } - ], - "SensorReadCmd": [ - { - "FeatureDescriptor": "Battery Level", - "SensorType": "Battery", - "SensorRange": [ - [ - 0, - 100 - ] - ] - } - ] - } - }, - { - "identifier": [ - "EA" - ], - "name": "Lovense Gravity", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Oscillate" - } - ], - "SensorReadCmd": [ - { - "FeatureDescriptor": "Battery Level", - "SensorType": "Battery", - "SensorRange": [ - [ - 0, - 100 - ] - ] - } - ] - } - }, - { - "identifier": [ - "Q" - ], - "name": "Lovense Tenera" - }, - { - "identifier": [ - "EL" - ], - "name": "Lovense Ridge", - "messages": { - "RotateCmd": [ - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Rotate" - } - ] - } - }, - { - "identifier": [ - "U" - ], - "name": "Lovense Lapis", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Vibrate", - "FeatureDescriptor": "Tip Vibe" - }, - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Vibrate", - "FeatureDescriptor": "Internal Vibe" - }, - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Vibrate", - "FeatureDescriptor": "External Vibe" - } - ], - "SensorReadCmd": [ - { - "FeatureDescriptor": "Battery Level", - "SensorType": "Battery", - "SensorRange": [ - [ - 0, - 100 - ] - ] - } - ] - } - }, - { - "identifier": [ - "SD" - ], - "name": "Lovense Vulse" - }, - { - "identifier": [ - "H" - ], - "name": "Lovense Solace", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Oscillate", - "FeatureDescriptor": "Stroker Oscillation Speed" - } - ] - } - } - ] - }, - "lovense-connect-service": { - "lovense-connect-service": { - "exists": true - }, - "defaults": { - "name": "Lovense Connect Service Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Vibrate" - } - ], - "SensorReadCmd": [ - { - "FeatureDescriptor": "Battery Level", - "SensorType": "Battery", - "SensorRange": [ - [ - 0, - 100 - ] - ] - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "Max" - ], - "name": "Lovense Max", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 20 - ], - "FeatureDescriptor": "Vibrator", - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 3 - ], - "FeatureDescriptor": "Air Pump", - "ActuatorType": "Constrict" - } - ] - } - }, - { - "identifier": [ - "Edge" - ], - "name": "Lovense Edge", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Vibrate" - } - ], - "SensorReadCmd": [ - { - "FeatureDescriptor": "Battery Level", - "SensorType": "Battery", - "SensorRange": [ - [ - 0, - 100 - ] - ] - } - ] - } - }, - { - "identifier": [ - "Nora" - ], - "name": "Lovense Nora", - "messages": { - "RotateCmd": [ - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Rotate" - } - ], - "SensorReadCmd": [ - { - "FeatureDescriptor": "Battery Level", - "SensorType": "Battery", - "SensorRange": [ - [ - 0, - 100 - ] - ] - } - ] - } - }, - { - "identifier": [ - "Ambi" - ], - "name": "Lovense Ambi" - }, - { - "identifier": [ - "Lush" - ], - "name": "Lovense Lush" - }, - { - "identifier": [ - "Hush" - ], - "name": "Lovense Hush" - }, - { - "identifier": [ - "Domi" - ], - "name": "Lovense Domi" - }, - { - "identifier": [ - "Osci" - ], - "name": "Lovense Osci" - }, - { - "identifier": [ - "Mission" - ], - "name": "Lovense Mission" - }, - { - "identifier": [ - "Ferri" - ], - "name": "Lovense Ferri" - }, - { - "identifier": [ - "Diamo" - ], - "name": "Lovense Diamo" - }, - { - "identifier": [ - "ToyS" - ], - "name": "Loveai Dolp" - }, - { - "identifier": [ - "XMachine" - ], - "name": "Lovense Sex Machine", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Oscillate", - "FeatureDescriptor": "Fucking Machine Oscillation Speed" - } - ] - } - }, - { - "identifier": [ - "Dolce" - ], - "name": "Lovense Dolce", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Vibrate" - } - ], - "SensorReadCmd": [ - { - "FeatureDescriptor": "Battery Level", - "SensorType": "Battery", - "SensorRange": [ - [ - 0, - 100 - ] - ] - } - ] - } - }, - { - "identifier": [ - "Gush" - ], - "name": "Lovense Gush" - }, - { - "identifier": [ - "Hyphy" - ], - "name": "Lovense Hyphy", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Vibrate" - } - ], - "SensorReadCmd": [ - { - "FeatureDescriptor": "Battery Level", - "SensorType": "Battery", - "SensorRange": [ - [ - 0, - 100 - ] - ] - } - ] - } - }, - { - "identifier": [ - "Calor" - ], - "name": "Lovense Calor" - }, - { - "identifier": [ - "Flexer" - ], - "name": "Lovense Flexer", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Vibrate", - "FeatureDescriptor": "Both Vibes" - }, - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Rotate", - "FeatureDescriptor": "Finger motion" - } - ], - "SensorReadCmd": [ - { - "FeatureDescriptor": "Battery Level", - "SensorType": "Battery", - "SensorRange": [ - [ - 0, - 100 - ] - ] - } - ] - } - }, - { - "identifier": [ - "Gemini" - ], - "name": "Lovense Gemini", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Vibrate" - } - ], - "SensorReadCmd": [ - { - "FeatureDescriptor": "Battery Level", - "SensorType": "Battery", - "SensorRange": [ - [ - 0, - 100 - ] - ] - } - ] - } - }, - { - "identifier": [ - "Gravity" - ], - "name": "Lovense Gravity", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Oscillate" - } - ], - "SensorReadCmd": [ - { - "FeatureDescriptor": "Battery Level", - "SensorType": "Battery", - "SensorRange": [ - [ - 0, - 100 - ] - ] - } - ] - } - }, - { - "identifier": [ - "Ridge" - ], - "name": "Lovense Ridge", - "messages": { - "RotateCmd": [ - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Rotate" - } - ], - "SensorReadCmd": [ - { - "FeatureDescriptor": "Battery Level", - "SensorType": "Battery", - "SensorRange": [ - [ - 0, - 100 - ] - ] - } - ] - } - }, - { - "identifier": [ - "Lapis" - ], - "name": "Lovense Lapis", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Vibrate", - "FeatureDescriptor": "Tip Vibe" - }, - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Vibrate", - "FeatureDescriptor": "Internal Vibe" - }, - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Vibrate", - "FeatureDescriptor": "External Vibe" - } - ], - "SensorReadCmd": [ - { - "FeatureDescriptor": "Battery Level", - "SensorType": "Battery", - "SensorRange": [ - [ - 0, - 100 - ] - ] - } - ] - } - }, - { - "identifier": [ - "Vulse" - ], - "name": "Lovense Vulse" - }, - { - "identifier": [ - "Solace" - ], - "name": "Lovense Solace", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Oscillate", - "FeatureDescriptor": "Stroker Oscillation Speed" - } - ] - } - } - ] - }, - "xinput": { - "xinput": { - "exists": true - }, - "defaults": { - "name": "XBox (XInput) Compatible Gamepad", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 65535 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 65535 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "kiiroo-v2": { - "btle": { - "names": [ - "Launch", - "Onyx2" - ], - "services": { - "88f80580-0000-01e6-aace-0002a5d5c51b": { - "tx": "88f80581-0000-01e6-aace-0002a5d5c51b", - "rx": "88f80582-0000-01e6-aace-0002a5d5c51b", - "firmware": "88f80583-0000-01e6-aace-0002a5d5c51b" - }, - "f60402a6-0293-4bdb-9f20-6758133f7090": { - "tx": "02962ac9-e86f-4094-989d-231d69995fc2", - "rx": "d44d0393-0731-43b3-a373-8fc70b1f3323", - "firmware": "c7b7a04b-2cc4-40ff-8b10-5d531d1161db" - } - } - }, - "defaults": { - "name": "Kiiroo v2 Device", - "messages": { - "LinearCmd": [ - { - "StepRange": [ - 0, - 99 - ], - "ActuatorType": "Position" - } - ], - "FleshlightLaunchFW12Cmd": {} - } - }, - "configurations": [ - { - "identifier": [ - "Launch" - ], - "name": "Fleshlight Launch" - }, - { - "identifier": [ - "Onyx2" - ], - "name": "Kiiroo Onyx 2" - } - ] - }, - "libo-elle": { - "btle": { - "names": [ - "PiPiJing", - "Shuidi" - ], - "services": { - "00006000-0000-1000-8000-00805f9b34fb": { - "tx": "00006001-0000-1000-8000-00805f9b34fb", - "txmode": "00006002-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Libo Elle Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 3 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "PiPiJing" - ], - "name": "LiBo Elle" - }, - { - "identifier": [ - "Shuidi" - ], - "name": "Libo Elle 2" - } - ] - }, - "libo-shark": { - "btle": { - "names": [ - "ShaYu" - ], - "services": { - "00006000-0000-1000-8000-00805f9b34fb": { - "tx": "00006001-0000-1000-8000-00805f9b34fb", - "txmode": "00006002-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Libo Shark", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 3 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 3 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "libo-karen": { - "btle": { - "names": [ - "SuoYinQiu" - ], - "services": { - "00006000-0000-1000-8000-00805f9b34fb": { - "tx": "00006001-0000-1000-8000-00805f9b34fb", - "txmode": "00006002-0000-1000-8000-00805f9b34fb" - }, - "00006050-0000-1000-8000-00805f9b34fb": { - "rxpressure": "00006051-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Libo Karen", - "messages": {} - } - }, - "libo-vibes": { - "btle": { - "names": [ - "XiaoLu", - "LuXiaoHan", - "BaiHu", - "Gugudai", - "Yuyi", - "LuWuShuang", - "LiBo", - "QingTing", - "Huohu", - "Yuyi", - "Haima" - ], - "services": { - "00006000-0000-1000-8000-00805f9b34fb": { - "tx": "00006001-0000-1000-8000-00805f9b34fb", - "txmode": "00006002-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Libo Vibes Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "XiaoLu" - ], - "name": "Libo Lottie" - }, - { - "identifier": [ - "LuXiaoHan" - ], - "name": "Libo LuLu" - }, - { - "identifier": [ - "Yuyi" - ], - "name": "Libo Lina" - }, - { - "identifier": [ - "LuWuShuang" - ], - "name": "Libo Adel" - }, - { - "identifier": [ - "LiBo" - ], - "name": "Libo Lily" - }, - { - "identifier": [ - "QingTing" - ], - "name": "Libo Lucy" - }, - { - "identifier": [ - "Huohu" - ], - "name": "Libo Lara" - }, - { - "identifier": [ - "Yuyi" - ], - "name": "Libo Feather", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 99 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "BaiHu" - ], - "name": "Libo LaLa", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 3 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "Gugudai" - ], - "name": "Libo Carlos", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 3 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "Haima" - ], - "name": "Libo Selina", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 3 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - ] - }, - "magic-motion-1": { - "btle": { - "names": [ - "Smart Mini Vibe*", - "Flamingo", - "Flamingo T", - "Smart Bean", - "Smart Bean3", - "Magic Cell", - "Magic Wand", - "Fugu", - "Fugu2", - "Gballs2", - "GBalls3", - "FM-LILAC-101", - "Xone", - "CBT002" - ], - "services": { - "78667579-7b48-43db-b8c5-7928a6b0a335": { - "tx": "78667579-a914-49a4-8333-aa3c0cd8fedc" - }, - "0000180f-0000-1000-8000-00805f9b34fb": { - "rxblebattery": "00002a19-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Magic Motion V1 Device", - "messages": { - "SensorReadCmd": [ - { - "FeatureDescriptor": "Battery Level", - "SensorType": "Battery", - "SensorRange": [ - [ - 0, - 100 - ] - ] - } - ], - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "Smart Bean" - ], - "name": "MagicMotion Smart Bean" - }, - { - "identifier": [ - "Smart Bean3" - ], - "name": "FitCute Kegel Rejuve" - }, - { - "identifier": [ - "Smart Mini Vibe" - ], - "name": "MagicMotion Smart Mini Vibe" - }, - { - "identifier": [ - "Smart Mini Vibe3" - ], - "name": "MagicMotion Vini" - }, - { - "identifier": [ - "Flamingo", - "Flamingo T" - ], - "name": "MagicMotion Flamingo" - }, - { - "identifier": [ - "Magic Bean" - ], - "name": "MagicMotion Kegel" - }, - { - "identifier": [ - "Magic Cell" - ], - "name": "MagicMotion Dante/Candy/Rise" - }, - { - "identifier": [ - "Magic Wand" - ], - "name": "MagicMotion Wand" - }, - { - "identifier": [ - "Magic Fugu", - "Fugu", - "Fugu2" - ], - "name": "MagicMotion Fugu" - }, - { - "identifier": [ - "Gballs2" - ], - "name": "G Vibe Gballs 2" - }, - { - "identifier": [ - "GBalls3" - ], - "name": "G Vibe Gballs 3" - }, - { - "identifier": [ - "FM-LILAC-101" - ], - "name": "Femometer Lilac" - }, - { - "identifier": [ - "Xone" - ], - "name": "MagicMotion Xone", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Oscillate" - } - ] - } - }, - { - "identifier": [ - "CBT002" - ], - "name": "FunTown Caleo" - } - ] - }, - "magic-motion-2": { - "btle": { - "names": [ - "Eidolon", - "Lipstick", - "Sword", - "Curve", - "Solstice X", - "funwand", - "CBT001" - ], - "services": { - "78667579-7b48-43db-b8c5-7928a6b0a335": { - "tx": "78667579-a914-49a4-8333-aa3c0cd8fedc" - }, - "0000180f-0000-1000-8000-00805f9b34fb": { - "rxblebattery": "00002a19-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Magic Motion V2 Device", - "messages": { - "SensorReadCmd": [ - { - "FeatureDescriptor": "Battery Level", - "SensorType": "Battery", - "SensorRange": [ - [ - 0, - 100 - ] - ] - } - ], - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "Lipstick" - ], - "name": "MagicMotion Awaken" - }, - { - "identifier": [ - "Sword" - ], - "name": "MagicMotion Equinox" - }, - { - "identifier": [ - "Curve" - ], - "name": "MagicMotion Solstice" - }, - { - "identifier": [ - "Eidolon" - ], - "name": "MagicMotion Eidolon", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "Solstice X" - ], - "name": "MagicMotion Solstice X", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "funwand" - ], - "name": "MagicMotion Zenith" - }, - { - "identifier": [ - "CBT001" - ], - "name": "FunTown Jive", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Oscillate" - } - ] - } - } - ] - }, - "magic-motion-3": { - "btle": { - "names": [ - "Krush" - ], - "services": { - "78667579-7b48-43db-b8c5-7928a6b0a335": { - "tx": "78667579-a914-49a4-8333-aa3c0cd8fedc" - }, - "0000180f-0000-1000-8000-00805f9b34fb": { - "rxblebattery": "00002a19-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "LoveLife Krush", - "messages": { - "SensorReadCmd": [ - { - "FeatureDescriptor": "Battery Level", - "SensorType": "Battery", - "SensorRange": [ - [ - 0, - 100 - ] - ] - } - ], - "ScalarCmd": [ - { - "StepRange": [ - 0, - 77 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "magic-motion-4": { - "btle": { - "names": [ - "funone", - "Magic Sundi", - "Kegel Coach", - "Magic Lotos", - "nyx", - "umi", - "funkegel", - "bobi2" - ], - "services": { - "78667579-7b48-43db-b8c5-7928a6b0a335": { - "tx": "78667579-a914-49a4-8333-aa3c0cd8fedc" - }, - "0000180f-0000-1000-8000-00805f9b34fb": { - "rxblebattery": "00002a19-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Magic Motion V4 Device", - "messages": { - "SensorReadCmd": [ - { - "FeatureDescriptor": "Battery Level", - "SensorType": "Battery", - "SensorRange": [ - [ - 0, - 100 - ] - ] - } - ], - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "funone" - ], - "name": "MagicMotion Bunny" - }, - { - "identifier": [ - "Magic Sundi" - ], - "name": "MagicMotion Sundae" - }, - { - "identifier": [ - "Kegel Coach" - ], - "name": "MagicMotion Kegel Coach" - }, - { - "identifier": [ - "Magic Lotos" - ], - "name": "MagicMotion Lotos" - }, - { - "identifier": [ - "nyx" - ], - "name": "MagicMotion Nyx" - }, - { - "identifier": [ - "umi" - ], - "name": "MagicMotion Umi", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "funkegel" - ], - "name": "MagicMotion Crystal" - }, - { - "identifier": [ - "bobi2" - ], - "name": "MagicMotion Bobi", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - ] - }, - "mysteryvibe": { - "btle": { - "names": [ - "MV Crescendo", - "MV Tenuto ", - "MV Poco " - ], - "services": { - "f0006900-110c-478b-b74b-6f403b364a9c": { - "txmode": "f0006901-110c-478b-b74b-6f403b364a9c", - "txvibrate": "f0006903-110c-478b-b74b-6f403b364a9c" - } - } - }, - "defaults": { - "name": "Mysteryvibe Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 56 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 56 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 56 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 56 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 56 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 56 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "MV Crescendo" - ], - "name": "MysteryVibe Crescendo" - }, - { - "identifier": [ - "MV Tenuto " - ], - "name": "MysteryVibe Tenuto" - }, - { - "identifier": [ - "MV Poco " - ], - "name": "MysteryVibe Poco", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 56 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 56 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - ] - }, - "mysteryvibe-v2": { - "btle": { - "names": [ - "6907 MV1", - "6908 MV1", - "6909 MV1", - "6914 MV1", - "6915 MV1" - ], - "services": { - "f0006900-110c-478b-b74b-6f403b364a9c": { - "txmode": "f0006901-110c-478b-b74b-6f403b364a9c", - "txvibrate": "f0006903-110c-478b-b74b-6f403b364a9c" - } - } - }, - "defaults": { - "name": "Mysteryvibe V2 Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 56 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 56 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 56 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "6907 MV1" - ], - "name": "MysteryVibe Tenuto Mini" - }, - { - "identifier": [ - "6908 MV1" - ], - "name": "MysteryVibe Crescendo 2", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 56 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 56 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 56 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 56 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 56 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 56 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "6909 MV1" - ], - "name": "MysteryVibe Tenuto 2", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 56 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 56 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 56 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 56 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "6914 MV1" - ], - "name": "MysteryVibe Legato", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 56 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 56 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 56 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 56 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "6915 MV1" - ], - "name": "MysteryVibe Molto", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 56 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - ] - }, - "picobong": { - "btle": { - "names": [ - "Blow hole", - "Picobong Male Toy", - "Diver", - "Picobong Egg", - "Life guard", - "Picobong Ring", - "Surfer", - "Picobong Butt Plug", - "Egg driver", - "Surfer_plug" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff1-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Picobong Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 10 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "Blow hole", - "Picobong Male Toy" - ], - "name": "Picobong Blow hole" - }, - { - "identifier": [ - "Diver", - "Picobong Egg" - ], - "name": "Picobong Diver" - }, - { - "identifier": [ - "Life guard", - "Picobong Ring" - ], - "name": "Picobong Life guard" - }, - { - "identifier": [ - "Surfer", - "Picobong Butt Plug", - "Egg driver", - "Surfer_plug" - ], - "name": "Picobong Surfer" - } - ] - }, - "vibratissimo": { - "btle": { - "names": [ - "Vibratissimo" - ], - "services": { - "00001523-1212-efde-1523-785feabcd123": { - "txmode": "00001524-1212-efde-1523-785feabcd123", - "txvibrate": "00001526-1212-efde-1523-785feabcd123", - "rx": "00001527-1212-efde-1523-785feabcd123" - }, - "0000180a-0000-1000-8000-00805f9b34fb": { - "rxblemodel": "00002a24-0000-1000-8000-00805f9b34fb" - }, - "0000180f-0000-1000-8000-00805f9b34fb": { - "rxblebattery": "00002a19-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Vibratissimo Device", - "messages": { - "SensorReadCmd": [ - { - "FeatureDescriptor": "Battery Level", - "SensorType": "Battery", - "SensorRange": [ - [ - 0, - 100 - ] - ] - } - ], - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "Licker", - "SecretKiss", - "Womenizer" - ], - "name": "Vibratissimo Licker", - "messages": { - "SensorReadCmd": [ - { - "FeatureDescriptor": "Battery Level", - "SensorType": "Battery", - "SensorRange": [ - [ - 0, - 100 - ] - ] - } - ], - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "Rabbit" - ], - "name": "Vibratissimo Rabbit", - "messages": { - "SensorReadCmd": [ - { - "FeatureDescriptor": "Battery Level", - "SensorType": "Battery", - "SensorRange": [ - [ - 0, - 100 - ] - ] - } - ], - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 2 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - ] - }, - "wevibe": { - "btle": { - "names": [ - "Cougar", - "4 Plus", - "4_Plus", - "4plus", - "Bloom", - "classic", - "Classic", - "Ditto", - "Gala", - "Jive", - "Nova", - "Pivot", - "Rave", - "Sync", - "Verge", - "Wish" - ], - "services": { - "f000bb03-0451-4000-b000-000000000000": { - "tx": "f000c000-0451-4000-b000-000000000000", - "rx": "f000b000-0451-4000-b000-000000000000" - } - } - }, - "defaults": { - "name": "WeVibe Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 15 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "Bloom" - ], - "name": "WeVibe Bloom" - }, - { - "identifier": [ - "Ditto" - ], - "name": "WeVibe Ditto" - }, - { - "identifier": [ - "Jive" - ], - "name": "WeVibe Jive" - }, - { - "identifier": [ - "Pivot" - ], - "name": "WeVibe Pivot" - }, - { - "identifier": [ - "Rave" - ], - "name": "WeVibe Rave" - }, - { - "identifier": [ - "Verge" - ], - "name": "WeVibe Verge" - }, - { - "identifier": [ - "Wish" - ], - "name": "WeVibe Wish" - }, - { - "identifier": [ - "Cougar", - "4 Plus", - "4_Plus", - "4plus", - "classic", - "Classic" - ], - "name": "WeVibe 4 Plus", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 15 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 15 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "Gala" - ], - "name": "WeVibe Gala", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 15 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 15 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "Nova" - ], - "name": "WeVibe Nova", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 15 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 15 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "Sync" - ], - "name": "WeVibe Sync", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 15 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 15 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - ] - }, - "wevibe-8bit": { - "btle": { - "names": [ - "Melt", - "Moxie", - "Vector", - "Wand", - "Bond", - "Nelson", - "Nova2", - "Nova_2", - "Nova 2" - ], - "services": { - "f000bb03-0451-4000-b000-000000000000": { - "tx": "f000c000-0451-4000-b000-000000000000", - "rx": "f000b000-0451-4000-b000-000000000000" - } - } - }, - "defaults": { - "name": "WeVibe 8-bit Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 12 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "Melt" - ], - "name": "WeVibe Melt", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 22 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "Moxie" - ], - "name": "WeVibe Moxie" - }, - { - "identifier": [ - "Vector" - ], - "name": "WeVibe Vector", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 12 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 12 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "Wand" - ], - "name": "WeVibe Wand", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 22 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "Bond", - "Nelson" - ], - "name": "WeVibe Bond", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 27 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "Nova2", - "Nova_2", - "Nova 2" - ], - "name": "WeVibe Nova 2", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 27 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 27 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - ] - }, - "wevibe-legacy": { - "btle": { - "names": [ - "Reina", - "imassager", - "Interactive Massager", - "03" - ], - "services": { - "f000bb03-0451-4000-b000-000000000000": { - "tx": "f000c000-0451-4000-b000-000000000000", - "rx": "f000b000-0451-4000-b000-000000000000" - } - } - }, - "defaults": { - "name": "WeVibe Realm Reina", - "messages": {} - } - }, - "wevibe-chorus": { - "btle": { - "names": [ - "Chorus", - "skeena", - "Sync 2", - "Sync Lite" - ], - "services": { - "f000bb03-0451-4000-b000-000000000000": { - "tx": "f000c000-0451-4000-b000-000000000000", - "rx": "f000b000-0451-4000-b000-000000000000" - } - } - }, - "defaults": { - "name": "WeVibe Chorus", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 30 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 30 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "Sync 2" - ], - "name": "WeVibe Sync 2", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 30 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 30 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "Sync Lite" - ], - "name": "WeVibe Sync Lite", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 30 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - ] - }, - "youcups": { - "btle": { - "names": [ - "Youcups" - ], - "services": { - "0000fee9-0000-1000-8000-00805f9b34fb": { - "tx": "d44bc439-abfd-45a2-b575-925416129600" - } - } - }, - "defaults": { - "name": "Youcups Warrior II", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 8 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "cueme": { - "btle": { - "names": [ - "FUNCODE_*" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff1-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Cueme Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 15 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 15 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 15 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 15 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 15 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 15 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 15 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 15 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "1" - ], - "name": "Cueme Mens" - }, - { - "identifier": [ - "2" - ], - "name": "Cueme Bra" - }, - { - "identifier": [ - "3" - ], - "name": "Cueme Womans", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 15 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 15 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 15 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 15 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - ] - }, - "kiiroo-v2-vibrator": { - "btle": { - "names": [ - "Pearl2", - "Fuse", - "Virtual Blowbot", - "Titan", - "Virtual Rabbit" - ], - "services": { - "88f82580-0000-01e6-aace-0002a5d5c51b": { - "tx": "88f82581-0000-01e6-aace-0002a5d5c51b", - "rxtouch": "88f82582-0000-01e6-aace-0002a5d5c51b", - "rxaccel": "88f82584-0000-01e6-aace-0002a5d5c51b" - } - } - }, - "defaults": { - "name": "Kiiroo V2 Vibrator Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "Pearl2" - ], - "name": "Kiiroo Pearl 2", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "Fuse" - ], - "name": "OhMiBod Fuse", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate", - "FeatureOrder": 1 - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate", - "FeatureOrder": 0 - } - ] - } - }, - { - "identifier": [ - "Virtual Rabbit" - ], - "name": "PornHub Virtual Rabbit", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate", - "FeatureOrder": 1 - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate", - "FeatureOrder": 0 - } - ] - } - }, - { - "identifier": [ - "Virtual Blowbot" - ], - "name": "PornHub Virtual Blowbot", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "Titan" - ], - "name": "Kiiroo Titan", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - ] - }, - "kiiroo-v21": { - "btle": { - "names": [ - "Titan1.1", - "Cliona", - "Pearl2.1", - "Pearl2+", - "Pearl 2+", - "Pearl3", - "Pearl 3", - "OhMiBod 4.0", - "OhMiBod LUMEN", - "OhMiBod NEX3", - "OhMiBod ESCA", - "OhMiBod Foxy", - "OhMiBod Chill Panty Vibe", - "OhMiBod Sphinx", - "Pulse Interactive", - "Fuse1.1" - ], - "services": { - "00001900-0000-1000-8000-00805f9b34fb": { - "whitelist": "00001901-0000-1000-8000-00805f9b34fb", - "tx": "00001902-0000-1000-8000-00805f9b34fb", - "rx": "00001903-0000-1000-8000-00805f9b34fb" - }, - "a0d70001-4c16-4ba7-977a-d394920e13a3": { - "tx": "a0d70002-4c16-4ba7-977a-d394920e13a3", - "rx": "a0d70003-4c16-4ba7-977a-d394920e13a3" - } - } - }, - "defaults": { - "name": "Kiiroo V2.1 Device", - "messages": {} - }, - "configurations": [ - { - "identifier": [ - "Pearl2.1" - ], - "name": "Kiiroo Pearl 2.1", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ], - "SensorReadCmd": [ - { - "SensorType": "Battery", - "FeatureDescriptor": "Battery Level", - "SensorRange": [ - [ - 0, - 100 - ] - ] - } - ], - "SensorSubscribeCmd": [ - { - "SensorType": "Pressure", - "FeatureDescriptor": "Pressure (analog)", - "SensorRange": [ - [ - 0, - 65535 - ], - [ - 0, - 65535 - ], - [ - 0, - 65535 - ], - [ - 0, - 65535 - ] - ] - }, - { - "SensorType": "Button", - "FeatureDescriptor": "Pressure (digital)", - "SensorRange": [ - [ - 0, - 1 - ], - [ - 0, - 1 - ], - [ - 0, - 1 - ], - [ - 0, - 1 - ] - ] - } - ] - } - }, - { - "identifier": [ - "Cliona" - ], - "name": "Kiiroo Cliona", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "OhMiBod 4.0", - "OhMiBod ESCA" - ], - "name": "OhMiBod Esca 2", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "Titan1.1" - ], - "name": "Kiiroo Titan 1.1", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ], - "LinearCmd": [ - { - "StepRange": [ - 0, - 99 - ], - "ActuatorType": "Position" - } - ], - "FleshlightLaunchFW12Cmd": {} - } - }, - { - "identifier": [ - "OhMiBod LUMEN" - ], - "name": "OhMiBod Lumen", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "OhMiBod NEX3" - ], - "name": "hMiBod NEX|3", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "Pulse Interactive" - ], - "name": "Hot Octopuss Pulse Solo Interactive", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 6 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "Fuse1.1" - ], - "name": "OhMiBod Fuse 1.1", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "OhMiBod Foxy" - ], - "name": "OhMiBod Foxy", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "OhMiBod Chill Panty Vibe" - ], - "name": "OhMiBod Chill", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "OhMiBod Sphinx" - ], - "name": "OhMiBod Sphinx", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "Pearl2+", - "Pearl 2+" - ], - "name": "Kiiroo Pearl 2+", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "Pearl3", - "Pearl 3" - ], - "name": "Kiiroo Pearl 3", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - ] - }, - "kiiroo-v21-initialized": { - "btle": { - "names": [ - "Rey", - "We-Vibe Rocketman", - "Realm1.1", - "Onyx2.1", - "Onyx+", - "KEON", - "Keon R2" - ], - "services": { - "00001900-0000-1000-8000-00805f9b34fb": { - "whitelist": "00001901-0000-1000-8000-00805f9b34fb", - "tx": "00001902-0000-1000-8000-00805f9b34fb", - "rx": "00001903-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Kiiroo V2.1 Initialized Device", - "messages": {} - }, - "configurations": [ - { - "identifier": [ - "Onyx2.1" - ], - "name": "Kiiroo Onyx 2.1", - "messages": { - "LinearCmd": [ - { - "StepRange": [ - 0, - 99 - ], - "ActuatorType": "Position" - } - ], - "FleshlightLaunchFW12Cmd": {} - } - }, - { - "identifier": [ - "Onyx+" - ], - "name": "Kiiroo Onyx+", - "messages": { - "LinearCmd": [ - { - "StepRange": [ - 0, - 99 - ], - "ActuatorType": "Position" - } - ], - "FleshlightLaunchFW12Cmd": {} - } - }, - { - "identifier": [ - "KEON", - "Keon R2" - ], - "name": "Kiiroo Keon", - "messages": { - "LinearCmd": [ - { - "StepRange": [ - 0, - 99 - ], - "ActuatorType": "Position" - } - ], - "FleshlightLaunchFW12Cmd": {} - } - }, - { - "identifier": [ - "Rey", - "We-Vibe Rocketman", - "Realm1.1" - ], - "name": "Kiiroo Onyx+ Realm Edition", - "messages": { - "LinearCmd": [ - { - "StepRange": [ - 0, - 99 - ], - "ActuatorType": "Position" - } - ], - "FleshlightLaunchFW12Cmd": {} - } - } - ] - }, - "vorze-cyclone-x": { - "hid": [ - { - "vendor-id": 1155, - "product-id": 22352 - } - ], - "defaults": { - "name": "Vorze Cyclone X10 Device", - "messages": { - "RotateCmd": [ - { - "StepRange": [ - 0, - 10 - ], - "ActuatorType": "Rotate" - } - ] - } - } - }, - "rez-trancevibrator": { - "usb": [ - { - "vendor-id": 2889, - "product-id": 1615 - } - ], - "defaults": { - "name": "Rez TranceVibrator", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "kiiroo-v1": { - "btle": { - "names": [ - "ONYX", - "PEARL" - ], - "services": { - "49535343-fe7d-4ae5-8fa9-9fafd205e455": { - "rx": "49535343-1e4d-4bd9-ba61-23c647249616", - "tx": "49535343-8841-43f4-a8d4-ecbe34729bb3", - "command": "49535343-aca3-481c-91ec-d85e28a60318" - } - } - }, - "defaults": { - "name": "Kiiroo V1 Device", - "messages": {} - }, - "configurations": [ - { - "identifier": [ - "PEARL" - ], - "name": "Kiiroo Pearl", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 4 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "ONYX" - ], - "name": "Kiiroo Onyx", - "messages": { - "LinearCmd": [ - { - "StepRange": [ - 0, - 4 - ], - "ActuatorType": "Position" - } - ] - } - } - ] - }, - "vorze-sa": { - "btle": { - "names": [ - "Bach smart", - "CycSA", - "UFOSA", - "UFO-TW", - "VorzePiston", - "ROCKET" - ], - "services": { - "40ee1111-63ec-4b7f-8ce7-712efd55b90e": { - "tx": "40ee2222-63ec-4b7f-8ce7-712efd55b90e" - } - } - }, - "defaults": { - "name": "Vorze Device", - "messages": {} - }, - "configurations": [ - { - "identifier": [ - "Bach smart" - ], - "name": "Vorze Bach", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "ROCKET" - ], - "name": "Adult Festa Rocket", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "CycSA" - ], - "name": "Vorze A10 Cyclone SA", - "messages": { - "RotateCmd": [ - { - "StepRange": [ - 0, - 99 - ], - "ActuatorType": "Rotate" - } - ], - "VorzeA10CycloneCmd": {} - } - }, - { - "identifier": [ - "UFOSA" - ], - "name": "Vorze UFO SA", - "messages": { - "RotateCmd": [ - { - "StepRange": [ - 0, - 99 - ], - "ActuatorType": "Rotate" - } - ], - "VorzeA10CycloneCmd": {} - } - }, - { - "identifier": [ - "UFO-TW" - ], - "name": "Vorze UFO TW", - "messages": { - "RotateCmd": [ - { - "StepRange": [ - 0, - 99 - ], - "ActuatorType": "Rotate" - }, - { - "StepRange": [ - 0, - 99 - ], - "ActuatorType": "Rotate" - } - ] - } - }, - { - "identifier": [ - "VorzePiston" - ], - "name": "Vorze Piston", - "messages": { - "LinearCmd": [ - { - "StepRange": [ - 0, - 99 - ], - "ActuatorType": "Position" - } - ] - } - } - ] - }, - "youou": { - "btle": { - "names": [ - "VX001_*" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff6-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Youou Wand Vibrator", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "realtouch": { - "hid": [ - { - "vendor-id": 8020, - "product-id": 1 - } - ], - "defaults": { - "name": "RealTouch", - "messages": { - "LinearCmd": [ - { - "StepRange": [ - 0, - 99 - ], - "ActuatorType": "Position" - } - ] - } - } - }, - "prettylove": { - "btle": { - "names": [ - "Aogu BLE *" - ], - "services": { - "0000ffe5-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe9-0000-1000-8000-00805f9b34fb", - "rx": "0000ffe2-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Pretty Love Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 3 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "svakom": { - "btle": { - "names": [ - "Aogu SUV", - "Aogu SCB", - "Emma NEO", - "Phoenix NEO" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb", - "rx": "0000ffe2-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Svakom Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 19 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "Aogu SCB" - ], - "name": "Svakom Ella" - }, - { - "identifier": [ - "Phoenix NEO" - ], - "name": "Svakom Phoenix Neo" - }, - { - "identifier": [ - "Emma NEO" - ], - "name": "Svakom Emma Neo" - } - ] - }, - "svakom-v2": { - "btle": { - "names": [ - "116", - "117", - "118", - "Viviana", - "Ella NEO", - "S38A", - "Vick NEO", - "Vick Neo", - "STG05A", - "QH-SJ007A" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb", - "rx": "0000ffe2-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Svakom Device v2", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 10 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "116" - ], - "name": "Svakom Phoenix Neo" - }, - { - "identifier": [ - "Viviana" - ], - "name": "Svakom Viviana" - }, - { - "identifier": [ - "Ella NEO" - ], - "name": "Svakom Ella Neo" - }, - { - "identifier": [ - "117" - ], - "name": "Svakom Edeny" - }, - { - "identifier": [ - "S38A" - ], - "name": "Svakom Tammy Pro" - }, - { - "identifier": [ - "Vick NEO", - "Vick Neo" - ], - "name": "Svakom Vick Neo" - }, - { - "identifier": [ - "STG05A" - ], - "name": "Svakom Aravinda" - }, - { - "identifier": [ - "118" - ], - "name": "ToyCod Vanesia" - }, - { - "identifier": [ - "QH-SJ007A" - ], - "name": "Svakom Winni 2" - } - ] - }, - "svakom-v3": { - "btle": { - "names": [ - "Phoenix Neo 2", - "FK008A", - "Hannes NEO", - "QH-SX007E" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb", - "rx": "0000ffe2-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Svakom Device v3", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 10 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "Phoenix Neo 2" - ], - "name": "Svakom Phoenix Neo 2" - }, - { - "identifier": [ - "FK008A" - ], - "name": "Fantasy Cup Theodore", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 10 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 1 - ], - "ActuatorType": "Rotate" - } - ] - } - }, - { - "identifier": [ - "Hannes NEO" - ], - "name": "Svakom Hannes Neo" - }, - { - "identifier": [ - "QH-SX007E" - ], - "name": "Svakom Alberta", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 10 - ], - "ActuatorType": "Vibrate", - "FeatureDescriptor": "Vibrating attachments" - }, - { - "StepRange": [ - 0, - 1 - ], - "ActuatorType": "Vibrate", - "FeatureDescriptor": "Suction lens" - } - ] - } - } - ] - }, - "svakom-v4": { - "btle": { - "names": [ - "B2CM6", - "ERICA" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb", - "rx": "0000ffe2-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Svakom Device v4", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 10 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 10 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "B2CM6" - ], - "name": "ToyCod Barzillai" - }, - { - "identifier": [ - "ERICA" - ], - "name": "Svakom Erica" - } - ] - }, - "svakom-v5": { - "btle": { - "names": [ - "Chika", - "Mora Neo" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb", - "rx": "0000ffe2-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Svakom Device v5", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 10 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 10 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "Chika" - ], - "name": "Svakom Chika" - }, - { - "identifier": [ - "Mora Neo" - ], - "name": "Svakom Mora Neo", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 10 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 10 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 3 - ], - "ActuatorType": "Oscillate" - } - ] - } - } - ] - }, - "svakom-sam": { - "btle": { - "names": [ - "Sam Neo" - ], - "services": { - "0000ae00-0000-1000-8000-00805f9b34fb": { - "tx": "0000ae01-0000-1000-8000-00805f9b34fb", - "rx": "0000ae02-0000-1000-8000-00805f9b34fb", - "txmode": "0000ae10-0000-1000-8000-00805f9b34fb" - }, - "0000ffac-0000-1000-8000-00805f9b34fb": { - "firmware": "0000ffb4-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Svakom Sam Neo", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 10 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 1 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "svakom-alex": { - "btle": { - "names": [ - "Alex NEO", - "S63E Alex NEO" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Svakom Alex Neo", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 3 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "svakom-alex-v2": { - "btle": { - "names": [ - "Alex NEO 2", - "S63E Alex NEO 2" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Svakom Alex Neo 2", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 3 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "svakom-dt250a": { - "btle": { - "names": [ - "DT250A" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb", - "rx": "0000ffe2-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Coleur Dor DT250A", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 3 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 3 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 2 - ], - "ActuatorType": "Constrict" - } - ] - } - } - }, - "svakom-iker": { - "btle": { - "names": [ - "Iker*" - ], - "manufacturer-data": [ - { - "company": 39, - "data": [ - 83, - 86, - 65, - 1, - 11, - 18, - 1, - 51, - 68, - 85, - 202, - 8 - ] - } - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Svakom Iker", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 10 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 5 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "svakom-pulse": { - "btle": { - "names": [ - "SWK-SX013A", - "Pulse Union", - "Pulse Galaxie", - "SX033APP", - "BX288A", - "QH-SX045A-B", - "SWK-SX067-B" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb", - "rx": "0000ffe2-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Svakom Pulse Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 9 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "SWK-SX013A" - ], - "name": "Svakom Pulse Lite Neo" - }, - { - "identifier": [ - "Pulse Union" - ], - "name": "Svakom Pulse Union" - }, - { - "identifier": [ - "Pulse Galaxie" - ], - "name": "Svakom Pulse Galaxie" - }, - { - "identifier": [ - "SX033APP" - ], - "name": "Svakom Mimiki" - }, - { - "identifier": [ - "BX288A" - ], - "name": "BeYourLover Kyukyu" - }, - { - "identifier": [ - "QH-SX045A-B" - ], - "name": "Coleur Dor VX045A" - }, - { - "identifier": [ - "SWK-SX067-B" - ], - "name": "Momonii Agatha" - } - ] - }, - "svakom-suitcase": { - "btle": { - "names": [ - "VX357A-BLE-V1.0", - "VX236A-BLE-V1.0" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb", - "rx": "0000ffe2-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Svakom Magic Suitcase", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 30 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 1 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "VX236A-BLE-V1.0" - ], - "name": "Coleur Dor VX236A" - } - ] - }, - "svakom-tarax": { - "btle": { - "names": [ - "SX218A" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb", - "rx": "0000ffe2-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "ToyCod Tara X", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 3 - ], - "ActuatorType": "Vibrate", - "FeatureDescriptor": "Internal vibrator" - }, - { - "StepRange": [ - 0, - 3 - ], - "ActuatorType": "Vibrate", - "FeatureDescriptor": "External pulsator" - } - ] - } - } - }, - "svakom-avaneo": { - "btle": { - "names": [ - "SX218A", - "Ava Neo" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb", - "rx": "0000ffe2-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Svakom Ava Neo", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 10 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 1 - ], - "ActuatorType": "Oscillate" - } - ] - } - } - }, - "svakom-barnard": { - "btle": { - "names": [ - "DG239A" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Fantasy Cup Barnard", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 3 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 3 - ], - "ActuatorType": "Oscillate" - } - ] - } - } - }, - "realov": { - "btle": { - "names": [ - "REALOV_VIBE" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Realov Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 50 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "motorbunny": { - "btle": { - "names": [ - "MB Controller", - "MB LINK 201" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff6-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Motorbunny Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate" - } - ], - "RotateCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Rotate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "MB Controller" - ], - "name": "Motorbunny Classic" - }, - { - "identifier": [ - "MB LINK 201" - ], - "name": "Motorbunny Buck" - } - ] - }, - "zalo": { - "btle": { - "names": [ - "ZALO-Queen", - "ZALO-King", - "ZALO-Jeanne" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff1-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Zalo Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 8 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "ZALO-Queen" - ], - "name": "Zalo Queen", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 8 - ], - "ActuatorType": "Vibrate", - "FeatureOrder": 1 - }, - { - "StepRange": [ - 0, - 8 - ], - "FeatureOrder": 0, - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "ZALO-King" - ], - "name": "Zalo King", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 8 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 8 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "ZALO-Jeanne" - ], - "name": "Zalo Jeanne" - } - ] - }, - "sayberx": { - "btle": { - "names": [ - "SayberX", - "X-Ring *" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff6-0000-1000-8000-00805f9b34fb", - "rx": "0000fff8-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "SayberX Device", - "messages": {} - }, - "configurations": [ - { - "identifier": [ - "SayberX" - ], - "name": "SayberX", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 4 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "X-Ring" - ], - "name": "Sayber X-Ring" - } - ] - }, - "muse": { - "btle": { - "names": [ - "WB-ZDB-WST", - "WB-TDD" - ], - "services": { - "0000aaa0-0000-1000-8000-00805f9b34fb": { - "tx": "0000aaa1-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Muse Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 9 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "WB-ZDB-WST" - ], - "name": "Dream Lover Archer 2" - }, - { - "identifier": [ - "WB-TDD" - ], - "name": "Galaku Panty Vib" - } - ] - }, - "lelo-f1s": { - "btle": { - "names": [ - "F1s" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff1-0000-1000-8000-00805f9b34fb", - "rx": "00000aa4-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Lelo F1s", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "lelo-f1sv2": { - "btle": { - "names": [ - "F1SV2A", - "F1SV2X" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff1-0000-1000-8000-00805f9b34fb", - "whitelist": "00000a10-0000-1000-8000-00805f9b34fb", - "rx": "00000a04-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Lelo F1s V2", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "lelo-harmony": { - "btle": { - "names": [ - "IdaWave", - "Ida Wave", - "TianiHarmony", - "Tiani Harmony", - "TOR3", - "Hugo2" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "command": "0000fff1-0000-1000-8000-00805f9b34fb", - "tx": "0000fff2-0000-1000-8000-00805f9b34fb", - "whitelist": "00000a11-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Lelo Tiani Harmony", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "IdaWave", - "Ida Wave" - ], - "name": "Lelo Ida Wave", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Rotate" - } - ] - } - }, - { - "identifier": [ - "TOR3" - ], - "name": "Lelo Tor 3", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "Hugo2" - ], - "name": "Lelo Hugo 2" - } - ] - }, - "aneros": { - "btle": { - "names": [ - "Massage Demo" - ], - "services": { - "0000ff00-0000-1000-8000-00805f9b34fb": { - "tx": "0000ff01-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Aneros Vivi", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 127 - ], - "FeatureDescriptor": "Perineum Vibrator", - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 127 - ], - "FeatureDescriptor": "Internal Vibrator", - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "lovehoney-desire": { - "btle": { - "names": [ - "PROSTATE VIBE", - "KNICKER VIBE", - "LOVE EGG" - ], - "services": { - "0000ff00-0000-1000-8000-00805f9b34fb": { - "tx": "0000ff01-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Lovehoney Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 127 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 127 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "PROSTATE VIBE" - ], - "name": "Lovehoney Desire Prostate Vibrator" - }, - { - "identifier": [ - "KNICKER VIBE" - ], - "name": "Lovehoney Desire Knicker Vibrator", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 127 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "LOVE EGG" - ], - "name": "Lovehoney Desire Love Egg", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 127 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - ] - }, - "twerkingbutt": { - "btle": { - "names": [ - "BODIKANG", - "Twerking Butt", - "TwerkingButt" - ], - "services": { - "00000a60-0000-1000-8000-00805f9b34fb": { - "tx": "00000a66-0000-1000-8000-00805f9b34fb", - "rx": "00000a67-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Twerking Butt", - "messages": {} - } - }, - "maxpro": { - "btle": { - "names": [ - "M2" - ], - "services": { - "6e400001-b5a3-f393-e0a9-e50e24dcca9e": { - "tx": "6e400002-b5a3-f393-e0a9-e50e24dcca9e" - } - } - }, - "defaults": { - "name": "MaxPro 2", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "nobra": { - "btle": { - "names": [ - "NobraControl*" - ], - "services": { - "0000abf0-0000-1000-8000-00805f9b34fb": { - "tx": "0000abf1-0000-1000-8000-00805f9b34fb" - } - } - }, - "serial": [ - { - "port": "default", - "baud-rate": 19200, - "data-bits": 8, - "parity": "N", - "stop-bits": 1 - } - ], - "defaults": { - "name": "Nobra's Silicone Dreams Toy", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 15 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "thehandy": { - "btle": { - "names": [ - "The Handy" - ], - "services": { - "1775244d-6b43-439b-877c-060f2d9bed07": { - "firmware": "1775ff51-6b43-439b-877c-060f2d9bed07", - "tx": "1775ff55-6b43-439b-877c-060f2d9bed07" - } - } - }, - "defaults": { - "name": "The Handy", - "messages": { - "LinearCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Position" - } - ], - "FleshlightLaunchFW12Cmd": {} - } - } - }, - "cachito": { - "btle": { - "names": [ - "CCTSK", - "CCTXueGao" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff2-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Cachito Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 5 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "CCTSK" - ], - "name": "Cachito Lure Tao" - }, - { - "identifier": [ - "CCTXueGao" - ], - "name": "Cachito Ice Cream" - } - ] - }, - "jejoue": { - "btle": { - "names": [ - "Je Joue" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff1-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Je Joue Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 5 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 5 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "lovenuts": { - "btle": { - "names": [ - "Love_Nuts" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff1-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Love Nut", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 15 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "patoo": { - "btle": { - "names": [ - "PTVEA*", - "PBT*", - "PCS*", - "PHT*" - ], - "services": { - "f000aa64-0451-4000-b000-000000000000": { - "txmode": "f000aa65-0451-4000-b000-000000000000", - "tx": "f000aa68-0451-4000-b000-000000000000" - } - } - }, - "defaults": { - "name": "Patoo Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "PTVEA" - ], - "name": "Patoo Carrot" - }, - { - "identifier": [ - "PCS" - ], - "name": "Patoo Vibrator" - }, - { - "identifier": [ - "PHT" - ], - "name": "Patoo Bean Sprout" - }, - { - "identifier": [ - "PBT" - ], - "name": "Patoo Devil", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - ] - }, - "tcode-v03": { - "serial": [ - { - "port": "default", - "baud-rate": 115200, - "data-bits": 8, - "parity": "N", - "stop-bits": 1 - } - ], - "defaults": { - "name": "TCode v0.3 (Single Linear Axis)", - "messages": { - "LinearCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Position" - } - ], - "FleshlightLaunchFW12Cmd": {} - } - } - }, - "fredorch": { - "btle": { - "names": [ - "YXlinksSPP" - ], - "services": { - "0000ffb0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffb1-0000-1000-8000-00805f9b34fb", - "rx": "0000ffb2-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Fredorch Device", - "messages": { - "LinearCmd": [ - { - "StepRange": [ - 0, - 150 - ], - "ActuatorType": "Position" - } - ], - "FleshlightLaunchFW12Cmd": {} - } - } - }, - "fredorch-rotary": { - "btle": { - "names": [ - "M1_*" - ], - "services": { - "0000ae10-0000-1000-8000-00805f9b34fb": { - "tx": "0000ae01-0000-1000-8000-00805f9b34fb", - "rx": "0000ae02-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Fredorch Rotary Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Oscillate", - "FeatureDescriptor": "Fucking Machine Oscillation Speed" - } - ] - } - } - }, - "mizzzee": { - "btle": { - "names": [ - "NFY008" - ], - "services": { - "0000eea0-0000-1000-8000-00805f9b34fb": { - "tx": "0000eea1-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Mizz Zee Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 68 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "mizzzee-v2": { - "btle": { - "names": [ - "XHT" - ], - "services": { - "0000eea0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ee01-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Mizz Zee Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 68 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "mizzzee-v3": { - "btle": { - "names": [ - "XHTKJ" - ], - "services": { - "0000ff10-0000-1000-8000-00805f9b34fb": { - "tx": "0000ff12-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Mizz Zee Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 1000 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "htk_bm": { - "btle": { - "names": [ - "HTK-BLE-BM001" - ], - "services": { - "0000180f-0000-1000-8000-00805f9b34fb": { - "rxblebattery": "00002a19-0000-1000-8000-00805f9b34fb" - }, - "00001802-0000-1000-8000-00805f9b34fb": { - "tx": "00002a06-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "HTK Breast Massager", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 1 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 1 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "ankni": { - "btle": { - "names": [ - "DSJM" - ], - "services": { - "0000fe00-0000-1000-8000-00805f9b34fb": { - "tx": "0000fe01-0000-1000-8000-00805f9b34fb" - }, - "0000fffe-0000-1000-8000-00805f9b34fb": { - "tx": "0000fe02-0000-1000-8000-00805f9b34fb" - }, - "0000180a-0000-1000-8000-00805f9b34fb": { - "generic0": "00002a50-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Roselex Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 3 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "hgod": { - "btle": { - "names": [ - "AMN NEO" - ], - "services": { - "0000ffe3-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb", - "rx": "0000ffe2-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Hgod Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 10 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "lovedistance": { - "btle": { - "names": [ - "REACH G", - "REACH", - "MAG", - "SPAN", - "RANGE", - "ORBIT", - "JOIN G", - "LINK", - "GRASP", - "RECEIVE" - ], - "services": { - "0000ff00-0000-1000-8000-00805f9b34fb": { - "tx": "0000ff01-0000-1000-8000-00805f9b34fb", - "rx": "0000ff02-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Love Distance Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 121 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "REACH G" - ], - "name": "Love Distance Reach G" - }, - { - "identifier": [ - "REACH" - ], - "name": "Love Distance Reach" - }, - { - "identifier": [ - "MAG" - ], - "name": "Love Distance Mag" - }, - { - "identifier": [ - "SPAN" - ], - "name": "Love Distance Span" - }, - { - "identifier": [ - "RANGE" - ], - "name": "Love Distance Range" - }, - { - "identifier": [ - "ORBIT" - ], - "name": "Love Distance Range" - }, - { - "identifier": [ - "JOIN G" - ], - "name": "Love Distance Join G" - }, - { - "identifier": [ - "LINK" - ], - "name": "Love Distance Link" - }, - { - "identifier": [ - "GRASP" - ], - "name": "Love Distance Grasp" - }, - { - "identifier": [ - "RECEIVE" - ], - "name": "Love Distance Receive" - } - ] - }, - "satisfyer": { - "btle": { - "names": [ - "SF *" - ], - "manufacturer-data": [ - { - "company": 93, - "data": [ - 0, - 0, - 39 - ] - }, - { - "company": 93, - "data": [ - 0, - 0, - 40 - ] - } - ], - "services": { - "0000180a-0000-1000-8000-00805f9b34fb": { - "rxblemodel": "00002a24-0000-1000-8000-00805f9b34fb" - }, - "51361500-c5e7-47c7-8a6e-47ebc99d80e8": { - "command": "51361501-c5e7-47c7-8a6e-47ebc99d80e8", - "tx": "51361502-c5e7-47c7-8a6e-47ebc99d80e8" - } - } - }, - "defaults": { - "name": "Satisfyer Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "10005" - ], - "name": "Satisfyer Hot Spot", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10006" - ], - "name": "Satisfyer Heated Affair", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10007" - ], - "name": "Satisfyer Big Heat" - }, - { - "identifier": [ - "10008" - ], - "name": "Satisfyer Heated Thrill", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10009" - ], - "name": "Satisfyer Hot Bunny", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10010" - ], - "name": "Satisfyer Heat Climax", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10011" - ], - "name": "Satisfyer Heat Climax+", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10012" - ], - "name": "Satisfyer Hot Passion", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10013" - ], - "name": "Satisfyer Haute Couture+", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10014" - ], - "name": "Satisfyer High Fashion+", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10015" - ], - "name": "Satisfyer Prêt-à-porter+", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10024", - "10025" - ], - "name": "Satisfyer Love Triangle", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10027", - "10028" - ], - "name": "Satisfyer Curvy 1+", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10030", - "10031" - ], - "name": "Satisfyer Curvy 2+", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10032" - ], - "name": "Satisfyer Double Wand-er" - }, - { - "identifier": [ - "10046", - "10047", - "10048" - ], - "name": "Satisfyer Double Joy", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10049", - "10050", - "10051" - ], - "name": "Satisfyer Double Fun", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10052", - "10053", - "10054" - ], - "name": "Satisfyer Double Love", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10055" - ], - "name": "Satisfyer Curvy 3+", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10059", - "10060", - "10061" - ], - "name": "Satisfyer Hot Lover", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10062", - "10063", - "10064" - ], - "name": "Satisfyer Mono Flex", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10065", - "10066", - "10067", - "10068" - ], - "name": "Satisfyer Double Flex", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10069", - "10070", - "10071" - ], - "name": "Satisfyer Heat Wave", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10072" - ], - "name": "Satisfyer Little Secret" - }, - { - "identifier": [ - "10073" - ], - "name": "Satisfyer Sexy Secret" - }, - { - "identifier": [ - "10074" - ], - "name": "Satisfyer Strong One" - }, - { - "identifier": [ - "10075" - ], - "name": "Satisfyer Mighty One" - }, - { - "identifier": [ - "10076" - ], - "name": "Satisfyer Powerful One" - }, - { - "identifier": [ - "10077" - ], - "name": "Satisfyer Royal One" - }, - { - "identifier": [ - "10078" - ], - "name": "Satisfyer Signet Ring" - }, - { - "identifier": [ - "10079", - "10080" - ], - "name": "Satisfyer Dual Love", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10081", - "10082" - ], - "name": "Satisfyer Dual Pleasure", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10090" - ], - "name": "Satisfyer Hero+", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10091" - ], - "name": "Satisfyer Knight+", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10092", - "10093" - ], - "name": "Satisfyer Newcomer+", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10100", - "10101" - ], - "name": "Satisfyer Plug-ilicious 1", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10102", - "10103", - "10104" - ], - "name": "Satisfyer Plug-ilicious 2", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10105" - ], - "name": "Satisfyer E-Love Foreplay", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10108" - ], - "name": "Satisfyer E-Love G-Hunter", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10109" - ], - "name": "Satisfyer E-Love G-Hunter+", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10110" - ], - "name": "Satisfyer E-Love G-Spotter", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10111" - ], - "name": "Satisfyer E-Love G-Spotter+", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10112" - ], - "name": "Satisfyer E-Love Story", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10119", - "10120", - "10182" - ], - "name": "Satisfyer Love Birds 1" - }, - { - "identifier": [ - "10121", - "10122", - "10123" - ], - "name": "Satisfyer Love Birds 2" - }, - { - "identifier": [ - "10124", - "10125", - "10126" - ], - "name": "Satisfyer Love Birds Vary" - }, - { - "identifier": [ - "10127", - "10128", - "10129", - "10201" - ], - "name": "Satisfyer Ribbed Petal" - }, - { - "identifier": [ - "10130", - "10131", - "10132", - "10133" - ], - "name": "Satisfyer Shiny Petal" - }, - { - "identifier": [ - "10134", - "10135", - "10136", - "10202" - ], - "name": "Satisfyer Smooth Petal" - }, - { - "identifier": [ - "10140" - ], - "name": "Satisfyer Men Vibration+", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10141" - ], - "name": "Satisfyer Power Plug" - }, - { - "identifier": [ - "10142", - "10143" - ], - "name": "Satisfyer Rotator Plug 1+", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10144", - "10145" - ], - "name": "Satisfyer Rotator Plug 2+", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10146", - "10147" - ], - "name": "Satisfyer Deep Diver" - }, - { - "identifier": [ - "10148", - "10149" - ], - "name": "Satisfyer Sweet Seal" - }, - { - "identifier": [ - "10150", - "10151" - ], - "name": "Satisfyer Trendsetter" - }, - { - "identifier": [ - "10154", - "10155", - "10156" - ], - "name": "Satisfyer Twirling Joy" - }, - { - "identifier": [ - "10157", - "10158" - ], - "name": "Satisfyer Ultra Power Bullet 8" - }, - { - "identifier": [ - "10160", - "10161", - "10162" - ], - "name": "Satisfyer Double Desire", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10163", - "10164", - "10165", - "10166" - ], - "name": "Satisfyer Double Lust", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10167" - ], - "name": "Satisfyer Epic Duo" - }, - { - "identifier": [ - "10168" - ], - "name": "Satisfyer Pleasure Wand+" - }, - { - "identifier": [ - "10169", - "10170", - "10171" - ], - "name": "Satisfyer Top Secret", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10172", - "10173", - "10174" - ], - "name": "Satisfyer Top Secret+", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10175", - "10176" - ], - "name": "Satisfyer Bullseye" - }, - { - "identifier": [ - "10177", - "10178", - "10179" - ], - "name": "Satisfyer Sunray", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10180", - "10181" - ], - "name": "Satisfyer Curvy Trinity 5+", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10183", - "10184" - ], - "name": "Satisfyer Intensity Plug" - }, - { - "identifier": [ - "10185" - ], - "name": "Satisfyer Power Masturbator" - }, - { - "identifier": [ - "10186", - "10187" - ], - "name": "Satisfyer Hug me", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10188" - ], - "name": "Satisfyer Air Pump Bunny 5+", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10189" - ], - "name": "Satisfyer Air Pump Vibrator 5+", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10190", - "10191" - ], - "name": "Satisfyer Threesome 4", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10192" - ], - "name": "Satisfyer G-Spot Flex 4+" - }, - { - "identifier": [ - "10193", - "10194" - ], - "name": "Satisfyer G-Spot Flex 5+" - }, - { - "identifier": [ - "10195" - ], - "name": "Satisfyer Air Pump Booty 5+", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10196" - ], - "name": "Satisfyer Pro+ Wave 4", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10197", - "10198" - ], - "name": "Satisfyer Mini Wand-er+", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10199", - "10200" - ], - "name": "Satisfyer Tropical Tip" - }, - { - "identifier": [ - "10203", - "10204" - ], - "name": "Satisfyer Twirling Pro+", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10205" - ], - "name": "Satisfyer Perfect Pair 4" - }, - { - "identifier": [ - "10206", - "10207", - "10208" - ], - "name": "Satisfyer Booty Absolute Beginners 5" - }, - { - "identifier": [ - "10241", - "10242" - ], - "name": "Satisfyer Rrrolling Sensation", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "10307", - "10308", - "10309" - ], - "name": "Satisfyer Pro 2 Gen 3", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - ] - }, - "mannuo": { - "btle": { - "names": [ - "Sex toys", - "Sex Toys", - "LXCDVP", - "MANO PRODUCT" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff1-0000-1000-8000-00805f9b34fb", - "rx": "0000fff4-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "ManNuo Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 3 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "kgoal-boost": { - "btle": { - "names": [ - "Boost" - ], - "services": { - "0000180f-0000-1000-8000-00805f9b34fb": { - "rxblebattery": "00002a19-0000-1000-8000-00805f9b34fb" - }, - "8e7c6065-7656-17ad-1b41-b53d1a548e0d": { - "rxpressure": "10c2be2d-d2d5-b7a8-5f42-e2468c9ebbf5" - } - } - }, - "defaults": { - "name": "KGoal Boost", - "messages": { - "SensorReadCmd": [ - { - "SensorType": "Battery", - "FeatureDescriptor": "Battery Level", - "SensorRange": [ - [ - 0, - 100 - ] - ] - } - ], - "SensorSubscribeCmd": [ - { - "SensorType": "Pressure", - "FeatureDescriptor": "Pelvic Pressure (Normalized)", - "SensorRange": [ - [ - 0, - 1000 - ] - ] - }, - { - "SensorType": "Pressure", - "FeatureDescriptor": "Pelvic Pressure (Unnormalized)", - "SensorRange": [ - [ - 0, - 1000 - ] - ] - } - ] - } - } - }, - "meese": { - "btle": { - "names": [ - "Meese-V389", - "Meese-cd" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Meese Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 10 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 3 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "Meese-V389" - ], - "name": "Meese Tera" - }, - { - "identifier": [ - "Meese-cd" - ], - "name": "Meese Modo", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 10 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - ] - }, - "hismith": { - "btle": { - "names": [ - "HISMITH", - "Wildolo", - "\u0007HISMITH" - ], - "services": { - "0000ffe5-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe9-0000-1000-8000-00805f9b34fb" - }, - "0000ff90-0000-1000-8000-00805f9b34fb": { - "rxblemodel": "0000ff96-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Hismith device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Oscillate", - "FeatureDescriptor": "Fucking Machine Oscillation Speed" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "1001" - ], - "name": "Hismith Sex Machine" - }, - { - "identifier": [ - "1002" - ], - "name": "Hismith Pro Traveler" - }, - { - "identifier": [ - "1003" - ], - "name": "Hismith Capsule" - }, - { - "identifier": [ - "2001" - ], - "name": "Hismith Thrusting Cup", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Oscillate", - "FeatureDescriptor": "Stroker Oscillation Speed" - }, - { - "StepRange": [ - 0, - 1 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "3001" - ], - "name": "Wildolo Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - ] - }, - "hismith-mini": { - "btle": { - "names": [ - "Auxfun-Box", - "Sinloli", - "Sinloli-Sherry", - "Eropair *", - "HISMITH S1" - ], - "services": { - "0000ffe5-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe9-0000-1000-8000-00805f9b34fb" - }, - "0000ff90-0000-1000-8000-00805f9b34fb": { - "rxblemodel": "0000ff96-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Hismith Mini device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Oscillate", - "FeatureDescriptor": "Fucking Machine Oscillation Speed" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "4001" - ], - "name": "Auxfun Sex Machine" - }, - { - "identifier": [ - "1005" - ], - "name": "Hismith Sex Machine" - }, - { - "identifier": [ - "2201" - ], - "name": "Sinloli Automatic Sex Doll", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "FeatureDescriptor": "Air Pump", - "ActuatorType": "Constrict" - }, - { - "StepRange": [ - 0, - 100 - ], - "FeatureDescriptor": "Vibrator", - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "3101" - ], - "name": "Eropair Rabbit Vibrator", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "FeatureDescriptor": "Internal Vibrator", - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "FeatureDescriptor": "External Vibrator", - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "3102" - ], - "name": "Eropair Thrusting Vibrating Dildo", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "FeatureDescriptor": "Thruster", - "ActuatorType": "Oscillate" - }, - { - "StepRange": [ - 0, - 100 - ], - "FeatureDescriptor": "Vibrator", - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "2101" - ], - "name": "Eropair Cup", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "FeatureDescriptor": "Air Pump", - "ActuatorType": "Constrict" - }, - { - "StepRange": [ - 0, - 100 - ], - "FeatureDescriptor": "Vibrator", - "ActuatorType": "Vibrate" - } - ] - } - } - ] - }, - "hismith-servo": { - "btle": { - "names": [ - "HISMITH S2" - ], - "services": { - "0000ffe5-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe9-0000-1000-8000-00805f9b34fb" - }, - "0000ff90-0000-1000-8000-00805f9b34fb": { - "rxblemodel": "0000ff96-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Hismith servo device", - "messages": { - "LinearCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Position", - "FeatureDescriptor": "Fucking Machine Position" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "1101" - ], - "name": "Hismith Servo" - } - ] - }, - "wetoy": { - "btle": { - "names": [ - "WeToy" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff3-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "WeToy MiNa", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 3 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "pink_punch": { - "btle": { - "names": [ - "Pink_Punch", - "PinkPunch_Peachu" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Pink Punch Sunset Mushroom", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - }, - "configurations": [ - { - "identifier": [ - "PinkPunch_Peachu" - ], - "name": "Pink Punch Peachu" - } - ] - } - }, - "sakuraneko": { - "btle": { - "names": [ - "sakuraneko-01", - "sakuraneko-02", - "sakuraneko-03", - "sakuraneko-04" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Sakuraneko Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "sakuraneko-01" - ], - "name": "Sakuraneko Korokoro" - }, - { - "identifier": [ - "sakuraneko-02" - ], - "name": "Sakuraneko Nukunuku" - }, - { - "identifier": [ - "sakuraneko-03" - ], - "name": "Sakuraneko Dokidoki" - }, - { - "identifier": [ - "sakuraneko-04" - ], - "name": "Sakuraneko Koikoi", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Rotate" - } - ] - } - } - ] - }, - "synchro": { - "btle": { - "names": [ - "Shinkuro", - "synchro2", - "synchro EX" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Synchro", - "messages": { - "RotateCmd": [ - { - "StepRange": [ - 0, - 6 - ], - "ActuatorType": "Rotate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "synchro EX" - ], - "name": "Synchro Exchange" - } - ] - }, - "tryfun": { - "btle": { - "names": [ - "TRYFUN-ONE" - ], - "services": { - "0000ff10-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff1-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "TryFun Yuan Series", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 9 - ], - "ActuatorType": "Oscillate" - }, - { - "StepRange": [ - 0, - 9 - ], - "ActuatorType": "Rotate" - } - ] - } - } - }, - "metaxsire": { - "btle": { - "names": [ - "Rex", - "Cali", - "Olis", - "LY213A01" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb", - "rx": "0000ffe2-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "metaXsire Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "Rex" - ], - "name": "metaXsire Rex" - }, - { - "identifier": [ - "Cali" - ], - "name": "metaXsire Cali", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Constrict" - } - ] - } - }, - { - "identifier": [ - "Olis" - ], - "name": "metaXsire Olis", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Rotate" - } - ] - } - }, - { - "identifier": [ - "LY213A01" - ], - "name": "metaXsire BuCUE", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Oscillate" - }, - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - ] - }, - "metaxsire-repeat": { - "btle": { - "names": [ - "LY199B01", - "LY234A01", - "LY271A01", - "LY270A01" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb", - "rx": "0000ffe2-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Cooxer Bullet Vibe", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "LY199B01" - ], - "name": "Cooxer Bullet Vibe" - }, - { - "identifier": [ - "LY234A01" - ], - "name": "metaXsire Tadpole" - }, - { - "identifier": [ - "LY271A01" - ], - "name": "metaXsire Upton" - }, - { - "identifier": [ - "LY270A01" - ], - "name": "metaXsire Una" - } - ] - }, - "metaxsire-v2": { - "btle": { - "names": [ - "LY272A01" - ], - "services": { - "0000bae0-0000-1000-8000-00805f9b34fb": { - "tx": "0000bae1-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "metaXsire Nolan", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Oscillate" - } - ] - } - } - }, - "metaxsire-v3": { - "btle": { - "names": [ - "TAY001" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fe02-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "metaXsire Tay", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 20 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "metaxsire-v4": { - "btle": { - "names": [ - "CFG1 vibrator" - ], - "services": { - "0000cfa2-0000-1000-8000-00805f9b34fb": { - "tx": "0000cf21-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "metaXsire G1 Vibrator", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 99 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "cowgirl": { - "btle": { - "names": [ - "THE COWGIRL", - "THE UNICORN" - ], - "services": { - "0000fe00-0000-1000-8000-00805f9b34fb": { - "tx": "0000fe01-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "The Cowgirl Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Rotate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "THE COWGIRL" - ], - "name": "The Cowgirl" - }, - { - "identifier": [ - "THE UNICORN" - ], - "name": "The Unicorn" - } - ] - }, - "galaku-pump": { - "btle": { - "names": [ - "V415" - ], - "services": { - "00001000-0000-1000-8000-00805f9b34fb": { - "tx": "00001001-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Galaku Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Oscillate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "V415" - ], - "name": "Galaku Nebula" - } - ] - }, - "galaku": { - "btle": { - "names": [ - "EJX-Para", - "GK03", - "GK10085", - "GS03", - "GS07", - "GS85", - "GS02", - "GS10", - "GS01", - "GS04", - "GS17", - "GS21", - "GS23", - "GS22", - "GS16", - "GS19", - "AK04", - "AS67", - "AS90", - "K020", - "GS25", - "GH28", - "GS28", - "LL18", - "GK23", - "GK27", - "G29B", - "GA23", - "L26H", - "GA25", - "GA26", - "GK22", - "GX85", - "GX07", - "GX17", - "GX21", - "GX33", - "GX22", - "GX16", - "GX29", - "GX23", - "GX26", - "GX36", - "GX39", - "GX25", - "G326", - "G335" - ], - "services": { - "00001000-0000-1000-8000-00805f9b34fb": { - "tx": "00001001-0000-1000-8000-00805f9b34fb", - "rxblebattery": "00001002-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Galaku Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate", - "FeatureDescriptor": "Vibrate" - } - ], - "SensorReadCmd": [ - { - "FeatureDescriptor": "Battery Level", - "SensorType": "Battery", - "SensorRange": [ - [ - 0, - 100 - ] - ] - } - ] - } - } - }, - "xibao": { - "btle": { - "names": [ - "CCYB_*" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff2-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Xibao Smart Masturbation Cup", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 99 - ], - "ActuatorType": "Oscillate" - } - ] - } - } - }, - "sensee": { - "btle": { - "names": [ - "CTY222S4" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff5-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Sensee Diandou Rabbit", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "sensee-capsule": { - "btle": { - "names": [ - "CCPA10S2" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff5-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Sensee Capsule", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 3 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 3 - ], - "ActuatorType": "Constrict" - } - ] - } - } - }, - "fox": { - "btle": { - "names": [ - "FOX", - "FOX M70 Pro", - "FoxM70Pro" - ], - "services": { - "0000ae00-0000-1000-8000-00805f9b34fb": { - "tx": "0000ae01-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Fox Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 3 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "kizuna": { - "serial": [ - { - "port": "default", - "baud-rate": 19200, - "data-bits": 8, - "parity": "N", - "stop-bits": 1 - } - ], - "defaults": { - "name": "Kizuna Smart", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 9 - ], - "ActuatorType": "Rotate" - } - ] - } - } - }, - "xiuxiuda": { - "btle": { - "names": [ - "XXD-Lush*" - ], - "services": { - "53300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "53300003-0023-4bd4-bbd5-a6920e4c5653" - } - } - }, - "defaults": { - "name": "Xiuxiuda Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 19 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "longlosttouch": { - "btle": { - "names": [ - "RS-KNW" - ], - "services": { - "0000cb60-0000-1000-8000-00805f9b34fb": { - "tx": "0000cb61-0000-1000-8000-00805f9b34fb", - "rx": "0000cb62-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Long Lost Touch Possible Kiss", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Oscillate" - } - ] - } - } - }, - "adrienlastic": { - "btle": { - "names": [ - "Placeholder to avoid conflict with bad attempt to clone a Lovense Lush" - ], - "advertised-services": [ - "00001320-0000-1000-8000-00805f9b34fb" - ], - "services": { - "6e400001-b5a3-f393-e0a9-e50e24dcca9e": { - "tx": "6e400002-b5a3-f393-e0a9-e50e24dcca9e" - } - } - }, - "defaults": { - "name": "Adrien Lastic Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 16 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "LVS-S001" - ], - "name": "Adrien Lastic Palpitation" - }, - { - "identifier": [ - "LVS-S002" - ], - "name": "Adrien Lastic Revelation" - } - ] - }, - "nintendo-joycon": { - "hid": [ - { - "vendor-id": 1406, - "product-id": 8199 - }, - { - "vendor-id": 1406, - "product-id": 8198 - }, - { - "vendor-id": 1406, - "product-id": 8201 - } - ], - "defaults": { - "name": "Nintendo Joycon", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 1000 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "foreo": { - "btle": { - "names": [ - "FOFO", - "LUNA fofo", - "LUNA FOFO", - "LUNA PLAY SMART", - "LUNA PLAYSMART2", - "LUNA PLAY SMART2", - "LUNA play smart2", - "LUNA play smart 2", - "LUNA 3", - "LUNA3", - "LUNA3PLUS", - "LUNA3 PLUS", - "LUNA 3 PLUS", - "LUNA 3 plus", - "LUNA 3 MEN", - "LUNA3MEN", - "LUNA MINI3", - "LUNA MINI 3", - "LUNA mini 3", - "LUNA4PLUS", - "LUNA4", - "LUNA 4", - "LUNA4PLUS", - "LUNA4 PLUS", - "LUNA 4 plus", - "LUNA4MEN", - "LUNA 4 MEN", - "LUNA 4 FOR MEN", - "LUNA MINI4", - "LUNA MINI 4", - "LUNA mini 4", - "LUNA 4 mini", - "UFO", - "UFO mini", - "UFO MINI", - "UFO MIN", - "UFO2", - "UFO 2", - "UFOMINI2", - "UFO mini 2", - "UFO3", - "UFO3mini", - "UFO3go", - "UFO3led", - "BEAR", - "BEAR_MINI", - "BEAR MINI", - "BEAR mini", - "BEAR2", - "BEAR 2", - "BEAR2go", - "BEAR2body", - "BEAR2eyes", - "KIWI", - "KIWI derma" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff1-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Foreo Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 10 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "FOFO", - "LUNA fofo", - "LUNA FOFO", - "LUNA PLAY SMART" - ], - "name": "Foreo LUNA fofo" - }, - { - "identifier": [ - "LUNA PLAYSMART2", - "LUNA PLAY SMART2", - "LUNA play smart2", - "LUNA play smart 2" - ], - "name": "Foreo LUNA play smart 2" - }, - { - "identifier": [ - "LUNA 3", - "LUNA3" - ], - "name": "Foreo LUNA 3" - }, - { - "identifier": [ - "LUNA3PLUS", - "LUNA3 PLUS", - "LUNA 3 PLUS", - "LUNA 3 plus" - ], - "name": "Foreo LUNA 3 plus" - }, - { - "identifier": [ - "LUNA 3 MEN", - "LUNA3MEN" - ], - "name": "Foreo LUNA 3 men" - }, - { - "identifier": [ - "LUNA MINI3", - "LUNA MINI 3", - "LUNA mini 3" - ], - "name": "Foreo LUNA 3 mini" - }, - { - "identifier": [ - "LUNA4", - "LUNA 4" - ], - "name": "Foreo LUNA 4" - }, - { - "identifier": [ - "LUNA4PLUS", - "LUNA4 PLUS", - "LUNA 4 plus" - ], - "name": "Foreo LUNA 4 plus" - }, - { - "identifier": [ - "LUNA4MEN", - "LUNA 4 MEN", - "LUNA 4 FOR MEN" - ], - "name": "Foreo LUNA 4 men" - }, - { - "identifier": [ - "LUNA MINI4", - "LUNA MINI 4", - "LUNA mini 4", - "LUNA 4 mini" - ], - "name": "Foreo LUNA 4 mini" - }, - { - "identifier": [ - "UFO" - ], - "name": "Foreo UFO" - }, - { - "identifier": [ - "UFO mini", - "UFO MINI", - "UFO MIN" - ], - "name": "Foreo UFO mini" - }, - { - "identifier": [ - "UFO2", - "UFO 2" - ], - "name": "Foreo UFO 2" - }, - { - "identifier": [ - "UFO3" - ], - "name": "Foreo UFO 3" - }, - { - "identifier": [ - "UFO3go" - ], - "name": "Foreo UFO 3 go" - }, - { - "identifier": [ - "UFO3eyes" - ], - "name": "Foreo UFO 3 led" - }, - { - "identifier": [ - "UFO3mini" - ], - "name": "Foreo UFO 3 mini" - }, - { - "identifier": [ - "UFOMINI2", - "UFO mini 2" - ], - "name": "Foreo UFO mini 2" - }, - { - "identifier": [ - "BEAR" - ], - "name": "Foreo BEAR" - }, - { - "identifier": [ - "BEAR_MINI", - "BEAR MINI", - "BEAR mini" - ], - "name": "Foreo BEAR mini" - }, - { - "identifier": [ - "BEAR2", - "BEAR 2" - ], - "name": "Foreo BEAR 2" - }, - { - "identifier": [ - "BEAR2go" - ], - "name": "Foreo BEAR 2 go" - }, - { - "identifier": [ - "BEAR2eyes" - ], - "name": "Foreo BEAR 2 eyes" - }, - { - "identifier": [ - "BEAR2body" - ], - "name": "Foreo BEAR 2 body" - }, - { - "identifier": [ - "KIWI" - ], - "name": "Foreo KIWI" - }, - { - "identifier": [ - "KIWI derma" - ], - "name": "Foreo KIWI derma" - } - ] - }, - "monsterpub": { - "btle": { - "names": [ - "MonsterPub" - ], - "services": { - "00006000-0000-1000-8000-00805f9b34fb": { - "tx": "00006001-0000-1000-8000-00805f9b34fb", - "txmode": "00006002-0000-1000-8000-00805f9b34fb", - "txvibrate": "00006003-0000-1000-8000-00805f9b34fb" - }, - "00006010-0000-1000-8000-00805f9b34fb": { - "rxblemodel": "00006014-0000-1000-8000-00805f9b34fb" - }, - "00008000-0000-1000-8000-00805f9b34fb": { - "rx": "00008001-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Sistalk MonsterPub Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "MP2_JK_N_P1" - ], - "name": "Sistalk MonsterPub 2 Doctor Whale", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "MP_MW_TL_P2" - ], - "name": "Sistalk MonsterPub Magic Kiss", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "MP2_QC_TL_P1" - ], - "name": "Sistalk MonsterPub 2 Mister Devil", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "MP_BABY_QC_N_P4" - ], - "name": "Sistalk MonsterPub Baby Youth Health", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "MP_MXY_N_P1" - ], - "name": "Sistalk MonsterPub KiniCat" - }, - { - "identifier": [ - "MP1N_QC_TL_P2" - ], - "name": "Sistalk MonsterPub BeatHeart" - } - ] - }, - "joyhub": { - "btle": { - "names": [ - "J-Petalwish2", - "J-VortexTongue", - "J-Velocity", - "JOYHUB-ROSELLA2", - "J-VibSiren", - "J-ElixirEgg", - "J-RetroGuard", - "J-TrueForm", - "J-TrueForm3", - "J-Rhythmic2", - "J-Rhythmic3", - "J-Mysticolor", - "J-VividWings", - "J-Rainbow", - "J-BlackBull", - "J-Peacock", - "J-Mariner" - ], - "services": { - "0000ffa0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffa1-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "JoyHub Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "JOYHUB-ROSELLA2" - ], - "name": "JoyHub Rosella 2" - }, - { - "identifier": [ - "J-Velocity" - ], - "name": "JoyHub Velocity" - }, - { - "identifier": [ - "J-ElixirEgg" - ], - "name": "JoyHub ElixirEgg" - }, - { - "identifier": [ - "J-RetroGuard" - ], - "name": "JoyHub Retro Guard" - }, - { - "identifier": [ - "J-TrueForm3" - ], - "name": "JoyHub TrueForm 3" - }, - { - "identifier": [ - "J-TrueForm" - ], - "name": "JoyHub TrueForm" - }, - { - "identifier": [ - "J-Rhythmic2" - ], - "name": "JoyHub Rhythmic 2" - }, - { - "identifier": [ - "J-Rhythmic3" - ], - "name": "JoyHub Rhythmic 3" - }, - { - "identifier": [ - "J-Rainbow" - ], - "name": "JoyHub Rainbow" - }, - { - "identifier": [ - "J-BlackBull" - ], - "name": "JoyHub Black Bull" - }, - { - "identifier": [ - "J-Peacock" - ], - "name": "JoyHub Peacock" - }, - { - "identifier": [ - "J-Petalwish2" - ], - "name": "JoyHub Petalwish 2", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Oscillate" - }, - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "J-VortexTongue" - ], - "name": "JoyHub Vortex Tongue", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 3 - ], - "FeatureDescriptor": "Air Pump", - "ActuatorType": "Constrict" - }, - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Rotate" - } - ] - } - }, - { - "identifier": [ - "J-VibSiren" - ], - "name": "JoyHub VibSiren", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate", - "FeatureDescriptor": "External vibrator" - }, - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Oscillate" - }, - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate", - "FeatureDescriptor": "Internal vibrator" - } - ] - } - }, - { - "identifier": [ - "J-Mysticolor" - ], - "name": "JoyHub Mysticolor", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Rotate" - }, - { - "StepRange": [ - 0, - 7 - ], - "FeatureDescriptor": "Air Pump", - "ActuatorType": "Constrict" - } - ] - } - }, - { - "identifier": [ - "J-VividWings" - ], - "name": "JoyHub Vivid Wings", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Oscillate" - } - ] - } - }, - { - "identifier": [ - "J-Mariner" - ], - "name": "JoyHub Mariner", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Rotate" - }, - { - "StepRange": [ - 0, - 2 - ], - "FeatureDescriptor": "Air Pump", - "ActuatorType": "Constrict" - } - ] - } - } - ] - }, - "joyhub-v2": { - "btle": { - "names": [ - "J-Pearlconch", - "J-PetiteRose", - "J-MoonHorn", - "J-VibTrefoil", - "J-Panther", - "J-Mecha", - "J-Lagoon", - "J-Firedragon", - "J-Dina", - "J-Vbarbie3f", - "J-CHERLY2c", - "J-Pathfinder2", - "J-VibRipple", - "J-Verax", - "J-Verax2", - "J-Euphoric2", - "J-ROSEBUD", - "J-Morningbuds2", - "J-Rhythmic4" - ], - "services": { - "0000ffa0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffa1-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "JoyHub Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "J-Pearlconch" - ], - "name": "JoyHub Pearlconch", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Rotate" - }, - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "J-Panther" - ], - "name": "JoyHub Panther", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Rotate" - } - ] - } - }, - { - "identifier": [ - "J-PetiteRose" - ], - "name": "JoyHub Petite Rose", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Rotate" - } - ] - } - }, - { - "identifier": [ - "J-MoonHorn" - ], - "name": "JoyHub Moon Horn", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 9 - ], - "FeatureDescriptor": "Suction", - "ActuatorType": "Constrict" - } - ] - } - }, - { - "identifier": [ - "J-Mecha" - ], - "name": "JoyHub Mecha", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 7 - ], - "FeatureDescriptor": "Suction", - "ActuatorType": "Constrict" - } - ] - } - }, - { - "identifier": [ - "J-Lagoon" - ], - "name": "JoyHub Lagoon", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 5 - ], - "FeatureDescriptor": "Suction", - "ActuatorType": "Constrict" - } - ] - } - }, - { - "identifier": [ - "J-VibTrefoil" - ], - "name": "JoyHub VibTrefoil", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate", - "FeatureDescriptor": "External vibrator" - }, - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate", - "FeatureDescriptor": "Internal vibrator" - } - ] - } - }, - { - "identifier": [ - "J-Firedragon" - ], - "name": "JoyHub Firedragon", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Oscillate" - }, - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "J-Dina" - ], - "name": "JoyHub Deena", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Oscillate" - }, - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate", - "FeatureDescriptor": "Internal vibrator" - }, - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate", - "FeatureDescriptor": "External vibrator" - } - ] - } - }, - { - "identifier": [ - "J-Vbarbie3f" - ], - "name": "JoyHub Cherly", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate", - "FeatureDescriptor": "External vibrator" - }, - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate", - "FeatureDescriptor": "Internal vibrator" - }, - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Oscillate" - } - ] - } - }, - { - "identifier": [ - "J-CHERLY2c" - ], - "name": "JoyHub Cherly 2c", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate", - "FeatureDescriptor": "Internal vibrator" - }, - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate", - "FeatureDescriptor": "Internal Whip" - }, - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate", - "FeatureDescriptor": "External vibrator" - } - ] - } - }, - { - "identifier": [ - "J-Pathfinder2" - ], - "name": "JoyHub Pathfinder 2", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Oscillate" - }, - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "J-VibRipple" - ], - "name": "JoyHub Angela", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate", - "FeatureDescriptor": "External vibrator" - }, - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate", - "FeatureDescriptor": "Internal vibrator" - } - ] - } - }, - { - "identifier": [ - "J-Verax" - ], - "name": "JoyHub Verax", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate", - "FeatureDescriptor": "Internal Whip" - }, - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate", - "FeatureDescriptor": "Internal vibrator" - } - ] - } - }, - { - "identifier": [ - "J-Verax2" - ], - "name": "JoyHub Verax 2", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Rotate" - } - ] - } - }, - { - "identifier": [ - "J-Euphoric2" - ], - "name": "JoyHub Euphoric 2", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Oscillate" - }, - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "J-ROSEBUD" - ], - "name": "JoyHub RoseBUD", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Rotate", - "FeatureDescriptor": "Flicker" - }, - { - "StepRange": [ - 0, - 5 - ], - "FeatureDescriptor": "Suction", - "ActuatorType": "Constrict" - } - ] - } - }, - { - "identifier": [ - "J-Morningbuds2" - ], - "name": "JoyHub Morningbuds", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Rotate" - }, - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - { - "identifier": [ - "J-Rhythmic4" - ], - "name": "JoyHub Rhythmic 4", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Oscillate" - }, - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - ] - }, - "joyhub-v3": { - "btle": { - "names": [ - "J-Ringstar", - "J-RapidTwist2" - ], - "services": { - "0000ffa0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffa1-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "JoyHub Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 255 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "J-Ringstar" - ], - "name": "JoyHub Starfish" - }, - { - "identifier": [ - "J-RapidTwist2" - ], - "name": "JoyHub Resi Ring 2" - } - ] - }, - "itoys": { - "btle": { - "names": [ - "26-021-B" - ], - "services": { - "0000ffa0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffa1-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "iToys Seagull", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 3 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "leten": { - "btle": { - "names": [ - "T528-LT", - "F537-LT", - "F520B-LT", - "F520A-LT" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff1-0000-1000-8000-00805f9b34fb" - }, - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "rx": "0000ffe1-0000-1000-8000-00805f9b34fb" - } - } - }, - "defaults": { - "name": "Leten Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 25 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - }, - "vibcrafter": { - "btle": { - "names": [ - "be gentle", - "Janna", - "Hayden", - "Nidalee" - ], - "services": { - "53300051-0060-4bd4-bbe5-a6920e4c5663": { - "tx": "53300052-0060-4bd4-bbe5-a6920e4c5663", - "rx": "53300053-0060-4bd4-bbe5-a6920e4c5663" - } - } - }, - "defaults": { - "name": "VibCrafter Device", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 99 - ], - "ActuatorType": "Vibrate" - }, - { - "StepRange": [ - 0, - 99 - ], - "ActuatorType": "Vibrate" - } - ] - } - }, - "configurations": [ - { - "identifier": [ - "be gentle" - ], - "name": "VibCrafter Harlow" - }, - { - "identifier": [ - "Hayden" - ], - "name": "VibCrafter Hayden" - }, - { - "identifier": [ - "Nidalee" - ], - "name": "VibCrafter Nidalee" - }, - { - "identifier": [ - "Janna" - ], - "name": "VibCrafter Janna", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 99 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - ] - }, - "lioness": { - "btle": { - "names": [ - "Lioness", - "Lioness2" - ], - "services": { - "d973f2ed-b19e-11e2-9e96-0800200c9a66": { - "tx": "d973f2f4-b19e-11e2-9e96-0800200c9a66" - }, - "d973f2e5-b19e-11e2-9e96-0800200c9a66": { - "rx": "d973f2e6-b19e-11e2-9e96-0800200c9a66" - } - } - }, - "defaults": { - "name": "Lioness", - "messages": { - "ScalarCmd": [ - { - "StepRange": [ - 0, - 100 - ], - "ActuatorType": "Vibrate" - } - ] - } - } - } - } -} diff --git a/buttplug/buttplug-device-config/build-config/buttplug-device-config-v3.json b/buttplug/buttplug-device-config/build-config/buttplug-device-config-v3.json deleted file mode 100644 index af4662f77..000000000 --- a/buttplug/buttplug-device-config/build-config/buttplug-device-config-v3.json +++ /dev/null @@ -1,19329 +0,0 @@ -{ - "version": { - "major": 3, - "minor": 15 - }, - "protocols": { - "lovense": { - "defaults": { - "name": "Lovense Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "B" - ], - "name": "Lovense Max", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrator", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Constrict", - "description": "Air Pump", - "actuator": { - "step-range": [ - 0, - 3 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "P" - ], - "name": "Lovense Edge", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "A", - "C" - ], - "name": "Lovense Nora", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "RotateCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "L" - ], - "name": "Lovense Ambi" - }, - { - "identifier": [ - "S" - ], - "name": "Lovense Lush" - }, - { - "identifier": [ - "Z" - ], - "name": "Lovense Hush" - }, - { - "identifier": [ - "W" - ], - "name": "Lovense Domi" - }, - { - "identifier": [ - "O" - ], - "name": "Lovense Osci" - }, - { - "identifier": [ - "V" - ], - "name": "Lovense Mission" - }, - { - "identifier": [ - "CA" - ], - "name": "Lovense Mission 2" - }, - { - "identifier": [ - "X" - ], - "name": "Lovense Ferri" - }, - { - "identifier": [ - "R" - ], - "name": "Lovense Diamo" - }, - { - "identifier": [ - "ToyS" - ], - "name": "Loveai Dolp" - }, - { - "identifier": [ - "F" - ], - "name": "Lovense Sex Machine", - "features": [ - { - "feature-type": "Oscillate", - "description": "Fucking Machine Oscillation Speed", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "FS" - ], - "name": "Lovense Mini Sex Machine", - "features": [ - { - "feature-type": "Oscillate", - "description": "Fucking Machine Oscillation Speed", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J" - ], - "name": "Lovense Dolce", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "OC" - ], - "name": "Lovense Osci 3", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "ED" - ], - "name": "Lovense Gush" - }, - { - "identifier": [ - "EZ" - ], - "name": "Lovense Gush 2" - }, - { - "identifier": [ - "EB" - ], - "name": "Lovense Hyphy", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "T" - ], - "name": "Lovense Calor" - }, - { - "identifier": [ - "EI" - ], - "name": "Lovense Flexer (Firmware update needed)" - }, - { - "identifier": [ - "EI-FW3" - ], - "name": "Lovense Flexer", - "features": [ - { - "feature-type": "Vibrate", - "description": "Internal Vibe", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "External Vibe", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Rotate", - "description": "Finger motion", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "N" - ], - "name": "Lovense Gemini", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "EA" - ], - "name": "Lovense Gravity", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Q" - ], - "name": "Lovense Tenera" - }, - { - "identifier": [ - "EL" - ], - "name": "Lovense Ridge", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "RotateCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "U" - ], - "name": "Lovense Lapis", - "features": [ - { - "feature-type": "Vibrate", - "description": "Tip Vibe", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Internal Vibe", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "External Vibe", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "SD" - ], - "name": "Lovense Vulse" - }, - { - "identifier": [ - "H" - ], - "name": "Lovense Solace", - "features": [ - { - "feature-type": "Oscillate", - "description": "Stroker Oscillation Speed", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "BA" - ], - "name": "Lovense Solace Pro", - "features": [ - { - "feature-type": "Oscillate", - "description": "Stroker Oscillation Speed", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Position", - "description": "Stroker Position Based Movement", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "LinearCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "LVS-*", - "LOVE-*" - ], - "manufacturer-data": [ - { - "company": 620, - "data": [ - 255, - 33 - ] - } - ], - "advertised-services": [ - "6e400001-b5a3-f393-e0a9-e50e24dcca9e", - "50300001-0024-4bd4-bbd5-a6920e4c5653", - "57300001-0023-4bd4-bbd5-a6920e4c5653", - "5a300001-0024-4bd4-bbd5-a6920e4c5653", - "50300001-0023-4bd4-bbd5-a6920e4c5653", - "53300001-0023-4bd4-bbd5-a6920e4c5653", - "5a300001-0023-4bd4-bbd5-a6920e4c5653", - "4f300001-0023-4bd4-bbd5-a6920e4c5653", - "42300001-0023-4bd4-bbd5-a6920e4c5653", - "43300001-0023-4bd4-bbd5-a6920e4c5653", - "4c300001-0023-4bd4-bbd5-a6920e4c5653", - "4c410001-0023-4bd4-bbd5-a6920e4c5653", - "56300001-0023-4bd4-bbd5-a6920e4c5653", - "58300001-0023-4bd4-bbd5-a6920e4c5653", - "52300001-0023-4bd4-bbd5-a6920e4c5653", - "46300001-0023-4bd4-bbd5-a6920e4c5653", - "50300011-0023-4bd4-bbd5-a6920e4c5653", - "4a300001-0023-4bd4-bbd5-a6920e4c5653", - "45440001-0023-4bd4-bbd5-a6920e4c5653", - "45420001-0023-4bd4-bbd5-a6920e4c5653", - "54300001-0023-4bd4-bbd5-a6920e4c5653", - "45490001-0023-4bd4-bbd5-a6920e4c5653", - "4e300001-0023-4bd4-bbd5-a6920e4c5653", - "45410001-0023-4bd4-bbd5-a6920e4c5653", - "51300001-0023-4bd4-bbd5-a6920e4c5653", - "45460001-0023-4bd4-bbd5-a6920e4c5653", - "454c0001-0023-4bd4-bbd5-a6920e4c5653", - "55300001-0023-4bd4-bbd5-a6920e4c5653", - "53440001-0023-4bd4-bbd5-a6920e4c5653", - "48300001-0023-4bd4-bbd5-a6920e4c5653", - "46530001-0023-4bd4-bbd5-a6920e4c5653", - "42410001-0023-4bd4-bbd5-a6920e4c5653", - "43410001-0023-4bd4-bbd5-a6920e4c5653", - "4f430001-0023-4bd4-bbd5-a6920e4c5653", - "455a0001-0023-4bd4-bbd5-a6920e4c5653" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff2-0000-1000-8000-00805f9b34fb", - "rx": "0000fff1-0000-1000-8000-00805f9b34fb" - }, - "6e400001-b5a3-f393-e0a9-e50e24dcca9e": { - "tx": "6e400002-b5a3-f393-e0a9-e50e24dcca9e", - "rx": "6e400003-b5a3-f393-e0a9-e50e24dcca9e" - }, - "50300001-0024-4bd4-bbd5-a6920e4c5653": { - "tx": "50300002-0024-4bd4-bbd5-a6920e4c5653", - "rx": "50300003-0024-4bd4-bbd5-a6920e4c5653" - }, - "57300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "57300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "57300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "5a300001-0024-4bd4-bbd5-a6920e4c5653": { - "tx": "5a300002-0024-4bd4-bbd5-a6920e4c5653", - "rx": "5a300003-0024-4bd4-bbd5-a6920e4c5653" - }, - "50300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "50300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "50300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "53300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "53300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "53300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "5a300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "5a300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "5a300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "4f300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "4f300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "4f300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "42300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "42300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "42300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "43300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "43300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "43300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "4c300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "4c300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "4c300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "4c410001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "4c410002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "4c410003-0023-4bd4-bbd5-a6920e4c5653" - }, - "56300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "56300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "56300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "58300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "58300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "58300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "52300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "52300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "52300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "46300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "46300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "46300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "50300011-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "50300012-0023-4bd4-bbd5-a6920e4c5653", - "rx": "50300013-0023-4bd4-bbd5-a6920e4c5653" - }, - "4a300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "4a300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "4a300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "45440001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "45440002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "45440003-0023-4bd4-bbd5-a6920e4c5653" - }, - "45420001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "45420002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "45420003-0023-4bd4-bbd5-a6920e4c5653" - }, - "54300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "54300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "54300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "45490001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "45490002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "45490003-0023-4bd4-bbd5-a6920e4c5653" - }, - "4e300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "4e300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "4e300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "45410001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "45410002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "45410003-0023-4bd4-bbd5-a6920e4c5653" - }, - "51300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "51300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "51300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "45460001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "45460002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "45460003-0023-4bd4-bbd5-a6920e4c5653" - }, - "454c0001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "454c0002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "454c0003-0023-4bd4-bbd5-a6920e4c5653" - }, - "55300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "55300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "55300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "53440001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "53440002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "53440003-0023-4bd4-bbd5-a6920e4c5653" - }, - "48300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "48300002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "48300003-0023-4bd4-bbd5-a6920e4c5653" - }, - "46530001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "46530002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "46530003-0023-4bd4-bbd5-a6920e4c5653" - }, - "42410001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "42410002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "42410003-0023-4bd4-bbd5-a6920e4c5653" - }, - "43410001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "43410002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "43410003-0023-4bd4-bbd5-a6920e4c5653" - }, - "4f430001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "4f430002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "4f430003-0023-4bd4-bbd5-a6920e4c5653" - }, - "455a0001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "455a0002-0023-4bd4-bbd5-a6920e4c5653", - "rx": "455a0003-0023-4bd4-bbd5-a6920e4c5653" - } - } - } - } - ] - }, - "lovense-connect-service": { - "defaults": { - "name": "Lovense Connect Service Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "Max" - ], - "name": "Lovense Max", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrator", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Constrict", - "description": "Air Pump", - "actuator": { - "step-range": [ - 0, - 3 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Edge" - ], - "name": "Lovense Edge", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Nora" - ], - "name": "Lovense Nora", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "RotateCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Ambi" - ], - "name": "Lovense Ambi" - }, - { - "identifier": [ - "Lush" - ], - "name": "Lovense Lush" - }, - { - "identifier": [ - "Hush" - ], - "name": "Lovense Hush" - }, - { - "identifier": [ - "Domi" - ], - "name": "Lovense Domi" - }, - { - "identifier": [ - "Osci" - ], - "name": "Lovense Osci" - }, - { - "identifier": [ - "Mission" - ], - "name": "Lovense Mission" - }, - { - "identifier": [ - "Ferri" - ], - "name": "Lovense Ferri" - }, - { - "identifier": [ - "Diamo" - ], - "name": "Lovense Diamo" - }, - { - "identifier": [ - "ToyS" - ], - "name": "Loveai Dolp" - }, - { - "identifier": [ - "XMachine" - ], - "name": "Lovense Sex Machine", - "features": [ - { - "feature-type": "Oscillate", - "description": "Fucking Machine Oscillation Speed", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Dolce" - ], - "name": "Lovense Dolce", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Gush" - ], - "name": "Lovense Gush" - }, - { - "identifier": [ - "Hyphy" - ], - "name": "Lovense Hyphy", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Calor" - ], - "name": "Lovense Calor" - }, - { - "identifier": [ - "Flexer" - ], - "name": "Lovense Flexer", - "features": [ - { - "feature-type": "Vibrate", - "description": "Both Vibes", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Rotate", - "description": "Finger motion", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Gemini" - ], - "name": "Lovense Gemini", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Gravity" - ], - "name": "Lovense Gravity", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Ridge" - ], - "name": "Lovense Ridge", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "RotateCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Lapis" - ], - "name": "Lovense Lapis", - "features": [ - { - "feature-type": "Vibrate", - "description": "Tip Vibe", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Internal Vibe", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "External Vibe", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Vulse" - ], - "name": "Lovense Vulse" - }, - { - "identifier": [ - "Solace" - ], - "name": "Lovense Solace", - "features": [ - { - "feature-type": "Oscillate", - "description": "Stroker Oscillation Speed", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "lovense-connect-service": { - "exists": true - } - } - ] - }, - "xinput": { - "defaults": { - "name": "XBox (XInput) Compatible Gamepad", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 65535 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 65535 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "xinput": { - "exists": true - } - } - ] - }, - "kiiroo-v2": { - "defaults": { - "name": "Kiiroo v2 Device", - "features": [ - { - "feature-type": "Position", - "actuator": { - "step-range": [ - 0, - 99 - ], - "messages": [ - "LinearCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "Launch" - ], - "name": "Fleshlight Launch" - }, - { - "identifier": [ - "Onyx2" - ], - "name": "Kiiroo Onyx 2" - } - ], - "communication": [ - { - "btle": { - "names": [ - "Launch", - "Onyx2" - ], - "services": { - "88f80580-0000-01e6-aace-0002a5d5c51b": { - "tx": "88f80581-0000-01e6-aace-0002a5d5c51b", - "rx": "88f80582-0000-01e6-aace-0002a5d5c51b", - "firmware": "88f80583-0000-01e6-aace-0002a5d5c51b" - }, - "f60402a6-0293-4bdb-9f20-6758133f7090": { - "tx": "02962ac9-e86f-4094-989d-231d69995fc2", - "rx": "d44d0393-0731-43b3-a373-8fc70b1f3323", - "firmware": "c7b7a04b-2cc4-40ff-8b10-5d531d1161db" - } - } - } - } - ] - }, - "libo-elle": { - "defaults": { - "name": "Libo Elle Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 3 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "PiPiJing" - ], - "name": "LiBo Elle" - }, - { - "identifier": [ - "Shuidi" - ], - "name": "Libo Elle 2" - } - ], - "communication": [ - { - "btle": { - "names": [ - "PiPiJing", - "Shuidi" - ], - "services": { - "00006000-0000-1000-8000-00805f9b34fb": { - "tx": "00006001-0000-1000-8000-00805f9b34fb", - "txmode": "00006002-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "libo-shark": { - "defaults": { - "name": "Libo Shark", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 3 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 3 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "ShaYu" - ], - "services": { - "00006000-0000-1000-8000-00805f9b34fb": { - "tx": "00006001-0000-1000-8000-00805f9b34fb", - "txmode": "00006002-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "libo-karen": { - "defaults": { - "name": "Libo Karen", - "features": [] - }, - "communication": [ - { - "btle": { - "names": [ - "SuoYinQiu" - ], - "services": { - "00006000-0000-1000-8000-00805f9b34fb": { - "tx": "00006001-0000-1000-8000-00805f9b34fb", - "txmode": "00006002-0000-1000-8000-00805f9b34fb" - }, - "00006050-0000-1000-8000-00805f9b34fb": { - "rxpressure": "00006051-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "libo-vibes": { - "defaults": { - "name": "Libo Vibes Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "XiaoLu" - ], - "name": "Libo Lottie" - }, - { - "identifier": [ - "LuXiaoHan" - ], - "name": "Libo LuLu" - }, - { - "identifier": [ - "Yuyi" - ], - "name": "Libo Lina" - }, - { - "identifier": [ - "LuWuShuang" - ], - "name": "Libo Adel" - }, - { - "identifier": [ - "LiBo" - ], - "name": "Libo Lily" - }, - { - "identifier": [ - "QingTing" - ], - "name": "Libo Lucy" - }, - { - "identifier": [ - "Huohu" - ], - "name": "Libo Lara" - }, - { - "identifier": [ - "Yuyi" - ], - "name": "Libo Feather", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 99 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "BaiHu" - ], - "name": "Libo LaLa", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 3 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Gugudai" - ], - "name": "Libo Carlos", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 3 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Haima" - ], - "name": "Libo Selina", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 3 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "XiaoLu", - "LuXiaoHan", - "BaiHu", - "Gugudai", - "Yuyi", - "LuWuShuang", - "LiBo", - "QingTing", - "Huohu", - "Yuyi", - "Haima" - ], - "services": { - "00006000-0000-1000-8000-00805f9b34fb": { - "tx": "00006001-0000-1000-8000-00805f9b34fb", - "txmode": "00006002-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "magic-motion-1": { - "defaults": { - "name": "Magic Motion V1 Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "Smart Bean" - ], - "name": "MagicMotion Smart Bean" - }, - { - "identifier": [ - "Smart Bean3" - ], - "name": "FitCute Kegel Rejuve" - }, - { - "identifier": [ - "Smart Mini Vibe" - ], - "name": "MagicMotion Smart Mini Vibe" - }, - { - "identifier": [ - "Smart Mini Vibe3" - ], - "name": "MagicMotion Vini" - }, - { - "identifier": [ - "Flamingo", - "Flamingo T" - ], - "name": "MagicMotion Flamingo" - }, - { - "identifier": [ - "Magic Bean" - ], - "name": "MagicMotion Kegel" - }, - { - "identifier": [ - "Magic Cell" - ], - "name": "MagicMotion Dante/Candy/Rise" - }, - { - "identifier": [ - "Magic Wand" - ], - "name": "MagicMotion Wand" - }, - { - "identifier": [ - "Magic Fugu", - "Fugu", - "Fugu2" - ], - "name": "MagicMotion Fugu" - }, - { - "identifier": [ - "Gballs2" - ], - "name": "G Vibe Gballs 2" - }, - { - "identifier": [ - "GBalls3" - ], - "name": "G Vibe Gballs 3" - }, - { - "identifier": [ - "FM-LILAC-101" - ], - "name": "Femometer Lilac" - }, - { - "identifier": [ - "Xone" - ], - "name": "MagicMotion Xone", - "features": [ - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "CBT002" - ], - "name": "FunTown Caleo" - } - ], - "communication": [ - { - "btle": { - "names": [ - "Smart Mini Vibe*", - "Flamingo", - "Flamingo T", - "Smart Bean", - "Smart Bean3", - "Magic Cell", - "Magic Wand", - "Fugu", - "Fugu2", - "Gballs2", - "GBalls3", - "FM-LILAC-101", - "Xone", - "CBT002" - ], - "services": { - "78667579-7b48-43db-b8c5-7928a6b0a335": { - "tx": "78667579-a914-49a4-8333-aa3c0cd8fedc" - }, - "0000180f-0000-1000-8000-00805f9b34fb": { - "rxblebattery": "00002a19-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "magic-motion-2": { - "defaults": { - "name": "Magic Motion V2 Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "Lipstick" - ], - "name": "MagicMotion Awaken" - }, - { - "identifier": [ - "Sword" - ], - "name": "MagicMotion Equinox" - }, - { - "identifier": [ - "Curve" - ], - "name": "MagicMotion Solstice" - }, - { - "identifier": [ - "Eidolon" - ], - "name": "MagicMotion Eidolon", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Solstice X" - ], - "name": "MagicMotion Solstice X", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "funwand" - ], - "name": "MagicMotion Zenith" - }, - { - "identifier": [ - "CBT001" - ], - "name": "FunTown Jive", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "Eidolon", - "Lipstick", - "Sword", - "Curve", - "Solstice X", - "funwand", - "CBT001" - ], - "services": { - "78667579-7b48-43db-b8c5-7928a6b0a335": { - "tx": "78667579-a914-49a4-8333-aa3c0cd8fedc" - }, - "0000180f-0000-1000-8000-00805f9b34fb": { - "rxblebattery": "00002a19-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "magic-motion-3": { - "defaults": { - "name": "LoveLife Krush", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 77 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "Krush" - ], - "services": { - "78667579-7b48-43db-b8c5-7928a6b0a335": { - "tx": "78667579-a914-49a4-8333-aa3c0cd8fedc" - }, - "0000180f-0000-1000-8000-00805f9b34fb": { - "rxblebattery": "00002a19-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "magic-motion-4": { - "defaults": { - "name": "Magic Motion V4 Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "funone" - ], - "name": "MagicMotion Bunny" - }, - { - "identifier": [ - "Magic Sundi" - ], - "name": "MagicMotion Sundae" - }, - { - "identifier": [ - "Kegel Coach" - ], - "name": "MagicMotion Kegel Coach" - }, - { - "identifier": [ - "Magic Lotos" - ], - "name": "MagicMotion Lotos" - }, - { - "identifier": [ - "nyx" - ], - "name": "MagicMotion Nyx" - }, - { - "identifier": [ - "umi" - ], - "name": "MagicMotion Umi", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "funkegel" - ], - "name": "MagicMotion Crystal" - }, - { - "identifier": [ - "bobi2" - ], - "name": "MagicMotion Bobi", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "funone", - "Magic Sundi", - "Kegel Coach", - "Magic Lotos", - "nyx", - "umi", - "funkegel", - "bobi2" - ], - "services": { - "78667579-7b48-43db-b8c5-7928a6b0a335": { - "tx": "78667579-a914-49a4-8333-aa3c0cd8fedc" - }, - "0000180f-0000-1000-8000-00805f9b34fb": { - "rxblebattery": "00002a19-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "mysteryvibe": { - "defaults": { - "name": "Mysteryvibe Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 56 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 56 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 56 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 56 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 56 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 56 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "MV Crescendo" - ], - "name": "MysteryVibe Crescendo" - }, - { - "identifier": [ - "MV Tenuto " - ], - "name": "MysteryVibe Tenuto" - }, - { - "identifier": [ - "MV Poco " - ], - "name": "MysteryVibe Poco", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 56 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 56 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "MV Crescendo", - "MV Tenuto ", - "MV Poco " - ], - "services": { - "f0006900-110c-478b-b74b-6f403b364a9c": { - "txmode": "f0006901-110c-478b-b74b-6f403b364a9c", - "txvibrate": "f0006903-110c-478b-b74b-6f403b364a9c" - } - } - } - } - ] - }, - "mysteryvibe-v2": { - "defaults": { - "name": "Mysteryvibe V2 Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 56 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 56 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 56 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "6907 MV1" - ], - "name": "MysteryVibe Tenuto Mini" - }, - { - "identifier": [ - "6908 MV1" - ], - "name": "MysteryVibe Crescendo 2", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 56 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 56 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 56 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 56 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 56 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 56 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "6909 MV1", - "6909 MV2" - ], - "name": "MysteryVibe Tenuto 2", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 56 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 56 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 56 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 56 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "6914 MV1" - ], - "name": "MysteryVibe Legato", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 56 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 56 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 56 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 56 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "6915 MV1" - ], - "name": "MysteryVibe Molto", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 56 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "6907 MV1", - "6908 MV1", - "6909 MV1", - "6909 MV2", - "6914 MV1", - "6915 MV1" - ], - "services": { - "f0006900-110c-478b-b74b-6f403b364a9c": { - "txmode": "f0006901-110c-478b-b74b-6f403b364a9c", - "txvibrate": "f0006903-110c-478b-b74b-6f403b364a9c" - } - } - } - } - ] - }, - "picobong": { - "defaults": { - "name": "Picobong Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 10 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "Blow hole", - "Picobong Male Toy" - ], - "name": "Picobong Blow hole" - }, - { - "identifier": [ - "Diver", - "Picobong Egg" - ], - "name": "Picobong Diver" - }, - { - "identifier": [ - "Life guard", - "Picobong Ring" - ], - "name": "Picobong Life guard" - }, - { - "identifier": [ - "Surfer", - "Picobong Butt Plug", - "Egg driver", - "Surfer_plug" - ], - "name": "Picobong Surfer" - } - ], - "communication": [ - { - "btle": { - "names": [ - "Blow hole", - "Picobong Male Toy", - "Diver", - "Picobong Egg", - "Life guard", - "Picobong Ring", - "Surfer", - "Picobong Butt Plug", - "Egg driver", - "Surfer_plug" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff1-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "vibratissimo": { - "defaults": { - "name": "Vibratissimo Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "Licker", - "SecretKiss", - "Womenizer" - ], - "name": "Vibratissimo Licker", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Rabbit" - ], - "name": "Vibratissimo Rabbit", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 2 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "Vibratissimo" - ], - "services": { - "00001523-1212-efde-1523-785feabcd123": { - "txmode": "00001524-1212-efde-1523-785feabcd123", - "txvibrate": "00001526-1212-efde-1523-785feabcd123", - "rx": "00001527-1212-efde-1523-785feabcd123" - }, - "0000180a-0000-1000-8000-00805f9b34fb": { - "rxblemodel": "00002a24-0000-1000-8000-00805f9b34fb" - }, - "0000180f-0000-1000-8000-00805f9b34fb": { - "rxblebattery": "00002a19-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "wevibe": { - "defaults": { - "name": "WeVibe Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 15 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "Bloom" - ], - "name": "WeVibe Bloom" - }, - { - "identifier": [ - "Ditto" - ], - "name": "WeVibe Ditto" - }, - { - "identifier": [ - "Jive" - ], - "name": "WeVibe Jive" - }, - { - "identifier": [ - "Pivot" - ], - "name": "WeVibe Pivot" - }, - { - "identifier": [ - "Rave" - ], - "name": "WeVibe Rave" - }, - { - "identifier": [ - "Verge" - ], - "name": "WeVibe Verge" - }, - { - "identifier": [ - "Wish" - ], - "name": "WeVibe Wish" - }, - { - "identifier": [ - "Cougar", - "4 Plus", - "4_Plus", - "4plus", - "classic", - "Classic" - ], - "name": "WeVibe 4 Plus", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 15 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 15 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Gala" - ], - "name": "WeVibe Gala", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 15 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 15 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Nova" - ], - "name": "WeVibe Nova", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 15 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 15 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Sync" - ], - "name": "WeVibe Sync", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 15 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 15 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "Cougar", - "4 Plus", - "4_Plus", - "4plus", - "Bloom", - "classic", - "Classic", - "Ditto", - "Gala", - "Jive", - "Nova", - "Pivot", - "Rave", - "Sync", - "Verge", - "Wish" - ], - "services": { - "f000bb03-0451-4000-b000-000000000000": { - "tx": "f000c000-0451-4000-b000-000000000000", - "rx": "f000b000-0451-4000-b000-000000000000" - } - } - } - } - ] - }, - "wevibe-8bit": { - "defaults": { - "name": "WeVibe 8-bit Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 12 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "Melt" - ], - "name": "WeVibe Melt", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 22 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Moxie" - ], - "name": "WeVibe Moxie" - }, - { - "identifier": [ - "Vector" - ], - "name": "WeVibe Vector", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 12 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 12 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Wand" - ], - "name": "WeVibe Wand", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 22 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Wand 2" - ], - "name": "WeVibe Wand 2", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 22 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Bond", - "Nelson" - ], - "name": "WeVibe Bond", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 27 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Nova2", - "Nova_2", - "Nova 2" - ], - "name": "WeVibe Nova 2", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 27 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 27 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Jive 2" - ], - "name": "WeVibe Jive 2" - } - ], - "communication": [ - { - "btle": { - "names": [ - "Melt", - "Moxie", - "Vector", - "Wand", - "Wand 2", - "Bond", - "Nelson", - "Nova2", - "Nova_2", - "Nova 2", - "Jive 2" - ], - "services": { - "f000bb03-0451-4000-b000-000000000000": { - "tx": "f000c000-0451-4000-b000-000000000000", - "rx": "f000b000-0451-4000-b000-000000000000" - } - } - } - } - ] - }, - "wevibe-legacy": { - "defaults": { - "name": "WeVibe Realm Reina", - "features": [] - }, - "communication": [ - { - "btle": { - "names": [ - "Reina", - "imassager", - "Interactive Massager", - "03" - ], - "services": { - "f000bb03-0451-4000-b000-000000000000": { - "tx": "f000c000-0451-4000-b000-000000000000", - "rx": "f000b000-0451-4000-b000-000000000000" - } - } - } - } - ] - }, - "wevibe-chorus": { - "defaults": { - "name": "WeVibe Chorus", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 30 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 30 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "Sync 2" - ], - "name": "WeVibe Sync 2", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 30 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 30 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Sync Lite" - ], - "name": "WeVibe Sync Lite", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 30 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "Chorus", - "skeena", - "Sync 2", - "Sync Lite" - ], - "services": { - "f000bb03-0451-4000-b000-000000000000": { - "tx": "f000c000-0451-4000-b000-000000000000", - "rx": "f000b000-0451-4000-b000-000000000000" - } - } - } - } - ] - }, - "youcups": { - "defaults": { - "name": "Youcups Warrior II", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 8 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "Youcups" - ], - "services": { - "0000fee9-0000-1000-8000-00805f9b34fb": { - "tx": "d44bc439-abfd-45a2-b575-925416129600" - } - } - } - } - ] - }, - "cueme": { - "defaults": { - "name": "Cueme Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 15 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 15 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 15 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 15 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 15 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 15 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 15 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 15 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "1" - ], - "name": "Cueme Mens" - }, - { - "identifier": [ - "2" - ], - "name": "Cueme Bra" - }, - { - "identifier": [ - "3" - ], - "name": "Cueme Womans", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 15 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 15 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 15 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 15 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "FUNCODE_*" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff1-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "kiiroo-v2-vibrator": { - "defaults": { - "name": "Kiiroo V2 Vibrator Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "Pearl2" - ], - "name": "Kiiroo Pearl 2", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Fuse" - ], - "name": "OhMiBod Fuse", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Virtual Rabbit" - ], - "name": "PornHub Virtual Rabbit", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Virtual Blowbot" - ], - "name": "PornHub Virtual Blowbot", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Titan" - ], - "name": "Kiiroo Titan", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "Pearl2", - "Fuse", - "Virtual Blowbot", - "Titan", - "Virtual Rabbit" - ], - "services": { - "88f82580-0000-01e6-aace-0002a5d5c51b": { - "tx": "88f82581-0000-01e6-aace-0002a5d5c51b", - "rxtouch": "88f82582-0000-01e6-aace-0002a5d5c51b", - "rxaccel": "88f82584-0000-01e6-aace-0002a5d5c51b" - } - } - } - } - ] - }, - "kiiroo-v21": { - "defaults": { - "name": "Kiiroo V2.1 Device", - "features": [] - }, - "configurations": [ - { - "identifier": [ - "Pearl2.1" - ], - "name": "Kiiroo Pearl 2.1", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Cliona" - ], - "name": "Kiiroo Cliona", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "OhMiBod 4.0", - "OhMiBod ESCA" - ], - "name": "OhMiBod Esca 2", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Titan1.1" - ], - "name": "Kiiroo Titan 1.1", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Position", - "actuator": { - "step-range": [ - 0, - 99 - ], - "messages": [ - "LinearCmd" - ] - } - } - ] - }, - { - "identifier": [ - "OhMiBod LUMEN" - ], - "name": "OhMiBod Lumen", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "OhMiBod NEX2" - ], - "name": "OhMiBod NEX|2", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "OhMiBod NEX3" - ], - "name": "OhMiBod NEX|3", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Pulse Interactive" - ], - "name": "Hot Octopuss Pulse Solo Interactive", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 6 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Fuse1.1" - ], - "name": "OhMiBod Fuse 1.1", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "OhMiBod Foxy" - ], - "name": "OhMiBod Foxy", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "OhMiBod Chill Panty Vibe" - ], - "name": "OhMiBod Chill", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "OhMiBod Sphinx" - ], - "name": "OhMiBod Sphinx", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Pearl2+", - "Pearl 2+" - ], - "name": "Kiiroo Pearl 2+", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Pearl3", - "Pearl 3" - ], - "name": "Kiiroo Pearl 3", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "Titan1.1", - "Cliona", - "Pearl2.1", - "Pearl2+", - "Pearl 2+", - "Pearl3", - "Pearl 3", - "OhMiBod 4.0", - "OhMiBod LUMEN", - "OhMiBod NEX2", - "OhMiBod NEX3", - "OhMiBod ESCA", - "OhMiBod Foxy", - "OhMiBod Chill Panty Vibe", - "OhMiBod Sphinx", - "Pulse Interactive", - "Fuse1.1" - ], - "services": { - "00001900-0000-1000-8000-00805f9b34fb": { - "whitelist": "00001901-0000-1000-8000-00805f9b34fb", - "tx": "00001902-0000-1000-8000-00805f9b34fb", - "rx": "00001903-0000-1000-8000-00805f9b34fb" - }, - "a0d70001-4c16-4ba7-977a-d394920e13a3": { - "tx": "a0d70002-4c16-4ba7-977a-d394920e13a3", - "rx": "a0d70003-4c16-4ba7-977a-d394920e13a3" - } - } - } - } - ] - }, - "kiiroo-v21-initialized": { - "defaults": { - "name": "Kiiroo V2.1 Initialized Device", - "features": [] - }, - "configurations": [ - { - "identifier": [ - "Onyx2.1" - ], - "name": "Kiiroo Onyx 2.1", - "features": [ - { - "feature-type": "Position", - "actuator": { - "step-range": [ - 0, - 99 - ], - "messages": [ - "LinearCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Onyx+" - ], - "name": "Kiiroo Onyx+", - "features": [ - { - "feature-type": "Position", - "actuator": { - "step-range": [ - 0, - 99 - ], - "messages": [ - "LinearCmd" - ] - } - } - ] - }, - { - "identifier": [ - "KEON", - "Keon R2" - ], - "name": "Kiiroo Keon", - "features": [ - { - "feature-type": "Position", - "actuator": { - "step-range": [ - 0, - 99 - ], - "messages": [ - "LinearCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Rey", - "We-Vibe Rocketman", - "Realm1.1" - ], - "name": "Kiiroo Onyx+ Realm Edition", - "features": [ - { - "feature-type": "Position", - "actuator": { - "step-range": [ - 0, - 99 - ], - "messages": [ - "LinearCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "Rey", - "We-Vibe Rocketman", - "Realm1.1", - "Onyx2.1", - "Onyx+", - "KEON", - "Keon R2" - ], - "services": { - "00001900-0000-1000-8000-00805f9b34fb": { - "whitelist": "00001901-0000-1000-8000-00805f9b34fb", - "tx": "00001902-0000-1000-8000-00805f9b34fb", - "rx": "00001903-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "kiiroo-prowand": { - "defaults": { - "name": "Kiiroo ProWand", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "ProWand" - ], - "services": { - "00001400-0000-1000-8000-00805f9b34fb": { - "tx": "00001401-0000-1000-8000-00805f9b34fb" - }, - "0000180f-0000-1000-8000-00805f9b34fb": { - "rxblebattery": "00002a19-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "kiiroo-spot": { - "defaults": { - "name": "Kiiroo Spot", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "SPOT W1" - ], - "services": { - "00001400-0000-1000-8000-00805f9b34fb": { - "tx": "00001401-0000-1000-8000-00805f9b34fb" - }, - "0000180f-0000-1000-8000-00805f9b34fb": { - "rxblebattery": "00002a19-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "vorze-cyclone-x": { - "defaults": { - "name": "Vorze Cyclone X10 Device", - "features": [ - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 10 - ], - "messages": [ - "RotateCmd" - ] - } - } - ] - }, - "communication": [ - { - "hid": { - "pairs": [ - { - "vendor-id": 1155, - "product-id": 22352 - } - ] - } - } - ] - }, - "rez-trancevibrator": { - "defaults": { - "name": "Rez TranceVibrator", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "usb": { - "pairs": [ - { - "vendor-id": 2889, - "product-id": 1615 - } - ] - } - } - ] - }, - "kiiroo-v1": { - "defaults": { - "name": "Kiiroo V1 Device", - "features": [] - }, - "configurations": [ - { - "identifier": [ - "PEARL" - ], - "name": "Kiiroo Pearl", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 4 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "ONYX" - ], - "name": "Kiiroo Onyx", - "features": [ - { - "feature-type": "Position", - "actuator": { - "step-range": [ - 0, - 4 - ], - "messages": [ - "LinearCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "ONYX", - "PEARL" - ], - "services": { - "49535343-fe7d-4ae5-8fa9-9fafd205e455": { - "rx": "49535343-1e4d-4bd9-ba61-23c647249616", - "tx": "49535343-8841-43f4-a8d4-ecbe34729bb3", - "command": "49535343-aca3-481c-91ec-d85e28a60318" - } - } - } - } - ] - }, - "vorze-sa": { - "defaults": { - "name": "Vorze Device", - "features": [] - }, - "configurations": [ - { - "identifier": [ - "Bach smart" - ], - "name": "Vorze Bach", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "ROCKET" - ], - "name": "Adult Festa Rocket", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "CycSA" - ], - "name": "Vorze A10 Cyclone SA", - "features": [ - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 99 - ], - "messages": [ - "RotateCmd" - ] - } - } - ] - }, - { - "identifier": [ - "UFOSA" - ], - "name": "Vorze UFO SA", - "features": [ - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 99 - ], - "messages": [ - "RotateCmd" - ] - } - } - ] - }, - { - "identifier": [ - "UFO-TW" - ], - "name": "Vorze UFO TW", - "features": [ - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 99 - ], - "messages": [ - "RotateCmd" - ] - } - }, - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 99 - ], - "messages": [ - "RotateCmd" - ] - } - } - ] - }, - { - "identifier": [ - "VorzePiston" - ], - "name": "Vorze Piston", - "features": [ - { - "feature-type": "Position", - "actuator": { - "step-range": [ - 0, - 99 - ], - "messages": [ - "LinearCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "Bach smart", - "CycSA", - "UFOSA", - "UFO-TW", - "VorzePiston", - "ROCKET" - ], - "services": { - "40ee1111-63ec-4b7f-8ce7-712efd55b90e": { - "tx": "40ee2222-63ec-4b7f-8ce7-712efd55b90e" - } - } - } - } - ] - }, - "youou": { - "defaults": { - "name": "Youou Wand Vibrator", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "VX001_*" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff6-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "realtouch": { - "defaults": { - "name": "RealTouch", - "features": [ - { - "feature-type": "Position", - "actuator": { - "step-range": [ - 0, - 99 - ], - "messages": [ - "LinearCmd" - ] - } - } - ] - }, - "communication": [ - { - "hid": { - "pairs": [ - { - "vendor-id": 8020, - "product-id": 1 - } - ] - } - } - ] - }, - "prettylove": { - "defaults": { - "name": "Pretty Love Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 3 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "Aogu BLE *", - "AB Shutter3 [Aogu BLE Device]" - ], - "services": { - "0000ffe5-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe9-0000-1000-8000-00805f9b34fb", - "rx": "0000ffe2-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "svakom": { - "defaults": { - "name": "Svakom Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 19 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "Aogu SCB" - ], - "name": "Svakom Ella" - }, - { - "identifier": [ - "Phoenix NEO" - ], - "name": "Svakom Phoenix Neo" - }, - { - "identifier": [ - "Emma NEO" - ], - "name": "Svakom Emma Neo" - } - ], - "communication": [ - { - "btle": { - "names": [ - "Aogu SUV", - "Aogu SCB", - "Emma NEO", - "Phoenix NEO" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb", - "rx": "0000ffe2-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "svakom-v2": { - "defaults": { - "name": "Svakom Device v2", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 10 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "116" - ], - "name": "Svakom Phoenix Neo" - }, - { - "identifier": [ - "Viviana" - ], - "name": "Svakom Viviana" - }, - { - "identifier": [ - "Ella NEO" - ], - "name": "Svakom Ella Neo" - }, - { - "identifier": [ - "117", - "Edeny" - ], - "name": "Svakom Edeny" - }, - { - "identifier": [ - "S38A" - ], - "name": "Svakom Tammy Pro" - }, - { - "identifier": [ - "Vick NEO", - "Vick Neo" - ], - "name": "Svakom Vick Neo" - }, - { - "identifier": [ - "STG05A" - ], - "name": "Svakom Aravinda" - }, - { - "identifier": [ - "118" - ], - "name": "ToyCod Vanesia" - }, - { - "identifier": [ - "QH-SJ007A" - ], - "name": "Svakom Winni 2" - }, - { - "identifier": [ - "Cici 2" - ], - "name": "Svakom Cici 2" - }, - { - "identifier": [ - "Emma Neo 2" - ], - "name": "Svakom Emma Neo 2" - } - ], - "communication": [ - { - "btle": { - "names": [ - "116", - "117", - "Edeny", - "118", - "Viviana", - "Ella NEO", - "S38A", - "Vick NEO", - "Vick Neo", - "STG05A", - "QH-SJ007A", - "Cici 2", - "Emma Neo 2" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb", - "rx": "0000ffe2-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "svakom-v3": { - "defaults": { - "name": "Svakom Device v3", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 10 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "Phoenix Neo 2" - ], - "name": "Svakom Phoenix Neo 2" - }, - { - "identifier": [ - "FK008A" - ], - "name": "Fantasy Cup Theodore", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 10 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 1 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Hannes NEO" - ], - "name": "Svakom Hannes Neo" - }, - { - "identifier": [ - "QH-SX007E" - ], - "name": "Svakom Alberta", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrating attachments", - "actuator": { - "step-range": [ - 0, - 10 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Suction lens", - "actuator": { - "step-range": [ - 0, - 1 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "Phoenix Neo 2", - "FK008A", - "Hannes NEO", - "QH-SX007E" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb", - "rx": "0000ffe2-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "svakom-v4": { - "defaults": { - "name": "Svakom Device v4", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 10 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 10 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "B2CM6" - ], - "name": "ToyCod Barzillai" - }, - { - "identifier": [ - "ERICA" - ], - "name": "Svakom Erica" - }, - { - "identifier": [ - "Cici+ 2" - ], - "name": "Svakom Cici+ 2" - } - ], - "communication": [ - { - "btle": { - "names": [ - "B2CM6", - "ERICA", - "Cici+ 2" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb", - "rx": "0000ffe2-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "svakom-v5": { - "defaults": { - "name": "Svakom Device v5", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 10 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 10 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "Chika" - ], - "name": "Svakom Chika" - }, - { - "identifier": [ - "Mora Neo" - ], - "name": "Svakom Mora Neo", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 10 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 10 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 3 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Trysta Neo" - ], - "name": "Svakom Trysta Neo", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 10 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 10 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 3 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Mini Emma Neo" - ], - "name": "Svakom Mini Emma Neo", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 10 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "Chika", - "Mora Neo", - "Trysta Neo", - "Mini Emma Neo" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb", - "rx": "0000ffe2-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "svakom-v6": { - "defaults": { - "name": "Svakom Device v6", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 10 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "CocoPro" - ], - "name": "Svakom Coco Pro" - }, - { - "identifier": [ - "Echo 2" - ], - "name": "Svakom Echo 2" - }, - { - "identifier": [ - "Vick Neo 2" - ], - "name": "Svakom Vick Neo 2", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 10 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 10 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Iker Neo" - ], - "name": "Svakom Iker Neo", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 10 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 10 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 5 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "CocoPro", - "Echo 2", - "Vick Neo 2", - "Iker Neo" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb", - "rx": "0000ffe2-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "svakom-sam": { - "defaults": { - "name": "Svakom Sam Neo", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 10 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 1 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "Sam Neo" - ], - "services": { - "0000ae00-0000-1000-8000-00805f9b34fb": { - "tx": "0000ae01-0000-1000-8000-00805f9b34fb", - "rx": "0000ae02-0000-1000-8000-00805f9b34fb", - "txmode": "0000ae10-0000-1000-8000-00805f9b34fb" - }, - "0000ffac-0000-1000-8000-00805f9b34fb": { - "firmware": "0000ffb4-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "svakom-sam2": { - "defaults": { - "name": "Svakom Sam Neo 2", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 10 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Constrict", - "actuator": { - "step-range": [ - 0, - 5 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "Sam Neo 2" - ], - "name": "Svakom Sam Neo 2" - }, - { - "identifier": [ - "Sam Neo 2 Pro" - ], - "name": "Svakom Sam Neo 2 Pro" - } - ], - "communication": [ - { - "btle": { - "names": [ - "Sam Neo 2", - "Sam Neo 2 Pro" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb", - "rx": "0000ffe2-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "svakom-alex": { - "defaults": { - "name": "Svakom Alex Neo", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 3 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "Alex NEO", - "S63E Alex NEO" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "svakom-alex-v2": { - "defaults": { - "name": "Svakom Alex Neo 2", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 3 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "Alex NEO 2", - "S63E Alex NEO 2" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "svakom-dice": { - "defaults": { - "name": "Zemalia Dice for Love", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "ZhiAi" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb", - "rx": "0000ffe2-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "svakom-dt250a": { - "defaults": { - "name": "Coleur Dor DT250A", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 3 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 3 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Constrict", - "actuator": { - "step-range": [ - 0, - 2 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "DT250A" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb", - "rx": "0000ffe2-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "svakom-iker": { - "defaults": { - "name": "Svakom Iker", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 10 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 5 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "Iker" - ], - "manufacturer-data": [ - { - "company": 39, - "data": [ - 83, - 86, - 65, - 1, - 11, - 18, - 1, - 51, - 68, - 85, - 202, - 8 - ] - } - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "svakom-jordan": { - "defaults": { - "name": "Svakom Jordan", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 10 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 5 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "Jordan" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb", - "rx": "0000ffe2-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "svakom-pulse": { - "defaults": { - "name": "Svakom Pulse Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 9 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "SWK-SX013A" - ], - "name": "Svakom Pulse Lite Neo" - }, - { - "identifier": [ - "Pulse Union" - ], - "name": "Svakom Pulse Union" - }, - { - "identifier": [ - "Pulse Galaxie" - ], - "name": "Svakom Pulse Galaxie" - }, - { - "identifier": [ - "SX033APP" - ], - "name": "Svakom Mimiki" - }, - { - "identifier": [ - "BX288A" - ], - "name": "BeYourLover Kyukyu" - }, - { - "identifier": [ - "QH-SX045A-B" - ], - "name": "Coleur Dor VX045A" - }, - { - "identifier": [ - "SWK-SX067-B" - ], - "name": "Momonii Agatha" - }, - { - "identifier": [ - "QH-HX029A-B" - ], - "name": "Coleur Dor HX029A" - } - ], - "communication": [ - { - "btle": { - "names": [ - "SWK-SX013A", - "Pulse Union", - "Pulse Galaxie", - "SX033APP", - "BX288A", - "QH-SX045A-B", - "SWK-SX067-B", - "QH-HX029A-B" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb", - "rx": "0000ffe2-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "svakom-suitcase": { - "defaults": { - "name": "Svakom Magic Suitcase", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 30 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 1 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "VX236A-BLE-V1.0" - ], - "name": "Coleur Dor VX236A" - } - ], - "communication": [ - { - "btle": { - "names": [ - "VX357A-BLE-V1.0", - "VX236A-BLE-V1.0" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb", - "rx": "0000ffe2-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "svakom-tarax": { - "defaults": { - "name": "ToyCod Tara X", - "features": [ - { - "feature-type": "Vibrate", - "description": "Internal vibrator", - "actuator": { - "step-range": [ - 0, - 3 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "External pulsator", - "actuator": { - "step-range": [ - 0, - 3 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "SX218A" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb", - "rx": "0000ffe2-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "svakom-avaneo": { - "defaults": { - "name": "Svakom Ava Neo", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 10 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 1 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "Ava Neo" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb", - "rx": "0000ffe2-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "svakom-barnard": { - "defaults": { - "name": "Fantasy Cup Barnard", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 3 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 3 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "DG239A" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "svakom-barney": { - "defaults": { - "name": "Mutufun Barney", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 10 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 10 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "DJ333A" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb", - "rx": "0000ffe2-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "realov": { - "defaults": { - "name": "Realov Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 50 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "REALOV_VIBE" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "motorbunny": { - "defaults": { - "name": "Motorbunny Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "RotateCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "MB Controller" - ], - "name": "Motorbunny Classic" - }, - { - "identifier": [ - "MB LINK 201" - ], - "name": "Motorbunny Buck" - } - ], - "communication": [ - { - "btle": { - "names": [ - "MB Controller", - "MB LINK 201" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff6-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "zalo": { - "defaults": { - "name": "Zalo Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 8 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "ZALO-Queen" - ], - "name": "Zalo Queen", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 8 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 8 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "ZALO-King" - ], - "name": "Zalo King", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 8 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 8 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "ZALO-Jeanne" - ], - "name": "Zalo Jeanne" - } - ], - "communication": [ - { - "btle": { - "names": [ - "ZALO-Queen", - "ZALO-King", - "ZALO-Jeanne" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff1-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "sayberx": { - "defaults": { - "name": "SayberX Device", - "features": [] - }, - "configurations": [ - { - "identifier": [ - "SayberX" - ], - "name": "SayberX", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 4 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "X-Ring" - ], - "name": "Sayber X-Ring" - } - ], - "communication": [ - { - "btle": { - "names": [ - "SayberX", - "X-Ring *" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff6-0000-1000-8000-00805f9b34fb", - "rx": "0000fff8-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "muse": { - "defaults": { - "name": "Muse Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 9 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "WB-ZDB-WST" - ], - "name": "Dream Lover Archer 2" - }, - { - "identifier": [ - "WB-TDD" - ], - "name": "Galaku Panty Vib" - } - ], - "communication": [ - { - "btle": { - "names": [ - "WB-ZDB-WST", - "WB-TDD" - ], - "services": { - "0000aaa0-0000-1000-8000-00805f9b34fb": { - "tx": "0000aaa1-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "lelo-f1s": { - "defaults": { - "name": "Lelo F1s", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "F1s" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff1-0000-1000-8000-00805f9b34fb", - "rx": "00000aa4-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "lelo-f1sv2": { - "defaults": { - "name": "Lelo F1s V2", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "F1SV2A", - "F1SV2X" - ], - "name": "Lelo F1s V2" - }, - { - "identifier": [ - "F1SV3" - ], - "name": "Lelo F1s V3" - } - ], - "communication": [ - { - "btle": { - "names": [ - "F1SV2A", - "F1SV2X", - "F1SV3" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff1-0000-1000-8000-00805f9b34fb", - "whitelist": "00000a10-0000-1000-8000-00805f9b34fb", - "rx": "00000a04-0000-1000-8000-00805f9b34fb", - "txvibrate": "0000fff2-0000-1000-8000-00805f9b34fb", - "generic0": "00000a11-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "lelo-harmony": { - "defaults": { - "name": "Lelo Tiani Harmony", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "IdaWave", - "Ida Wave" - ], - "name": "Lelo Ida Wave", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "TOR3" - ], - "name": "Lelo Tor 3", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Hugo2" - ], - "name": "Lelo Hugo 2" - }, - { - "identifier": [ - "DoubleSonic" - ], - "name": "Lelo Enigma Double Sonic", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "GIGI3" - ], - "name": "Lelo Gigi 3", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "LIV3" - ], - "name": "Lelo Liv 3", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "IdaWave", - "Ida Wave", - "TianiHarmony", - "Tiani Harmony", - "TOR3", - "Hugo2", - "DoubleSonic", - "GIGI3", - "LIV3" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "command": "0000fff1-0000-1000-8000-00805f9b34fb", - "tx": "0000fff2-0000-1000-8000-00805f9b34fb", - "whitelist": "00000a11-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "aneros": { - "defaults": { - "name": "Aneros Vivi", - "features": [ - { - "feature-type": "Vibrate", - "description": "Perineum Vibrator", - "actuator": { - "step-range": [ - 0, - 127 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Internal Vibrator", - "actuator": { - "step-range": [ - 0, - 127 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "Massage Demo" - ], - "services": { - "0000ff00-0000-1000-8000-00805f9b34fb": { - "tx": "0000ff01-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "lovehoney-desire": { - "defaults": { - "name": "Lovehoney Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 127 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 127 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "PROSTATE VIBE" - ], - "name": "Lovehoney Desire Prostate Vibrator" - }, - { - "identifier": [ - "KNICKER VIBE" - ], - "name": "Lovehoney Desire Knicker Vibrator", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 127 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "LOVE EGG" - ], - "name": "Lovehoney Desire Love Egg", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 127 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "PROSTATE VIBE", - "KNICKER VIBE", - "LOVE EGG" - ], - "services": { - "0000ff00-0000-1000-8000-00805f9b34fb": { - "tx": "0000ff01-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "twerkingbutt": { - "defaults": { - "name": "Twerking Butt", - "features": [] - }, - "communication": [ - { - "btle": { - "names": [ - "BODIKANG", - "Twerking Butt", - "TwerkingButt" - ], - "services": { - "00000a60-0000-1000-8000-00805f9b34fb": { - "tx": "00000a66-0000-1000-8000-00805f9b34fb", - "rx": "00000a67-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "maxpro": { - "defaults": { - "name": "MaxPro 2", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "M2" - ], - "services": { - "6e400001-b5a3-f393-e0a9-e50e24dcca9e": { - "tx": "6e400002-b5a3-f393-e0a9-e50e24dcca9e" - } - } - } - } - ] - }, - "nobra": { - "defaults": { - "name": "Nobra's Silicone Dreams Toy", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 15 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "NobraControl*" - ], - "services": { - "0000abf0-0000-1000-8000-00805f9b34fb": { - "tx": "0000abf1-0000-1000-8000-00805f9b34fb" - } - } - } - }, - { - "serial": { - "port": "default", - "baud-rate": 19200, - "data-bits": 8, - "parity": "N", - "stop-bits": 1 - } - } - ] - }, - "thehandy": { - "defaults": { - "name": "The Handy", - "features": [ - { - "feature-type": "Position", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "LinearCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "The Handy" - ], - "services": { - "1775244d-6b43-439b-877c-060f2d9bed07": { - "firmware": "1775ff51-6b43-439b-877c-060f2d9bed07", - "tx": "1775ff55-6b43-439b-877c-060f2d9bed07" - } - } - } - } - ] - }, - "cachito": { - "defaults": { - "name": "Cachito Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 5 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "CCTSK" - ], - "name": "Cachito Lure Tao" - }, - { - "identifier": [ - "CCTXueGao" - ], - "name": "Cachito Ice Cream" - } - ], - "communication": [ - { - "btle": { - "names": [ - "CCTSK", - "CCTXueGao" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff2-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "jejoue": { - "defaults": { - "name": "Je Joue Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 5 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 5 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "Je Joue" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff1-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "lovenuts": { - "defaults": { - "name": "Love Nut", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 15 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "Love_Nuts" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff1-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "patoo": { - "defaults": { - "name": "Patoo Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "PTVEA" - ], - "name": "Patoo Carrot" - }, - { - "identifier": [ - "PCS" - ], - "name": "Patoo Vibrator" - }, - { - "identifier": [ - "PHT" - ], - "name": "Patoo Bean Sprout" - }, - { - "identifier": [ - "PBT" - ], - "name": "Patoo Devil", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "PTVEA*", - "PBT*", - "PCS*", - "PHT*" - ], - "services": { - "f000aa64-0451-4000-b000-000000000000": { - "txmode": "f000aa65-0451-4000-b000-000000000000", - "tx": "f000aa68-0451-4000-b000-000000000000" - } - } - } - } - ] - }, - "tcode-v03": { - "defaults": { - "name": "TCode v0.3 (Single Linear Axis)", - "features": [ - { - "feature-type": "Position", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "LinearCmd" - ] - } - } - ] - }, - "communication": [ - { - "serial": { - "port": "default", - "baud-rate": 115200, - "data-bits": 8, - "parity": "N", - "stop-bits": 1 - } - } - ] - }, - "fredorch": { - "defaults": { - "name": "Fredorch Device", - "features": [ - { - "feature-type": "Position", - "actuator": { - "step-range": [ - 0, - 150 - ], - "messages": [ - "LinearCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "YXlinksSPP" - ], - "services": { - "0000ffb0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffb1-0000-1000-8000-00805f9b34fb", - "rx": "0000ffb2-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "fredorch-rotary": { - "defaults": { - "name": "Fredorch Rotary Device", - "features": [ - { - "feature-type": "Oscillate", - "description": "Fucking Machine Oscillation Speed", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "M1_*" - ], - "services": { - "0000ae10-0000-1000-8000-00805f9b34fb": { - "tx": "0000ae01-0000-1000-8000-00805f9b34fb", - "rx": "0000ae02-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "mizzzee": { - "defaults": { - "name": "Mizz Zee Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 68 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "NFY008" - ], - "services": { - "0000eea0-0000-1000-8000-00805f9b34fb": { - "tx": "0000eea1-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "mizzzee-v2": { - "defaults": { - "name": "Mizz Zee Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 68 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "XHT" - ], - "services": { - "0000eea0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ee01-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "mizzzee-v3": { - "defaults": { - "name": "Mizz Zee Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 1000 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "XHTKJ" - ], - "services": { - "0000ff10-0000-1000-8000-00805f9b34fb": { - "tx": "0000ff12-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "htk_bm": { - "defaults": { - "name": "HTK Breast Massager", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 1 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 1 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "HTK-BLE-BM001" - ], - "services": { - "0000180f-0000-1000-8000-00805f9b34fb": { - "rxblebattery": "00002a19-0000-1000-8000-00805f9b34fb" - }, - "00001802-0000-1000-8000-00805f9b34fb": { - "tx": "00002a06-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "ankni": { - "defaults": { - "name": "Roselex Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 3 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "DSJM" - ], - "services": { - "0000fe00-0000-1000-8000-00805f9b34fb": { - "tx": "0000fe01-0000-1000-8000-00805f9b34fb" - }, - "0000fffe-0000-1000-8000-00805f9b34fb": { - "tx": "0000fe02-0000-1000-8000-00805f9b34fb" - }, - "0000180a-0000-1000-8000-00805f9b34fb": { - "generic0": "00002a50-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "hgod": { - "defaults": { - "name": "Hgod Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 10 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "AMN NEO" - ], - "services": { - "0000ffe3-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb", - "rx": "0000ffe2-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "lovedistance": { - "defaults": { - "name": "Love Distance Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 121 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "REACH G" - ], - "name": "Love Distance Reach G" - }, - { - "identifier": [ - "REACH" - ], - "name": "Love Distance Reach" - }, - { - "identifier": [ - "MAG" - ], - "name": "Love Distance Mag" - }, - { - "identifier": [ - "SPAN" - ], - "name": "Love Distance Span" - }, - { - "identifier": [ - "RANGE" - ], - "name": "Love Distance Range" - }, - { - "identifier": [ - "ORBIT" - ], - "name": "Love Distance Range" - }, - { - "identifier": [ - "JOIN G" - ], - "name": "Love Distance Join G" - }, - { - "identifier": [ - "LINK" - ], - "name": "Love Distance Link" - }, - { - "identifier": [ - "GRASP" - ], - "name": "Love Distance Grasp" - }, - { - "identifier": [ - "RECEIVE" - ], - "name": "Love Distance Receive" - } - ], - "communication": [ - { - "btle": { - "names": [ - "REACH G", - "REACH", - "MAG", - "SPAN", - "RANGE", - "ORBIT", - "JOIN G", - "LINK", - "GRASP", - "RECEIVE" - ], - "services": { - "0000ff00-0000-1000-8000-00805f9b34fb": { - "tx": "0000ff01-0000-1000-8000-00805f9b34fb", - "rx": "0000ff02-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "satisfyer": { - "defaults": { - "name": "Satisfyer Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "10005" - ], - "name": "Satisfyer Hot Spot", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10006" - ], - "name": "Satisfyer Heated Affair", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10007" - ], - "name": "Satisfyer Big Heat" - }, - { - "identifier": [ - "10008" - ], - "name": "Satisfyer Heated Thrill", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10009" - ], - "name": "Satisfyer Hot Bunny", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10010" - ], - "name": "Satisfyer Heat Climax", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10011" - ], - "name": "Satisfyer Heat Climax+", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10012" - ], - "name": "Satisfyer Hot Passion", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10013" - ], - "name": "Satisfyer Haute Couture+", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10014" - ], - "name": "Satisfyer High Fashion+", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10015" - ], - "name": "Satisfyer Prêt-à-porter+", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10024", - "10025" - ], - "name": "Satisfyer Love Triangle", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10027", - "10028" - ], - "name": "Satisfyer Curvy 1+", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10030", - "10031" - ], - "name": "Satisfyer Curvy 2+", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10032" - ], - "name": "Satisfyer Double Wand-er" - }, - { - "identifier": [ - "10046", - "10047", - "10048" - ], - "name": "Satisfyer Double Joy", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10049", - "10050", - "10051" - ], - "name": "Satisfyer Double Fun", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10052", - "10053", - "10054" - ], - "name": "Satisfyer Double Love", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10055" - ], - "name": "Satisfyer Curvy 3+", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10059", - "10060", - "10061" - ], - "name": "Satisfyer Hot Lover", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10062", - "10063", - "10064" - ], - "name": "Satisfyer Mono Flex", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10065", - "10066", - "10067", - "10068" - ], - "name": "Satisfyer Double Flex", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10069", - "10070", - "10071" - ], - "name": "Satisfyer Heat Wave", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10072" - ], - "name": "Satisfyer Little Secret" - }, - { - "identifier": [ - "10073" - ], - "name": "Satisfyer Sexy Secret" - }, - { - "identifier": [ - "10074" - ], - "name": "Satisfyer Strong One" - }, - { - "identifier": [ - "10075" - ], - "name": "Satisfyer Mighty One" - }, - { - "identifier": [ - "10076" - ], - "name": "Satisfyer Powerful One" - }, - { - "identifier": [ - "10077" - ], - "name": "Satisfyer Royal One" - }, - { - "identifier": [ - "10078" - ], - "name": "Satisfyer Signet Ring" - }, - { - "identifier": [ - "10079", - "10080" - ], - "name": "Satisfyer Dual Love", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10081", - "10082" - ], - "name": "Satisfyer Dual Pleasure", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10090" - ], - "name": "Satisfyer Hero+", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10091" - ], - "name": "Satisfyer Knight+", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10092", - "10093" - ], - "name": "Satisfyer Newcomer+", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10100", - "10101" - ], - "name": "Satisfyer Plug-ilicious 1", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10102", - "10103", - "10104" - ], - "name": "Satisfyer Plug-ilicious 2", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10105" - ], - "name": "Satisfyer E-Love Foreplay", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10108" - ], - "name": "Satisfyer E-Love G-Hunter", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10109" - ], - "name": "Satisfyer E-Love G-Hunter+", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10110" - ], - "name": "Satisfyer E-Love G-Spotter", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10111" - ], - "name": "Satisfyer E-Love G-Spotter+", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10112" - ], - "name": "Satisfyer E-Love Story", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10119", - "10120", - "10182" - ], - "name": "Satisfyer Love Birds 1" - }, - { - "identifier": [ - "10121", - "10122", - "10123" - ], - "name": "Satisfyer Love Birds 2" - }, - { - "identifier": [ - "10124", - "10125", - "10126" - ], - "name": "Satisfyer Love Birds Vary" - }, - { - "identifier": [ - "10127", - "10128", - "10129", - "10201" - ], - "name": "Satisfyer Ribbed Petal" - }, - { - "identifier": [ - "10130", - "10131", - "10132", - "10133" - ], - "name": "Satisfyer Shiny Petal" - }, - { - "identifier": [ - "10134", - "10135", - "10136", - "10202" - ], - "name": "Satisfyer Smooth Petal" - }, - { - "identifier": [ - "10140" - ], - "name": "Satisfyer Men Vibration+", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10141" - ], - "name": "Satisfyer Power Plug" - }, - { - "identifier": [ - "10142", - "10143" - ], - "name": "Satisfyer Rotator Plug 1+", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10144", - "10145" - ], - "name": "Satisfyer Rotator Plug 2+", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10146", - "10147" - ], - "name": "Satisfyer Deep Diver" - }, - { - "identifier": [ - "10148", - "10149" - ], - "name": "Satisfyer Sweet Seal" - }, - { - "identifier": [ - "10150", - "10151" - ], - "name": "Satisfyer Trendsetter" - }, - { - "identifier": [ - "10154", - "10155", - "10156" - ], - "name": "Satisfyer Twirling Joy" - }, - { - "identifier": [ - "10157", - "10158" - ], - "name": "Satisfyer Ultra Power Bullet 8" - }, - { - "identifier": [ - "10160", - "10161", - "10162" - ], - "name": "Satisfyer Double Desire", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10163", - "10164", - "10165", - "10166" - ], - "name": "Satisfyer Double Lust", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10167" - ], - "name": "Satisfyer Epic Duo" - }, - { - "identifier": [ - "10168" - ], - "name": "Satisfyer Pleasure Wand+" - }, - { - "identifier": [ - "10169", - "10170", - "10171" - ], - "name": "Satisfyer Top Secret", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10172", - "10173", - "10174" - ], - "name": "Satisfyer Top Secret+", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10175", - "10176" - ], - "name": "Satisfyer Bullseye" - }, - { - "identifier": [ - "10177", - "10178", - "10179" - ], - "name": "Satisfyer Sunray", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10180", - "10181" - ], - "name": "Satisfyer Curvy Trinity 5+", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10183", - "10184" - ], - "name": "Satisfyer Intensity Plug" - }, - { - "identifier": [ - "10185" - ], - "name": "Satisfyer Power Masturbator" - }, - { - "identifier": [ - "10186", - "10187" - ], - "name": "Satisfyer Hug me", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10188" - ], - "name": "Satisfyer Air Pump Bunny 5+", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10189" - ], - "name": "Satisfyer Air Pump Vibrator 5+", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10190", - "10191" - ], - "name": "Satisfyer Threesome 4", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10192" - ], - "name": "Satisfyer G-Spot Flex 4+" - }, - { - "identifier": [ - "10193", - "10194" - ], - "name": "Satisfyer G-Spot Flex 5+" - }, - { - "identifier": [ - "10195" - ], - "name": "Satisfyer Air Pump Booty 5+", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10196" - ], - "name": "Satisfyer Pro+ Wave 4", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10197", - "10198" - ], - "name": "Satisfyer Mini Wand-er+", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10199", - "10200" - ], - "name": "Satisfyer Tropical Tip" - }, - { - "identifier": [ - "10203", - "10204" - ], - "name": "Satisfyer Twirling Pro+", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10205" - ], - "name": "Satisfyer Perfect Pair 4" - }, - { - "identifier": [ - "10206", - "10207", - "10208" - ], - "name": "Satisfyer Booty Absolute Beginners 5" - }, - { - "identifier": [ - "10241", - "10242" - ], - "name": "Satisfyer Rrrolling Sensation", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "10307", - "10308", - "10309" - ], - "name": "Satisfyer Pro 2 Gen 3", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "SF *" - ], - "manufacturer-data": [ - { - "company": 93, - "data": [ - 0, - 0, - 39 - ] - }, - { - "company": 93, - "data": [ - 0, - 0, - 40 - ] - } - ], - "services": { - "0000180a-0000-1000-8000-00805f9b34fb": { - "rxblemodel": "00002a24-0000-1000-8000-00805f9b34fb" - }, - "51361500-c5e7-47c7-8a6e-47ebc99d80e8": { - "command": "51361501-c5e7-47c7-8a6e-47ebc99d80e8", - "tx": "51361502-c5e7-47c7-8a6e-47ebc99d80e8" - } - } - } - } - ] - }, - "mannuo": { - "defaults": { - "name": "ManNuo Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 3 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "Sex toys", - "Sex Toys", - "LXCDVP", - "MANO PRODUCT" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff1-0000-1000-8000-00805f9b34fb", - "rx": "0000fff4-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "kgoal-boost": { - "defaults": { - "name": "KGoal Boost", - "features": [ - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "Boost" - ], - "services": { - "0000180f-0000-1000-8000-00805f9b34fb": { - "rxblebattery": "00002a19-0000-1000-8000-00805f9b34fb" - }, - "8e7c6065-7656-17ad-1b41-b53d1a548e0d": { - "rxpressure": "10c2be2d-d2d5-b7a8-5f42-e2468c9ebbf5" - } - } - } - } - ] - }, - "meese": { - "defaults": { - "name": "Meese Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 10 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 3 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "Meese-V389" - ], - "name": "Meese Tera" - }, - { - "identifier": [ - "Meese-cd" - ], - "name": "Meese Modo", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 10 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "Meese-V389", - "Meese-cd" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "hismith": { - "defaults": { - "name": "Hismith device", - "features": [ - { - "feature-type": "Oscillate", - "description": "Fucking Machine Oscillation Speed", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "1001" - ], - "name": "Hismith Sex Machine" - }, - { - "identifier": [ - "1002" - ], - "name": "Hismith Pro Traveler" - }, - { - "identifier": [ - "1003" - ], - "name": "Hismith Capsule" - }, - { - "identifier": [ - "2001" - ], - "name": "Hismith Thrusting Cup", - "features": [ - { - "feature-type": "Oscillate", - "description": "Stroker Oscillation Speed", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 1 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "1006" - ], - "name": "Hismith G011", - "features": [ - { - "feature-type": "Oscillate", - "description": "Stroker Oscillation Speed", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 1 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "3001" - ], - "name": "Wildolo Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "HISMITH", - "Wildolo", - "\u0007HISMITH" - ], - "services": { - "0000ffe5-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe9-0000-1000-8000-00805f9b34fb" - }, - "0000ff90-0000-1000-8000-00805f9b34fb": { - "rxblemodel": "0000ff96-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "hismith-mini": { - "defaults": { - "name": "Hismith Mini device", - "features": [ - { - "feature-type": "Oscillate", - "description": "Fucking Machine Oscillation Speed", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "4001" - ], - "name": "Auxfun Sex Machine" - }, - { - "identifier": [ - "1005", - "1102" - ], - "name": "Hismith Sex Machine" - }, - { - "identifier": [ - "1004" - ], - "name": "Hismith Mini Sex Machine" - }, - { - "identifier": [ - "1101" - ], - "name": "Hismith Servo Sex Machine" - }, - { - "identifier": [ - "1402" - ], - "name": "Hismith Ukulele" - }, - { - "identifier": [ - "1501" - ], - "name": "Hismith PleasureDrive" - }, - { - "identifier": [ - "2201" - ], - "name": "Sinloli Automatic Sex Doll", - "features": [ - { - "feature-type": "Constrict", - "description": "Air Pump", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrator", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "3101" - ], - "name": "Eropair Rabbit Vibrator", - "features": [ - { - "feature-type": "Vibrate", - "description": "Internal Vibrator", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "External Vibrator", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "3102" - ], - "name": "Eropair Thrusting Vibrating Dildo", - "features": [ - { - "feature-type": "Oscillate", - "description": "Thruster", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrator", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "2101" - ], - "name": "Eropair Cup", - "features": [ - { - "feature-type": "Constrict", - "description": "Air Pump", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrator", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "2204" - ], - "name": "Sinloli Cosima", - "features": [ - { - "feature-type": "Oscillate", - "description": "Stroker Oscillation Speed", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Constrict", - "description": "Air Pump", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "2202" - ], - "name": "Sinloli Ethel", - "features": [ - { - "feature-type": "Oscillate", - "description": "Stroker Oscillation Speed", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrator", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "2205" - ], - "name": "Sinloli Aston" - } - ], - "communication": [ - { - "btle": { - "names": [ - "Auxfun-Box", - "Sinloli", - "Sinloli-Sherry", - "Eropair *", - "HISMITH S1", - "HISMITH S2", - "HISMITH S3", - "Sinloli Cosima", - "Sinloli-Ethel", - "Sinloli Aston" - ], - "services": { - "0000ffe5-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe9-0000-1000-8000-00805f9b34fb" - }, - "0000ff90-0000-1000-8000-00805f9b34fb": { - "rxblemodel": "0000ff96-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "wetoy": { - "defaults": { - "name": "WeToy MiNa", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 3 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "WeToy" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff3-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "pink_punch": { - "defaults": { - "name": "Pink Punch Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "Pink_Punch" - ], - "name": "Pink Punch Sunset Mushroom" - }, - { - "identifier": [ - "PinkPunch_Peachu" - ], - "name": "Pink Punch Peachu" - }, - { - "identifier": [ - "PinkPunch_DreamBunny" - ], - "name": "Pink Punch Dream Bunny" - } - ], - "communication": [ - { - "btle": { - "names": [ - "Pink_Punch", - "PinkPunch_Peachu", - "PinkPunch_DreamBunny" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "sakuraneko": { - "defaults": { - "name": "Sakuraneko Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "sakuraneko-01" - ], - "name": "Sakuraneko Korokoro" - }, - { - "identifier": [ - "sakuraneko-02" - ], - "name": "Sakuraneko Nukunuku" - }, - { - "identifier": [ - "sakuraneko-03" - ], - "name": "Sakuraneko Dokidoki" - }, - { - "identifier": [ - "sakuraneko-04" - ], - "name": "Sakuraneko Koikoi", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "sakuraneko-01", - "sakuraneko-02", - "sakuraneko-03", - "sakuraneko-04" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "synchro": { - "defaults": { - "name": "Synchro", - "features": [ - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 6 - ], - "messages": [ - "RotateCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "synchro EX" - ], - "name": "Synchro Exchange" - } - ], - "communication": [ - { - "btle": { - "names": [ - "Shinkuro", - "synchro2", - "synchro EX" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "tryfun": { - "defaults": { - "name": "TryFun Yuan Series", - "features": [ - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 9 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 9 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "TF-SPRAY" - ], - "name": "TryFun Surge Pro", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 4 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "TRYFUN-ONE", - "TF-SPRAY" - ], - "services": { - "0000ff10-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff1-0000-1000-8000-00805f9b34fb" - }, - "0000ffac-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffb5-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "tryfun-meta2": { - "defaults": { - "name": "TryFun Meta 2", - "features": [ - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "RotateCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "TF-META2" - ], - "services": { - "0000ffac-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffb7-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "tryfun-blackhole": { - "defaults": { - "name": "TryFun Black Hole Plus", - "features": [ - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "TF-BHPLUS" - ], - "services": { - "0000ffac-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffb7-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "metaxsire": { - "defaults": { - "name": "metaXsire Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "Rex" - ], - "name": "metaXsire Rex" - }, - { - "identifier": [ - "Cali", - "LY165A01" - ], - "name": "metaXsire Cali", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Constrict", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "Olis" - ], - "name": "metaXsire Olis", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "LY213A01" - ], - "name": "metaXsire BuCUE", - "features": [ - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "Rex", - "Cali", - "LY165A01", - "Olis", - "LY213A01" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb", - "rx": "0000ffe2-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "metaxsire-repeat": { - "defaults": { - "name": "Cooxer Bullet Vibe", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "LY199B01" - ], - "name": "Cooxer Bullet Vibe" - }, - { - "identifier": [ - "LY234A01" - ], - "name": "metaXsire Tadpole" - }, - { - "identifier": [ - "LY271A01" - ], - "name": "metaXsire Upton" - }, - { - "identifier": [ - "LY270A01" - ], - "name": "metaXsire Una" - } - ], - "communication": [ - { - "btle": { - "names": [ - "LY199B01", - "LY234A01", - "LY271A01", - "LY270A01" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb", - "rx": "0000ffe2-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "metaxsire-v2": { - "defaults": { - "name": "metaXsire Nolan", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "LB-W01" - ], - "name": "Libo Miao", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "HH010" - ], - "name": "metaXsire HH010", - "features": [ - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "LY272A01", - "LB-W01", - "HH010" - ], - "services": { - "0000bae0-0000-1000-8000-00805f9b34fb": { - "tx": "0000bae1-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "metaxsire-v3": { - "defaults": { - "name": "metaXsire Tay", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "TAY001" - ], - "name": "metaXsire Tay 1" - }, - { - "identifier": [ - "TAY009" - ], - "name": "metaXsire Tay 9" - }, - { - "identifier": [ - "TAY006" - ], - "name": "metaXsire Tay 6" - }, - { - "identifier": [ - "TA-S001A" - ], - "name": "metaXsire Zeus", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 20 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "TAY001", - "TAY006", - "TAY009", - "TA-S001A" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fe02-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "metaxsire-v4": { - "defaults": { - "name": "metaXsire G1 Vibrator", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 99 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "CFG1 vibrator" - ], - "services": { - "0000cfa2-0000-1000-8000-00805f9b34fb": { - "tx": "0000cf21-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "sexverse-lg389": { - "defaults": { - "name": "Sexverse LG389", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 3 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 10 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "LG389" - ], - "services": { - "0000bae0-0000-1000-8000-00805f9b34fb": { - "tx": "0000bae1-0000-1000-8000-00805f9b34fb", - "rx": "0000bae2-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "cowgirl": { - "defaults": { - "name": "The Cowgirl Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "THE COWGIRL" - ], - "name": "The Cowgirl" - }, - { - "identifier": [ - "THE UNICORN" - ], - "name": "The Unicorn" - } - ], - "communication": [ - { - "btle": { - "names": [ - "THE COWGIRL", - "THE UNICORN" - ], - "services": { - "0000fe00-0000-1000-8000-00805f9b34fb": { - "tx": "0000fe01-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "cowgirl-cone": { - "defaults": { - "name": "The Cowgirl Cone", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 128 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "CG-CONE" - ], - "name": "The Cowgirl Cone" - } - ], - "communication": [ - { - "btle": { - "names": [ - "CG-CONE" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "galaku-pump": { - "defaults": { - "name": "Galaku Device", - "features": [ - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "V415" - ], - "name": "Galaku Nebula" - } - ], - "communication": [ - { - "btle": { - "names": [ - "V415" - ], - "services": { - "00001000-0000-1000-8000-00805f9b34fb": { - "tx": "00001001-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "galaku": { - "defaults": { - "name": "Galaku Device", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "V415" - ], - "name": "Galaku Nebula" - }, - { - "identifier": [ - "GX85" - ], - "name": "Galaku Shana" - }, - { - "identifier": [ - "GX07" - ], - "name": "Galaku Miya" - }, - { - "identifier": [ - "GX17" - ], - "name": "Galaku Capsule lipstick" - }, - { - "identifier": [ - "GX21" - ], - "name": "Galaku Vitality Cat" - }, - { - "identifier": [ - "GX22" - ], - "name": "Galaku Phantom X" - }, - { - "identifier": [ - "GX16" - ], - "name": "Galaku Vitality Strawberry" - }, - { - "identifier": [ - "GX29" - ], - "name": "Galaku Little Magic Box" - }, - { - "identifier": [ - "GX23" - ], - "name": "Galaku Little Whale" - }, - { - "identifier": [ - "GX25" - ], - "name": "Galaku Happy Vibrator" - }, - { - "identifier": [ - "GX26" - ], - "name": "Galaku Xiaobao Beans" - }, - { - "identifier": [ - "GK03" - ], - "name": "Galaku Capsule Vibrator" - }, - { - "identifier": [ - "GX39" - ], - "name": "Galaku Ice cone miniAV stick" - }, - { - "identifier": [ - "G321" - ], - "name": "Galaku mini ice cream cone" - }, - { - "identifier": [ - "G304" - ], - "name": "Galaku Shia's Collar" - }, - { - "identifier": [ - "G336" - ], - "name": "Galaku The Second Generation of Vitality Bird" - }, - { - "identifier": [ - "G331" - ], - "name": "Galaku Octopus glans massager" - }, - { - "identifier": [ - "G326" - ], - "name": "Galaku Alice" - }, - { - "identifier": [ - "G335" - ], - "name": "Galaku Unicorn Butt Plug" - }, - { - "identifier": [ - "G341" - ], - "name": "Galaku Ace" - }, - { - "identifier": [ - "G355" - ], - "name": "Galaku Little cute turtle" - }, - { - "identifier": [ - "G349" - ], - "name": "Galaku Little Bullet" - }, - { - "identifier": [ - "G407" - ], - "name": "Galaku Joy Vibrator" - }, - { - "identifier": [ - "G204" - ], - "name": "Galaku Bowling" - }, - { - "identifier": [ - "G171" - ], - "name": "Galaku Mixin Controller" - }, - { - "identifier": [ - "G12D" - ], - "name": "Galaku Hua Chao Brush" - }, - { - "identifier": [ - "G123" - ], - "name": "Galaku 花sai" - }, - { - "identifier": [ - "G23A" - ], - "name": "Galaku Dream Vibration" - }, - { - "identifier": [ - "G336" - ], - "name": "Galaku The Second Generation of Vitality Bird" - }, - { - "identifier": [ - "G23A" - ], - "name": "Galaku Dream Vibration" - }, - { - "identifier": [ - "A073" - ], - "name": "Galaku Joy Vibrator" - }, - { - "identifier": [ - "GLMT" - ], - "name": "Galaku Rogue Rabbit" - }, - { - "identifier": [ - "G901" - ], - "name": "Galaku Suck the vibrator" - }, - { - "identifier": [ - "G912" - ], - "name": "Galaku Donut" - }, - { - "identifier": [ - "G901" - ], - "name": "Galaku Suck the vibrator" - }, - { - "identifier": [ - "G20B" - ], - "name": "Galaku Ballet Vibrator" - }, - { - "identifier": [ - "K112" - ], - "name": "Galaku Donut" - }, - { - "identifier": [ - "G202" - ], - "name": "Galaku Flirting Pen" - }, - { - "identifier": [ - "K118" - ], - "name": "Galaku Ball vibrator" - }, - { - "identifier": [ - "K107" - ], - "name": "Galaku Cyberpunk Airplane Cup" - }, - { - "identifier": [ - "G203" - ], - "name": "Galaku Vitality Cute Pet" - }, - { - "identifier": [ - "TXHL" - ], - "name": "Galaku Little gourd vibrating egg" - }, - { - "identifier": [ - "TXMM" - ], - "name": "Galaku little kitten" - }, - { - "identifier": [ - "TXKL" - ], - "name": "Galaku Little Dinosaur" - }, - { - "identifier": [ - "K108" - ], - "name": "Galaku Bell sucking" - }, - { - "identifier": [ - "K109" - ], - "name": "Galaku Ring vibration" - }, - { - "identifier": [ - "KWL2" - ], - "name": "Galaku Erection Booster" - }, - { - "identifier": [ - "TFHL" - ], - "name": "Galaku Gyoyo-G (meaning Yue-little gourd)" - }, - { - "identifier": [ - "TFMM" - ], - "name": "Galaku Gyoyo (meaning joy)" - }, - { - "identifier": [ - "TFKL" - ], - "name": "Galaku Gyoyo (meaning joy)" - }, - { - "identifier": [ - "K120" - ], - "name": "Galaku Pinky stick" - }, - { - "identifier": [ - "K12A" - ], - "name": "Galaku Little Turtle Stick" - }, - { - "identifier": [ - "K12C" - ], - "name": "Galaku Xiao Xian Wan" - }, - { - "identifier": [ - "LL18" - ], - "name": "Galaku Mitang" - }, - { - "identifier": [ - "CYX2" - ], - "name": "Secret Lover Simon" - }, - { - "identifier": [ - "RC31" - ], - "name": "Secret Lover Betty" - }, - { - "identifier": [ - "MD19" - ], - "name": "Secret Lover Kevin" - }, - { - "identifier": [ - "G317" - ], - "name": "Galaku Zaku Aircraft Cup", - "features": [ - { - "feature-type": "Oscillate", - "description": "Oscillate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "G312" - ], - "name": "Galaku Mecha-Original Owner's Aircraft Cup", - "features": [ - { - "feature-type": "Oscillate", - "description": "Oscillate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "G302" - ], - "name": "Galaku Little Devil", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "G320" - ], - "name": "Galaku Athena", - "features": [ - { - "feature-type": "Oscillate", - "description": "Oscillate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "G314" - ], - "name": "Galaku Vitality Octopus II", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "G228" - ], - "name": "Galaku Little Dolphin", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "G315" - ], - "name": "Galaku Unicorn", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "G307" - ], - "name": "Galaku Queen Bee Gun", - "features": [ - { - "feature-type": "Oscillate", - "description": "Oscillate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "K311" - ], - "name": "Galaku Freya", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "G339" - ], - "name": "Galaku Rhino Prostate Massager", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "G354" - ], - "name": "Galaku Double-A Aircraft Cup", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "G12B" - ], - "name": "Galaku Flower Season", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "G29C" - ], - "name": "Galaku Little Rubik's Cube", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "G29D" - ], - "name": "Galaku Small powder cake", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "GKML" - ], - "name": "Galaku Milly", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "G348" - ], - "name": "Galaku Rhinoceros Back Court", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "G913" - ], - "name": "Galaku Unicorn II", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "G213" - ], - "name": "Galaku Phantom", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "TFF1" - ], - "name": "Galaku F1 Aircraft Cup", - "features": [ - { - "feature-type": "Oscillate", - "description": "Oscillate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "G310" - ], - "name": "Galaku Scepter AV Stick", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "K113" - ], - "name": "Galaku Unicorn II", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "G228" - ], - "name": "Galaku Little Dolphin", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "G310" - ], - "name": "Galaku Scepter AV Stick", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "TFF1" - ], - "name": "Galaku F1 Aircraft Cup", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "D358" - ], - "name": "Galaku Classic vibration-absorbing AV state", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "G322" - ], - "name": "Galaku Unicorn", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "D402" - ], - "name": "Galaku New series of vibrators", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "G40A" - ], - "name": "Galaku New series of vibrators", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "G403" - ], - "name": "Galaku New series of vibrators", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "G43A" - ], - "name": "Galaku New series of vibrators", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "K12B" - ], - "name": "Galaku Little Turtle Stick", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "QCVW" - ], - "name": "Kisstoy Lost (Vibrating)", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "QCSW" - ], - "name": "Kisstoy Lost (Sucking)", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "QCPW" - ], - "name": "Kisstoy Lost (Insertable)", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "TFG1" - ], - "name": "Galaku Aurora Aircraft Cup", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Constrict", - "description": "Suction Pump", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "GK27" - ], - "name": "Galaku Cannon-GT", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "GK25" - ], - "name": "Galaku Phantom PLUS", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "AC695X_1(BLE)" - ], - "name": "Galaku Vision", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "GX33" - ], - "name": "Galaku Dimension No. 1", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - }, - { - "identifier": [ - "WSXK" - ], - "name": "Galaku Starry Sky CUP", - "features": [ - { - "feature-type": "Vibrate", - "description": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "GX85", - "GX07", - "GX17", - "GX21", - "GX22", - "GX16", - "GX29", - "GX23", - "GX25", - "GX26", - "GK03", - "GX39", - "G321", - "G304", - "G336", - "G331", - "G326", - "G335", - "G341", - "G355", - "G349", - "G407", - "G204", - "G171", - "G12D", - "G123", - "G23A", - "G336", - "G23A", - "A073", - "GLMT", - "G901", - "G912", - "G901", - "G20B", - "K112", - "G202", - "K118", - "K107", - "G203", - "TXHL", - "TXMM", - "TXKL", - "K108", - "K109", - "KWL2", - "TFHL", - "TFMM", - "TFKL", - "K120", - "K12A", - "K12C", - "LL18", - "CYX2", - "RC31", - "MD19", - "G317", - "G312", - "G302", - "G320", - "G314", - "G228", - "G315", - "G307", - "K311", - "G339", - "G354", - "G12B", - "G29C", - "G29D", - "GKML", - "G348", - "G913", - "G213", - "TFF1", - "G310", - "K113", - "G228", - "G310", - "TFF1", - "D358", - "G322", - "D402", - "G40A", - "G403", - "G43A", - "K12B", - "QCVW", - "QCSW", - "QCPW", - "TFG1", - "GK27", - "GK25", - "AC695X_1(BLE)", - "GX33", - "WSXK" - ], - "services": { - "00001000-0000-1000-8000-00805f9b34fb": { - "tx": "00001001-0000-1000-8000-00805f9b34fb", - "rxblebattery": "00001002-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "xibao": { - "defaults": { - "name": "Xibao Smart Masturbation Cup", - "features": [ - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 99 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "CCYB_*" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff2-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "sensee": { - "defaults": { - "name": "Sensee Diandou Rabbit", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "CTY222S4" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff5-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "sensee-v2": { - "defaults": { - "name": "Sensee Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Constrict", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "CCPA10S2" - ], - "name": "Sensee Capsule" - }, - { - "identifier": [ - "CCPA18S5" - ], - "name": "Sensee Astronaut" - }, - { - "identifier": [ - "Easylive NO8 Cup" - ], - "name": "Sensee No8", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "CCP322S5" - ], - "name": "Easylive Vader", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "CTY508S5" - ], - "name": "Sensee Voice-Interactive Female Vibrator", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "PTYB22S2" - ], - "name": "Sensee Moonlight", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Constrict", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "CTY823S5" - ], - "name": "Sensee Little Seahorse", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Constrict", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "CTY916S4" - ], - "name": "Sensee Dream Stick", - "features": [ - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "CCPA10S2", - "CCPA18S5", - "Easylive NO8 Cup", - "CTY508S5", - "CTY916S4", - "PTYB22S2", - "CCP322S5", - "CTY823S5" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff5-0000-1000-8000-00805f9b34fb", - "rx": "0000fff4-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "fox": { - "defaults": { - "name": "Fox Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 3 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "FOX", - "FOX M70 Pro", - "FoxM70Pro", - "FOX M70-2" - ], - "services": { - "0000ae00-0000-1000-8000-00805f9b34fb": { - "tx": "0000ae01-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "kizuna": { - "defaults": { - "name": "Kizuna Smart", - "features": [ - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 9 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "serial": { - "port": "default", - "baud-rate": 19200, - "data-bits": 8, - "parity": "N", - "stop-bits": 1 - } - } - ] - }, - "xiuxiuda": { - "defaults": { - "name": "Xiuxiuda Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 19 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "XXD-Lush*" - ], - "services": { - "53300001-0023-4bd4-bbd5-a6920e4c5653": { - "tx": "53300003-0023-4bd4-bbd5-a6920e4c5653" - } - } - } - } - ] - }, - "longlosttouch": { - "defaults": { - "name": "Long Lost Touch Possible Kiss", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "RS-KNW" - ], - "services": { - "0000cb60-0000-1000-8000-00805f9b34fb": { - "tx": "0000cb61-0000-1000-8000-00805f9b34fb", - "rx": "0000cb62-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "adrienlastic": { - "defaults": { - "name": "Adrien Lastic Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 16 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "LVS-S001" - ], - "name": "Adrien Lastic Palpitation" - }, - { - "identifier": [ - "LVS-S002" - ], - "name": "Adrien Lastic Revelation" - } - ], - "communication": [ - { - "btle": { - "names": [ - "Placeholder to avoid conflict with bad attempt to clone a Lovense Lush" - ], - "advertised-services": [ - "00001320-0000-1000-8000-00805f9b34fb" - ], - "services": { - "6e400001-b5a3-f393-e0a9-e50e24dcca9e": { - "tx": "6e400002-b5a3-f393-e0a9-e50e24dcca9e" - } - } - } - } - ] - }, - "nintendo-joycon": { - "defaults": { - "name": "Nintendo Joycon", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 1000 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "hid": { - "pairs": [ - { - "vendor-id": 1406, - "product-id": 8199 - }, - { - "vendor-id": 1406, - "product-id": 8198 - }, - { - "vendor-id": 1406, - "product-id": 8201 - } - ] - } - } - ] - }, - "foreo": { - "defaults": { - "name": "Foreo Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 10 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "FOFO", - "LUNA fofo", - "LUNA FOFO", - "LUNA PLAY SMART" - ], - "name": "Foreo LUNA fofo" - }, - { - "identifier": [ - "LUNA PLAYSMART2", - "LUNA PLAY SMART2", - "LUNA play smart2", - "LUNA play smart 2" - ], - "name": "Foreo LUNA play smart 2" - }, - { - "identifier": [ - "LUNA 3", - "LUNA3" - ], - "name": "Foreo LUNA 3" - }, - { - "identifier": [ - "LUNA3PLUS", - "LUNA3 PLUS", - "LUNA 3 PLUS", - "LUNA 3 plus" - ], - "name": "Foreo LUNA 3 plus" - }, - { - "identifier": [ - "LUNA 3 MEN", - "LUNA3MEN" - ], - "name": "Foreo LUNA 3 men" - }, - { - "identifier": [ - "LUNA MINI3", - "LUNA MINI 3", - "LUNA mini 3" - ], - "name": "Foreo LUNA 3 mini" - }, - { - "identifier": [ - "LUNA4", - "LUNA 4" - ], - "name": "Foreo LUNA 4" - }, - { - "identifier": [ - "LUNA4PLUS", - "LUNA4 PLUS", - "LUNA 4 plus" - ], - "name": "Foreo LUNA 4 plus" - }, - { - "identifier": [ - "LUNA4MEN", - "LUNA 4 MEN", - "LUNA 4 FOR MEN" - ], - "name": "Foreo LUNA 4 men" - }, - { - "identifier": [ - "LUNA MINI4", - "LUNA MINI 4", - "LUNA mini 4", - "LUNA 4 mini" - ], - "name": "Foreo LUNA 4 mini" - }, - { - "identifier": [ - "UFO" - ], - "name": "Foreo UFO" - }, - { - "identifier": [ - "UFO mini", - "UFO MINI", - "UFO MIN" - ], - "name": "Foreo UFO mini" - }, - { - "identifier": [ - "UFO2", - "UFO 2" - ], - "name": "Foreo UFO 2" - }, - { - "identifier": [ - "UFO3" - ], - "name": "Foreo UFO 3" - }, - { - "identifier": [ - "UFO3go" - ], - "name": "Foreo UFO 3 go" - }, - { - "identifier": [ - "UFO3eyes" - ], - "name": "Foreo UFO 3 led" - }, - { - "identifier": [ - "UFO3mini" - ], - "name": "Foreo UFO 3 mini" - }, - { - "identifier": [ - "UFOMINI2", - "UFO mini 2" - ], - "name": "Foreo UFO mini 2" - }, - { - "identifier": [ - "BEAR" - ], - "name": "Foreo BEAR" - }, - { - "identifier": [ - "BEAR_MINI", - "BEAR MINI", - "BEAR mini" - ], - "name": "Foreo BEAR mini" - }, - { - "identifier": [ - "BEAR2", - "BEAR 2" - ], - "name": "Foreo BEAR 2" - }, - { - "identifier": [ - "BEAR2go" - ], - "name": "Foreo BEAR 2 go" - }, - { - "identifier": [ - "BEAR2eyes" - ], - "name": "Foreo BEAR 2 eyes" - }, - { - "identifier": [ - "BEAR2body" - ], - "name": "Foreo BEAR 2 body" - }, - { - "identifier": [ - "KIWI" - ], - "name": "Foreo KIWI" - }, - { - "identifier": [ - "KIWI derma" - ], - "name": "Foreo KIWI derma" - } - ], - "communication": [ - { - "btle": { - "names": [ - "FOFO", - "LUNA fofo", - "LUNA FOFO", - "LUNA PLAY SMART", - "LUNA PLAYSMART2", - "LUNA PLAY SMART2", - "LUNA play smart2", - "LUNA play smart 2", - "LUNA 3", - "LUNA3", - "LUNA3PLUS", - "LUNA3 PLUS", - "LUNA 3 PLUS", - "LUNA 3 plus", - "LUNA 3 MEN", - "LUNA3MEN", - "LUNA MINI3", - "LUNA MINI 3", - "LUNA mini 3", - "LUNA4PLUS", - "LUNA4", - "LUNA 4", - "LUNA4PLUS", - "LUNA4 PLUS", - "LUNA 4 plus", - "LUNA4MEN", - "LUNA 4 MEN", - "LUNA 4 FOR MEN", - "LUNA MINI4", - "LUNA MINI 4", - "LUNA mini 4", - "LUNA 4 mini", - "UFO", - "UFO mini", - "UFO MINI", - "UFO MIN", - "UFO2", - "UFO 2", - "UFOMINI2", - "UFO mini 2", - "UFO3", - "UFO3mini", - "UFO3go", - "UFO3led", - "BEAR", - "BEAR_MINI", - "BEAR MINI", - "BEAR mini", - "BEAR2", - "BEAR 2", - "BEAR2go", - "BEAR2body", - "BEAR2eyes", - "KIWI", - "KIWI derma" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff1-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "monsterpub": { - "defaults": { - "name": "Sistalk MonsterPub Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "MP2_JK_N_P1" - ], - "name": "Sistalk MonsterPub 2 Doctor Whale", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "MP_MW_TL_P2" - ], - "name": "Sistalk MonsterPub Magic Kiss", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "MP2_QC_TL_P1" - ], - "name": "Sistalk MonsterPub 2 Mister Devil", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "MP_BABY_QC_N_P4" - ], - "name": "Sistalk MonsterPub Baby Youth Health", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "MP_MXY_N_P1" - ], - "name": "Sistalk MonsterPub KiniCat" - }, - { - "identifier": [ - "MP1N_QC_TL_P2" - ], - "name": "Sistalk MonsterPub BeatHeart" - }, - { - "identifier": [ - "TDG_LIP_PT2" - ], - "name": "Tracy's Dog Surreal" - }, - { - "identifier": [ - "MP1P_QC_TL_P6" - ], - "name": "Sistalk MonsterPub 1P Mister Devil" - }, - { - "identifier": [ - "MPMB_QC_TL_P2" - ], - "name": "Sistalk MonsterPub Sweet" - }, - { - "identifier": [ - "MPAV_QC_TL_P1" - ], - "name": "Sistalk MonsterPub Amazing" - }, - { - "identifier": [ - "MH_TOR_TL_P5" - ], - "name": "Sistalk MonsterHub Tornado" - }, - { - "identifier": [ - "MP_SUCKBANG_P5" - ], - "name": "Sistalk MonsterPub Pop", - "features": [ - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "TDG_CRAYBIT_PT" - ], - "name": "Tracy's Dog Craybit Pro", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "MonsterPub", - "MonsterHub", - "TracyDog" - ], - "services": { - "00006000-0000-1000-8000-00805f9b34fb": { - "tx": "00006001-0000-1000-8000-00805f9b34fb", - "txmode": "00006002-0000-1000-8000-00805f9b34fb", - "txvibrate": "00006003-0000-1000-8000-00805f9b34fb", - "generic0": "0000600a-0000-1000-8000-00805f9b34fb" - }, - "00006010-0000-1000-8000-00805f9b34fb": { - "rxblemodel": "00006014-0000-1000-8000-00805f9b34fb" - }, - "00008000-0000-1000-8000-00805f9b34fb": { - "rx": "00008001-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "joyhub": { - "defaults": { - "name": "JoyHub Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "JOYHUB-ROSELLA2" - ], - "name": "JoyHub Rosella 2" - }, - { - "identifier": [ - "J-Velocity" - ], - "name": "JoyHub Velocity" - }, - { - "identifier": [ - "J-ElixirEgg" - ], - "name": "JoyHub ElixirEgg" - }, - { - "identifier": [ - "J-RetroGuard" - ], - "name": "JoyHub Retro Guard" - }, - { - "identifier": [ - "J-TrueForm3" - ], - "name": "JoyHub TrueForm 3" - }, - { - "identifier": [ - "J-TrueForm" - ], - "name": "JoyHub TrueForm" - }, - { - "identifier": [ - "J-Rhythmic2" - ], - "name": "JoyHub Rhythmic 2" - }, - { - "identifier": [ - "J-Rhythmic3" - ], - "name": "JoyHub Rhythmic 3" - }, - { - "identifier": [ - "J-Rainbow" - ], - "name": "JoyHub Rainbow" - }, - { - "identifier": [ - "J-BlackBull" - ], - "name": "JoyHub Black Bull" - }, - { - "identifier": [ - "J-Peacock" - ], - "name": "JoyHub Peacock" - }, - { - "identifier": [ - "J-Mace" - ], - "name": "JoyHub Mace" - }, - { - "identifier": [ - "J-Tarian" - ], - "name": "JoyHub Tarian" - }, - { - "identifier": [ - "J-Euphoric" - ], - "name": "JoyHub Euphoric" - }, - { - "identifier": [ - "J-Euphoric3" - ], - "name": "JoyHub Euphoric3" - }, - { - "identifier": [ - "J-Torrian" - ], - "name": "JoyHub Torrian" - }, - { - "identifier": [ - "J-Rayen" - ], - "name": "JoyHub Rayen" - }, - { - "identifier": [ - "J-Mackay" - ], - "name": "JoyHub Mackay" - }, - { - "identifier": [ - "J-Rowdy3" - ], - "name": "JoyHub Rowdy 3" - }, - { - "identifier": [ - "J-Eclipse" - ], - "name": "JoyHub Eclipse" - }, - { - "identifier": [ - "J-Scarlett" - ], - "name": "JoyHub Scarlett" - }, - { - "identifier": [ - "J-Tarik" - ], - "name": "JoyHub Tarik" - }, - { - "identifier": [ - "J-UricaGuard2" - ], - "name": "JoyHub Urica Guard 2" - }, - { - "identifier": [ - "J-Viva" - ], - "name": "JoyHub Viva" - }, - { - "identifier": [ - "J-Ryden" - ], - "name": "JoyHub Ryden" - }, - { - "identifier": [ - "J-Petalwish2" - ], - "name": "JoyHub Petalwish 2", - "features": [ - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-VortexTongue" - ], - "name": "JoyHub Vortex Tongue", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Constrict", - "description": "Air Pump", - "actuator": { - "step-range": [ - 0, - 3 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-VibSiren" - ], - "name": "JoyHub VibSiren", - "features": [ - { - "feature-type": "Vibrate", - "description": "External vibrator", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Internal vibrator", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Mysticolor" - ], - "name": "JoyHub Mysticolor", - "features": [ - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Constrict", - "description": "Air Pump", - "actuator": { - "step-range": [ - 0, - 7 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-VividWings" - ], - "name": "JoyHub Vivid Wings", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Mariner" - ], - "name": "JoyHub Mariner", - "features": [ - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Constrict", - "description": "Air Pump", - "actuator": { - "step-range": [ - 0, - 2 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-MarsLion" - ], - "name": "JoyHub MarsLion", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Constrict", - "description": "Air Pump", - "actuator": { - "step-range": [ - 0, - 5 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Pul" - ], - "name": "JoyHub Pul", - "features": [ - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-ROSELLA3" - ], - "name": "JoyHub Rose Love", - "features": [ - { - "feature-type": "Constrict", - "description": "Air Pump", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-DukeDazzle2" - ], - "name": "JoyHub Edasich", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Mars" - ], - "name": "JoyHub Mars", - "features": [ - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Martino" - ], - "name": "JoyHub Martino", - "features": [ - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-MarsLion2" - ], - "name": "JoyHub Mars Lion 2", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Constrict", - "actuator": { - "step-range": [ - 0, - 5 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Myrna" - ], - "name": "JoyHub Myrna", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Constrict", - "actuator": { - "step-range": [ - 0, - 9 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Vase2" - ], - "name": "JoyHub Vase 2", - "features": [ - { - "feature-type": "Vibrate", - "description": "Biting lips", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Sideways flicker", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "J-Petalwish2", - "J-VortexTongue", - "J-Velocity", - "JOYHUB-ROSELLA2", - "J-VibSiren", - "J-ElixirEgg", - "J-RetroGuard", - "J-TrueForm", - "J-TrueForm3", - "J-Rhythmic2", - "J-Rhythmic3", - "J-Mysticolor", - "J-VividWings", - "J-Rainbow", - "J-BlackBull", - "J-Peacock", - "J-Mariner", - "J-Mace", - "J-MarsLion", - "J-Tarian", - "J-Pul", - "J-Euphoric", - "J-Euphoric3", - "J-Torrian", - "J-Rayen", - "J-ROSELLA3", - "J-Mackay", - "J-Rowdy3", - "J-Eclipse", - "J-DukeDazzle2", - "J-Scarlett", - "J-Tarik", - "J-UricaGuard2", - "J-Viva", - "J-Ryden", - "J-Mars", - "J-MarsLion2", - "J-Myrna", - "J-Vase2", - "J-Martino" - ], - "services": { - "0000ffa0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffa1-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "joyhub-v2": { - "defaults": { - "name": "JoyHub Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "J-Pearlconch" - ], - "name": "JoyHub Pearlconch", - "features": [ - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Pearlconch" - ], - "name": "JoyHub Pearlconch", - "features": [ - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-PearlconchL" - ], - "name": "JoyHub Pearlconch L", - "features": [ - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Piet2" - ], - "name": "JoyHub Piet 2", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Panther" - ], - "name": "JoyHub Panther", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-PetiteRose" - ], - "name": "JoyHub Petite Rose", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-MoonHorn" - ], - "name": "JoyHub Moon Horn", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Constrict", - "description": "Suction", - "actuator": { - "step-range": [ - 0, - 9 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Mecha" - ], - "name": "JoyHub Mecha", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Constrict", - "description": "Suction", - "actuator": { - "step-range": [ - 0, - 7 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Lagoon" - ], - "name": "JoyHub Lagoon", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Constrict", - "description": "Suction", - "actuator": { - "step-range": [ - 0, - 5 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-VibTrefoil" - ], - "name": "JoyHub VibTrefoil", - "features": [ - { - "feature-type": "Vibrate", - "description": "External vibrator", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Internal vibrator", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Firedragon" - ], - "name": "JoyHub Firedragon", - "features": [ - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Dina" - ], - "name": "JoyHub Deena", - "features": [ - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Internal vibrator", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "External vibrator", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Vbarbie3f" - ], - "name": "JoyHub Cherly", - "features": [ - { - "feature-type": "Vibrate", - "description": "External vibrator", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Internal vibrator", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-CHERLY2c" - ], - "name": "JoyHub Cherly 2c", - "features": [ - { - "feature-type": "Vibrate", - "description": "Internal vibrator", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Internal Whip", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "External vibrator", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Pathfinder2" - ], - "name": "JoyHub Pathfinder 2", - "features": [ - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Pathfinder" - ], - "name": "JoyHub Pathfinder", - "features": [ - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-VibRipple" - ], - "name": "JoyHub Angela", - "features": [ - { - "feature-type": "Vibrate", - "description": "External vibrator", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Internal vibrator", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Verax" - ], - "name": "JoyHub Verax", - "features": [ - { - "feature-type": "Vibrate", - "description": "Internal Whip", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Internal vibrator", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Verax2" - ], - "name": "JoyHub Verax 2", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Euphoric2" - ], - "name": "JoyHub Euphoric 2", - "features": [ - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-ROSEBUD" - ], - "name": "JoyHub RoseBUD", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Rotate", - "description": "Flicker", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Constrict", - "description": "Suction", - "actuator": { - "step-range": [ - 0, - 5 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Morningbuds2" - ], - "name": "JoyHub Morningbuds", - "features": [ - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Rhythmic4" - ], - "name": "JoyHub Rhythmic 4", - "features": [ - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Virtuoso2" - ], - "name": "JoyHub Virtuoso 2", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Constrict", - "description": "Suction", - "actuator": { - "step-range": [ - 0, - 3 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Dyllis" - ], - "name": "JoyHub Dyllis", - "features": [ - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Flamewing" - ], - "name": "JoyHub PhoenixGP", - "features": [ - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Fabledragon" - ], - "name": "JoyHub Fable Dragon", - "features": [ - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Faunus" - ], - "name": "JoyHub Faunus", - "features": [ - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-VelvetRabbit" - ], - "name": "JoyHub Velvet Rabbit", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-VividPulse" - ], - "name": "JoyHub Vivid Pulse", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-VioletVine" - ], - "name": "JoyHub Violet Vine", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-VibSiren2" - ], - "name": "JoyHub VibSiren 2", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Veemy" - ], - "name": "JoyHub Veemy", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Viball" - ], - "name": "JoyHub Viball", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Vase" - ], - "name": "JoyHub Vase", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Vortex2s" - ], - "name": "JoyHub Vortex 2s", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-VortexTongue2" - ], - "name": "JoyHub Lips", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Constrict", - "description": "Air Pump", - "actuator": { - "step-range": [ - 0, - 3 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Torin" - ], - "name": "JoyHub Torin", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-VBarbiep" - ], - "name": "JoyHub VBarbie p", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Vbarbie" - ], - "name": "JoyHub VBarbie", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Royaleye" - ], - "name": "JoyHub Royaleye", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-VBarbie2t" - ], - "name": "JoyHub Norma", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Pau" - ], - "name": "JoyHub Pau", - "features": [ - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Petalwish3" - ], - "name": "JoyHub Petalwish 3", - "features": [ - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Marshal" - ], - "name": "JoyHub Marshal", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Constrict", - "description": "Air Pump", - "actuator": { - "step-range": [ - 0, - 9 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Vince" - ], - "name": "JoyHub Vince", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Dallin" - ], - "name": "JoyHub Dallin", - "features": [ - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Mace2" - ], - "name": "JoyHub Maynor", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Constrict", - "description": "Air Pump", - "actuator": { - "step-range": [ - 0, - 9 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Verax4" - ], - "name": "JoyHub Verax 4", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Palmyra" - ], - "name": "JoyHub Palmyra", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Xylia" - ], - "name": "JoyHub Xylia", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Maiden" - ], - "name": "JoyHub Maiden", - "features": [ - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Constrict", - "actuator": { - "step-range": [ - 0, - 5 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Viele3" - ], - "name": "JoyHub Viele 3", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Troi" - ], - "name": "JoyHub Troi", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Tanmouth" - ], - "name": "JoyHub Tanmouth", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Marcela" - ], - "name": "JoyHub Marcela", - "features": [ - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Vita" - ], - "name": "JoyHub Vita", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-LACH" - ], - "name": "JoyHub Lach", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Constrict", - "actuator": { - "step-range": [ - 0, - 5 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - { - "identifier": [ - "J-Markel" - ], - "name": "JoyHub Markel", - "features": [ - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Constrict", - "description": "Suction", - "actuator": { - "step-range": [ - 0, - 9 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "J-Pearlconch", - "J-PearlconchL", - "J-PetiteRose", - "J-MoonHorn", - "J-VibTrefoil", - "J-Panther", - "J-Mecha", - "J-Lagoon", - "J-Firedragon", - "J-Dina", - "J-Vbarbie3f", - "J-CHERLY2c", - "J-Pathfinder2", - "J-Pathfinder", - "J-VibRipple", - "J-Verax", - "J-Verax2", - "J-Euphoric2", - "J-ROSEBUD", - "J-Morningbuds2", - "J-Rhythmic4", - "J-Virtuoso2", - "J-Dyllis", - "J-Flamewing", - "J-VelvetRabbit", - "J-VividPulse", - "J-VioletVine", - "J-VibSiren2", - "J-Veemy", - "J-Fabledragon", - "J-Faunus", - "J-VortexTongue2", - "J-Torin", - "J-VBarbiep", - "J-Vbarbie", - "J-Viball", - "J-Vase", - "J-Vortex2s", - "J-Royaleye", - "J-VBarbie2t", - "J-Pau", - "J-Petalwish3", - "J-Marshal", - "J-Piet2", - "J-Vince", - "J-Dallin", - "J-Mace2", - "J-Verax4", - "J-Palmyra", - "J-Maiden", - "J-Viele3", - "J-Xylia", - "J-Troi", - "J-Tanmouth", - "J-Marcela", - "J-Vita", - "J-LACH", - "J-Markel" - ], - "services": { - "0000ffa0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffa1-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "joyhub-v3": { - "defaults": { - "name": "JoyHub Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "J-Ringstar" - ], - "name": "JoyHub Starfish" - }, - { - "identifier": [ - "J-RapidTwist2" - ], - "name": "JoyHub Resi Ring 2" - } - ], - "communication": [ - { - "btle": { - "names": [ - "J-Ringstar", - "J-RapidTwist2" - ], - "services": { - "0000ffa0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffa1-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "joyhub-v4": { - "defaults": { - "name": "JoyHub Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Constrict", - "description": "Suction", - "actuator": { - "step-range": [ - 0, - 4 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "J-RoseLin" - ], - "name": "JoyHub RoseLin" - }, - { - "identifier": [ - "J-Viele" - ], - "name": "JoyHub Viele", - "features": [ - { - "feature-type": "Rotate", - "description": "Internal Simulator", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Internal Whip", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Internal Vibrator", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "J-RoseLin", - "J-Viele" - ], - "services": { - "0000ffa0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffa1-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "joyhub-v5": { - "defaults": { - "name": "JoyHub Device", - "features": [ - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Constrict", - "description": "Suction", - "actuator": { - "step-range": [ - 0, - 1 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "J-Virtuoso" - ], - "name": "JoyHub Virtuoso" - }, - { - "identifier": [ - "J-Pathfinder3" - ], - "name": "JoyHub Pathfinder 3", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "J-Virtuoso", - "J-Pathfinder3" - ], - "services": { - "0000ffa0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffa1-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "joyhub-v6": { - "defaults": { - "name": "JoyHub Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Constrict", - "description": "Suction", - "actuator": { - "step-range": [ - 0, - 9 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "J-Melody" - ], - "name": "JoyHub Melody" - } - ], - "communication": [ - { - "btle": { - "names": [ - "J-Melody" - ], - "services": { - "0000ffa0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffa1-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "itoys": { - "defaults": { - "name": "iToys Seagull", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 3 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "26-021-B" - ], - "services": { - "0000ffa0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffa1-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "leten": { - "defaults": { - "name": "Leten Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 25 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "T528-LT", - "F537-LT", - "F520B-LT", - "F520A-LT" - ], - "services": { - "0000fff0-0000-1000-8000-00805f9b34fb": { - "tx": "0000fff1-0000-1000-8000-00805f9b34fb" - }, - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "rx": "0000ffe1-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "vibcrafter": { - "defaults": { - "name": "VibCrafter Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 99 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 99 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "be gentle" - ], - "name": "VibCrafter Harlow" - }, - { - "identifier": [ - "Hayden" - ], - "name": "VibCrafter Hayden" - }, - { - "identifier": [ - "Nidalee" - ], - "name": "VibCrafter Nidalee" - }, - { - "identifier": [ - "Janna" - ], - "name": "VibCrafter Janna", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 99 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - } - ], - "communication": [ - { - "btle": { - "names": [ - "be gentle", - "Janna", - "Hayden", - "Nidalee" - ], - "services": { - "53300051-0060-4bd4-bbe5-a6920e4c5663": { - "tx": "53300052-0060-4bd4-bbe5-a6920e4c5663", - "rx": "53300053-0060-4bd4-bbe5-a6920e4c5663" - } - } - } - } - ] - }, - "lioness": { - "defaults": { - "name": "Lioness", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "Lioness", - "Lioness2" - ], - "services": { - "d973f2ed-b19e-11e2-9e96-0800200c9a66": { - "tx": "d973f2f4-b19e-11e2-9e96-0800200c9a66" - }, - "d973f2e5-b19e-11e2-9e96-0800200c9a66": { - "rx": "d973f2e6-b19e-11e2-9e96-0800200c9a66" - } - } - } - } - ] - }, - "activejoy": { - "defaults": { - "name": "IntoYou Remote Egg Vibrator", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "SS-TD-YDTD-001" - ], - "services": { - "0000f0b0-0000-1000-8000-00805f9b34fb": { - "tx": "0000f0b1-0000-1000-8000-00805f9b34fb", - "rx": "0000f0b2-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "cupido": { - "defaults": { - "name": "Cupido Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "MY2607-BLE-V1.0" - ], - "services": { - "0000f0b0-0000-1000-8000-00805f9b34fb": { - "tx": "0000f0b1-0000-1000-8000-00805f9b34fb", - "rx": "0000f0b2-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "amorelie-joy": { - "defaults": { - "name": "Amorelie Joy Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "4D02" - ], - "name": "Amorelie Joy Move" - }, - { - "identifier": [ - "4D05" - ], - "name": "Amorelie Joy Cha-Cha" - }, - { - "identifier": [ - "4D06" - ], - "name": "Amorelie Joy Boogie" - }, - { - "identifier": [ - "4D01" - ], - "name": "Amorelie Joy Shimmer" - }, - { - "identifier": [ - "4D03" - ], - "name": "Amorelie Joy Grow" - }, - { - "identifier": [ - "4D04" - ], - "name": "Amorelie Joy Shuffle" - }, - { - "identifier": [ - "4D07" - ], - "name": "Amorelie Joy Salsa" - } - ], - "communication": [ - { - "btle": { - "names": [ - "4D01", - "4D02", - "4D03", - "4D04", - "4D05", - "4D06", - "4D07", - "4D08", - "4D09" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "rx": "0000ffe2-0000-1000-8000-00805f9b34fb", - "tx": "0000ffe3-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "feelingso": { - "defaults": { - "name": "FeelingSo Flair Feel", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 19 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 19 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "Flair Feel" - ], - "services": { - "42410001-0000-0101-0000-736278637a72": { - "tx": "42410002-0000-0101-0000-736278637a72", - "rx": "42410003-0000-0101-0000-736278637a72" - } - } - } - } - ] - }, - "deepsire": { - "defaults": { - "name": "DeepSire Device", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "configurations": [ - { - "identifier": [ - "IMP 3" - ], - "name": "Kuirkish Imp 3" - } - ], - "communication": [ - { - "btle": { - "names": [ - "IMP 3" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "nextlevelracing": { - "defaults": { - "name": "Next Level Racing HF8 Haptic Gaming Pad", - "features": [ - { - "feature-type": "Vibrate", - "description": "Right thigh", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Left thigh", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Right buttock", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Left buttock", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Right back", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Left back", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Right shoulder", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "description": "Left shoulder", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "serial": { - "port": "default", - "baud-rate": 115200, - "data-bits": 8, - "parity": "N", - "stop-bits": 1 - } - } - ] - }, - "xuanhuan": { - "defaults": { - "name": "Xuanhuan Masturbator", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 10 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "QUXIN" - ], - "services": { - "0000fffe-0000-1000-8000-00805f9b34fb": { - "tx": "0000fe02-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "serveu": { - "defaults": { - "name": "ServeU", - "features": [ - { - "feature-type": "Position", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "LinearCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "ServeU" - ], - "services": { - "31bb1111-33e3-4f3c-a7fb-104288e7cb77": { - "tx": "31bb2222-33e3-4f3c-a7fb-104288e7cb77" - } - } - } - } - ] - }, - "fleshy-thrust": { - "defaults": { - "name": "Fleshy Thrust Sync", - "features": [ - { - "feature-type": "Position", - "actuator": { - "step-range": [ - 0, - 180 - ], - "messages": [ - "LinearCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "BT05" - ], - "services": { - "0000ffe0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "nexus-revo": { - "defaults": { - "name": "Nexus Revo Stealth", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 10 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 2 - ], - "messages": [ - "RotateCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "XW-LW3" - ], - "services": { - "0000c570-0000-1000-8000-00805f9b34fb": { - "tx": "0000c571-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "luvmazer": { - "defaults": { - "name": "Luvmazer Finger Magic", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Rotate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "TKLM-W001-BT" - ], - "services": { - "0000ffa0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffa1-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "loob": { - "defaults": { - "name": "Joyroid Loob", - "features": [ - { - "feature-type": "Position", - "actuator": { - "step-range": [ - 0, - 1000 - ], - "messages": [ - "LinearCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "LOOB" - ], - "services": { - "b75c49d2-04a3-4071-a0b5-35853eb08307": { - "tx": "ba5c49d2-04a3-4071-a0b5-35853eb08307" - } - } - } - } - ] - }, - "bananasome": { - "defaults": { - "name": "Bananasome Rocket X7", - "features": [ - { - "feature-type": "Oscillate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 255 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "火箭X7" - ], - "services": { - "0000ae00-0000-1000-8000-00805f9b34fb": { - "tx": "0000ae01-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - }, - "omobo": { - "defaults": { - "name": "Omobo ViVegg Vibrator", - "features": [ - { - "feature-type": "Vibrate", - "actuator": { - "step-range": [ - 0, - 100 - ], - "messages": [ - "ScalarCmd" - ] - } - } - ] - }, - "communication": [ - { - "btle": { - "names": [ - "S6" - ], - "services": { - "0000ffb0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ffb2-0000-1000-8000-00805f9b34fb" - } - } - } - } - ] - } - } -} diff --git a/buttplug/buttplug-device-config/convert-v2-to-v3.js b/buttplug/buttplug-device-config/convert-v2-to-v3.js deleted file mode 100644 index dd7f6ef50..000000000 --- a/buttplug/buttplug-device-config/convert-v2-to-v3.js +++ /dev/null @@ -1,133 +0,0 @@ -const yaml = require('js-yaml'); -const fs = require('fs'); - -function moveDefaults(def, config) { - if (def["ScalarCmd"] !== undefined && config["ScalarCmd"] === undefined) { - config["ScalarCmd"] = JSON.parse(JSON.stringify(def["ScalarCmd"])) - } - if (def["RotateCmd"] !== undefined && config["RotateCmd"] === undefined) { - config["RotateCmd"] = JSON.parse(JSON.stringify(def["RotateCmd"])) - } - if (def["LinearCmd"] !== undefined && config["LinearCmd"] === undefined) { - config["LinearCmd"] = JSON.parse(JSON.stringify(def["LinearCmd"])) - } - if (def["SensorReadCmd"] !== undefined && config["SensorReadCmd"] === undefined) { - config["SensorReadCmd"] = JSON.parse(JSON.stringify(def["SensorReadCmd"])) - } -} - -function convertMessagesObject(messages) { - let features = []; - console.log(messages["ScalarCmd"]); - if (messages["ScalarCmd"] !== undefined) { - for (var scalarcmd of messages["ScalarCmd"]) { - let featureObj = {}; - console.log(scalarcmd); - featureObj["feature-type"] = scalarcmd["ActuatorType"]; - if (scalarcmd["FeatureDescriptor"] !== undefined) { - featureObj["description"] = scalarcmd["FeatureDescriptor"]; - } - featureObj["actuator"] = {}; - if (scalarcmd["StepRange"] !== undefined) { - featureObj["actuator"]["step-range"] = scalarcmd["StepRange"]; - } - featureObj["actuator"]["messages"] = ["ScalarCmd"]; - features.push(featureObj); - } -} -if (messages["RotateCmd"] !== undefined) { - for (var scalarcmd of messages["RotateCmd"]) { - let featureObj = {}; - console.log(scalarcmd); - featureObj["feature-type"] = scalarcmd["ActuatorType"]; - if (scalarcmd["FeatureDescriptor"] !== undefined) { - featureObj["description"] = scalarcmd["FeatureDescriptor"]; - } - featureObj["actuator"] = {}; - if (scalarcmd["StepRange"] !== undefined) { - featureObj["actuator"]["step-range"] = scalarcmd["StepRange"]; - } - featureObj["actuator"]["messages"] = ["RotateCmd"]; - features.push(featureObj); - } -} -if (messages["LinearCmd"] !== undefined) { - for (var scalarcmd of messages["LinearCmd"]) { - let featureObj = {}; - console.log(scalarcmd); - featureObj["feature-type"] = scalarcmd["ActuatorType"]; - if (scalarcmd["FeatureDescriptor"] !== undefined) { - featureObj["description"] = scalarcmd["FeatureDescriptor"]; - } - featureObj["actuator"] = {}; - if (scalarcmd["StepRange"] !== undefined) { - featureObj["actuator"]["step-range"] = scalarcmd["StepRange"]; - } - featureObj["actuator"]["messages"] = ["LinearCmd"]; - features.push(featureObj); - } -} -if (messages["SensorReadCmd"] !== undefined) { - for (var sensorcmd of messages["SensorReadCmd"]) { - let featureObj = {}; - console.log(scalarcmd); - featureObj["feature-type"] = sensorcmd["SensorType"]; - if (sensorcmd["FeatureDescriptor"] !== undefined) { - featureObj["description"] = sensorcmd["FeatureDescriptor"]; - } - featureObj["sensor"] = {}; - if (sensorcmd["SensorRange"] !== undefined) { - featureObj["sensor"]["value-range"] = sensorcmd["SensorRange"]; - } - featureObj["sensor"]["messages"] = ["SensorReadCmd"]; - features.push(featureObj); - } -} - return features; -} - -// Get document, or throw exception on error -const doc = yaml.load(fs.readFileSync('./device-config-v2/buttplug-device-config-v2.yml', 'utf8')); -for (var protocol in doc["protocols"]) { - console.log(protocol); - let comm_array = []; - for (var comm_type of ["btle", "hid", "usb", "serial", "xinput", "lovense-connect-service"]) - if (doc["protocols"][protocol][comm_type]) { - let obj = {}; - if (["serial"].includes(comm_type)) { - obj[comm_type] = {}; - obj[comm_type]["ports"] = doc["protocols"][protocol][comm_type]; - } else if (["hid", "usb"].includes(comm_type)) { - obj[comm_type] = {}; - obj[comm_type]["pairs"] = doc["protocols"][protocol][comm_type]; - } else { - obj[comm_type] = doc["protocols"][protocol][comm_type]; - } - comm_array.push(obj); - doc["protocols"][protocol][comm_type] = undefined; - } - doc["protocols"][protocol]["communication"] = comm_array; - - if (doc["protocols"][protocol]["defaults"] === undefined) { - console.log("No defaults for protocol"); - } - let def = undefined; - if (doc["protocols"][protocol]["defaults"]["messages"] !== undefined) { - def = doc["protocols"][protocol]["defaults"]["messages"]; - doc["protocols"][protocol]["defaults"]["features"] = convertMessagesObject(doc["protocols"][protocol]["defaults"]["messages"]); - doc["protocols"][protocol]["defaults"]["messages"] = undefined; - } - if (doc["protocols"][protocol]["configurations"] !== undefined) { - for (var config of doc["protocols"][protocol]["configurations"]) { - if (config["messages"] !== undefined) { - if (def !== undefined) { - moveDefaults(def, config["messages"]) - } - config["features"] = convertMessagesObject(config["messages"]); - config["messages"] = undefined; - } - } - } -} - -fs.writeFileSync("device-config-v3/buttplug-device-config-v3.yml", yaml.dump(doc)); diff --git a/buttplug/buttplug-device-config/device-config-v2/buttplug-device-config-schema-v2.json b/buttplug/buttplug-device-config/device-config-v2/buttplug-device-config-schema-v2.json deleted file mode 100644 index ab43b7594..000000000 --- a/buttplug/buttplug-device-config/device-config-v2/buttplug-device-config-schema-v2.json +++ /dev/null @@ -1,505 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Buttplug Device Config Schema", - "version": 2, - "description": "JSON format for Buttplug 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}$" - }, - "endpoint": { - "type": "object", - "patternProperties": { - "^(command|firmware|rx|rxaccel|rxblebattery|rxblemodel|rxpressure|rxtouch|tx|txmode|txshock|txvibrate|txvendorcontrol|whitelist|generic[1-2]?[0-9]|generic3[0-1])$": { - "$ref": "#/components/uuid" - } - }, - "additionalProperties": false, - "minProperties": 1 - }, - "btle-definition": { - "type": "object", - "properties": { - "names": { - "type": "array", - "items": { - "type": "string" - }, - "minItems": 1 - }, - "manufacturer-data": { - "type": "array", - "items": { - "type": "object", - "properties": { - "company": { - "type": "integer" - }, - "expected-length": { - "type": "integer" - }, - "data": { - "type": "array", - "items": { - "type": "integer" - } - } - }, - "required": [ - "company" - ] - } - }, - "advertised-services": { - "type": "array", - "items": { - "type": "string", - "$ref": "#/components/uuid" - } - }, - "services": { - "type": "object", - "patternProperties": { - "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$": { - "$ref": "#/components/endpoint" - } - }, - "minProperties": 1, - "additionalProperties": false - } - }, - "additionalProperties": false, - "required": [ - "names", - "services" - ] - }, - "websocket-definition": { - "type": "object", - "properties": { - "names": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false, - "required": [ - "names" - ] - }, - "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 - }, - "xinput-definition": { - "type": "object", - "properties": { - "exists": { - "type": "boolean" - } - } - }, - "lovense-connect-service-definition": { - "type": "object", - "properties": { - "exists": { - "type": "boolean" - } - } - }, - "usb-definition": { - "type": "array", - "items": { - "type": "object", - "properties": { - "vendor-id": { - "type": "integer", - "minimum": 0, - "maximum": 65535 - }, - "product-id": { - "type": "integer", - "minimum": 0, - "maximum": 65535 - } - }, - "required": [ - "vendor-id", - "product-id" - ], - "additionalProperties": false - }, - "minItems": 1 - }, - "FeatureCount": { - "description": "Number of features on device.", - "type": "integer", - "minimum": 1 - }, - "StepRange": { - "description": "Specifies the range of steps to use for a device. Devices will use the low end value as a stop.", - "type": "array", - "items": { - "type": "integer" - }, - "minItems": 2, - "maxItems": 2 - }, - "FeatureOrder": { - "description": "Specifies the order features are exposed in by the ButtplugMessages.", - "minimum": 0, - "type": "integer" - }, - "NullMessageAttributes": { - "description": "Attributes for device message that have no attributes.", - "type": "object", - "additionalProperties": false, - "minProperties": 0, - "maxProperties": 0 - }, - "GenericMessageAttributes": { - "description": "Attributes for device messages.", - "type": "array", - "items": { - "type": "object", - "properties": { - "StepRange": { - "$ref": "#/components/StepRange" - }, - "FeatureOrder": { - "$ref": "#/components/FeatureOrder" - }, - "FeatureDescriptor": { - "type": "string" - }, - "ActuatorType": { - "type": "string", - "pattern": "^(Vibrate|Rotate|Oscillate|Constrict|Inflate|Position)$" - } - }, - "required": [ - "StepRange", - "ActuatorType" - ], - "additionalProperties": false, - "minProperties": 0 - }, - "minItems": 1 - }, - "SensorMessageAttributes": { - "description": "Attributes for sensor messages.", - "type": "array", - "items": { - "type": "object", - "properties": { - "SensorType": { - "type": "string" - }, - "FeatureDescriptor": { - "type": "string" - }, - "SensorRange": { - "type": "array", - "items": { - "$ref": "#/components/StepRange" - }, - "minItems": 1 - } - }, - "required": [ - "SensorType", - "FeatureDescriptor", - "SensorRange" - ], - "additionalProperties": false, - "minProperties": 0 - }, - "minItems": 1 - }, - "DeviceMessagesEx": { - "description": "A list of the messages a device will accept on this server implementation.", - "type": "object", - "properties": { - "StopDeviceCmd": { - "$ref": "#/components/NullMessageAttributes" - }, - "ScalarCmd": { - "$ref": "#/components/GenericMessageAttributes" - }, - "VibrateCmd": { - "$ref": "#/components/GenericMessageAttributes" - }, - "LinearCmd": { - "$ref": "#/components/GenericMessageAttributes" - }, - "RotateCmd": { - "$ref": "#/components/GenericMessageAttributes" - }, - "SensorReadCmd": { - "$ref": "#/components/SensorMessageAttributes" - }, - "SensorSubscribeCmd": { - "$ref": "#/components/SensorMessageAttributes" - }, - "SensorUnsubscribeCmd": { - "$ref": "#/components/SensorMessageAttributes" - }, - "LovenseCmd": { - "$ref": "#/components/NullMessageAttributes" - }, - "VorzeA10CycloneCmd": { - "$ref": "#/components/NullMessageAttributes" - }, - "KiirooCmd": { - "$ref": "#/components/NullMessageAttributes" - }, - "SingleMotorVibrateCmd": { - "$ref": "#/components/NullMessageAttributes" - }, - "FleshlightLaunchFW12Cmd": { - "$ref": "#/components/NullMessageAttributes" - } - }, - "additionalProperties": false - }, - "UserDeviceMessagesEx": { - "description": "A list of the messages that can be configured in user device settings.", - "type": "object", - "properties": { - "ScalarCmd": { - "$ref": "#/components/GenericMessageAttributes" - }, - "VibrateCmd": { - "$ref": "#/components/GenericMessageAttributes" - }, - "LinearCmd": { - "$ref": "#/components/GenericMessageAttributes" - }, - "RotateCmd": { - "$ref": "#/components/GenericMessageAttributes" - } - }, - "additionalProperties": false - }, - "user-config": { - "type": "object", - "properties": { - "allow": { - "type": "boolean" - }, - "deny": { - "type": "boolean" - }, - "display-name": { - "type": "string" - }, - "index": { - "type": "integer" - }, - "messages": { - "$ref": "#/components/UserDeviceMessagesEx" - } - }, - "additionalProperties": false - }, - "defaults-definition": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "messages": { - "$ref": "#/components/DeviceMessagesEx" - } - }, - "required": [ - "name", - "messages" - ] - }, - "configurations-definition": { - "type": "array", - "items": { - "type": "object", - "properties": { - "identifier": { - "type": "array", - "items": { - "type": "string" - }, - "minItems": 1 - }, - "name": { - "type": "string" - }, - "messages": { - "$ref": "#/components/DeviceMessagesEx" - } - }, - "required": [ - "identifier" - ], - "additionalProperties": false - }, - "minItems": 1 - } - }, - "type": "object", - "properties": { - "version": { - "description": "Version of the device configuration file.", - "type": "object", - "properties": { - "major": { - "type": "integer", - "minimum": 1 - }, - "minor": { - "type": "integer", - "minimum": 0 - } - } - }, - "protocols": { - "type": "object", - "patternProperties": { - "^.*$": { - "type": "object", - "properties": { - "btle": { - "$ref": "#/components/btle-definition" - }, - "serial": { - "$ref": "#/components/serial-definition" - }, - "websocket": { - "$ref": "#/components/websocket-definition" - }, - "usb": { - "$ref": "#/components/usb-definition" - }, - "hid": { - "$ref": "#/components/usb-definition" - }, - "xinput": { - "$ref": "#/components/xinput-definition" - }, - "lovense-connect-service": { - "$ref": "#/components/lovense-connect-service-definition" - }, - "defaults": { - "$ref": "#/components/defaults-definition" - }, - "configurations": { - "$ref": "#/components/configurations-definition" - } - } - } - }, - "additionalProperties": false - }, - "user-configs": { - "type": "object", - "properties": { - "specifiers": { - "type": "object", - "patternProperties": { - "^.*$": { - "type": "object", - "properties": { - "btle": { - "$ref": "#/components/btle-definition" - }, - "serial": { - "$ref": "#/components/serial-definition" - }, - "websocket": { - "$ref": "#/components/websocket-definition" - }, - "usb": { - "$ref": "#/components/usb-definition" - }, - "hid": { - "$ref": "#/components/usb-definition" - } - } - }, - "additionalProperties": false - } - }, - "devices": { - "type": "array", - "items": { - "type": "object", - "properties": { - "identifier": { - "type": "object", - "properties": { - "address": { - "type": "string" - }, - "protocol": { - "type": "string" - }, - "identifier": { - "type": "string" - } - }, - "additionalProperties": false, - "required": [ - "address", - "protocol" - ] - }, - "config": { - "$ref": "#/components/user-config" - } - }, - "additionalProperties": false, - "required": [ - "identifier", - "config" - ] - } - } - }, - "additionalProperties": false - }, - "additionalProperties": false - }, - "required": [ - "version" - ], - "additionalProperties": false -} \ No newline at end of file diff --git a/buttplug/buttplug-device-config/device-config-v2/buttplug-device-config-v2.yml b/buttplug/buttplug-device-config/device-config-v2/buttplug-device-config-v2.yml deleted file mode 100644 index bb7401663..000000000 --- a/buttplug/buttplug-device-config/device-config-v2/buttplug-device-config-v2.yml +++ /dev/null @@ -1,4976 +0,0 @@ -# Welcome to Buttplug Device Config -# -# DO NOT EDIT THIS FILE. YOUR CHANGES WILL BE OVERWRITTEN. -# -# Use user-device-config.yml for local device definitions. -# -# Now that we've got that bit of impoliteness out of the way... Hi, I'm qDot. Welcome to the -# buttplug device configuration file. -# -# You've managed to wander into this place we keep all of the sex toy information. You're not really -# supposed to be here, but since you've shown up, might as give you a tour. -# -# This file is used in Rust Buttplug implementation. It's our main source of truth for devices we -# know about. -# -# Devices in Buttplug are defined in terms of their protocol. We group together all devices that -# speak the same language. It will look something like this: -# -# protocols: -# lovense: -# btle: -# names: -# - LVS-* -# services: -# 0000fff0-0000-1000-8000-00805f9b34fb: null -# -# The protocols portion of the config file lists all of the protocols we know, and how the devices -# that speak those protocols can be identified. For the above example, we're looking at Lovense -# brand hardware, all of which uses bluetooth LE, so we create a "btle" configuration section for -# that protocol. We list the device names (using a * as a wildcard, so this means "try to connect to -# anything that starts with LVS-"). We also list the services we're interested in. If a service only -# has rx/tx characteristics (many devices emulate serial in this way), we just put "null" and let -# the Bluetooth subtype manager sort out the characteristics on connect. Otherwise, if there are -# multiple characteristics, we list those in name/uuid form. See the definitions below for more -# examples. -# -# Other devices are similar. For USB/HID, we just list VID/PID pairs, as that's all we have to -# identify with. -# -# Serial ports are a bit different in that we don't have a specifier for them, they could be -# anywhere. Therefore, we just specify the port settings in this file. Baud rate, data bits, etc. In -# Buttplug Reference Servers, we allow the user to pass in another device config file like this one -# that they've built, that can define things like local port names. So the user may have a file that -# looks like: -# -# protocols: -# nobra: -# serial: -# ports: -# - COM4 -# -# The user's config will be merged with this config file in the server, which lets any Serial -# subtype manager know which port to discover the device on, as well as all of the other port -# settings to use. -# -# Users can also define things we're missing here, like new BTLE names or IDs we haven't gotten to -# yet. The only thing users can't define is new protocols, since those have to be implemented in -# source code. -# -# That's pretty much it for how this file works. Now for the actual protocol definitions. This is -# gonna get wild, so I'll keep a list of rules that you can refer to up here. -# -# - If you are curious about device identifiers or protocols, all of those are documented at -# https://stpihkal.docs.buttplug.io -# -# - BTLE name fields can be wildcarded using "*". This allows us to do things like searching for all -# devices of a certain name. -# -# - Where possible, we assume all devices have at least one output (so we can send the commands), -# and maybe one input. In cases otherwise, a comment should be left denoting what we're doing -# something different. -# -# - Most toys just mirror good ol' serial. With that in mind, we call the host-to-device line tx, -# and device-to-host line rx. -# -# - A "btle" connection info with multiple service listings can mean one of two things. Either we -# expect the device to be identifiable as multiple services (like Lovense), or the device may have -# multiple services we use (like some BTLE toys that divide control and sensor between different -# services). -# -# - If you're connecting to a buttplug server and don't see a device that's listed here, it's not -# the fault of this config file. Servers may not implement all protocols or busses for various -# reasons. We just define which devices we may know about here. File an issue on the library -# you're using. -# -# - If you're connecting to a buttplug server and some device doesn't take a Buttplug message you're -# expecting it to, it's not the fault of this config file. Servers implement protocols, and -# protocols dictate which messages they are capable of sending. We don't really keep a standard -# for that, we just define which devices we may know about here. File an issue on the library -# you're using. -# -# - Serial info here is for default device configuration. Port names will have to be added by the -# user in the user device config file. -version: - major: 2 - minor: 31 -protocols: - lovense: - # Lovense is special. Special in oh so many ways. - # - # Lovense have changed BLE name formats at least once. For this generic device, we just use the - # largest wildcard we can. We do our name and capabilities finding in the protocol - # implementation, because we have to query the device after connection. - # - # The service uuids change constantly. This list is overly exhaustive, because we have to - # specify services in WebBluetooth and can't wildcard them. We'll add more as we find them. - btle: - names: - - LVS-* - - LOVE-* - # This conflicts with Hismith, and we don't need this for identification anymore since we've moved to - # advertised-services: - # - 0000ffb0-0000-1000-8000-00805f9b34fb # Folove advertised service - manufacturer-data: - - company: 0x026c - data: [0xff, 0x21] # Folove manufacturer data (start of a pretty long array) - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff2-0000-1000-8000-00805f9b34fb - rx: 0000fff1-0000-1000-8000-00805f9b34fb - 6e400001-b5a3-f393-e0a9-e50e24dcca9e: - tx: 6e400002-b5a3-f393-e0a9-e50e24dcca9e - rx: 6e400003-b5a3-f393-e0a9-e50e24dcca9e - 50300001-0024-4bd4-bbd5-a6920e4c5653: - tx: 50300002-0024-4bd4-bbd5-a6920e4c5653 - rx: 50300003-0024-4bd4-bbd5-a6920e4c5653 - 57300001-0023-4bd4-bbd5-a6920e4c5653: - tx: 57300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 57300003-0023-4bd4-bbd5-a6920e4c5653 - 5a300001-0024-4bd4-bbd5-a6920e4c5653: - tx: 5a300002-0024-4bd4-bbd5-a6920e4c5653 - rx: 5a300003-0024-4bd4-bbd5-a6920e4c5653 - 50300001-0023-4bd4-bbd5-a6920e4c5653: - tx: 50300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 50300003-0023-4bd4-bbd5-a6920e4c5653 - 53300001-0023-4bd4-bbd5-a6920e4c5653: - tx: 53300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 53300003-0023-4bd4-bbd5-a6920e4c5653 - 5a300001-0023-4bd4-bbd5-a6920e4c5653: - tx: 5a300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 5a300003-0023-4bd4-bbd5-a6920e4c5653 - 4f300001-0023-4bd4-bbd5-a6920e4c5653: - tx: 4f300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 4f300003-0023-4bd4-bbd5-a6920e4c5653 - 42300001-0023-4bd4-bbd5-a6920e4c5653: # Max 2 - tx: 42300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 42300003-0023-4bd4-bbd5-a6920e4c5653 - 43300001-0023-4bd4-bbd5-a6920e4c5653: # New Nora - tx: 43300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 43300003-0023-4bd4-bbd5-a6920e4c5653 - 4c300001-0023-4bd4-bbd5-a6920e4c5653: # Ambi - tx: 4c300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 4c300003-0023-4bd4-bbd5-a6920e4c5653 - 4c410001-0023-4bd4-bbd5-a6920e4c5653: # Ambi - tx: 4c410002-0023-4bd4-bbd5-a6920e4c5653 - rx: 4c410003-0023-4bd4-bbd5-a6920e4c5653 - 56300001-0023-4bd4-bbd5-a6920e4c5653: # Mission - tx: 56300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 56300003-0023-4bd4-bbd5-a6920e4c5653 - 58300001-0023-4bd4-bbd5-a6920e4c5653: # Ferri - tx: 58300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 58300003-0023-4bd4-bbd5-a6920e4c5653 - 52300001-0023-4bd4-bbd5-a6920e4c5653: # Diamo - tx: 52300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 52300003-0023-4bd4-bbd5-a6920e4c5653 - 46300001-0023-4bd4-bbd5-a6920e4c5653: # Blast/Ridge - tx: 46300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 46300003-0023-4bd4-bbd5-a6920e4c5653 - 50300011-0023-4bd4-bbd5-a6920e4c5653: # Edge2 paired - tx: 50300012-0023-4bd4-bbd5-a6920e4c5653 - rx: 50300013-0023-4bd4-bbd5-a6920e4c5653 - 4a300001-0023-4bd4-bbd5-a6920e4c5653: # Quake/Dolce - tx: 4a300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 4a300003-0023-4bd4-bbd5-a6920e4c5653 - 45440001-0023-4bd4-bbd5-a6920e4c5653: # Gush - tx: 45440002-0023-4bd4-bbd5-a6920e4c5653 - rx: 45440003-0023-4bd4-bbd5-a6920e4c5653 - 45420001-0023-4bd4-bbd5-a6920e4c5653: # Hyphy - tx: 45420002-0023-4bd4-bbd5-a6920e4c5653 - rx: 45420003-0023-4bd4-bbd5-a6920e4c5653 - 54300001-0023-4bd4-bbd5-a6920e4c5653: # Calor - tx: 54300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 54300003-0023-4bd4-bbd5-a6920e4c5653 - 45490001-0023-4bd4-bbd5-a6920e4c5653: # Flexer - tx: 45490002-0023-4bd4-bbd5-a6920e4c5653 - rx: 45490003-0023-4bd4-bbd5-a6920e4c5653 - 4e300001-0023-4bd4-bbd5-a6920e4c5653: # Gemini - tx: 4e300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 4e300003-0023-4bd4-bbd5-a6920e4c5653 - 45410001-0023-4bd4-bbd5-a6920e4c5653: # Gravity - tx: 45410002-0023-4bd4-bbd5-a6920e4c5653 - rx: 45410003-0023-4bd4-bbd5-a6920e4c5653 - 51300001-0023-4bd4-bbd5-a6920e4c5653: # Tenera - tx: 51300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 51300003-0023-4bd4-bbd5-a6920e4c5653 - 45460001-0023-4bd4-bbd5-a6920e4c5653: # Exomoon - tx: 45460002-0023-4bd4-bbd5-a6920e4c5653 - rx: 45460003-0023-4bd4-bbd5-a6920e4c5653 - 454c0001-0023-4bd4-bbd5-a6920e4c5653: # Exomoon - tx: 454c0002-0023-4bd4-bbd5-a6920e4c5653 - rx: 454c0003-0023-4bd4-bbd5-a6920e4c5653 - 55300001-0023-4bd4-bbd5-a6920e4c5653: # Lapis - tx: 55300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 55300003-0023-4bd4-bbd5-a6920e4c5653 - 53440001-0023-4bd4-bbd5-a6920e4c5653: # Vulse - tx: 53440002-0023-4bd4-bbd5-a6920e4c5653 - rx: 53440003-0023-4bd4-bbd5-a6920e4c5653 - 48300001-0023-4bd4-bbd5-a6920e4c5653: # Solace - tx: 48300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 48300003-0023-4bd4-bbd5-a6920e4c5653 - defaults: - name: Lovense Device - messages: - ScalarCmd: - - StepRange: [0, 20] - ActuatorType: Vibrate - SensorReadCmd: - - FeatureDescriptor: Battery Level - SensorType: Battery - SensorRange: [[0, 100]] - configurations: - # For lovense, our identifiers are the letters returned from the - # DeviceInfo query sent on initialization. - - identifier: - - B - name: Lovense Max - messages: - ScalarCmd: - - StepRange: [0, 20] - FeatureDescriptor: Vibrator - ActuatorType: Vibrate - - StepRange: [0, 3] - FeatureDescriptor: Air Pump - ActuatorType: Constrict - - identifier: - - P - name: Lovense Edge - messages: - ScalarCmd: - - StepRange: [0, 20] - ActuatorType: Vibrate - - StepRange: [0, 20] - ActuatorType: Vibrate - - identifier: - - A - - C - name: Lovense Nora - messages: - RotateCmd: - - StepRange: [0, 20] - ActuatorType: Rotate - - identifier: - - L - name: Lovense Ambi - - identifier: - - S - name: Lovense Lush - - identifier: - - Z - name: Lovense Hush - - identifier: - - W - name: Lovense Domi - - identifier: - - O - name: Lovense Osci - - identifier: - - V - name: Lovense Mission - - identifier: - - X - name: Lovense Ferri - - identifier: - - R - name: Lovense Diamo - - identifier: - - ToyS - name: Loveai Dolp - - identifier: - - F - name: Lovense Sex Machine - messages: - ScalarCmd: - - StepRange: [0, 20] - ActuatorType: Oscillate - FeatureDescriptor: Fucking Machine Oscillation Speed - - identifier: - - FS - name: Lovense Mini Sex Machine - messages: - ScalarCmd: - - StepRange: [0, 20] - ActuatorType: Oscillate - FeatureDescriptor: Fucking Machine Oscillation Speed - - identifier: - - J - name: Lovense Dolce - messages: - ScalarCmd: - - StepRange: [0, 20] - ActuatorType: Vibrate - - StepRange: [0, 20] - ActuatorType: Vibrate - - identifier: - - ED - name: Lovense Gush - - identifier: - - EB - name: Lovense Hyphy - messages: - ScalarCmd: - - StepRange: [0, 20] - ActuatorType: Vibrate - - StepRange: [0, 20] - ActuatorType: Vibrate - - identifier: - - T - name: Lovense Calor - - identifier: - - EI - name: Lovense Flexer (Firmware update needed) - - identifier: - - EI-FW3 - name: Lovense Flexer - messages: - ScalarCmd: - - StepRange: [0, 20] - ActuatorType: Vibrate - FeatureDescriptor: Internal Vibe - - StepRange: [0, 20] - ActuatorType: Vibrate - FeatureDescriptor: External Vibe - - StepRange: [0, 20] - ActuatorType: Rotate - FeatureDescriptor: Finger motion - SensorReadCmd: - - FeatureDescriptor: Battery Level - SensorType: Battery - SensorRange: [[0, 100]] - - identifier: - - N - name: Lovense Gemini - messages: - ScalarCmd: - - StepRange: [0, 20] - ActuatorType: Vibrate - - StepRange: [0, 20] - ActuatorType: Vibrate - SensorReadCmd: - - FeatureDescriptor: Battery Level - SensorType: Battery - SensorRange: [[0, 100]] - - identifier: - - EA - name: Lovense Gravity - messages: - ScalarCmd: - - StepRange: [0, 20] - ActuatorType: Vibrate - - StepRange: [0, 20] - ActuatorType: Oscillate - SensorReadCmd: - - FeatureDescriptor: Battery Level - SensorType: Battery - SensorRange: [[0, 100]] - - identifier: - - Q - name: Lovense Tenera - - identifier: - - EL - name: Lovense Ridge - messages: - RotateCmd: - - StepRange: [0, 20] - ActuatorType: Rotate - - identifier: - - U - name: Lovense Lapis - messages: - ScalarCmd: - - StepRange: [0, 20] - ActuatorType: Vibrate - FeatureDescriptor: Tip Vibe - - StepRange: [0, 20] - ActuatorType: Vibrate - FeatureDescriptor: Internal Vibe - - StepRange: [0, 20] - ActuatorType: Vibrate - FeatureDescriptor: External Vibe - SensorReadCmd: - - FeatureDescriptor: Battery Level - SensorType: Battery - SensorRange: [[0, 100]] - - identifier: - - SD - name: Lovense Vulse - - identifier: - - H - name: Lovense Solace - messages: - ScalarCmd: - - StepRange: [0, 20] - ActuatorType: Oscillate - FeatureDescriptor: Stroker Oscillation Speed - lovense-connect-service: - lovense-connect-service: - exists: true - defaults: - name: Lovense Connect Service Device - messages: - ScalarCmd: - - StepRange: [0, 20] - ActuatorType: Vibrate - SensorReadCmd: - - FeatureDescriptor: Battery Level - SensorType: Battery - SensorRange: [[0, 100]] - configurations: - # For lovense service, our identifiers are the device names as the service - # reports them. Note that if we're getting device info from the remote - # server, the names are all lower case. From the local server, they start - # with a capitalized letter. - - identifier: - - Max - name: Lovense Max - messages: - ScalarCmd: - - StepRange: [0, 20] - FeatureDescriptor: Vibrator - ActuatorType: Vibrate - - StepRange: [0, 3] - FeatureDescriptor: Air Pump - ActuatorType: Constrict - - identifier: - - Edge - name: Lovense Edge - messages: - ScalarCmd: - - StepRange: [0, 20] - ActuatorType: Vibrate - - StepRange: [0, 20] - ActuatorType: Vibrate - SensorReadCmd: - - FeatureDescriptor: Battery Level - SensorType: Battery - SensorRange: [[0, 100]] - - identifier: - - Nora - name: Lovense Nora - messages: - RotateCmd: - - StepRange: [0, 20] - ActuatorType: Rotate - SensorReadCmd: - - FeatureDescriptor: Battery Level - SensorType: Battery - SensorRange: [[0, 100]] - - identifier: - - Ambi - name: Lovense Ambi - - identifier: - - Lush - name: Lovense Lush - - identifier: - - Hush - name: Lovense Hush - - identifier: - - Domi - name: Lovense Domi - - identifier: - - Osci - name: Lovense Osci - - identifier: - - Mission - name: Lovense Mission - - identifier: - - Ferri - name: Lovense Ferri - - identifier: - - Diamo - name: Lovense Diamo - - identifier: - - ToyS - name: Loveai Dolp - - identifier: - - XMachine - name: Lovense Sex Machine - messages: - ScalarCmd: - - StepRange: [0, 20] - ActuatorType: Oscillate - FeatureDescriptor: Fucking Machine Oscillation Speed - - identifier: - - Dolce - name: Lovense Dolce - messages: - ScalarCmd: - - StepRange: [0, 20] - ActuatorType: Vibrate - - StepRange: [0, 20] - ActuatorType: Vibrate - SensorReadCmd: - - FeatureDescriptor: Battery Level - SensorType: Battery - SensorRange: [[0, 100]] - - identifier: - - Gush - name: Lovense Gush - - identifier: - - Hyphy - name: Lovense Hyphy - messages: - ScalarCmd: - - StepRange: [0, 20] - ActuatorType: Vibrate - - StepRange: [0, 20] - ActuatorType: Vibrate - SensorReadCmd: - - FeatureDescriptor: Battery Level - SensorType: Battery - SensorRange: [[0, 100]] - - identifier: - - Calor - name: Lovense Calor - - identifier: - - Flexer - name: Lovense Flexer - messages: - ScalarCmd: - # Over Connect, the Flexer's vibes cannot be independently controlled - - StepRange: [0, 20] - ActuatorType: Vibrate - FeatureDescriptor: Both Vibes - - StepRange: [0, 20] - ActuatorType: Rotate - FeatureDescriptor: Finger motion - SensorReadCmd: - - FeatureDescriptor: Battery Level - SensorType: Battery - SensorRange: [[0, 100]] - - identifier: - - Gemini - name: Lovense Gemini - messages: - ScalarCmd: - - StepRange: [0, 20] - ActuatorType: Vibrate - - StepRange: [0, 20] - ActuatorType: Vibrate - SensorReadCmd: - - FeatureDescriptor: Battery Level - SensorType: Battery - SensorRange: [[0, 100]] - - identifier: - - Gravity - name: Lovense Gravity - messages: - ScalarCmd: - - StepRange: [0, 20] - ActuatorType: Vibrate - - StepRange: [0, 20] - ActuatorType: Oscillate - SensorReadCmd: - - FeatureDescriptor: Battery Level - SensorType: Battery - SensorRange: [[0, 100]] - - identifier: - - Ridge - name: Lovense Ridge - messages: - RotateCmd: - - StepRange: [0, 20] - ActuatorType: Rotate - SensorReadCmd: - - FeatureDescriptor: Battery Level - SensorType: Battery - SensorRange: [[0, 100]] - - identifier: - - Lapis - name: Lovense Lapis - messages: - ScalarCmd: - - StepRange: [0, 20] - ActuatorType: Vibrate - FeatureDescriptor: Tip Vibe - - StepRange: [0, 20] - ActuatorType: Vibrate - FeatureDescriptor: Internal Vibe - - StepRange: [0, 20] - ActuatorType: Vibrate - FeatureDescriptor: External Vibe - SensorReadCmd: - - FeatureDescriptor: Battery Level - SensorType: Battery - SensorRange: [[0, 100]] - - identifier: - - Vulse - name: Lovense Vulse - - identifier: - - Solace - name: Lovense Solace - messages: - ScalarCmd: - - StepRange: [0, 20] - ActuatorType: Oscillate - FeatureDescriptor: Stroker Oscillation Speed - xinput: - # This will actually be ANY gamepad that supports XInput. XInput - # is its own connector type, so we don't have any special - # connection info here. - # - # TODO Maybe just start calling this "gamepad"? Maybe add "VR - # Controller" too? - # - # The specifier needs to be an object and have some content, but it - # doesn't matter what. - xinput: - exists: true - defaults: - name: XBox (XInput) Compatible Gamepad - messages: - ScalarCmd: - - StepRange: [0, 65535] - ActuatorType: Vibrate - - StepRange: [0, 65535] - ActuatorType: Vibrate - kiiroo-v2: - btle: - names: - - Launch - - Onyx2 - services: - 88f80580-0000-01e6-aace-0002a5d5c51b: - tx: 88f80581-0000-01e6-aace-0002a5d5c51b - rx: 88f80582-0000-01e6-aace-0002a5d5c51b - # The Launch has a special characteristic for loading - # firmware. - firmware: 88f80583-0000-01e6-aace-0002a5d5c51b - f60402a6-0293-4bdb-9f20-6758133f7090: - tx: 02962ac9-e86f-4094-989d-231d69995fc2 - rx: d44d0393-0731-43b3-a373-8fc70b1f3323 - # The Onyx2 has a special characteristic for loading - # firmware. - firmware: c7b7a04b-2cc4-40ff-8b10-5d531d1161db - defaults: - name: Kiiroo v2 Device - messages: - LinearCmd: - - StepRange: [0, 99] - ActuatorType: Position - FleshlightLaunchFW12Cmd: {} - configurations: - - identifier: - - Launch - name: Fleshlight Launch - - identifier: - - Onyx2 - name: Kiiroo Onyx 2 - libo-elle: - btle: - names: - - PiPiJing # Whale/ELLE - Shock Egg - - Shuidi # ELLE2 - Shock Egg - services: - # Write Service - 00006000-0000-1000-8000-00805f9b34fb: - tx: 00006001-0000-1000-8000-00805f9b34fb - txmode: 00006002-0000-1000-8000-00805f9b34fb - defaults: - name: Libo Elle Device - messages: - ScalarCmd: - - StepRange: [0, 3] # Vibe - ActuatorType: Vibrate - configurations: - - identifier: - - PiPiJing - name: LiBo Elle - - identifier: - - Shuidi - name: Libo Elle 2 - libo-shark: - btle: - names: - - ShaYu # Shark - Inflating Rabbit - services: - # Write Service - 00006000-0000-1000-8000-00805f9b34fb: - tx: 00006001-0000-1000-8000-00805f9b34fb - txmode: 00006002-0000-1000-8000-00805f9b34fb - defaults: - name: Libo Shark - messages: - ScalarCmd: - - StepRange: [0, 3] - ActuatorType: Vibrate - - StepRange: [0, 3] - ActuatorType: Vibrate - libo-karen: - btle: - names: - - SuoYinQiu # Karen - Kegel - services: - # Write Service - 00006000-0000-1000-8000-00805f9b34fb: - tx: 00006001-0000-1000-8000-00805f9b34fb - txmode: 00006002-0000-1000-8000-00805f9b34fb - # pressure - 00006050-0000-1000-8000-00805f9b34fb: - rxpressure: 00006051-0000-1000-8000-00805f9b34fb - defaults: - name: Libo Karen - messages: {} - libo-vibes: - btle: - names: - - XiaoLu # Lottie - Rabbit - - LuXiaoHan # LuLu - Egg - - BaiHu # LaLa - Suction Rabbit - - Gugudai # Carlos - Suction Chicken - - Yuyi # Lina - Leaf - - LuWuShuang # Adel - Curved Rabbit - - LiBo # Lily - Double ended mini wand - - QingTing # Lucy - Dragonfly egg - - Huohu # Lara/Sexy Fox - Rabbit - - Yuyi # Feather - - Haima # Selina - Suction Seahorse - services: - # Write Service - 00006000-0000-1000-8000-00805f9b34fb: - tx: 00006001-0000-1000-8000-00805f9b34fb - txmode: 00006002-0000-1000-8000-00805f9b34fb - defaults: - name: Libo Vibes Device - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - configurations: - - identifier: - - XiaoLu - name: Libo Lottie - - identifier: - - LuXiaoHan - name: Libo LuLu - - identifier: - - Yuyi - name: Libo Lina - - identifier: - - LuWuShuang - name: Libo Adel - - identifier: - - LiBo - name: Libo Lily - - identifier: - - QingTing - name: Libo Lucy - - identifier: - - Huohu - name: Libo Lara - - identifier: - - Yuyi - name: Libo Feather - messages: - ScalarCmd: - - StepRange: [0, 99] - ActuatorType: Vibrate - # Suction Vibes - - identifier: - - BaiHu - name: Libo LaLa - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 3] - ActuatorType: Vibrate - - identifier: - - Gugudai - name: Libo Carlos - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 3] - ActuatorType: Vibrate - - identifier: - - Haima - name: Libo Selina - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 3] - ActuatorType: Vibrate - magic-motion-1: - btle: - names: - - Smart Mini Vibe* - - Flamingo - - Flamingo T - - Smart Bean # Magic Kegel Twins/Master/Master V2 - - Smart Bean3 # FitCute Kegel Rejuve - - Magic Cell # Candy/Dante - - Magic Wand - - Fugu - - Fugu2 - - Gballs2 - - GBalls3 - - FM-LILAC-101 - - Xone - - CBT002 # FunTown Caleo - services: - 78667579-7b48-43db-b8c5-7928a6b0a335: - tx: 78667579-a914-49a4-8333-aa3c0cd8fedc - # Battery service - 0000180f-0000-1000-8000-00805f9b34fb: - rxblebattery: 00002a19-0000-1000-8000-00805f9b34fb - defaults: - name: Magic Motion V1 Device - messages: - SensorReadCmd: - - FeatureDescriptor: Battery Level - SensorType: Battery - SensorRange: [[0, 100]] - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - configurations: - - identifier: - - Smart Bean - name: MagicMotion Smart Bean - - identifier: - - Smart Bean3 - name: FitCute Kegel Rejuve - - identifier: - - Smart Mini Vibe - name: MagicMotion Smart Mini Vibe - - identifier: - - Smart Mini Vibe3 - name: MagicMotion Vini - - identifier: - - Flamingo - - Flamingo T - name: MagicMotion Flamingo - - identifier: - - Magic Bean - name: MagicMotion Kegel - - identifier: - - Magic Cell - name: MagicMotion Dante/Candy/Rise - - identifier: - - Magic Wand - name: MagicMotion Wand - - identifier: - - Magic Fugu - - Fugu - - Fugu2 - name: MagicMotion Fugu - - identifier: - - Gballs2 - name: G Vibe Gballs 2 - - identifier: - - GBalls3 - name: G Vibe Gballs 3 - - identifier: - - FM-LILAC-101 - name: Femometer Lilac - - identifier: - - Xone - name: MagicMotion Xone - messages: - ScalarCmd: - - StepRange: [ 0, 100 ] - ActuatorType: Oscillate - - identifier: - - CBT002 - name: FunTown Caleo - magic-motion-2: - btle: - names: - - Eidolon - - Lipstick # Awaken - - Sword # Equinox - - Curve # Solstice - - Solstice X - - funwand # Zenith - - CBT001 # FunTown Jive - services: - 78667579-7b48-43db-b8c5-7928a6b0a335: - tx: 78667579-a914-49a4-8333-aa3c0cd8fedc - # Battery service - 0000180f-0000-1000-8000-00805f9b34fb: - rxblebattery: 00002a19-0000-1000-8000-00805f9b34fb - defaults: - name: Magic Motion V2 Device - messages: - SensorReadCmd: - - FeatureDescriptor: Battery Level - SensorType: Battery - SensorRange: [[0, 100]] - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - configurations: - - identifier: - - Lipstick - name: MagicMotion Awaken - - identifier: - - Sword - name: MagicMotion Equinox - - identifier: - - Curve - name: MagicMotion Solstice - - identifier: - - Eidolon - name: MagicMotion Eidolon - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - Solstice X - name: MagicMotion Solstice X - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - funwand - name: MagicMotion Zenith - - identifier: - - CBT001 - name: FunTown Jive - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Oscillate - magic-motion-3: - btle: - names: - - Krush # Lovelife Krush - services: - 78667579-7b48-43db-b8c5-7928a6b0a335: - tx: 78667579-a914-49a4-8333-aa3c0cd8fedc - # Battery service - 0000180f-0000-1000-8000-00805f9b34fb: - rxblebattery: 00002a19-0000-1000-8000-00805f9b34fb - defaults: - name: LoveLife Krush - messages: - SensorReadCmd: - - FeatureDescriptor: Battery Level - SensorType: Battery - SensorRange: [[0, 100]] - ScalarCmd: - - StepRange: [0, 77] - ActuatorType: Vibrate - magic-motion-4: - btle: - names: - - funone # Bunny - - Magic Sundi - - Kegel Coach - - Magic Lotos - - nyx - - umi - - funkegel # Crystal - - bobi2 # Magic Bobi - services: - 78667579-7b48-43db-b8c5-7928a6b0a335: - tx: 78667579-a914-49a4-8333-aa3c0cd8fedc - # Battery service - 0000180f-0000-1000-8000-00805f9b34fb: - rxblebattery: 00002a19-0000-1000-8000-00805f9b34fb - defaults: - name: Magic Motion V4 Device - messages: - SensorReadCmd: - - FeatureDescriptor: Battery Level - SensorType: Battery - SensorRange: [[0, 100]] - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - configurations: - - identifier: - - funone - name: MagicMotion Bunny - - identifier: - - Magic Sundi - name: MagicMotion Sundae - - identifier: - - Kegel Coach - name: MagicMotion Kegel Coach - - identifier: - - Magic Lotos - name: MagicMotion Lotos - - identifier: - - nyx - name: MagicMotion Nyx - - identifier: - - umi - name: MagicMotion Umi - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - funkegel - name: MagicMotion Crystal - - identifier: - - bobi2 - name: MagicMotion Bobi - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - mysteryvibe: - btle: - names: - - MV Crescendo - - "MV Tenuto " - - "MV Poco " - services: - f0006900-110c-478b-b74b-6f403b364a9c: - txmode: f0006901-110c-478b-b74b-6f403b364a9c - txvibrate: f0006903-110c-478b-b74b-6f403b364a9c - defaults: - name: Mysteryvibe Device - messages: - ScalarCmd: - - StepRange: [0, 56] - ActuatorType: Vibrate - - StepRange: [0, 56] - ActuatorType: Vibrate - - StepRange: [0, 56] - ActuatorType: Vibrate - - StepRange: [0, 56] - ActuatorType: Vibrate - - StepRange: [0, 56] - ActuatorType: Vibrate - - StepRange: [0, 56] - ActuatorType: Vibrate - configurations: - - identifier: - - MV Crescendo - name: MysteryVibe Crescendo - - identifier: - - "MV Tenuto " - name: MysteryVibe Tenuto - - identifier: - - "MV Poco " - name: MysteryVibe Poco - messages: - ScalarCmd: - - StepRange: [0, 56] - ActuatorType: Vibrate - - StepRange: [0, 56] - ActuatorType: Vibrate - mysteryvibe-v2: - btle: - names: - - "6907 MV1" - - "6908 MV1" - - "6909 MV1" - - "6914 MV1" - - "6915 MV1" - services: - f0006900-110c-478b-b74b-6f403b364a9c: - txmode: f0006901-110c-478b-b74b-6f403b364a9c - txvibrate: f0006903-110c-478b-b74b-6f403b364a9c - defaults: - name: Mysteryvibe V2 Device - messages: - ScalarCmd: - - StepRange: [0, 56] - ActuatorType: Vibrate - - StepRange: [0, 56] - ActuatorType: Vibrate - - StepRange: [0, 56] - ActuatorType: Vibrate - configurations: - - identifier: - - "6907 MV1" - name: MysteryVibe Tenuto Mini - - identifier: - - "6908 MV1" - name: MysteryVibe Crescendo 2 - messages: - ScalarCmd: - - StepRange: [ 0, 56 ] - ActuatorType: Vibrate - - StepRange: [ 0, 56 ] - ActuatorType: Vibrate - - StepRange: [ 0, 56 ] - ActuatorType: Vibrate - - StepRange: [ 0, 56 ] - ActuatorType: Vibrate - - StepRange: [ 0, 56 ] - ActuatorType: Vibrate - - StepRange: [ 0, 56 ] - ActuatorType: Vibrate - - identifier: - - "6909 MV1" - name: MysteryVibe Tenuto 2 - messages: - ScalarCmd: - - StepRange: [ 0, 56 ] - ActuatorType: Vibrate - - StepRange: [ 0, 56 ] - ActuatorType: Vibrate - - StepRange: [ 0, 56 ] - ActuatorType: Vibrate - - StepRange: [ 0, 56 ] - ActuatorType: Vibrate - - identifier: - - "6914 MV1" - name: MysteryVibe Legato - messages: - ScalarCmd: - - StepRange: [ 0, 56 ] - ActuatorType: Vibrate - - StepRange: [ 0, 56 ] - ActuatorType: Vibrate - - StepRange: [ 0, 56 ] - ActuatorType: Vibrate - - StepRange: [ 0, 56 ] - ActuatorType: Vibrate - - identifier: - - "6915 MV1" - name: MysteryVibe Molto - messages: - ScalarCmd: - - StepRange: [ 0, 56 ] - ActuatorType: Vibrate - picobong: - btle: - names: - - Blow hole - - Picobong Male Toy - - Diver - - Picobong Egg - - Life guard - - Picobong Ring - - Surfer - - Picobong Butt Plug - - Egg driver - - Surfer_plug - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff1-0000-1000-8000-00805f9b34fb - defaults: - name: Picobong Device - messages: - ScalarCmd: - - StepRange: [0, 10] - ActuatorType: Vibrate - configurations: - - identifier: - - Blow hole - - Picobong Male Toy - name: Picobong Blow hole - - identifier: - - Diver - - Picobong Egg - name: Picobong Diver - - identifier: - - Life guard - - Picobong Ring - name: Picobong Life guard - - identifier: - - Surfer - - Picobong Butt Plug - - Egg driver - - Surfer_plug - name: Picobong Surfer - vibratissimo: - btle: - names: - - Vibratissimo - services: - 00001523-1212-efde-1523-785feabcd123: - txmode: 00001524-1212-efde-1523-785feabcd123 - txvibrate: 00001526-1212-efde-1523-785feabcd123 - rx: 00001527-1212-efde-1523-785feabcd123 - # Device info service - 0000180a-0000-1000-8000-00805f9b34fb: - rxblemodel: 00002a24-0000-1000-8000-00805f9b34fb - # Battery service - 0000180f-0000-1000-8000-00805f9b34fb: - rxblebattery: 00002a19-0000-1000-8000-00805f9b34fb - defaults: - name: Vibratissimo Device - messages: - SensorReadCmd: - - FeatureDescriptor: Battery Level - SensorType: Battery - SensorRange: [[0, 100]] - ScalarCmd: - - StepRange: [0, 255] - ActuatorType: Vibrate - configurations: - - identifier: - - Licker - - SecretKiss - - Womenizer - name: Vibratissimo Licker - messages: - SensorReadCmd: - - FeatureDescriptor: Battery Level - SensorType: Battery - SensorRange: [[0, 100]] - ScalarCmd: - - StepRange: [0, 255] - ActuatorType: Vibrate - - StepRange: [0, 255] - ActuatorType: Vibrate - - identifier: - - Rabbit - name: Vibratissimo Rabbit - messages: - SensorReadCmd: - - FeatureDescriptor: Battery Level - SensorType: Battery - SensorRange: [[0, 100]] - ScalarCmd: - - StepRange: [0, 255] - ActuatorType: Vibrate - - StepRange: [0, 255] - ActuatorType: Vibrate - - StepRange: [0, 02] - ActuatorType: Vibrate - wevibe: - btle: - names: - - Cougar # 4 Plus alias - - 4 Plus - - 4_Plus # 4 Plus alias - - 4plus # 4 Plus alias - - Bloom - - classic # 4 Plus alias - - Classic # 4 Plus alias - - Ditto - - Gala - - Jive - - Nova - - Pivot - - Rave - - Sync - - Verge - - Wish - services: - f000bb03-0451-4000-b000-000000000000: - tx: f000c000-0451-4000-b000-000000000000 - rx: f000b000-0451-4000-b000-000000000000 - defaults: - name: WeVibe Device - messages: - ScalarCmd: - - StepRange: [0, 15] - ActuatorType: Vibrate - configurations: - # Single Vibes - - identifier: - - Bloom - name: WeVibe Bloom - - identifier: - - Ditto - name: WeVibe Ditto - - identifier: - - Jive - name: WeVibe Jive - - identifier: - - Pivot - name: WeVibe Pivot - - identifier: - - Rave - name: WeVibe Rave - - identifier: - - Verge - name: WeVibe Verge - - identifier: - - Wish - name: WeVibe Wish - # Double Vibes - - identifier: - - Cougar - - 4 Plus - - 4_Plus - - 4plus - - classic - - Classic - name: WeVibe 4 Plus - messages: - ScalarCmd: - - StepRange: [0, 15] - ActuatorType: Vibrate - - StepRange: [0, 15] - ActuatorType: Vibrate - - identifier: - - Gala - name: WeVibe Gala - messages: - ScalarCmd: - - StepRange: [0, 15] - ActuatorType: Vibrate - - StepRange: [0, 15] - ActuatorType: Vibrate - - identifier: - - Nova - name: WeVibe Nova - messages: - ScalarCmd: - - StepRange: [0, 15] - ActuatorType: Vibrate - - StepRange: [0, 15] - ActuatorType: Vibrate - - identifier: - - Sync - name: WeVibe Sync - messages: - ScalarCmd: - - StepRange: [0, 15] - ActuatorType: Vibrate - - StepRange: [0, 15] - ActuatorType: Vibrate - wevibe-8bit: - btle: - names: - - Melt - - Moxie - - Vector - - Wand - - Bond - - Nelson # Bond alias - - Nova2 - - Nova_2 - - Nova 2 - services: - f000bb03-0451-4000-b000-000000000000: - tx: f000c000-0451-4000-b000-000000000000 - rx: f000b000-0451-4000-b000-000000000000 - defaults: - name: WeVibe 8-bit Device - messages: - ScalarCmd: - - StepRange: [0, 12] - ActuatorType: Vibrate - configurations: - - identifier: - - Melt - name: WeVibe Melt - messages: - ScalarCmd: - - StepRange: [0, 22] - ActuatorType: Vibrate - - identifier: - - Moxie - name: WeVibe Moxie - - identifier: - - Vector - name: WeVibe Vector - messages: - ScalarCmd: - - StepRange: [0, 12] - ActuatorType: Vibrate - - StepRange: [0, 12] - ActuatorType: Vibrate - - identifier: - - Wand - name: WeVibe Wand - messages: - ScalarCmd: - - StepRange: [0, 22] - ActuatorType: Vibrate - - identifier: - - Bond - - Nelson - name: WeVibe Bond - messages: - ScalarCmd: - - StepRange: [0, 27] - ActuatorType: Vibrate - - identifier: - - Nova2 - - Nova_2 - - Nova 2 - name: WeVibe Nova 2 - messages: - ScalarCmd: - - StepRange: [0, 27] - ActuatorType: Vibrate - - StepRange: [0, 27] - ActuatorType: Vibrate - wevibe-legacy: - btle: - names: - - Reina # Branded Realm - - imassager # Reina alias - - Interactive Massager # Reina alias - - "03" # Reina alias - services: - f000bb03-0451-4000-b000-000000000000: - tx: f000c000-0451-4000-b000-000000000000 - rx: f000b000-0451-4000-b000-000000000000 - defaults: - name: WeVibe Realm Reina - messages: {} - wevibe-chorus: - btle: - names: - - Chorus - - skeena - - Sync 2 - - Sync Lite - services: - f000bb03-0451-4000-b000-000000000000: - tx: f000c000-0451-4000-b000-000000000000 - rx: f000b000-0451-4000-b000-000000000000 - defaults: - name: WeVibe Chorus - messages: - ScalarCmd: - - StepRange: [0, 30] - ActuatorType: Vibrate - - StepRange: [0, 30] - ActuatorType: Vibrate - configurations: - - identifier: - - Sync 2 - name: WeVibe Sync 2 - messages: - ScalarCmd: - - StepRange: [0, 30] - ActuatorType: Vibrate - - StepRange: [0, 30] - ActuatorType: Vibrate - - identifier: - - Sync Lite - name: WeVibe Sync Lite - messages: - ScalarCmd: - - StepRange: [0, 30] - ActuatorType: Vibrate - youcups: - btle: - names: - - Youcups - services: - 0000fee9-0000-1000-8000-00805f9b34fb: - tx: d44bc439-abfd-45a2-b575-925416129600 - defaults: - name: Youcups Warrior II - messages: - ScalarCmd: - - StepRange: [0, 8] - ActuatorType: Vibrate - cueme: - btle: - names: - # Cueme names have the bluetooth address in the middle because - # sure. Why not. Of course they do. - - FUNCODE_* - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff1-0000-1000-8000-00805f9b34fb - defaults: - name: Cueme Device - messages: - ScalarCmd: - - StepRange: [0, 15] - ActuatorType: Vibrate - - StepRange: [0, 15] - ActuatorType: Vibrate - - StepRange: [0, 15] - ActuatorType: Vibrate - - StepRange: [0, 15] - ActuatorType: Vibrate - - StepRange: [0, 15] - ActuatorType: Vibrate - - StepRange: [0, 15] - ActuatorType: Vibrate - - StepRange: [0, 15] - ActuatorType: Vibrate - - StepRange: [0, 15] - ActuatorType: Vibrate - configurations: - - identifier: - - "1" - name: Cueme Mens - - identifier: - - "2" - name: Cueme Bra - - identifier: - - "3" - name: Cueme Womans - messages: - ScalarCmd: - - StepRange: [0, 15] - ActuatorType: Vibrate - - StepRange: [0, 15] - ActuatorType: Vibrate - - StepRange: [0, 15] - ActuatorType: Vibrate - - StepRange: [0, 15] - ActuatorType: Vibrate - kiiroo-v2-vibrator: - btle: - names: - - Pearl2 - - Fuse - - Virtual Blowbot - - Titan - - Virtual Rabbit - services: - 88f82580-0000-01e6-aace-0002a5d5c51b: - tx: 88f82581-0000-01e6-aace-0002a5d5c51b - rxtouch: 88f82582-0000-01e6-aace-0002a5d5c51b - rxaccel: 88f82584-0000-01e6-aace-0002a5d5c51b - defaults: - name: Kiiroo V2 Vibrator Device - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - configurations: - - identifier: - - Pearl2 - name: Kiiroo Pearl 2 - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - Fuse - name: OhMiBod Fuse - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - FeatureOrder: 1 - - StepRange: [0, 100] - ActuatorType: Vibrate - FeatureOrder: 0 - - identifier: - - Virtual Rabbit - name: PornHub Virtual Rabbit - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - FeatureOrder: 1 - - StepRange: [0, 100] - ActuatorType: Vibrate - FeatureOrder: 0 - - identifier: - - Virtual Blowbot - name: PornHub Virtual Blowbot - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - Titan - name: Kiiroo Titan - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - kiiroo-v21: - btle: - names: - - Titan1.1 - - Cliona - - Pearl2.1 - - Pearl2+ - - Pearl 2+ - - Pearl3 - - Pearl 3 - - OhMiBod 4.0 - - OhMiBod LUMEN - - OhMiBod NEX3 - - OhMiBod ESCA - - OhMiBod Foxy - - OhMiBod Chill Panty Vibe - - OhMiBod Sphinx - - Pulse Interactive - - Fuse1.1 - services: - 00001900-0000-1000-8000-00805f9b34fb: - # Used to clear the whitelist and read battery level - whitelist: 00001901-0000-1000-8000-00805f9b34fb - tx: 00001902-0000-1000-8000-00805f9b34fb - rx: 00001903-0000-1000-8000-00805f9b34fb - a0d70001-4c16-4ba7-977a-d394920e13a3: - tx: a0d70002-4c16-4ba7-977a-d394920e13a3 - rx: a0d70003-4c16-4ba7-977a-d394920e13a3 - defaults: - name: Kiiroo V2.1 Device - messages: {} - configurations: - - identifier: - - Pearl2.1 - name: Kiiroo Pearl 2.1 - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - SensorReadCmd: - - SensorType: Battery - FeatureDescriptor: Battery Level - SensorRange: [[0, 100]] - SensorSubscribeCmd: - - SensorType: Pressure - FeatureDescriptor: Pressure (analog) - SensorRange: [[0, 65535], [0, 65535], [0, 65535], [0, 65535]] - - SensorType: Button - FeatureDescriptor: Pressure (digital) - SensorRange: [[0, 1], [0, 1], [0, 1], [0, 1]] - - identifier: - - Cliona - name: Kiiroo Cliona - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - OhMiBod 4.0 - - OhMiBod ESCA - name: OhMiBod Esca 2 - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - Titan1.1 - name: Kiiroo Titan 1.1 - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - LinearCmd: - - StepRange: [0, 99] - ActuatorType: Position - FleshlightLaunchFW12Cmd: {} - - identifier: - - OhMiBod LUMEN - name: OhMiBod Lumen - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - OhMiBod NEX3 - name: hMiBod NEX|3 - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - Pulse Interactive - name: Hot Octopuss Pulse Solo Interactive - messages: - ScalarCmd: - - StepRange: [0, 6] - ActuatorType: Vibrate - - identifier: - - Fuse1.1 - name: OhMiBod Fuse 1.1 - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - # The external vive should be independently controllable? - #- StepRange: [0, 100] - # ActuatorType: Vibrate - - identifier: - - OhMiBod Foxy - name: OhMiBod Foxy - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - OhMiBod Chill Panty Vibe - name: OhMiBod Chill - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - OhMiBod Sphinx - name: OhMiBod Sphinx - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - Pearl2+ - - Pearl 2+ - name: Kiiroo Pearl 2+ - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - Pearl3 - - Pearl 3 - name: Kiiroo Pearl 3 - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - kiiroo-v21-initialized: - btle: - names: - - Rey # Branded Realm - - We-Vibe Rocketman # Rey alias - - Realm1.1 # Rey alias - - Onyx2.1 - - Onyx+ - - KEON - - Keon R2 - services: - 00001900-0000-1000-8000-00805f9b34fb: - # Used to clear the whitelist - whitelist: 00001901-0000-1000-8000-00805f9b34fb - tx: 00001902-0000-1000-8000-00805f9b34fb - rx: 00001903-0000-1000-8000-00805f9b34fb - defaults: - name: Kiiroo V2.1 Initialized Device - messages: { } - configurations: - - identifier: - - Onyx2.1 - name: Kiiroo Onyx 2.1 - messages: - LinearCmd: - - StepRange: [0, 99] - ActuatorType: Position - FleshlightLaunchFW12Cmd: { } - - identifier: - - Onyx+ - name: Kiiroo Onyx+ - messages: - LinearCmd: - - StepRange: [0, 99] - ActuatorType: Position - FleshlightLaunchFW12Cmd: { } - - identifier: - - KEON - - Keon R2 - name: Kiiroo Keon - messages: - LinearCmd: - - StepRange: [0, 99] - ActuatorType: Position - FleshlightLaunchFW12Cmd: { } - - identifier: - - Rey - - We-Vibe Rocketman - - Realm1.1 - name: Kiiroo Onyx+ Realm Edition - messages: - LinearCmd: - - StepRange: [0, 99] - ActuatorType: Position - FleshlightLaunchFW12Cmd: { } - vorze-cyclone-x: - hid: - - vendor-id: 0x0483 - product-id: 0x5750 - defaults: - name: Vorze Cyclone X10 Device - messages: - RotateCmd: - - StepRange: [0, 10] - ActuatorType: Rotate - rez-trancevibrator: - usb: - - vendor-id: 0xb49 - product-id: 0x064f - defaults: - name: Rez TranceVibrator - messages: - ScalarCmd: - - StepRange: [0, 255] - ActuatorType: Vibrate - kiiroo-v1: - btle: - names: - - ONYX - - PEARL - services: - 49535343-fe7d-4ae5-8fa9-9fafd205e455: - rx: 49535343-1e4d-4bd9-ba61-23c647249616 - tx: 49535343-8841-43f4-a8d4-ecbe34729bb3 - command: 49535343-aca3-481c-91ec-d85e28a60318 - # Not actually used at the moment, should be renamed to - # command2 if we ever use it (which, it's Kiiroo v1, so we - # probably won't.) - # - # cmd2: 49535343-6daa-4d02-abf6-19569aca69fe - defaults: - name: Kiiroo V1 Device - messages: {} - configurations: - - identifier: - - PEARL - name: Kiiroo Pearl - messages: - ScalarCmd: - - StepRange: [0, 4] - ActuatorType: Vibrate - - identifier: - - ONYX - name: Kiiroo Onyx - messages: - LinearCmd: - - StepRange: [0, 4] - ActuatorType: Position - vorze-sa: - btle: - names: - - Bach smart - - CycSA - - UFOSA - - UFO-TW - - VorzePiston - - ROCKET - services: - 40ee1111-63ec-4b7f-8ce7-712efd55b90e: - tx: 40ee2222-63ec-4b7f-8ce7-712efd55b90e - defaults: - name: Vorze Device - messages: {} - configurations: - - identifier: - - Bach smart - name: Vorze Bach - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - ROCKET - name: Adult Festa Rocket - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - CycSA - name: Vorze A10 Cyclone SA - messages: - RotateCmd: - - StepRange: [0, 99] - ActuatorType: Rotate - VorzeA10CycloneCmd: {} - - identifier: - - UFOSA - name: Vorze UFO SA - messages: - RotateCmd: - - StepRange: [0, 99] - ActuatorType: Rotate - VorzeA10CycloneCmd: {} - - identifier: - - UFO-TW - name: Vorze UFO TW - messages: - RotateCmd: - - StepRange: [0, 99] - ActuatorType: Rotate - - StepRange: [0, 99] - ActuatorType: Rotate - - identifier: - - VorzePiston - name: Vorze Piston - messages: - LinearCmd: - - StepRange: [0, 99] - ActuatorType: Position - youou: - btle: - names: - - VX001_* - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff6-0000-1000-8000-00805f9b34fb - defaults: - name: Youou Wand Vibrator - messages: - ScalarCmd: - - StepRange: [0, 255] - ActuatorType: Vibrate - realtouch: - hid: - - vendor-id: 0x1f54 - product-id: 0x0001 - defaults: - name: RealTouch - messages: - LinearCmd: - - StepRange: [0, 99] - ActuatorType: Position - prettylove: - btle: - names: - - Aogu BLE * - services: - 0000ffe5-0000-1000-8000-00805f9b34fb: - tx: 0000ffe9-0000-1000-8000-00805f9b34fb - rx: 0000ffe2-0000-1000-8000-00805f9b34fb - defaults: - name: Pretty Love Device - messages: - ScalarCmd: - - StepRange: [0, 3] - ActuatorType: Vibrate - svakom: - btle: - names: - - Aogu SUV # Unknown - - Aogu SCB # Ella - - Emma NEO - - Phoenix NEO - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - rx: 0000ffe2-0000-1000-8000-00805f9b34fb - defaults: - name: Svakom Device - messages: - ScalarCmd: - - StepRange: [0, 19] - ActuatorType: Vibrate - configurations: - - identifier: - - Aogu SCB - name: Svakom Ella - - identifier: - - Phoenix NEO - name: Svakom Phoenix Neo - - identifier: - - Emma NEO - name: Svakom Emma Neo - svakom-v2: - btle: - names: - - "116" # Phoenix NEO - - "117" # Edeny - - "118" # Vanesia - - Viviana - - Ella NEO - - S38A # Svakom Tammy Pro - - Vick NEO - - Vick Neo - - STG05A # Svakom Aravinda - - QH-SJ007A # Svakom Winni 2 - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - rx: 0000ffe2-0000-1000-8000-00805f9b34fb - defaults: - name: Svakom Device v2 - messages: - ScalarCmd: - - StepRange: [ 0, 10 ] - ActuatorType: Vibrate - configurations: - - identifier: - - "116" - name: Svakom Phoenix Neo - - identifier: - - Viviana - name: Svakom Viviana - # messages: - # ScalarCmd: - # - StepRange: [ 0, 10 ] - # ActuatorType: Vibrate - # - StepRange: [ 0, 5 ] - # ActuatorType: Vibrate - # FeatureDescriptor: Unsupported feature - - identifier: - - Ella NEO - name: Svakom Ella Neo - - identifier: - - "117" - name: Svakom Edeny - - identifier: - - S38A - name: Svakom Tammy Pro - - identifier: - - Vick NEO - - Vick Neo - name: Svakom Vick Neo - - identifier: - - STG05A - name: Svakom Aravinda - - identifier: - - "118" - name: ToyCod Vanesia - - identifier: - - QH-SJ007A - name: Svakom Winni 2 - svakom-v3: - btle: - names: - - Phoenix Neo 2 - - FK008A # Theodore - - Hannes NEO - - QH-SX007E # Alberta - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - rx: 0000ffe2-0000-1000-8000-00805f9b34fb - defaults: - name: Svakom Device v3 - messages: - ScalarCmd: - - StepRange: [ 0, 10 ] - ActuatorType: Vibrate - configurations: - - identifier: - - Phoenix Neo 2 - name: Svakom Phoenix Neo 2 - - identifier: - - FK008A - name: Fantasy Cup Theodore - messages: - ScalarCmd: - - StepRange: [ 0, 10 ] - ActuatorType: Vibrate - - StepRange: [ 0, 1 ] - ActuatorType: Rotate - # - StepRange: [ 0, 5 ] - # ActuatorType: Temperature - # FeatureDescriptor: Unsupported feature - - identifier: - - Hannes NEO - name: Svakom Hannes Neo - - identifier: - - QH-SX007E - name: Svakom Alberta - messages: - ScalarCmd: - - StepRange: [ 0, 10 ] - ActuatorType: Vibrate - FeatureDescriptor: Vibrating attachments - - StepRange: [ 0, 1 ] - ActuatorType: Vibrate - FeatureDescriptor: Suction lens - svakom-v4: - btle: - names: - - B2CM6 # Barzillai - - ERICA - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - rx: 0000ffe2-0000-1000-8000-00805f9b34fb - defaults: - name: Svakom Device v4 - messages: - ScalarCmd: - - StepRange: [ 0, 10 ] - ActuatorType: Vibrate - - StepRange: [ 0, 10 ] - ActuatorType: Vibrate - configurations: - - identifier: - - B2CM6 - name: ToyCod Barzillai - - identifier: - - ERICA - name: Svakom Erica - svakom-v5: - btle: - names: - - Chika - - Mora Neo - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - rx: 0000ffe2-0000-1000-8000-00805f9b34fb - defaults: - name: Svakom Device v5 - messages: - ScalarCmd: - - StepRange: [ 0, 10 ] - ActuatorType: Vibrate - - StepRange: [ 0, 10 ] - ActuatorType: Vibrate - configurations: - - identifier: - - Chika - name: Svakom Chika - - identifier: - - Mora Neo - name: Svakom Mora Neo - messages: - ScalarCmd: - - StepRange: [ 0, 10 ] - ActuatorType: Vibrate - - StepRange: [ 0, 10 ] - ActuatorType: Vibrate - - StepRange: [ 0, 3 ] - ActuatorType: Oscillate - svakom-sam: - btle: - names: - - Sam Neo - services: - 0000ae00-0000-1000-8000-00805f9b34fb: - tx: 0000ae01-0000-1000-8000-00805f9b34fb - rx: 0000ae02-0000-1000-8000-00805f9b34fb - txmode: 0000ae10-0000-1000-8000-00805f9b34fb - 0000ffac-0000-1000-8000-00805f9b34fb: - firmware: 0000ffb4-0000-1000-8000-00805f9b34fb - defaults: - name: Svakom Sam Neo - messages: - ScalarCmd: - - StepRange: [0, 10] - ActuatorType: Vibrate - - StepRange: [0, 1] - ActuatorType: Vibrate - svakom-alex: - btle: - names: - - Alex NEO - - S63E Alex NEO - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - defaults: - name: Svakom Alex Neo - messages: - ScalarCmd: - - StepRange: [0, 3] - ActuatorType: Vibrate - svakom-alex-v2: - btle: - names: - - Alex NEO 2 - - S63E Alex NEO 2 - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - defaults: - name: Svakom Alex Neo 2 - messages: - ScalarCmd: - - StepRange: [0, 3] - ActuatorType: Vibrate - svakom-dt250a: - btle: - names: - - DT250A - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - rx: 0000ffe2-0000-1000-8000-00805f9b34fb - defaults: - name: Coleur Dor DT250A - messages: - ScalarCmd: - - StepRange: [ 0, 3 ] - ActuatorType: Vibrate - - StepRange: [ 0, 3 ] - ActuatorType: Vibrate - - StepRange: [ 0, 2 ] - ActuatorType: Constrict - svakom-iker: - btle: - names: - - Iker* - manufacturer-data: - - company: 0x27 - data: [0x53, 0x56, 0x41, 0x01, 0x0B, 0x12, 0x01, 0x33, 0x44, 0x55, 0xCA, 0x08] - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - defaults: - name: Svakom Iker - messages: - ScalarCmd: - - StepRange: [0, 10] - ActuatorType: Vibrate - - StepRange: [0, 5] - ActuatorType: Vibrate - svakom-pulse: - btle: - names: - - SWK-SX013A # Pulse Lite Neo - - Pulse Union - - Pulse Galaxie - - SX033APP - - BX288A - - QH-SX045A-B - - SWK-SX067-B - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - rx: 0000ffe2-0000-1000-8000-00805f9b34fb - defaults: - name: Svakom Pulse Device - messages: - ScalarCmd: - - StepRange: [0, 9] - ActuatorType: Vibrate - configurations: - - identifier: - - SWK-SX013A - name: Svakom Pulse Lite Neo - - identifier: - - Pulse Union - name: Svakom Pulse Union - - identifier: - - Pulse Galaxie - name: Svakom Pulse Galaxie - - identifier: - - SX033APP - name: Svakom Mimiki - - identifier: - - BX288A - name: BeYourLover Kyukyu - - identifier: - - QH-SX045A-B - name: Coleur Dor VX045A - - identifier: - - SWK-SX067-B - name: Momonii Agatha - svakom-suitcase: - btle: - names: - - VX357A-BLE-V1.0 - - VX236A-BLE-V1.0 - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - rx: 0000ffe2-0000-1000-8000-00805f9b34fb - defaults: - name: Svakom Magic Suitcase - messages: - ScalarCmd: - - StepRange: [ 0, 30 ] - ActuatorType: Vibrate - - StepRange: [ 0, 1 ] - ActuatorType: Vibrate - configurations: - - identifier: - - VX236A-BLE-V1.0 - name: Coleur Dor VX236A - svakom-tarax: - btle: - names: - - SX218A - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - rx: 0000ffe2-0000-1000-8000-00805f9b34fb - defaults: - name: ToyCod Tara X - messages: - ScalarCmd: - - StepRange: [0, 3] - ActuatorType: Vibrate - FeatureDescriptor: Internal vibrator - - StepRange: [0, 3] - ActuatorType: Vibrate - FeatureDescriptor: External pulsator - svakom-avaneo: - btle: - names: - - SX218A - - Ava Neo - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - rx: 0000ffe2-0000-1000-8000-00805f9b34fb - defaults: - name: Svakom Ava Neo - messages: - ScalarCmd: - - StepRange: [ 0, 10 ] - ActuatorType: Vibrate - - StepRange: [ 0, 1 ] - ActuatorType: Oscillate - svakom-barnard: - btle: - names: - - DG239A - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - defaults: - name: Fantasy Cup Barnard - messages: - ScalarCmd: - - StepRange: [ 0, 3 ] - ActuatorType: Vibrate - - StepRange: [ 0, 3 ] - ActuatorType: Oscillate - realov: - btle: - names: - - REALOV_VIBE - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - defaults: - name: Realov Device - messages: - ScalarCmd: - - StepRange: [0, 50] - ActuatorType: Vibrate - motorbunny: - btle: - names: - - MB Controller - - MB LINK 201 - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff6-0000-1000-8000-00805f9b34fb - # Motorbunny has a WRITE/NOTIFY characteristic, meaning rx/tx are the - # same characteristic. Do we support this? Not really sure it matters - # since I don't think we get any data off notify for this anyways? - # - # rx: 0000fff6-0000-1000-8000-00805f9b34fb - defaults: - name: Motorbunny Device - messages: - ScalarCmd: - - StepRange: [0, 255] - ActuatorType: Vibrate - RotateCmd: - - StepRange: [0, 255] - ActuatorType: Rotate - configurations: - - identifier: - - MB Controller - name: Motorbunny Classic - - identifier: - - MB LINK 201 - name: Motorbunny Buck - zalo: - btle: - names: - - ZALO-Queen - - ZALO-King - - ZALO-Jeanne - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff1-0000-1000-8000-00805f9b34fb - defaults: - name: Zalo Device - messages: - ScalarCmd: - - StepRange: [0, 8] - ActuatorType: Vibrate - configurations: - - identifier: - - ZALO-Queen - name: Zalo Queen - messages: - ScalarCmd: - - StepRange: [0, 8] # Oscillator - ActuatorType: Vibrate - FeatureOrder: 1 - - StepRange: [0, 8] # Vibe - FeatureOrder: 0 - ActuatorType: Vibrate - - identifier: - - ZALO-King - name: Zalo King - messages: - ScalarCmd: - - StepRange: [0, 8] # Vibe - ActuatorType: Vibrate - - StepRange: [0, 8] # Oscillator - ActuatorType: Vibrate - - identifier: - - ZALO-Jeanne - name: Zalo Jeanne - sayberx: - btle: - names: - - SayberX - - X-Ring * - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff6-0000-1000-8000-00805f9b34fb - rx: 0000fff8-0000-1000-8000-00805f9b34fb - defaults: - name: SayberX Device - messages: {} - configurations: - - identifier: - - SayberX - name: SayberX - messages: - ScalarCmd: - - StepRange: [0, 4] - ActuatorType: Vibrate - - identifier: - - X-Ring - name: Sayber X-Ring - muse: - btle: - names: - - WB-ZDB-WST - - WB-TDD - services: - 0000aaa0-0000-1000-8000-00805f9b34fb: - tx: 0000aaa1-0000-1000-8000-00805f9b34fb - defaults: - name: Muse Device - messages: - ScalarCmd: - - StepRange: [0, 9] - ActuatorType: Vibrate - configurations: - - identifier: - - WB-ZDB-WST - name: Dream Lover Archer 2 - - identifier: - - WB-TDD - name: Galaku Panty Vib - lelo-f1s: - btle: - names: - - F1s - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff1-0000-1000-8000-00805f9b34fb - # Rx is hooked up to the button characteristic - # to help with the connection process - rx: 00000aa4-0000-1000-8000-00805f9b34fb - # There are a LOT of other sensor characteristics - # I figure we'll add them as support for those sensor - # types is added to Buttplug - defaults: - name: Lelo F1s - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - lelo-f1sv2: - btle: - names: - - F1SV2A - - F1SV2X - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff1-0000-1000-8000-00805f9b34fb - # whitelist is hooked up to the security characteristic - # which must be written to during the handshake - whitelist: 00000a10-0000-1000-8000-00805f9b34fb - # Rx is hooked up to the button characteristic - # to help with the connection process - rx: 00000a04-0000-1000-8000-00805f9b34fb - # There are a LOT of other sensor characteristics - # I figure we'll add them as support for those sensor - # types is added to Buttplug - defaults: - name: Lelo F1s V2 - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - lelo-harmony: - btle: - names: - - IdaWave - - Ida Wave - - TianiHarmony - - Tiani Harmony - - TOR3 - - Hugo2 - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - command: 0000fff1-0000-1000-8000-00805f9b34fb - tx: 0000fff2-0000-1000-8000-00805f9b34fb - # whitelist is hooked up to the security characteristic - # which must be written to during the handshake - whitelist: 00000a11-0000-1000-8000-00805f9b34fb - defaults: - name: Lelo Tiani Harmony - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - configurations: - - identifier: - - IdaWave - - Ida Wave - name: Lelo Ida Wave - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Rotate - - identifier: - - TOR3 - name: Lelo Tor 3 - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - Hugo2 - name: Lelo Hugo 2 - aneros: - btle: - names: - - Massage Demo - services: - 0000ff00-0000-1000-8000-00805f9b34fb: - tx: 0000ff01-0000-1000-8000-00805f9b34fb - defaults: - name: Aneros Vivi - messages: - ScalarCmd: - - StepRange: [0, 127] - FeatureDescriptor: Perineum Vibrator - ActuatorType: Vibrate - - StepRange: [0, 127] - FeatureDescriptor: Internal Vibrator - ActuatorType: Vibrate - lovehoney-desire: - btle: - names: - - PROSTATE VIBE - - KNICKER VIBE - - LOVE EGG - services: - 0000ff00-0000-1000-8000-00805f9b34fb: - tx: 0000ff01-0000-1000-8000-00805f9b34fb - defaults: - name: Lovehoney Device - messages: - ScalarCmd: - - StepRange: [0, 127] - ActuatorType: Vibrate - - StepRange: [0, 127] - ActuatorType: Vibrate - configurations: - - identifier: - - PROSTATE VIBE - name: Lovehoney Desire Prostate Vibrator - - identifier: - - KNICKER VIBE - name: Lovehoney Desire Knicker Vibrator - messages: - ScalarCmd: - - StepRange: [0, 127] - ActuatorType: Vibrate - - identifier: - - LOVE EGG - name: Lovehoney Desire Love Egg - messages: - ScalarCmd: - - StepRange: [0, 127] - ActuatorType: Vibrate - twerkingbutt: - btle: - names: - - BODIKANG - - Twerking Butt - - TwerkingButt - services: - 00000a60-0000-1000-8000-00805f9b34fb: - tx: 00000a66-0000-1000-8000-00805f9b34fb - rx: 00000a67-0000-1000-8000-00805f9b34fb - defaults: - name: Twerking Butt - messages: {} - # Vibration and Rotation protocols still need to be establised - # Twerk mode will be represented as a vibrator - maxpro: - btle: - names: - - M2 - services: - 6e400001-b5a3-f393-e0a9-e50e24dcca9e: - tx: 6e400002-b5a3-f393-e0a9-e50e24dcca9e - defaults: - name: MaxPro 2 - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - nobra: - btle: - names: - - NobraControl* - services: - 0000abf0-0000-1000-8000-00805f9b34fb: - tx: 0000abf1-0000-1000-8000-00805f9b34fb - serial: - - port: default - baud-rate: 19200 - data-bits: 8 - parity: N - stop-bits: 1 - defaults: - name: Nobra's Silicone Dreams Toy - messages: - ScalarCmd: - - StepRange: [0, 15] - ActuatorType: Vibrate - thehandy: - btle: - names: - - The Handy - services: - 1775244d-6b43-439b-877c-060f2d9bed07: - # This is 'prov-session' in protocomm, required for establishing the - # connection session. - firmware: 1775ff51-6b43-439b-877c-060f2d9bed07 - # This is both the main tx AND rx. God damnit, handy. - tx: 1775ff55-6b43-439b-877c-060f2d9bed07 - # There are 5 other characteristics, all of which seem to be unused. - defaults: - name: The Handy - messages: - LinearCmd: - - StepRange: [0, 100] - ActuatorType: Position - FleshlightLaunchFW12Cmd: {} - cachito: - btle: - names: - - CCTSK - - CCTXueGao - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff2-0000-1000-8000-00805f9b34fb - defaults: - name: Cachito Device - messages: - ScalarCmd: - - StepRange: [0, 5] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - configurations: - - identifier: - - CCTSK - name: Cachito Lure Tao - - identifier: - - CCTXueGao - name: Cachito Ice Cream - jejoue: - btle: - names: - - Je Joue - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff1-0000-1000-8000-00805f9b34fb - defaults: - name: Je Joue Device - messages: - ScalarCmd: - - StepRange: [0, 5] - ActuatorType: Vibrate - - StepRange: [0, 5] - ActuatorType: Vibrate - lovenuts: - btle: - names: - - Love_Nuts - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff1-0000-1000-8000-00805f9b34fb - defaults: - name: Love Nut - messages: - ScalarCmd: - - StepRange: [0, 15] - ActuatorType: Vibrate - patoo: - btle: - names: - - PTVEA* # Carrot - - PBT* # Devil - - PCS* # Vibrator - - PHT* # Bean Sprout - services: - f000aa64-0451-4000-b000-000000000000: - txmode: f000aa65-0451-4000-b000-000000000000 - tx: f000aa68-0451-4000-b000-000000000000 - defaults: - name: Patoo Device - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - configurations: - - identifier: - - PTVEA - name: Patoo Carrot - - identifier: - - PCS - name: Patoo Vibrator - - identifier: - - PHT - name: Patoo Bean Sprout - - identifier: - - PBT - name: Patoo Devil - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - tcode-v03: - serial: - - port: default - baud-rate: 115200 - data-bits: 8 - parity: N - stop-bits: 1 - defaults: - name: TCode v0.3 (Single Linear Axis) - messages: - LinearCmd: - - StepRange: [0, 100] - ActuatorType: Position - FleshlightLaunchFW12Cmd: {} - fredorch: - btle: - names: - - YXlinksSPP - services: - 0000ffb0-0000-1000-8000-00805f9b34fb: - tx: 0000ffb1-0000-1000-8000-00805f9b34fb - rx: 0000ffb2-0000-1000-8000-00805f9b34fb - defaults: - name: Fredorch Device - messages: - LinearCmd: - - StepRange: [0, 150] - ActuatorType: Position - FleshlightLaunchFW12Cmd: {} - fredorch-rotary: - btle: - names: - - M1_* - services: - 0000ae10-0000-1000-8000-00805f9b34fb: - tx: 0000ae01-0000-1000-8000-00805f9b34fb - rx: 0000ae02-0000-1000-8000-00805f9b34fb - defaults: - name: Fredorch Rotary Device - messages: - ScalarCmd: - - StepRange: [0, 20] - ActuatorType: Oscillate - FeatureDescriptor: Fucking Machine Oscillation Speed - mizzzee: - btle: - names: - - NFY008 - services: - 0000eea0-0000-1000-8000-00805f9b34fb: - tx: 0000eea1-0000-1000-8000-00805f9b34fb - defaults: - name: Mizz Zee Device - messages: - ScalarCmd: - - StepRange: [0, 68] - ActuatorType: Vibrate - mizzzee-v2: - btle: - names: - - XHT - services: - 0000eea0-0000-1000-8000-00805f9b34fb: - tx: 0000ee01-0000-1000-8000-00805f9b34fb - defaults: - name: Mizz Zee Device - messages: - ScalarCmd: - - StepRange: [0, 68] - ActuatorType: Vibrate - mizzzee-v3: - btle: - names: - - XHTKJ - services: - 0000ff10-0000-1000-8000-00805f9b34fb: - tx: 0000ff12-0000-1000-8000-00805f9b34fb - defaults: - name: Mizz Zee Device - messages: - ScalarCmd: - - StepRange: [0, 1000] - ActuatorType: Vibrate - htk_bm: - btle: - names: - - HTK-BLE-BM001 - services: - 0000180f-0000-1000-8000-00805f9b34fb: - rxblebattery: 00002a19-0000-1000-8000-00805f9b34fb - 00001802-0000-1000-8000-00805f9b34fb: - tx: 00002a06-0000-1000-8000-00805f9b34fb - defaults: - name: HTK Breast Massager - messages: - ScalarCmd: - - StepRange: [0, 1] - ActuatorType: Vibrate - - StepRange: [0, 1] - ActuatorType: Vibrate - ankni: - btle: - names: - - DSJM - services: - 0000fe00-0000-1000-8000-00805f9b34fb: - tx: 0000fe01-0000-1000-8000-00805f9b34fb - 0000fffe-0000-1000-8000-00805f9b34fb: - tx: 0000fe02-0000-1000-8000-00805f9b34fb - 0000180a-0000-1000-8000-00805f9b34fb: - generic0: 00002a50-0000-1000-8000-00805f9b34fb - defaults: - name: Roselex Device - messages: - ScalarCmd: - - StepRange: [0, 3] - ActuatorType: Vibrate - hgod: - btle: - names: - - AMN NEO - services: - 0000ffe3-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - rx: 0000ffe2-0000-1000-8000-00805f9b34fb - defaults: - name: Hgod Device - messages: - ScalarCmd: - - StepRange: [0, 10] - ActuatorType: Vibrate - lovedistance: - btle: - names: - - REACH G - - REACH - - MAG - - SPAN - - RANGE - - ORBIT - - JOIN G - - LINK - - GRASP - - RECEIVE - services: - 0000ff00-0000-1000-8000-00805f9b34fb: - tx: 0000ff01-0000-1000-8000-00805f9b34fb - rx: 0000ff02-0000-1000-8000-00805f9b34fb - defaults: - name: Love Distance Device - messages: - ScalarCmd: - - StepRange: [0, 121] - ActuatorType: Vibrate - configurations: - - identifier: - - REACH G - name: Love Distance Reach G - - identifier: - - REACH - name: Love Distance Reach - - identifier: - - MAG - name: Love Distance Mag - - identifier: - - SPAN - name: Love Distance Span - - identifier: - - RANGE - name: Love Distance Range - - identifier: - - ORBIT - name: Love Distance Range - - identifier: - - JOIN G - name: Love Distance Join G - - identifier: - - LINK - name: Love Distance Link - - identifier: - - GRASP - name: Love Distance Grasp - - identifier: - - RECEIVE - name: Love Distance Receive - satisfyer: - btle: - names: - - SF * - manufacturer-data: - - company: 0x5D - data: [0x00, 0x00, 0x27] - - company: 0x5D - data: [0x00, 0x00, 0x28] - services: - 0000180a-0000-1000-8000-00805f9b34fb: - rxblemodel: 00002a24-0000-1000-8000-00805f9b34fb - 51361500-c5e7-47c7-8a6e-47ebc99d80e8: - command: 51361501-c5e7-47c7-8a6e-47ebc99d80e8 # Motor mode - tx: 51361502-c5e7-47c7-8a6e-47ebc99d80e8 # Motor level - defaults: - name: Satisfyer Device - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - configurations: - - identifier: - - "10005" - name: Satisfyer Hot Spot - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - # Heater? - - identifier: - - "10006" - name: Satisfyer Heated Affair - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - # Heater? - - identifier: - - "10007" - name: Satisfyer Big Heat - - identifier: - - "10008" - name: Satisfyer Heated Thrill - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - # Heater? - - identifier: - - "10009" - name: Satisfyer Hot Bunny - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - # Heater? - - identifier: - - "10010" - name: Satisfyer Heat Climax - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - # Heater? - - identifier: - - "10011" - name: Satisfyer Heat Climax+ - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - # Heater? - - identifier: - - "10012" - name: Satisfyer Hot Passion - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - # Heater? - - identifier: - - "10013" - name: Satisfyer Haute Couture+ - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] # Airpulse? - ActuatorType: Vibrate - - identifier: - - "10014" - name: Satisfyer High Fashion+ - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] # Airpulse? - ActuatorType: Vibrate - - identifier: - - "10015" - name: Satisfyer Prêt-à-porter+ - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] # Airpulse? - ActuatorType: Vibrate - - identifier: - - "10024" - - "10025" - name: Satisfyer Love Triangle - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] # Airpulse? - ActuatorType: Vibrate - - identifier: - - "10027" - - "10028" - name: Satisfyer Curvy 1+ - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] # Airpulse? - ActuatorType: Vibrate - - identifier: - - "10030" - - "10031" - name: Satisfyer Curvy 2+ - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] # Airpulse? - ActuatorType: Vibrate - - identifier: - - "10032" - name: Satisfyer Double Wand-er - - identifier: - - "10046" - - "10047" - - "10048" - name: Satisfyer Double Joy - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - "10049" - - "10050" - - "10051" - name: Satisfyer Double Fun - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - "10052" - - "10053" - - "10054" - name: Satisfyer Double Love - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - "10055" - name: Satisfyer Curvy 3+ - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] # Airpulse? - ActuatorType: Vibrate - - identifier: - - "10059" - - "10060" - - "10061" - name: Satisfyer Hot Lover - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - # Heater? - - identifier: - - "10062" - - "10063" - - "10064" - name: Satisfyer Mono Flex - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - "10065" - - "10066" - - "10067" - - "10068" - name: Satisfyer Double Flex - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - "10069" - - "10070" - - "10071" - name: Satisfyer Heat Wave - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - # Heater? - - identifier: - - "10072" - name: Satisfyer Little Secret - - identifier: - - "10073" - name: Satisfyer Sexy Secret - - identifier: - - "10074" - name: Satisfyer Strong One - - identifier: - - "10075" - name: Satisfyer Mighty One - - identifier: - - "10076" - name: Satisfyer Powerful One - - identifier: - - "10077" - name: Satisfyer Royal One - - identifier: - - "10078" - name: Satisfyer Signet Ring - - identifier: - - "10079" - - "10080" - name: Satisfyer Dual Love - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] # Airpulse? - ActuatorType: Vibrate - - identifier: - - "10081" - - "10082" - name: Satisfyer Dual Pleasure - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] # Airpulse? - ActuatorType: Vibrate - - identifier: - - "10090" - name: Satisfyer Hero+ - messages: - ScalarCmd: - - StepRange: [0, 100] # Airpulse? - ActuatorType: Vibrate - - identifier: - - "10091" - name: Satisfyer Knight+ - messages: - ScalarCmd: - - StepRange: [0, 100] # Airpulse? - ActuatorType: Vibrate - - identifier: - - "10092" - - "10093" - name: Satisfyer Newcomer+ - messages: - ScalarCmd: - - StepRange: [0, 100] # Airpulse? - ActuatorType: Vibrate - - identifier: - - "10100" - - "10101" - name: Satisfyer Plug-ilicious 1 - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - "10102" - - "10103" - - "10104" - name: Satisfyer Plug-ilicious 2 - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - "10105" - name: Satisfyer E-Love Foreplay - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - # - 100 e-amplitude - # - 100 e-pulsewidth - # - 100 e-frequency - - identifier: - - "10108" - name: Satisfyer E-Love G-Hunter - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - # - 100 e-amplitude - # - 100 e-pulsewidth - # - 100 e-frequency - - identifier: - - "10109" - name: Satisfyer E-Love G-Hunter+ - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - # - 100 e-amplitude - # - 100 e-pulsewidth - # - 100 e-frequency - - identifier: - - "10110" - name: Satisfyer E-Love G-Spotter - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - # - 100 e-amplitude - # - 100 e-pulsewidth - # - 100 e-frequency - - identifier: - - "10111" - name: Satisfyer E-Love G-Spotter+ - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - # - 100 e-amplitude - # - 100 e-pulsewidth - # - 100 e-frequency - - identifier: - - "10112" - name: Satisfyer E-Love Story - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - # - 100 e-amplitude - # - 100 e-pulsewidth - # - 100 e-frequency - - identifier: - - "10119" - - "10120" - - "10182" - name: Satisfyer Love Birds 1 - - identifier: - - "10121" - - "10122" - - "10123" - name: Satisfyer Love Birds 2 - - identifier: - - "10124" - - "10125" - - "10126" - name: Satisfyer Love Birds Vary - - identifier: - - "10127" - - "10128" - - "10129" - - "10201" - name: Satisfyer Ribbed Petal - - identifier: - - "10130" - - "10131" - - "10132" - - "10133" - name: Satisfyer Shiny Petal - - identifier: - - "10134" - - "10135" - - "10136" - - "10202" - name: Satisfyer Smooth Petal - - identifier: - - "10140" - name: Satisfyer Men Vibration+ - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - "10141" - name: Satisfyer Power Plug - - identifier: - - "10142" - - "10143" - name: Satisfyer Rotator Plug 1+ - messages: - ScalarCmd: - - StepRange: [0, 100] # Single direction rotator - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - "10144" - - "10145" - name: Satisfyer Rotator Plug 2+ - messages: - ScalarCmd: - - StepRange: [0, 100] # Single direction rotator - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - "10146" - - "10147" - name: Satisfyer Deep Diver - - identifier: - - "10148" - - "10149" - name: Satisfyer Sweet Seal - - identifier: - - "10150" - - "10151" - name: Satisfyer Trendsetter - - identifier: - - "10154" - - "10155" - - "10156" - name: Satisfyer Twirling Joy - - identifier: - - "10157" - - "10158" - name: Satisfyer Ultra Power Bullet 8 - - identifier: - - "10160" - - "10161" - - "10162" - name: Satisfyer Double Desire - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - "10163" - - "10164" - - "10165" - - "10166" - name: Satisfyer Double Lust - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - "10167" - name: Satisfyer Epic Duo - - identifier: - - "10168" - name: Satisfyer Pleasure Wand+ - - identifier: - - "10169" - - "10170" - - "10171" - name: Satisfyer Top Secret - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - "10172" - - "10173" - - "10174" - name: Satisfyer Top Secret+ - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - "10175" - - "10176" - name: Satisfyer Bullseye - - identifier: - - "10177" - - "10178" - - "10179" - name: Satisfyer Sunray - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] # Airpulse? - ActuatorType: Vibrate - - identifier: - - "10180" - - "10181" - name: Satisfyer Curvy Trinity 5+ - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] # Airpulse? - ActuatorType: Vibrate - - identifier: - - "10183" - - "10184" - name: Satisfyer Intensity Plug - - identifier: - - "10185" - name: Satisfyer Power Masturbator - - identifier: - - "10186" - - "10187" - name: Satisfyer Hug me - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - "10188" - name: Satisfyer Air Pump Bunny 5+ - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - # Pump? - - identifier: - - "10189" - name: Satisfyer Air Pump Vibrator 5+ - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - # Pump? - - identifier: - - "10190" - - "10191" - name: Satisfyer Threesome 4 - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - "10192" - name: Satisfyer G-Spot Flex 4+ - - identifier: - - "10193" - - "10194" - name: Satisfyer G-Spot Flex 5+ - - identifier: - - "10195" - name: Satisfyer Air Pump Booty 5+ - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - # Pump? - - identifier: - - "10196" - name: Satisfyer Pro+ Wave 4 - messages: - ScalarCmd: - - StepRange: [0, 100] # Wave? - ActuatorType: Vibrate - - StepRange: [0, 100] # Airpulse? - ActuatorType: Vibrate - - identifier: - - "10197" - - "10198" - name: Satisfyer Mini Wand-er+ - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - "10199" - - "10200" - name: Satisfyer Tropical Tip - - identifier: - - "10203" - - "10204" - name: Satisfyer Twirling Pro+ - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] # Airpulse? - ActuatorType: Vibrate - - identifier: - - "10205" - name: Satisfyer Perfect Pair 4 - - identifier: - - "10206" - - "10207" - - "10208" - name: Satisfyer Booty Absolute Beginners 5 - - identifier: - - "10241" - - "10242" - name: Satisfyer Rrrolling Sensation - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] # Rolling? - ActuatorType: Vibrate - - identifier: - - "10307" - - "10308" - - "10309" - name: Satisfyer Pro 2 Gen 3 - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] # Airpulse? - ActuatorType: Vibrate - mannuo: - btle: - names: - - Sex toys - - Sex Toys - - LXCDVP - - MANO PRODUCT - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff1-0000-1000-8000-00805f9b34fb - rx: 0000fff4-0000-1000-8000-00805f9b34fb - defaults: - name: ManNuo Device - messages: - ScalarCmd: - - StepRange: [0, 3] - ActuatorType: Vibrate - kgoal-boost: - btle: - names: - - Boost - services: - 0000180f-0000-1000-8000-00805f9b34fb: - rxblebattery: 00002a19-0000-1000-8000-00805f9b34fb - 8e7c6065-7656-17ad-1b41-b53d1a548e0d: - rxpressure: 10c2be2d-d2d5-b7a8-5f42-e2468c9ebbf5 - defaults: - name: KGoal Boost - messages: - SensorReadCmd: - - SensorType: Battery - FeatureDescriptor: Battery Level - SensorRange: [[0, 100]] - SensorSubscribeCmd: - - SensorType: Pressure - FeatureDescriptor: Pelvic Pressure (Normalized) - SensorRange: [[0, 1000]] - - SensorType: Pressure - FeatureDescriptor: Pelvic Pressure (Unnormalized) - SensorRange: [[0, 1000]] - meese: - btle: - names: - - Meese-V389 - - Meese-cd - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - defaults: - name: Meese Device - messages: - ScalarCmd: - - StepRange: [0, 10] - ActuatorType: Vibrate - - StepRange: [0, 3] - ActuatorType: Vibrate - configurations: - - identifier: - - Meese-V389 - name: Meese Tera - - identifier: - - Meese-cd - name: Meese Modo - messages: - ScalarCmd: - - StepRange: [0, 10] - ActuatorType: Vibrate - hismith: - btle: - names: - - HISMITH - - Wildolo - - "\u0007HISMITH" - services: - 0000ffe5-0000-1000-8000-00805f9b34fb: - tx: 0000ffe9-0000-1000-8000-00805f9b34fb - 0000ff90-0000-1000-8000-00805f9b34fb: - rxblemodel: 0000ff96-0000-1000-8000-00805f9b34fb - defaults: - name: Hismith device - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Oscillate - FeatureDescriptor: Fucking Machine Oscillation Speed - configurations: - - identifier: - - "1001" - name: Hismith Sex Machine - - identifier: - - "1002" - name: Hismith Pro Traveler - - identifier: - - "1003" - name: Hismith Capsule - - identifier: - - "2001" - name: Hismith Thrusting Cup - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Oscillate - FeatureDescriptor: Stroker Oscillation Speed - - StepRange: [0, 1] - ActuatorType: Vibrate # Vibrator - - identifier: - - "3001" - name: Wildolo Device - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - hismith-mini: - btle: - names: - - Auxfun-Box - - Sinloli - - Sinloli-Sherry - - Eropair * - - HISMITH S1 - services: - 0000ffe5-0000-1000-8000-00805f9b34fb: - tx: 0000ffe9-0000-1000-8000-00805f9b34fb - 0000ff90-0000-1000-8000-00805f9b34fb: - rxblemodel: 0000ff96-0000-1000-8000-00805f9b34fb - defaults: - name: Hismith Mini device - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Oscillate - FeatureDescriptor: Fucking Machine Oscillation Speed - configurations: - - identifier: - - "4001" - name: Auxfun Sex Machine - - identifier: - - "1005" - name: Hismith Sex Machine - - identifier: - - "2201" - name: Sinloli Automatic Sex Doll - messages: - ScalarCmd: - - StepRange: [0, 100] - FeatureDescriptor: Air Pump - ActuatorType: Constrict - - StepRange: [0, 100] - FeatureDescriptor: Vibrator - ActuatorType: Vibrate - - identifier: - - "3101" - name: Eropair Rabbit Vibrator - messages: - ScalarCmd: - - StepRange: [0, 100] - FeatureDescriptor: Internal Vibrator - ActuatorType: Vibrate - - StepRange: [0, 100] - FeatureDescriptor: External Vibrator - ActuatorType: Vibrate - - identifier: - - "3102" - name: Eropair Thrusting Vibrating Dildo - messages: - ScalarCmd: - - StepRange: [0, 100] - FeatureDescriptor: Thruster - ActuatorType: Oscillate - - StepRange: [0, 100] - FeatureDescriptor: Vibrator - ActuatorType: Vibrate - - identifier: - - "2101" - name: Eropair Cup - messages: - ScalarCmd: - - StepRange: [0, 100] - FeatureDescriptor: Air Pump - ActuatorType: Constrict - - StepRange: [0, 100] - FeatureDescriptor: Vibrator - ActuatorType: Vibrate - hismith-servo: - btle: - names: - - HISMITH S2 - services: - 0000ffe5-0000-1000-8000-00805f9b34fb: - tx: 0000ffe9-0000-1000-8000-00805f9b34fb - 0000ff90-0000-1000-8000-00805f9b34fb: - rxblemodel: 0000ff96-0000-1000-8000-00805f9b34fb - defaults: - name: Hismith servo device - messages: - LinearCmd: - - StepRange: [0, 100] - ActuatorType: Position - FeatureDescriptor: Fucking Machine Position - configurations: - - identifier: - - "1101" - name: Hismith Servo - wetoy: - btle: - names: - - WeToy - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff3-0000-1000-8000-00805f9b34fb - defaults: - name: WeToy MiNa - messages: - ScalarCmd: - - StepRange: [0, 3] - ActuatorType: Vibrate - pink_punch: - btle: - names: - - Pink_Punch - - PinkPunch_Peachu - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - defaults: - name: Pink Punch Sunset Mushroom - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - configurations: - - identifier: - - PinkPunch_Peachu - name: Pink Punch Peachu - sakuraneko: - btle: - names: - - sakuraneko-01 - - sakuraneko-02 - - sakuraneko-03 - - sakuraneko-04 - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - defaults: - name: Sakuraneko Device - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - configurations: - - identifier: - - sakuraneko-01 - name: Sakuraneko Korokoro - - identifier: - - sakuraneko-02 - name: Sakuraneko Nukunuku - - identifier: - - sakuraneko-03 - name: Sakuraneko Dokidoki - - identifier: - - sakuraneko-04 - name: Sakuraneko Koikoi - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Rotate - synchro: - btle: - names: - - Shinkuro - - synchro2 - - synchro EX - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - defaults: - name: Synchro - messages: - RotateCmd: - - StepRange: [0, 6] - ActuatorType: Rotate - configurations: - - identifier: - - synchro EX - name: Synchro Exchange - tryfun: - btle: - names: - - TRYFUN-ONE - services: - 0000ff10-0000-1000-8000-00805f9b34fb: - tx: 0000fff1-0000-1000-8000-00805f9b34fb - defaults: - name: TryFun Yuan Series - messages: - ScalarCmd: - - StepRange: [0, 9] - ActuatorType: Oscillate - - StepRange: [0, 9] - ActuatorType: Rotate - metaxsire: - btle: - names: - - Rex - - Cali - - Olis - - LY213A01 - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - rx: 0000ffe2-0000-1000-8000-00805f9b34fb - defaults: - name: metaXsire Device - messages: - ScalarCmd: - - StepRange: [0, 255] - ActuatorType: Vibrate - configurations: - - identifier: - - Rex - name: metaXsire Rex - - identifier: - - Cali - name: metaXsire Cali - messages: - ScalarCmd: - - StepRange: [0, 255] - ActuatorType: Vibrate - - StepRange: [0, 255] - ActuatorType: Constrict # More like "suck" - - identifier: - - Olis - name: metaXsire Olis - messages: - ScalarCmd: - - StepRange: [0, 255] - ActuatorType: Vibrate - - StepRange: [0, 255] - ActuatorType: Vibrate - - StepRange: [0, 255] - ActuatorType: Rotate - - identifier: - - LY213A01 - name: metaXsire BuCUE - messages: - ScalarCmd: - - StepRange: [0, 255] - ActuatorType: Oscillate - - StepRange: [0, 255] - ActuatorType: Vibrate - metaxsire-repeat: - btle: - names: - - LY199B01 - - LY234A01 - - LY271A01 - - LY270A01 - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - rx: 0000ffe2-0000-1000-8000-00805f9b34fb - defaults: - name: Cooxer Bullet Vibe - messages: - ScalarCmd: - - StepRange: [0, 255] - ActuatorType: Vibrate - configurations: - - identifier: - - LY199B01 - name: Cooxer Bullet Vibe - - identifier: - - LY234A01 - name: metaXsire Tadpole - - identifier: - - LY271A01 - name: metaXsire Upton - - identifier: - - LY270A01 - name: metaXsire Una - metaxsire-v2: - btle: - names: - - LY272A01 - services: - 0000bae0-0000-1000-8000-00805f9b34fb: - tx: 0000bae1-0000-1000-8000-00805f9b34fb - defaults: - name: metaXsire Nolan - messages: - ScalarCmd: - - StepRange: [0, 20] - ActuatorType: Vibrate - - StepRange: [0, 20] - ActuatorType: Oscillate - metaxsire-v3: - btle: - names: - - TAY001 - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fe02-0000-1000-8000-00805f9b34fb - defaults: - name: metaXsire Tay - messages: - ScalarCmd: - - StepRange: [0, 20] - ActuatorType: Vibrate - metaxsire-v4: - btle: - names: - - CFG1 vibrator - services: - 0000cfa2-0000-1000-8000-00805f9b34fb: - tx: 0000cf21-0000-1000-8000-00805f9b34fb - defaults: - name: metaXsire G1 Vibrator - messages: - ScalarCmd: - - StepRange: [0, 99] - ActuatorType: Vibrate - cowgirl: - btle: - names: - - THE COWGIRL - - THE UNICORN - services: - 0000fe00-0000-1000-8000-00805f9b34fb: - tx: 0000fe01-0000-1000-8000-00805f9b34fb - defaults: - name: The Cowgirl Device - messages: - ScalarCmd: - - StepRange: [0, 255] - ActuatorType: Vibrate - - StepRange: [0, 255] - ActuatorType: Rotate - configurations: - - identifier: - - THE COWGIRL - name: The Cowgirl - - identifier: - - THE UNICORN - name: The Unicorn - galaku-pump: - btle: - names: - - V415 - services: - 00001000-0000-1000-8000-00805f9b34fb: - tx: 00001001-0000-1000-8000-00805f9b34fb - defaults: - name: Galaku Device - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Oscillate - - StepRange: [0, 100] - ActuatorType: Vibrate - configurations: - - identifier: - - V415 - name: Galaku Nebula - galaku: - btle: - names: - - EJX-Para - - GK03 - - GK10085 - - GS03 - - GS07 - - GS85 - - GS02 - - GS10 - - GS01 - - GS04 - - GS17 - - GS21 - - GS23 - - GS22 - - GS16 - - GS19 - - AK04 - - AS67 - - AS90 - - K020 - - GS25 - - GH28 - - GS28 - - LL18 - - GK23 - - GK27 - - G29B - - GA23 - - L26H - - GA25 - - GA26 - - GK22 - - GX85 - - GX07 - - GX17 - - GX21 - - GX33 - - GX22 - - GX16 - - GX29 - - GX23 - - GX26 - - GX36 - - GX39 - - GX25 - - G326 - - G335 - services: - 00001000-0000-1000-8000-00805f9b34fb: - tx: 00001001-0000-1000-8000-00805f9b34fb - rxblebattery: 00001002-0000-1000-8000-00805f9b34fb - defaults: - name: Galaku Device - messages: - ScalarCmd: - - StepRange: [ 0, 100 ] - ActuatorType: Vibrate - FeatureDescriptor: Vibrate - SensorReadCmd: - - FeatureDescriptor: Battery Level - SensorType: Battery - SensorRange: [ [ 0, 100 ] ] - xibao: - btle: - names: - - CCYB_* - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff2-0000-1000-8000-00805f9b34fb - defaults: - name: Xibao Smart Masturbation Cup - messages: - ScalarCmd: - - StepRange: [0, 99] - ActuatorType: Oscillate - sensee: - btle: - names: - - CTY222S4 - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff5-0000-1000-8000-00805f9b34fb - defaults: - name: Sensee Diandou Rabbit - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - sensee-capsule: - btle: - names: - - CCPA10S2 - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff5-0000-1000-8000-00805f9b34fb - defaults: - name: Sensee Capsule - messages: - ScalarCmd: - - StepRange: [ 0, 3 ] - ActuatorType: Vibrate - - StepRange: [ 0, 3 ] - ActuatorType: Constrict # More like "suck" - fox: - btle: - names: - - FOX - - FOX M70 Pro - - FoxM70Pro - services: - 0000ae00-0000-1000-8000-00805f9b34fb: - tx: 0000ae01-0000-1000-8000-00805f9b34fb - defaults: - name: Fox Device - messages: - ScalarCmd: - - StepRange: [0, 3] - ActuatorType: Vibrate - kizuna: - serial: - - port: default - baud-rate: 19200 - data-bits: 8 - parity: N - stop-bits: 1 - defaults: - name: Kizuna Smart - messages: - ScalarCmd: - - StepRange: [0, 9] - ActuatorType: Rotate - xiuxiuda: - btle: - names: - - XXD-Lush* - services: - 53300001-0023-4bd4-bbd5-a6920e4c5653: - tx: 53300003-0023-4bd4-bbd5-a6920e4c5653 - defaults: - name: Xiuxiuda Device - messages: - ScalarCmd: - - StepRange: [0, 19] - ActuatorType: Vibrate - longlosttouch: - btle: - names: - - RS-KNW - services: - 0000cb60-0000-1000-8000-00805f9b34fb: - tx: 0000cb61-0000-1000-8000-00805f9b34fb - rx: 0000cb62-0000-1000-8000-00805f9b34fb - defaults: - name: Long Lost Touch Possible Kiss - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Oscillate - adrienlastic: - # Welp, this was going to happen at some point right? - # They cloned the UUIDs and names of the Lush but not the protocol... - btle: - names: - - Placeholder to avoid conflict with bad attempt to clone a Lovense Lush - # - LVS-S001 - # - LVS-S002 - advertised-services: - - 00001320-0000-1000-8000-00805f9b34fb # Folove advertised service - services: - 6e400001-b5a3-f393-e0a9-e50e24dcca9e: - tx: 6e400002-b5a3-f393-e0a9-e50e24dcca9e - defaults: - name: Adrien Lastic Device - messages: - ScalarCmd: - - StepRange: [0, 16] - ActuatorType: Vibrate - configurations: - - identifier: - - LVS-S001 - name: Adrien Lastic Palpitation - - identifier: - - LVS-S002 - name: Adrien Lastic Revelation - nintendo-joycon: - hid: - # Joycon L - - vendor-id: 0x057e - product-id: 0x2007 - # Joycon R - - vendor-id: 0x057e - product-id: 0x2006 - # Pro Controller - - vendor-id: 0x057e - product-id: 0x2009 - defaults: - name: Nintendo Joycon - messages: - ScalarCmd: - - StepRange: [0, 1000] - ActuatorType: Vibrate - foreo: - btle: - names: - - FOFO - - LUNA fofo - - LUNA FOFO - - LUNA PLAY SMART - - LUNA PLAYSMART2 - - LUNA PLAY SMART2 - - LUNA play smart2 - - LUNA play smart 2 - - LUNA 3 - - LUNA3 - - LUNA3PLUS - - LUNA3 PLUS - - LUNA 3 PLUS - - LUNA 3 plus - - LUNA 3 MEN - - LUNA3MEN - - LUNA MINI3 - - LUNA MINI 3 - - LUNA mini 3 - - LUNA4PLUS - - LUNA4 - - LUNA 4 - - LUNA4PLUS - - LUNA4 PLUS - - LUNA 4 plus - - LUNA4MEN - - LUNA 4 MEN - - LUNA 4 FOR MEN - - LUNA MINI4 - - LUNA MINI 4 - - LUNA mini 4 - - LUNA 4 mini - - UFO - - UFO mini - - UFO MINI - - UFO MIN - - UFO2 - - UFO 2 - - UFOMINI2 - - UFO mini 2 - - UFO3 - - UFO3mini - - UFO3go - - UFO3led - - BEAR - - BEAR_MINI - - BEAR MINI - - BEAR mini - - BEAR2 - - BEAR 2 - - BEAR2go - - BEAR2body - - BEAR2eyes - - KIWI - - KIWI derma - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff1-0000-1000-8000-00805f9b34fb - defaults: - name: Foreo Device - messages: - ScalarCmd: - - StepRange: [0, 10] - ActuatorType: Vibrate - configurations: - - identifier: - - FOFO - - LUNA fofo - - LUNA FOFO - - LUNA PLAY SMART - name: Foreo LUNA fofo - - identifier: - - LUNA PLAYSMART2 - - LUNA PLAY SMART2 - - LUNA play smart2 - - LUNA play smart 2 - name: Foreo LUNA play smart 2 - - identifier: - - LUNA 3 - - LUNA3 - name: Foreo LUNA 3 - - identifier: - - LUNA3PLUS - - LUNA3 PLUS - - LUNA 3 PLUS - - LUNA 3 plus - name: Foreo LUNA 3 plus - - identifier: - - LUNA 3 MEN - - LUNA3MEN - name: Foreo LUNA 3 men - - identifier: - - LUNA MINI3 - - LUNA MINI 3 - - LUNA mini 3 - name: Foreo LUNA 3 mini - - identifier: - - LUNA4 - - LUNA 4 - name: Foreo LUNA 4 - - identifier: - - LUNA4PLUS - - LUNA4 PLUS - - LUNA 4 plus - name: Foreo LUNA 4 plus - - identifier: - - LUNA4MEN - - LUNA 4 MEN - - LUNA 4 FOR MEN - name: Foreo LUNA 4 men - - identifier: - - LUNA MINI4 - - LUNA MINI 4 - - LUNA mini 4 - - LUNA 4 mini - name: Foreo LUNA 4 mini - - identifier: - - UFO - name: Foreo UFO - - identifier: - - UFO mini - - UFO MINI - - UFO MIN - name: Foreo UFO mini - - identifier: - - UFO2 - - UFO 2 - name: Foreo UFO 2 - - identifier: - - UFO3 - name: Foreo UFO 3 - - identifier: - - UFO3go - name: Foreo UFO 3 go - - identifier: - - UFO3eyes - name: Foreo UFO 3 led - - identifier: - - UFO3mini - name: Foreo UFO 3 mini - - identifier: - - UFOMINI2 - - UFO mini 2 - name: Foreo UFO mini 2 - - identifier: - - BEAR - name: Foreo BEAR - - identifier: - - BEAR_MINI - - BEAR MINI - - BEAR mini - name: Foreo BEAR mini - - identifier: - - BEAR2 - - BEAR 2 - name: Foreo BEAR 2 - - identifier: - - BEAR2go - name: Foreo BEAR 2 go - - identifier: - - BEAR2eyes - name: Foreo BEAR 2 eyes - - identifier: - - BEAR2body - name: Foreo BEAR 2 body - - identifier: - - KIWI - name: Foreo KIWI - - identifier: - - KIWI derma - name: Foreo KIWI derma - monsterpub: - btle: - names: - - MonsterPub - services: - 00006000-0000-1000-8000-00805f9b34fb: - tx: 00006001-0000-1000-8000-00805f9b34fb - txmode: 00006002-0000-1000-8000-00805f9b34fb - txvibrate: 00006003-0000-1000-8000-00805f9b34fb - 00006010-0000-1000-8000-00805f9b34fb: - rxblemodel: 00006014-0000-1000-8000-00805f9b34fb - 00008000-0000-1000-8000-00805f9b34fb: - rx: 00008001-0000-1000-8000-00805f9b34fb - defaults: - name: Sistalk MonsterPub Device - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - configurations: - - identifier: - - MP2_JK_N_P1 - name: Sistalk MonsterPub 2 Doctor Whale - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - MP_MW_TL_P2 - name: Sistalk MonsterPub Magic Kiss - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - MP2_QC_TL_P1 - name: Sistalk MonsterPub 2 Mister Devil - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - MP_BABY_QC_N_P4 - name: Sistalk MonsterPub Baby Youth Health - messages: - ScalarCmd: - - StepRange: [0, 100] - ActuatorType: Vibrate - - StepRange: [0, 100] - ActuatorType: Vibrate - - identifier: - - MP_MXY_N_P1 - name: Sistalk MonsterPub KiniCat - - identifier: - - MP1N_QC_TL_P2 - name: Sistalk MonsterPub BeatHeart - joyhub: - btle: - names: - - J-Petalwish2 - - J-VortexTongue - - J-Velocity - - JOYHUB-ROSELLA2 - - J-VibSiren - - J-ElixirEgg - - J-RetroGuard - - J-TrueForm - - J-TrueForm3 - - J-Rhythmic2 - - J-Rhythmic3 - - J-Mysticolor - - J-VividWings - - J-Rainbow - - J-BlackBull - - J-Peacock - - J-Mariner - services: - 0000ffa0-0000-1000-8000-00805f9b34fb: - tx: 0000ffa1-0000-1000-8000-00805f9b34fb - defaults: - name: JoyHub Device - messages: - ScalarCmd: - - StepRange: [0, 255] - ActuatorType: Vibrate - configurations: - - identifier: - - JOYHUB-ROSELLA2 - name: JoyHub Rosella 2 - - identifier: - - J-Velocity - name: JoyHub Velocity - - identifier: - - J-ElixirEgg - name: JoyHub ElixirEgg - - identifier: - - J-RetroGuard - name: JoyHub Retro Guard - - identifier: - - J-TrueForm3 - name: JoyHub TrueForm 3 - - identifier: - - J-TrueForm - name: JoyHub TrueForm - - identifier: - - J-Rhythmic2 - name: JoyHub Rhythmic 2 - - identifier: - - J-Rhythmic3 - name: JoyHub Rhythmic 3 - - identifier: - - J-Rainbow - name: JoyHub Rainbow - - identifier: - - J-BlackBull - name: JoyHub Black Bull - - identifier: - - J-Peacock - name: JoyHub Peacock - - identifier: - - J-Petalwish2 - name: JoyHub Petalwish 2 - messages: - ScalarCmd: - - StepRange: [0, 255] - ActuatorType: Oscillate - - StepRange: [0, 255] - ActuatorType: Vibrate - - identifier: - - J-VortexTongue - name: JoyHub Vortex Tongue - messages: - ScalarCmd: - - StepRange: [0, 255] - ActuatorType: Vibrate - - StepRange: [0, 3] - FeatureDescriptor: Air Pump - ActuatorType: Constrict - - StepRange: [0, 255] - ActuatorType: Rotate - - identifier: - - J-VibSiren - name: JoyHub VibSiren - messages: - ScalarCmd: - - StepRange: [0, 255] - ActuatorType: Vibrate - FeatureDescriptor: External vibrator - - StepRange: [0, 255] - ActuatorType: Oscillate - - StepRange: [0, 255] - ActuatorType: Vibrate - FeatureDescriptor: Internal vibrator - - identifier: - - J-Mysticolor - name: JoyHub Mysticolor - messages: - ScalarCmd: - - StepRange: [0, 255] - ActuatorType: Rotate - - StepRange: [0, 7] - FeatureDescriptor: Air Pump - ActuatorType: Constrict - - identifier: - - J-VividWings - name: JoyHub Vivid Wings - messages: - ScalarCmd: - - StepRange: [0, 255] - ActuatorType: Vibrate - - StepRange: [0, 255] - ActuatorType: Oscillate - - identifier: - - J-Mariner - name: JoyHub Mariner - messages: - ScalarCmd: - - StepRange: [0, 255] - ActuatorType: Rotate - - StepRange: [0, 2] - FeatureDescriptor: Air Pump - ActuatorType: Constrict - joyhub-v2: - btle: - names: - - J-Pearlconch - - J-PetiteRose - - J-MoonHorn - - J-VibTrefoil - - J-Panther - - J-Mecha - - J-Lagoon - - J-Firedragon - - J-Dina - - J-Vbarbie3f - - J-CHERLY2c - - J-Pathfinder2 - - J-VibRipple - - J-Verax - - J-Verax2 - # - J-VeraxS - # - J-Verax2s - # - J-Verax3 - - J-Euphoric2 - - J-ROSEBUD - - J-Morningbuds2 - - J-Rhythmic4 - services: - 0000ffa0-0000-1000-8000-00805f9b34fb: - tx: 0000ffa1-0000-1000-8000-00805f9b34fb - defaults: - name: JoyHub Device - messages: - ScalarCmd: - - StepRange: [ 0, 255 ] - ActuatorType: Vibrate - configurations: - - identifier: - - J-Pearlconch - name: JoyHub Pearlconch - messages: - ScalarCmd: - - StepRange: [ 0, 255 ] - ActuatorType: Rotate - - StepRange: [ 0, 255 ] - ActuatorType: Vibrate - - identifier: - - J-Panther - name: JoyHub Panther - messages: - ScalarCmd: - - StepRange: [ 0, 255 ] - ActuatorType: Vibrate - - StepRange: [ 0, 255 ] - ActuatorType: Rotate - - identifier: - - J-PetiteRose - name: JoyHub Petite Rose - messages: - ScalarCmd: - - StepRange: [ 0, 255 ] - ActuatorType: Vibrate - - StepRange: [ 0, 255 ] - ActuatorType: Rotate - - identifier: - - J-MoonHorn - name: JoyHub Moon Horn - messages: - ScalarCmd: - - StepRange: [ 0, 255 ] - ActuatorType: Vibrate - - StepRange: [ 0, 9 ] - FeatureDescriptor: Suction - ActuatorType: Constrict - # - StepRange: [0, 1] - # ActuatorType: Heater - - identifier: - - J-Mecha - name: JoyHub Mecha - messages: - ScalarCmd: - - StepRange: [ 0, 255 ] - ActuatorType: Vibrate - - StepRange: [ 0, 7 ] - FeatureDescriptor: Suction - ActuatorType: Constrict - # - StepRange: [0, 1] - # ActuatorType: Heater - - identifier: - - J-Lagoon - name: JoyHub Lagoon - messages: - ScalarCmd: - - StepRange: [ 0, 255 ] - ActuatorType: Vibrate - - StepRange: [ 0, 5 ] - FeatureDescriptor: Suction - ActuatorType: Constrict - - identifier: - - J-VibTrefoil - name: JoyHub VibTrefoil - messages: - ScalarCmd: - - StepRange: [0, 255] - ActuatorType: Vibrate - FeatureDescriptor: External vibrator - - StepRange: [0, 255] - ActuatorType: Vibrate - FeatureDescriptor: Internal vibrator - - identifier: - - J-Firedragon - name: JoyHub Firedragon - messages: - ScalarCmd: - - StepRange: [0, 255] - ActuatorType: Oscillate - - StepRange: [0, 255] - ActuatorType: Vibrate - - identifier: - - J-Dina - name: JoyHub Deena - messages: - ScalarCmd: - - StepRange: [0, 255] - ActuatorType: Oscillate - - StepRange: [0, 255] - ActuatorType: Vibrate - FeatureDescriptor: Internal vibrator - - StepRange: [0, 255] - ActuatorType: Vibrate - FeatureDescriptor: External vibrator - - identifier: - - J-Vbarbie3f - name: JoyHub Cherly - messages: - ScalarCmd: - - StepRange: [0, 255] - ActuatorType: Vibrate - FeatureDescriptor: External vibrator - - StepRange: [0, 255] - ActuatorType: Vibrate - FeatureDescriptor: Internal vibrator - - StepRange: [0, 255] - ActuatorType: Oscillate - - identifier: - - J-CHERLY2c - name: JoyHub Cherly 2c - messages: - ScalarCmd: - - StepRange: [0, 255] - ActuatorType: Vibrate - FeatureDescriptor: Internal vibrator - - StepRange: [0, 255] - ActuatorType: Vibrate - FeatureDescriptor: Internal Whip - - StepRange: [0, 255] - ActuatorType: Vibrate - FeatureDescriptor: External vibrator - - identifier: - - J-Pathfinder2 - name: JoyHub Pathfinder 2 - messages: - ScalarCmd: - - StepRange: [0, 255] - ActuatorType: Oscillate - - StepRange: [0, 255] - ActuatorType: Vibrate - - identifier: - - J-VibRipple - name: JoyHub Angela - messages: - ScalarCmd: - - StepRange: [0, 255] - ActuatorType: Vibrate - FeatureDescriptor: External vibrator - - StepRange: [0, 255] - ActuatorType: Vibrate - FeatureDescriptor: Internal vibrator - - identifier: - - J-Verax - name: JoyHub Verax - messages: - ScalarCmd: - - StepRange: [0, 255] - ActuatorType: Vibrate - FeatureDescriptor: Internal Whip - - StepRange: [0, 255] - ActuatorType: Vibrate - FeatureDescriptor: Internal vibrator - - identifier: - - J-Verax2 - name: JoyHub Verax 2 - messages: - ScalarCmd: - - StepRange: [0, 255] - ActuatorType: Vibrate - - StepRange: [0, 255] - ActuatorType: Rotate - - identifier: - - J-Euphoric2 - name: JoyHub Euphoric 2 - messages: - ScalarCmd: - - StepRange: [0, 255] - ActuatorType: Oscillate - - StepRange: [0, 255] - ActuatorType: Vibrate - - identifier: - - J-ROSEBUD - name: JoyHub RoseBUD - messages: - ScalarCmd: - - StepRange: [0, 255] - ActuatorType: Vibrate - - StepRange: [0, 255] - ActuatorType: Rotate - FeatureDescriptor: Flicker - - StepRange: [0, 5] - FeatureDescriptor: Suction - ActuatorType: Constrict - - identifier: - - J-Morningbuds2 - name: JoyHub Morningbuds - messages: - ScalarCmd: - - StepRange: [ 0, 255 ] - ActuatorType: Rotate - - StepRange: [ 0, 255 ] - ActuatorType: Vibrate - - identifier: - - J-Rhythmic4 - name: JoyHub Rhythmic 4 - messages: - ScalarCmd: - - StepRange: [0, 255] - ActuatorType: Oscillate - - StepRange: [0, 255] - ActuatorType: Vibrate - joyhub-v3: - btle: - names: - - J-Ringstar - - J-RapidTwist2 - services: - 0000ffa0-0000-1000-8000-00805f9b34fb: - tx: 0000ffa1-0000-1000-8000-00805f9b34fb - defaults: - name: JoyHub Device - messages: - ScalarCmd: - - StepRange: [ 0, 255 ] - ActuatorType: Vibrate - configurations: - - identifier: - - J-Ringstar - name: JoyHub Starfish - - identifier: - - J-RapidTwist2 - name: JoyHub Resi Ring 2 - itoys: - btle: - names: - - 26-021-B - services: - 0000ffa0-0000-1000-8000-00805f9b34fb: - tx: 0000ffa1-0000-1000-8000-00805f9b34fb - defaults: - name: iToys Seagull - messages: - ScalarCmd: - - StepRange: [ 0, 3 ] - ActuatorType: Vibrate - leten: - btle: - names: - - T528-LT # antlers - - F537-LT # cup - - F520B-LT # clip + butterfly - - F520A-LT # long/butterfly - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff1-0000-1000-8000-00805f9b34fb - 0000ffe0-0000-1000-8000-00805f9b34fb: - rx: 0000ffe1-0000-1000-8000-00805f9b34fb - defaults: - name: Leten Device - messages: - ScalarCmd: - - StepRange: [ 0, 25 ] - ActuatorType: Vibrate - vibcrafter: - btle: - names: - - be gentle # Harlow - - Janna - - Hayden - - Nidalee - services: - 53300051-0060-4bd4-bbe5-a6920e4c5663: - tx: 53300052-0060-4bd4-bbe5-a6920e4c5663 - rx: 53300053-0060-4bd4-bbe5-a6920e4c5663 - defaults: - name: VibCrafter Device - messages: - ScalarCmd: - - StepRange: [ 0, 99 ] - ActuatorType: Vibrate - - StepRange: [ 0, 99 ] - ActuatorType: Vibrate - configurations: - - identifier: - - be gentle - name: VibCrafter Harlow - - identifier: - - Hayden - name: VibCrafter Hayden - - identifier: - - Nidalee - name: VibCrafter Nidalee - - identifier: - - Janna - name: VibCrafter Janna - messages: - ScalarCmd: - - StepRange: [ 0, 99 ] - ActuatorType: Vibrate - lioness: - btle: - names: - - Lioness - - Lioness2 - services: - d973f2ed-b19e-11e2-9e96-0800200c9a66: - tx: d973f2f4-b19e-11e2-9e96-0800200c9a66 - d973f2e5-b19e-11e2-9e96-0800200c9a66: - rx: d973f2e6-b19e-11e2-9e96-0800200c9a66 - defaults: - name: Lioness - messages: - ScalarCmd: - - StepRange: [ 0, 100 ] - ActuatorType: Vibrate diff --git a/buttplug/buttplug-device-config/device-config-v3/buttplug-device-config-v3.yml b/buttplug/buttplug-device-config/device-config-v3/buttplug-device-config-v3.yml deleted file mode 100644 index d9f7e4389..000000000 --- a/buttplug/buttplug-device-config/device-config-v3/buttplug-device-config-v3.yml +++ /dev/null @@ -1,11081 +0,0 @@ -version: - major: 3 - minor: 15 -protocols: - lovense: - defaults: - name: Lovense Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - configurations: - - identifier: - - B - name: Lovense Max - features: - - feature-type: Vibrate - description: Vibrator - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Constrict - description: Air Pump - actuator: - step-range: - - 0 - - 3 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - P - name: Lovense Edge - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - A - - C - name: Lovense Nora - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Rotate - actuator: - step-range: - - 0 - - 20 - messages: - - RotateCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - L - name: Lovense Ambi - - identifier: - - S - name: Lovense Lush - - identifier: - - Z - name: Lovense Hush - - identifier: - - W - name: Lovense Domi - - identifier: - - O - name: Lovense Osci - - identifier: - - V - name: Lovense Mission - - identifier: - - CA - name: Lovense Mission 2 - - identifier: - - X - name: Lovense Ferri - - identifier: - - R - name: Lovense Diamo - - identifier: - - ToyS - name: Loveai Dolp - - identifier: - - F - name: Lovense Sex Machine - features: - - feature-type: Oscillate - description: Fucking Machine Oscillation Speed - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - FS - name: Lovense Mini Sex Machine - features: - - feature-type: Oscillate - description: Fucking Machine Oscillation Speed - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - J - name: Lovense Dolce - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - OC - name: Lovense Osci 3 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - ED - name: Lovense Gush - - identifier: - - EZ - name: Lovense Gush 2 - - identifier: - - EB - name: Lovense Hyphy - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - T - name: Lovense Calor - - identifier: - - EI - name: Lovense Flexer (Firmware update needed) - - identifier: - - EI-FW3 - name: Lovense Flexer - features: - - feature-type: Vibrate - description: Internal Vibe - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Vibrate - description: External Vibe - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Rotate - description: Finger motion - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - 'N' - name: Lovense Gemini - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - EA - name: Lovense Gravity - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - Q - name: Lovense Tenera - - identifier: - - EL - name: Lovense Ridge - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Rotate - actuator: - step-range: - - 0 - - 20 - messages: - - RotateCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - U - name: Lovense Lapis - features: - - feature-type: Vibrate - description: Tip Vibe - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Internal Vibe - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Vibrate - description: External Vibe - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - SD - name: Lovense Vulse - - identifier: - - H - name: Lovense Solace - features: - - feature-type: Oscillate - description: Stroker Oscillation Speed - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - BA - name: Lovense Solace Pro - features: - - feature-type: Oscillate - description: Stroker Oscillation Speed - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Position - description: Stroker Position Based Movement - actuator: - step-range: - - 0 - - 100 - messages: - - LinearCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - communication: - - btle: - names: - - LVS-* - - LOVE-* - manufacturer-data: - - company: 620 - data: - - 255 - - 33 - advertised-services: - - 6e400001-b5a3-f393-e0a9-e50e24dcca9e - - 50300001-0024-4bd4-bbd5-a6920e4c5653 - - 57300001-0023-4bd4-bbd5-a6920e4c5653 - - 5a300001-0024-4bd4-bbd5-a6920e4c5653 - - 50300001-0023-4bd4-bbd5-a6920e4c5653 - - 53300001-0023-4bd4-bbd5-a6920e4c5653 - - 5a300001-0023-4bd4-bbd5-a6920e4c5653 - - 4f300001-0023-4bd4-bbd5-a6920e4c5653 - - 42300001-0023-4bd4-bbd5-a6920e4c5653 - - 43300001-0023-4bd4-bbd5-a6920e4c5653 - - 4c300001-0023-4bd4-bbd5-a6920e4c5653 - - 4c410001-0023-4bd4-bbd5-a6920e4c5653 - - 56300001-0023-4bd4-bbd5-a6920e4c5653 - - 58300001-0023-4bd4-bbd5-a6920e4c5653 - - 52300001-0023-4bd4-bbd5-a6920e4c5653 - - 46300001-0023-4bd4-bbd5-a6920e4c5653 - - 50300011-0023-4bd4-bbd5-a6920e4c5653 - - 4a300001-0023-4bd4-bbd5-a6920e4c5653 - - 45440001-0023-4bd4-bbd5-a6920e4c5653 - - 45420001-0023-4bd4-bbd5-a6920e4c5653 - - 54300001-0023-4bd4-bbd5-a6920e4c5653 - - 45490001-0023-4bd4-bbd5-a6920e4c5653 - - 4e300001-0023-4bd4-bbd5-a6920e4c5653 - - 45410001-0023-4bd4-bbd5-a6920e4c5653 - - 51300001-0023-4bd4-bbd5-a6920e4c5653 - - 45460001-0023-4bd4-bbd5-a6920e4c5653 - - 454c0001-0023-4bd4-bbd5-a6920e4c5653 - - 55300001-0023-4bd4-bbd5-a6920e4c5653 - - 53440001-0023-4bd4-bbd5-a6920e4c5653 - - 48300001-0023-4bd4-bbd5-a6920e4c5653 - - 46530001-0023-4bd4-bbd5-a6920e4c5653 - - 42410001-0023-4bd4-bbd5-a6920e4c5653 - - 43410001-0023-4bd4-bbd5-a6920e4c5653 - - 4f430001-0023-4bd4-bbd5-a6920e4c5653 - - 455a0001-0023-4bd4-bbd5-a6920e4c5653 - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff2-0000-1000-8000-00805f9b34fb - rx: 0000fff1-0000-1000-8000-00805f9b34fb - 6e400001-b5a3-f393-e0a9-e50e24dcca9e: - tx: 6e400002-b5a3-f393-e0a9-e50e24dcca9e - rx: 6e400003-b5a3-f393-e0a9-e50e24dcca9e - 50300001-0024-4bd4-bbd5-a6920e4c5653: - tx: 50300002-0024-4bd4-bbd5-a6920e4c5653 - rx: 50300003-0024-4bd4-bbd5-a6920e4c5653 - 57300001-0023-4bd4-bbd5-a6920e4c5653: - tx: 57300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 57300003-0023-4bd4-bbd5-a6920e4c5653 - 5a300001-0024-4bd4-bbd5-a6920e4c5653: - tx: 5a300002-0024-4bd4-bbd5-a6920e4c5653 - rx: 5a300003-0024-4bd4-bbd5-a6920e4c5653 - 50300001-0023-4bd4-bbd5-a6920e4c5653: - tx: 50300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 50300003-0023-4bd4-bbd5-a6920e4c5653 - 53300001-0023-4bd4-bbd5-a6920e4c5653: - tx: 53300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 53300003-0023-4bd4-bbd5-a6920e4c5653 - 5a300001-0023-4bd4-bbd5-a6920e4c5653: - tx: 5a300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 5a300003-0023-4bd4-bbd5-a6920e4c5653 - 4f300001-0023-4bd4-bbd5-a6920e4c5653: - tx: 4f300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 4f300003-0023-4bd4-bbd5-a6920e4c5653 - 42300001-0023-4bd4-bbd5-a6920e4c5653: - tx: 42300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 42300003-0023-4bd4-bbd5-a6920e4c5653 - 43300001-0023-4bd4-bbd5-a6920e4c5653: - tx: 43300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 43300003-0023-4bd4-bbd5-a6920e4c5653 - 4c300001-0023-4bd4-bbd5-a6920e4c5653: - tx: 4c300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 4c300003-0023-4bd4-bbd5-a6920e4c5653 - 4c410001-0023-4bd4-bbd5-a6920e4c5653: - tx: 4c410002-0023-4bd4-bbd5-a6920e4c5653 - rx: 4c410003-0023-4bd4-bbd5-a6920e4c5653 - 56300001-0023-4bd4-bbd5-a6920e4c5653: - tx: 56300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 56300003-0023-4bd4-bbd5-a6920e4c5653 - 58300001-0023-4bd4-bbd5-a6920e4c5653: - tx: 58300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 58300003-0023-4bd4-bbd5-a6920e4c5653 - 52300001-0023-4bd4-bbd5-a6920e4c5653: - tx: 52300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 52300003-0023-4bd4-bbd5-a6920e4c5653 - 46300001-0023-4bd4-bbd5-a6920e4c5653: - tx: 46300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 46300003-0023-4bd4-bbd5-a6920e4c5653 - 50300011-0023-4bd4-bbd5-a6920e4c5653: - tx: 50300012-0023-4bd4-bbd5-a6920e4c5653 - rx: 50300013-0023-4bd4-bbd5-a6920e4c5653 - 4a300001-0023-4bd4-bbd5-a6920e4c5653: - tx: 4a300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 4a300003-0023-4bd4-bbd5-a6920e4c5653 - 45440001-0023-4bd4-bbd5-a6920e4c5653: - tx: 45440002-0023-4bd4-bbd5-a6920e4c5653 - rx: 45440003-0023-4bd4-bbd5-a6920e4c5653 - 45420001-0023-4bd4-bbd5-a6920e4c5653: - tx: 45420002-0023-4bd4-bbd5-a6920e4c5653 - rx: 45420003-0023-4bd4-bbd5-a6920e4c5653 - 54300001-0023-4bd4-bbd5-a6920e4c5653: - tx: 54300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 54300003-0023-4bd4-bbd5-a6920e4c5653 - 45490001-0023-4bd4-bbd5-a6920e4c5653: - tx: 45490002-0023-4bd4-bbd5-a6920e4c5653 - rx: 45490003-0023-4bd4-bbd5-a6920e4c5653 - 4e300001-0023-4bd4-bbd5-a6920e4c5653: - tx: 4e300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 4e300003-0023-4bd4-bbd5-a6920e4c5653 - 45410001-0023-4bd4-bbd5-a6920e4c5653: - tx: 45410002-0023-4bd4-bbd5-a6920e4c5653 - rx: 45410003-0023-4bd4-bbd5-a6920e4c5653 - 51300001-0023-4bd4-bbd5-a6920e4c5653: - tx: 51300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 51300003-0023-4bd4-bbd5-a6920e4c5653 - 45460001-0023-4bd4-bbd5-a6920e4c5653: - tx: 45460002-0023-4bd4-bbd5-a6920e4c5653 - rx: 45460003-0023-4bd4-bbd5-a6920e4c5653 - 454c0001-0023-4bd4-bbd5-a6920e4c5653: - tx: 454c0002-0023-4bd4-bbd5-a6920e4c5653 - rx: 454c0003-0023-4bd4-bbd5-a6920e4c5653 - 55300001-0023-4bd4-bbd5-a6920e4c5653: - tx: 55300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 55300003-0023-4bd4-bbd5-a6920e4c5653 - 53440001-0023-4bd4-bbd5-a6920e4c5653: - tx: 53440002-0023-4bd4-bbd5-a6920e4c5653 - rx: 53440003-0023-4bd4-bbd5-a6920e4c5653 - 48300001-0023-4bd4-bbd5-a6920e4c5653: - tx: 48300002-0023-4bd4-bbd5-a6920e4c5653 - rx: 48300003-0023-4bd4-bbd5-a6920e4c5653 - 46530001-0023-4bd4-bbd5-a6920e4c5653: - tx: 46530002-0023-4bd4-bbd5-a6920e4c5653 - rx: 46530003-0023-4bd4-bbd5-a6920e4c5653 - 42410001-0023-4bd4-bbd5-a6920e4c5653: - tx: 42410002-0023-4bd4-bbd5-a6920e4c5653 - rx: 42410003-0023-4bd4-bbd5-a6920e4c5653 - 43410001-0023-4bd4-bbd5-a6920e4c5653: - tx: 43410002-0023-4bd4-bbd5-a6920e4c5653 - rx: 43410003-0023-4bd4-bbd5-a6920e4c5653 - 4f430001-0023-4bd4-bbd5-a6920e4c5653: # Osci 3 - tx: 4f430002-0023-4bd4-bbd5-a6920e4c5653 - rx: 4f430003-0023-4bd4-bbd5-a6920e4c5653 - 455a0001-0023-4bd4-bbd5-a6920e4c5653: # Gush 2 - tx: 455a0002-0023-4bd4-bbd5-a6920e4c5653 - rx: 455a0003-0023-4bd4-bbd5-a6920e4c5653 - lovense-connect-service: - defaults: - name: Lovense Connect Service Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - configurations: - - identifier: - - Max - name: Lovense Max - features: - - feature-type: Vibrate - description: Vibrator - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Constrict - description: Air Pump - actuator: - step-range: - - 0 - - 3 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - Edge - name: Lovense Edge - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - Nora - name: Lovense Nora - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Rotate - actuator: - step-range: - - 0 - - 20 - messages: - - RotateCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - Ambi - name: Lovense Ambi - - identifier: - - Lush - name: Lovense Lush - - identifier: - - Hush - name: Lovense Hush - - identifier: - - Domi - name: Lovense Domi - - identifier: - - Osci - name: Lovense Osci - - identifier: - - Mission - name: Lovense Mission - - identifier: - - Ferri - name: Lovense Ferri - - identifier: - - Diamo - name: Lovense Diamo - - identifier: - - ToyS - name: Loveai Dolp - - identifier: - - XMachine - name: Lovense Sex Machine - features: - - feature-type: Oscillate - description: Fucking Machine Oscillation Speed - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - Dolce - name: Lovense Dolce - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - Gush - name: Lovense Gush - - identifier: - - Hyphy - name: Lovense Hyphy - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - Calor - name: Lovense Calor - - identifier: - - Flexer - name: Lovense Flexer - features: - - feature-type: Vibrate - description: Both Vibes - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Rotate - description: Finger motion - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - Gemini - name: Lovense Gemini - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - Gravity - name: Lovense Gravity - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - Ridge - name: Lovense Ridge - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Rotate - actuator: - step-range: - - 0 - - 20 - messages: - - RotateCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - Lapis - name: Lovense Lapis - features: - - feature-type: Vibrate - description: Tip Vibe - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Internal Vibe - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Vibrate - description: External Vibe - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - Vulse - name: Lovense Vulse - - identifier: - - Solace - name: Lovense Solace - features: - - feature-type: Oscillate - description: Stroker Oscillation Speed - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - communication: - - lovense-connect-service: - exists: true - xinput: - defaults: - name: XBox (XInput) Compatible Gamepad - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 65535 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 65535 - messages: - - ScalarCmd - communication: - - xinput: - exists: true - kiiroo-v2: - defaults: - name: Kiiroo v2 Device - features: - - feature-type: Position - actuator: - step-range: - - 0 - - 99 - messages: - - LinearCmd - configurations: - - identifier: - - Launch - name: Fleshlight Launch - - identifier: - - Onyx2 - name: Kiiroo Onyx 2 - communication: - - btle: - names: - - Launch - - Onyx2 - services: - 88f80580-0000-01e6-aace-0002a5d5c51b: - tx: 88f80581-0000-01e6-aace-0002a5d5c51b - rx: 88f80582-0000-01e6-aace-0002a5d5c51b - firmware: 88f80583-0000-01e6-aace-0002a5d5c51b - f60402a6-0293-4bdb-9f20-6758133f7090: - tx: 02962ac9-e86f-4094-989d-231d69995fc2 - rx: d44d0393-0731-43b3-a373-8fc70b1f3323 - firmware: c7b7a04b-2cc4-40ff-8b10-5d531d1161db - libo-elle: - defaults: - name: Libo Elle Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 3 - messages: - - ScalarCmd - configurations: - - identifier: - - PiPiJing - name: LiBo Elle - - identifier: - - Shuidi - name: Libo Elle 2 - communication: - - btle: - names: - - PiPiJing - - Shuidi - services: - 00006000-0000-1000-8000-00805f9b34fb: - tx: 00006001-0000-1000-8000-00805f9b34fb - txmode: 00006002-0000-1000-8000-00805f9b34fb - libo-shark: - defaults: - name: Libo Shark - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 3 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 3 - messages: - - ScalarCmd - communication: - - btle: - names: - - ShaYu - services: - 00006000-0000-1000-8000-00805f9b34fb: - tx: 00006001-0000-1000-8000-00805f9b34fb - txmode: 00006002-0000-1000-8000-00805f9b34fb - libo-karen: - defaults: - name: Libo Karen - features: [] - communication: - - btle: - names: - - SuoYinQiu - services: - 00006000-0000-1000-8000-00805f9b34fb: - tx: 00006001-0000-1000-8000-00805f9b34fb - txmode: 00006002-0000-1000-8000-00805f9b34fb - 00006050-0000-1000-8000-00805f9b34fb: - rxpressure: 00006051-0000-1000-8000-00805f9b34fb - libo-vibes: - defaults: - name: Libo Vibes Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - configurations: - - identifier: - - XiaoLu - name: Libo Lottie - - identifier: - - LuXiaoHan - name: Libo LuLu - - identifier: - - Yuyi - name: Libo Lina - - identifier: - - LuWuShuang - name: Libo Adel - - identifier: - - LiBo - name: Libo Lily - - identifier: - - QingTing - name: Libo Lucy - - identifier: - - Huohu - name: Libo Lara - - identifier: - - Yuyi - name: Libo Feather - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 99 - messages: - - ScalarCmd - - identifier: - - BaiHu - name: Libo LaLa - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 3 - messages: - - ScalarCmd - - identifier: - - Gugudai - name: Libo Carlos - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 3 - messages: - - ScalarCmd - - identifier: - - Haima - name: Libo Selina - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 3 - messages: - - ScalarCmd - communication: - - btle: - names: - - XiaoLu - - LuXiaoHan - - BaiHu - - Gugudai - - Yuyi - - LuWuShuang - - LiBo - - QingTing - - Huohu - - Yuyi - - Haima - services: - 00006000-0000-1000-8000-00805f9b34fb: - tx: 00006001-0000-1000-8000-00805f9b34fb - txmode: 00006002-0000-1000-8000-00805f9b34fb - magic-motion-1: - defaults: - name: Magic Motion V1 Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - configurations: - - identifier: - - Smart Bean - name: MagicMotion Smart Bean - - identifier: - - Smart Bean3 - name: FitCute Kegel Rejuve - - identifier: - - Smart Mini Vibe - name: MagicMotion Smart Mini Vibe - - identifier: - - Smart Mini Vibe3 - name: MagicMotion Vini - - identifier: - - Flamingo - - Flamingo T - name: MagicMotion Flamingo - - identifier: - - Magic Bean - name: MagicMotion Kegel - - identifier: - - Magic Cell - name: MagicMotion Dante/Candy/Rise - - identifier: - - Magic Wand - name: MagicMotion Wand - - identifier: - - Magic Fugu - - Fugu - - Fugu2 - name: MagicMotion Fugu - - identifier: - - Gballs2 - name: G Vibe Gballs 2 - - identifier: - - GBalls3 - name: G Vibe Gballs 3 - - identifier: - - FM-LILAC-101 - name: Femometer Lilac - - identifier: - - Xone - name: MagicMotion Xone - features: - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - CBT002 - name: FunTown Caleo - communication: - - btle: - names: - - Smart Mini Vibe* - - Flamingo - - Flamingo T - - Smart Bean - - Smart Bean3 - - Magic Cell - - Magic Wand - - Fugu - - Fugu2 - - Gballs2 - - GBalls3 - - FM-LILAC-101 - - Xone - - CBT002 - services: - 78667579-7b48-43db-b8c5-7928a6b0a335: - tx: 78667579-a914-49a4-8333-aa3c0cd8fedc - 0000180f-0000-1000-8000-00805f9b34fb: - rxblebattery: 00002a19-0000-1000-8000-00805f9b34fb - magic-motion-2: - defaults: - name: Magic Motion V2 Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - configurations: - - identifier: - - Lipstick - name: MagicMotion Awaken - - identifier: - - Sword - name: MagicMotion Equinox - - identifier: - - Curve - name: MagicMotion Solstice - - identifier: - - Eidolon - name: MagicMotion Eidolon - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - Solstice X - name: MagicMotion Solstice X - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - funwand - name: MagicMotion Zenith - - identifier: - - CBT001 - name: FunTown Jive - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - communication: - - btle: - names: - - Eidolon - - Lipstick - - Sword - - Curve - - Solstice X - - funwand - - CBT001 - services: - 78667579-7b48-43db-b8c5-7928a6b0a335: - tx: 78667579-a914-49a4-8333-aa3c0cd8fedc - 0000180f-0000-1000-8000-00805f9b34fb: - rxblebattery: 00002a19-0000-1000-8000-00805f9b34fb - magic-motion-3: - defaults: - name: LoveLife Krush - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 77 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - communication: - - btle: - names: - - Krush - services: - 78667579-7b48-43db-b8c5-7928a6b0a335: - tx: 78667579-a914-49a4-8333-aa3c0cd8fedc - 0000180f-0000-1000-8000-00805f9b34fb: - rxblebattery: 00002a19-0000-1000-8000-00805f9b34fb - magic-motion-4: - defaults: - name: Magic Motion V4 Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - configurations: - - identifier: - - funone - name: MagicMotion Bunny - - identifier: - - Magic Sundi - name: MagicMotion Sundae - - identifier: - - Kegel Coach - name: MagicMotion Kegel Coach - - identifier: - - Magic Lotos - name: MagicMotion Lotos - - identifier: - - nyx - name: MagicMotion Nyx - - identifier: - - umi - name: MagicMotion Umi - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - funkegel - name: MagicMotion Crystal - - identifier: - - bobi2 - name: MagicMotion Bobi - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - communication: - - btle: - names: - - funone - - Magic Sundi - - Kegel Coach - - Magic Lotos - - nyx - - umi - - funkegel - - bobi2 - services: - 78667579-7b48-43db-b8c5-7928a6b0a335: - tx: 78667579-a914-49a4-8333-aa3c0cd8fedc - 0000180f-0000-1000-8000-00805f9b34fb: - rxblebattery: 00002a19-0000-1000-8000-00805f9b34fb - mysteryvibe: - defaults: - name: Mysteryvibe Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 56 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 56 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 56 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 56 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 56 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 56 - messages: - - ScalarCmd - configurations: - - identifier: - - MV Crescendo - name: MysteryVibe Crescendo - - identifier: - - 'MV Tenuto ' - name: MysteryVibe Tenuto - - identifier: - - 'MV Poco ' - name: MysteryVibe Poco - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 56 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 56 - messages: - - ScalarCmd - communication: - - btle: - names: - - MV Crescendo - - 'MV Tenuto ' - - 'MV Poco ' - services: - f0006900-110c-478b-b74b-6f403b364a9c: - txmode: f0006901-110c-478b-b74b-6f403b364a9c - txvibrate: f0006903-110c-478b-b74b-6f403b364a9c - mysteryvibe-v2: - defaults: - name: Mysteryvibe V2 Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 56 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 56 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 56 - messages: - - ScalarCmd - configurations: - - identifier: - - 6907 MV1 - name: MysteryVibe Tenuto Mini - - identifier: - - 6908 MV1 - name: MysteryVibe Crescendo 2 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 56 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 56 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 56 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 56 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 56 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 56 - messages: - - ScalarCmd - - identifier: - - 6909 MV1 - - 6909 MV2 - name: MysteryVibe Tenuto 2 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 56 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 56 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 56 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 56 - messages: - - ScalarCmd - - identifier: - - 6914 MV1 - name: MysteryVibe Legato - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 56 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 56 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 56 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 56 - messages: - - ScalarCmd - - identifier: - - 6915 MV1 - name: MysteryVibe Molto - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 56 - messages: - - ScalarCmd - communication: - - btle: - names: - - 6907 MV1 - - 6908 MV1 - - 6909 MV1 - - 6909 MV2 - - 6914 MV1 - - 6915 MV1 - services: - f0006900-110c-478b-b74b-6f403b364a9c: - txmode: f0006901-110c-478b-b74b-6f403b364a9c - txvibrate: f0006903-110c-478b-b74b-6f403b364a9c - picobong: - defaults: - name: Picobong Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 10 - messages: - - ScalarCmd - configurations: - - identifier: - - Blow hole - - Picobong Male Toy - name: Picobong Blow hole - - identifier: - - Diver - - Picobong Egg - name: Picobong Diver - - identifier: - - Life guard - - Picobong Ring - name: Picobong Life guard - - identifier: - - Surfer - - Picobong Butt Plug - - Egg driver - - Surfer_plug - name: Picobong Surfer - communication: - - btle: - names: - - Blow hole - - Picobong Male Toy - - Diver - - Picobong Egg - - Life guard - - Picobong Ring - - Surfer - - Picobong Butt Plug - - Egg driver - - Surfer_plug - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff1-0000-1000-8000-00805f9b34fb - vibratissimo: - defaults: - name: Vibratissimo Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - configurations: - - identifier: - - Licker - - SecretKiss - - Womenizer - name: Vibratissimo Licker - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - Rabbit - name: Vibratissimo Rabbit - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 2 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - communication: - - btle: - names: - - Vibratissimo - services: - 00001523-1212-efde-1523-785feabcd123: - txmode: 00001524-1212-efde-1523-785feabcd123 - txvibrate: 00001526-1212-efde-1523-785feabcd123 - rx: 00001527-1212-efde-1523-785feabcd123 - 0000180a-0000-1000-8000-00805f9b34fb: - rxblemodel: 00002a24-0000-1000-8000-00805f9b34fb - 0000180f-0000-1000-8000-00805f9b34fb: - rxblebattery: 00002a19-0000-1000-8000-00805f9b34fb - wevibe: - defaults: - name: WeVibe Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 15 - messages: - - ScalarCmd - configurations: - - identifier: - - Bloom - name: WeVibe Bloom - - identifier: - - Ditto - name: WeVibe Ditto - - identifier: - - Jive - name: WeVibe Jive - - identifier: - - Pivot - name: WeVibe Pivot - - identifier: - - Rave - name: WeVibe Rave - - identifier: - - Verge - name: WeVibe Verge - - identifier: - - Wish - name: WeVibe Wish - - identifier: - - Cougar - - 4 Plus - - 4_Plus - - 4plus - - classic - - Classic - name: WeVibe 4 Plus - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 15 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 15 - messages: - - ScalarCmd - - identifier: - - Gala - name: WeVibe Gala - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 15 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 15 - messages: - - ScalarCmd - - identifier: - - Nova - name: WeVibe Nova - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 15 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 15 - messages: - - ScalarCmd - - identifier: - - Sync - name: WeVibe Sync - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 15 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 15 - messages: - - ScalarCmd - communication: - - btle: - names: - - Cougar - - 4 Plus - - 4_Plus - - 4plus - - Bloom - - classic - - Classic - - Ditto - - Gala - - Jive - - Nova - - Pivot - - Rave - - Sync - - Verge - - Wish - services: - f000bb03-0451-4000-b000-000000000000: - tx: f000c000-0451-4000-b000-000000000000 - rx: f000b000-0451-4000-b000-000000000000 - wevibe-8bit: - defaults: - name: WeVibe 8-bit Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 12 - messages: - - ScalarCmd - configurations: - - identifier: - - Melt - name: WeVibe Melt - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 22 - messages: - - ScalarCmd - - identifier: - - Moxie - name: WeVibe Moxie - - identifier: - - Vector - name: WeVibe Vector - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 12 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 12 - messages: - - ScalarCmd - - identifier: - - Wand - name: WeVibe Wand - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 22 - messages: - - ScalarCmd - - identifier: - - Wand 2 - name: WeVibe Wand 2 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 22 - messages: - - ScalarCmd - - identifier: - - Bond - - Nelson - name: WeVibe Bond - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 27 - messages: - - ScalarCmd - - identifier: - - Nova2 - - Nova_2 - - Nova 2 - name: WeVibe Nova 2 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 27 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 27 - messages: - - ScalarCmd - - identifier: - - Jive 2 - name: WeVibe Jive 2 - communication: - - btle: - names: - - Melt - - Moxie - - Vector - - Wand - - Wand 2 - - Bond - - Nelson - - Nova2 - - Nova_2 - - Nova 2 - - Jive 2 - services: - f000bb03-0451-4000-b000-000000000000: - tx: f000c000-0451-4000-b000-000000000000 - rx: f000b000-0451-4000-b000-000000000000 - wevibe-legacy: - defaults: - name: WeVibe Realm Reina - features: [] - communication: - - btle: - names: - - Reina - - imassager - - Interactive Massager - - '03' - services: - f000bb03-0451-4000-b000-000000000000: - tx: f000c000-0451-4000-b000-000000000000 - rx: f000b000-0451-4000-b000-000000000000 - wevibe-chorus: - defaults: - name: WeVibe Chorus - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 30 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 30 - messages: - - ScalarCmd - configurations: - - identifier: - - Sync 2 - name: WeVibe Sync 2 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 30 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 30 - messages: - - ScalarCmd - - identifier: - - Sync Lite - name: WeVibe Sync Lite - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 30 - messages: - - ScalarCmd - communication: - - btle: - names: - - Chorus - - skeena - - Sync 2 - - Sync Lite - services: - f000bb03-0451-4000-b000-000000000000: - tx: f000c000-0451-4000-b000-000000000000 - rx: f000b000-0451-4000-b000-000000000000 - youcups: - defaults: - name: Youcups Warrior II - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 8 - messages: - - ScalarCmd - communication: - - btle: - names: - - Youcups - services: - 0000fee9-0000-1000-8000-00805f9b34fb: - tx: d44bc439-abfd-45a2-b575-925416129600 - cueme: - defaults: - name: Cueme Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 15 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 15 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 15 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 15 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 15 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 15 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 15 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 15 - messages: - - ScalarCmd - configurations: - - identifier: - - '1' - name: Cueme Mens - - identifier: - - '2' - name: Cueme Bra - - identifier: - - '3' - name: Cueme Womans - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 15 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 15 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 15 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 15 - messages: - - ScalarCmd - communication: - - btle: - names: - - FUNCODE_* - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff1-0000-1000-8000-00805f9b34fb - kiiroo-v2-vibrator: - defaults: - name: Kiiroo V2 Vibrator Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - configurations: - - identifier: - - Pearl2 - name: Kiiroo Pearl 2 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - Fuse - name: OhMiBod Fuse - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - Virtual Rabbit - name: PornHub Virtual Rabbit - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - Virtual Blowbot - name: PornHub Virtual Blowbot - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - Titan - name: Kiiroo Titan - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - communication: - - btle: - names: - - Pearl2 - - Fuse - - Virtual Blowbot - - Titan - - Virtual Rabbit - services: - 88f82580-0000-01e6-aace-0002a5d5c51b: - tx: 88f82581-0000-01e6-aace-0002a5d5c51b - rxtouch: 88f82582-0000-01e6-aace-0002a5d5c51b - rxaccel: 88f82584-0000-01e6-aace-0002a5d5c51b - kiiroo-v21: - defaults: - name: Kiiroo V2.1 Device - features: [] - configurations: - - identifier: - - Pearl2.1 - name: Kiiroo Pearl 2.1 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - Cliona - name: Kiiroo Cliona - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - OhMiBod 4.0 - - OhMiBod ESCA - name: OhMiBod Esca 2 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - Titan1.1 - name: Kiiroo Titan 1.1 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Position - actuator: - step-range: - - 0 - - 99 - messages: - - LinearCmd - - identifier: - - OhMiBod LUMEN - name: OhMiBod Lumen - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - OhMiBod NEX2 - name: OhMiBod NEX|2 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - OhMiBod NEX3 - name: OhMiBod NEX|3 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - Pulse Interactive - name: Hot Octopuss Pulse Solo Interactive - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 6 - messages: - - ScalarCmd - - identifier: - - Fuse1.1 - name: OhMiBod Fuse 1.1 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - OhMiBod Foxy - name: OhMiBod Foxy - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - OhMiBod Chill Panty Vibe - name: OhMiBod Chill - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - OhMiBod Sphinx - name: OhMiBod Sphinx - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - Pearl2+ - - Pearl 2+ - name: Kiiroo Pearl 2+ - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - Pearl3 - - Pearl 3 - name: Kiiroo Pearl 3 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - communication: - - btle: - names: - - Titan1.1 - - Cliona - - Pearl2.1 - - Pearl2+ - - Pearl 2+ - - Pearl3 - - Pearl 3 - - OhMiBod 4.0 - - OhMiBod LUMEN - - OhMiBod NEX2 - - OhMiBod NEX3 - - OhMiBod ESCA - - OhMiBod Foxy - - OhMiBod Chill Panty Vibe - - OhMiBod Sphinx - - Pulse Interactive - - Fuse1.1 - services: - 00001900-0000-1000-8000-00805f9b34fb: - whitelist: 00001901-0000-1000-8000-00805f9b34fb - tx: 00001902-0000-1000-8000-00805f9b34fb - rx: 00001903-0000-1000-8000-00805f9b34fb - a0d70001-4c16-4ba7-977a-d394920e13a3: - tx: a0d70002-4c16-4ba7-977a-d394920e13a3 - rx: a0d70003-4c16-4ba7-977a-d394920e13a3 - kiiroo-v21-initialized: - defaults: - name: Kiiroo V2.1 Initialized Device - features: [] - configurations: - - identifier: - - Onyx2.1 - name: Kiiroo Onyx 2.1 - features: - - feature-type: Position - actuator: - step-range: - - 0 - - 99 - messages: - - LinearCmd - - identifier: - - Onyx+ - name: Kiiroo Onyx+ - features: - - feature-type: Position - actuator: - step-range: - - 0 - - 99 - messages: - - LinearCmd - - identifier: - - KEON - - Keon R2 - name: Kiiroo Keon - features: - - feature-type: Position - actuator: - step-range: - - 0 - - 99 - messages: - - LinearCmd - - identifier: - - Rey - - We-Vibe Rocketman - - Realm1.1 - name: Kiiroo Onyx+ Realm Edition - features: - - feature-type: Position - actuator: - step-range: - - 0 - - 99 - messages: - - LinearCmd - communication: - - btle: - names: - - Rey - - We-Vibe Rocketman - - Realm1.1 - - Onyx2.1 - - Onyx+ - - KEON - - Keon R2 - services: - 00001900-0000-1000-8000-00805f9b34fb: - whitelist: 00001901-0000-1000-8000-00805f9b34fb - tx: 00001902-0000-1000-8000-00805f9b34fb - rx: 00001903-0000-1000-8000-00805f9b34fb - kiiroo-prowand: - defaults: - name: Kiiroo ProWand - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - communication: - - btle: - names: - - ProWand - services: - 00001400-0000-1000-8000-00805f9b34fb: - tx: 00001401-0000-1000-8000-00805f9b34fb - 0000180f-0000-1000-8000-00805f9b34fb: - rxblebattery: 00002a19-0000-1000-8000-00805f9b34fb - kiiroo-spot: - defaults: - name: Kiiroo Spot - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - communication: - - btle: - names: - - SPOT W1 - services: - 00001400-0000-1000-8000-00805f9b34fb: - tx: 00001401-0000-1000-8000-00805f9b34fb - 0000180f-0000-1000-8000-00805f9b34fb: - rxblebattery: 00002a19-0000-1000-8000-00805f9b34fb - vorze-cyclone-x: - defaults: - name: Vorze Cyclone X10 Device - features: - - feature-type: Rotate - actuator: - step-range: - - 0 - - 10 - messages: - - RotateCmd - communication: - - hid: - pairs: - - vendor-id: 1155 - product-id: 22352 - rez-trancevibrator: - defaults: - name: Rez TranceVibrator - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - communication: - - usb: - pairs: - - vendor-id: 2889 - product-id: 1615 - kiiroo-v1: - defaults: - name: Kiiroo V1 Device - features: [] - configurations: - - identifier: - - PEARL - name: Kiiroo Pearl - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 4 - messages: - - ScalarCmd - - identifier: - - ONYX - name: Kiiroo Onyx - features: - - feature-type: Position - actuator: - step-range: - - 0 - - 4 - messages: - - LinearCmd - communication: - - btle: - names: - - ONYX - - PEARL - services: - 49535343-fe7d-4ae5-8fa9-9fafd205e455: - rx: 49535343-1e4d-4bd9-ba61-23c647249616 - tx: 49535343-8841-43f4-a8d4-ecbe34729bb3 - command: 49535343-aca3-481c-91ec-d85e28a60318 - vorze-sa: - defaults: - name: Vorze Device - features: [] - configurations: - - identifier: - - Bach smart - name: Vorze Bach - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - ROCKET - name: Adult Festa Rocket - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - CycSA - name: Vorze A10 Cyclone SA - features: - - feature-type: Rotate - actuator: - step-range: - - 0 - - 99 - messages: - - RotateCmd - - identifier: - - UFOSA - name: Vorze UFO SA - features: - - feature-type: Rotate - actuator: - step-range: - - 0 - - 99 - messages: - - RotateCmd - - identifier: - - UFO-TW - name: Vorze UFO TW - features: - - feature-type: Rotate - actuator: - step-range: - - 0 - - 99 - messages: - - RotateCmd - - feature-type: Rotate - actuator: - step-range: - - 0 - - 99 - messages: - - RotateCmd - - identifier: - - VorzePiston - name: Vorze Piston - features: - - feature-type: Position - actuator: - step-range: - - 0 - - 99 - messages: - - LinearCmd - communication: - - btle: - names: - - Bach smart - - CycSA - - UFOSA - - UFO-TW - - VorzePiston - - ROCKET - services: - 40ee1111-63ec-4b7f-8ce7-712efd55b90e: - tx: 40ee2222-63ec-4b7f-8ce7-712efd55b90e - youou: - defaults: - name: Youou Wand Vibrator - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - communication: - - btle: - names: - - VX001_* - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff6-0000-1000-8000-00805f9b34fb - realtouch: - defaults: - name: RealTouch - features: - - feature-type: Position - actuator: - step-range: - - 0 - - 99 - messages: - - LinearCmd - communication: - - hid: - pairs: - - vendor-id: 8020 - product-id: 1 - prettylove: - defaults: - name: Pretty Love Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 3 - messages: - - ScalarCmd - communication: - - btle: - names: - - Aogu BLE * - - AB Shutter3 [Aogu BLE Device] - services: - 0000ffe5-0000-1000-8000-00805f9b34fb: - tx: 0000ffe9-0000-1000-8000-00805f9b34fb - rx: 0000ffe2-0000-1000-8000-00805f9b34fb - svakom: - defaults: - name: Svakom Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 19 - messages: - - ScalarCmd - configurations: - - identifier: - - Aogu SCB - name: Svakom Ella - - identifier: - - Phoenix NEO - name: Svakom Phoenix Neo - - identifier: - - Emma NEO - name: Svakom Emma Neo - communication: - - btle: - names: - - Aogu SUV - - Aogu SCB - - Emma NEO - - Phoenix NEO - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - rx: 0000ffe2-0000-1000-8000-00805f9b34fb - svakom-v2: - defaults: - name: Svakom Device v2 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 10 - messages: - - ScalarCmd - configurations: - - identifier: - - '116' - name: Svakom Phoenix Neo - - identifier: - - Viviana - name: Svakom Viviana - - identifier: - - Ella NEO - name: Svakom Ella Neo - - identifier: - - '117' - - Edeny - name: Svakom Edeny - - identifier: - - S38A - name: Svakom Tammy Pro - - identifier: - - Vick NEO - - Vick Neo - name: Svakom Vick Neo - - identifier: - - STG05A - name: Svakom Aravinda - - identifier: - - '118' - name: ToyCod Vanesia - - identifier: - - QH-SJ007A - name: Svakom Winni 2 - - identifier: - - Cici 2 - name: Svakom Cici 2 - - identifier: - - Emma Neo 2 - name: Svakom Emma Neo 2 - communication: - - btle: - names: - - '116' - - '117' - - Edeny - - '118' - - Viviana - - Ella NEO - - S38A - - Vick NEO - - Vick Neo - - STG05A - - QH-SJ007A - - Cici 2 - - Emma Neo 2 - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - rx: 0000ffe2-0000-1000-8000-00805f9b34fb - svakom-v3: - defaults: - name: Svakom Device v3 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 10 - messages: - - ScalarCmd - configurations: - - identifier: - - Phoenix Neo 2 - name: Svakom Phoenix Neo 2 - - identifier: - - FK008A - name: Fantasy Cup Theodore - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 10 - messages: - - ScalarCmd - - feature-type: Rotate - actuator: - step-range: - - 0 - - 1 - messages: - - ScalarCmd - - identifier: - - Hannes NEO - name: Svakom Hannes Neo - - identifier: - - QH-SX007E - name: Svakom Alberta - features: - - feature-type: Vibrate - description: Vibrating attachments - actuator: - step-range: - - 0 - - 10 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Suction lens - actuator: - step-range: - - 0 - - 1 - messages: - - ScalarCmd - communication: - - btle: - names: - - Phoenix Neo 2 - - FK008A - - Hannes NEO - - QH-SX007E - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - rx: 0000ffe2-0000-1000-8000-00805f9b34fb - svakom-v4: - defaults: - name: Svakom Device v4 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 10 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 10 - messages: - - ScalarCmd - configurations: - - identifier: - - B2CM6 - name: ToyCod Barzillai - - identifier: - - ERICA - name: Svakom Erica - - identifier: - - Cici+ 2 - name: Svakom Cici+ 2 - communication: - - btle: - names: - - B2CM6 - - ERICA - - Cici+ 2 - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - rx: 0000ffe2-0000-1000-8000-00805f9b34fb - svakom-v5: - defaults: - name: Svakom Device v5 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 10 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 10 - messages: - - ScalarCmd - configurations: - - identifier: - - Chika - name: Svakom Chika - - identifier: - - Mora Neo - name: Svakom Mora Neo - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 10 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 10 - messages: - - ScalarCmd - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 3 - messages: - - ScalarCmd - - identifier: - - Trysta Neo - name: Svakom Trysta Neo - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 10 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 10 - messages: - - ScalarCmd - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 3 - messages: - - ScalarCmd - - identifier: - - Mini Emma Neo - name: Svakom Mini Emma Neo - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 10 - messages: - - ScalarCmd - communication: - - btle: - names: - - Chika - - Mora Neo - - Trysta Neo - - Mini Emma Neo - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - rx: 0000ffe2-0000-1000-8000-00805f9b34fb - svakom-v6: - defaults: - name: Svakom Device v6 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 10 - messages: - - ScalarCmd - configurations: - - identifier: - - CocoPro - name: Svakom Coco Pro - - identifier: - - Echo 2 - name: Svakom Echo 2 - - identifier: - - Vick Neo 2 - name: Svakom Vick Neo 2 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 10 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 10 - messages: - - ScalarCmd - - identifier: - - Iker Neo - name: Svakom Iker Neo - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 10 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 10 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 5 - messages: - - ScalarCmd - communication: - - btle: - names: - - CocoPro - - Echo 2 - - Vick Neo 2 - - Iker Neo - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - rx: 0000ffe2-0000-1000-8000-00805f9b34fb - svakom-sam: - defaults: - name: Svakom Sam Neo - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 10 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 1 - messages: - - ScalarCmd - communication: - - btle: - names: - - Sam Neo - services: - 0000ae00-0000-1000-8000-00805f9b34fb: - tx: 0000ae01-0000-1000-8000-00805f9b34fb - rx: 0000ae02-0000-1000-8000-00805f9b34fb - txmode: 0000ae10-0000-1000-8000-00805f9b34fb - 0000ffac-0000-1000-8000-00805f9b34fb: - firmware: 0000ffb4-0000-1000-8000-00805f9b34fb - svakom-sam2: - defaults: - name: Svakom Sam Neo 2 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 10 - messages: - - ScalarCmd - - feature-type: Constrict - actuator: - step-range: - - 0 - - 5 - messages: - - ScalarCmd - configurations: - - identifier: - - Sam Neo 2 - name: Svakom Sam Neo 2 - - identifier: - - Sam Neo 2 Pro - name: Svakom Sam Neo 2 Pro - communication: - - btle: - names: - - Sam Neo 2 - - Sam Neo 2 Pro - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - rx: 0000ffe2-0000-1000-8000-00805f9b34fb - svakom-alex: - defaults: - name: Svakom Alex Neo - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 3 - messages: - - ScalarCmd - communication: - - btle: - names: - - Alex NEO - - S63E Alex NEO - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - svakom-alex-v2: - defaults: - name: Svakom Alex Neo 2 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 3 - messages: - - ScalarCmd - communication: - - btle: - names: - - Alex NEO 2 - - S63E Alex NEO 2 - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - svakom-dice: - defaults: - name: Zemalia Dice for Love - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - communication: - - btle: - names: - - ZhiAi - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - rx: 0000ffe2-0000-1000-8000-00805f9b34fb - svakom-dt250a: - defaults: - name: Coleur Dor DT250A - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 3 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 3 - messages: - - ScalarCmd - - feature-type: Constrict - actuator: - step-range: - - 0 - - 2 - messages: - - ScalarCmd - communication: - - btle: - names: - - DT250A - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - rx: 0000ffe2-0000-1000-8000-00805f9b34fb - svakom-iker: - defaults: - name: Svakom Iker - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 10 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 5 - messages: - - ScalarCmd - communication: - - btle: - names: - - Iker - manufacturer-data: - - company: 39 - data: - - 83 - - 86 - - 65 - - 1 - - 11 - - 18 - - 1 - - 51 - - 68 - - 85 - - 202 - - 8 - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - svakom-jordan: - defaults: - name: Svakom Jordan - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 10 - messages: - - ScalarCmd - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 5 - messages: - - ScalarCmd - communication: - - btle: - names: - - Jordan - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - rx: 0000ffe2-0000-1000-8000-00805f9b34fb - svakom-pulse: - defaults: - name: Svakom Pulse Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 9 - messages: - - ScalarCmd - configurations: - - identifier: - - SWK-SX013A - name: Svakom Pulse Lite Neo - - identifier: - - Pulse Union - name: Svakom Pulse Union - - identifier: - - Pulse Galaxie - name: Svakom Pulse Galaxie - - identifier: - - SX033APP - name: Svakom Mimiki - - identifier: - - BX288A - name: BeYourLover Kyukyu - - identifier: - - QH-SX045A-B - name: Coleur Dor VX045A - - identifier: - - SWK-SX067-B - name: Momonii Agatha - - identifier: - - QH-HX029A-B - name: Coleur Dor HX029A - communication: - - btle: - names: - - SWK-SX013A - - Pulse Union - - Pulse Galaxie - - SX033APP - - BX288A - - QH-SX045A-B - - SWK-SX067-B - - QH-HX029A-B - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - rx: 0000ffe2-0000-1000-8000-00805f9b34fb - svakom-suitcase: - defaults: - name: Svakom Magic Suitcase - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 30 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 1 - messages: - - ScalarCmd - configurations: - - identifier: - - VX236A-BLE-V1.0 - name: Coleur Dor VX236A - communication: - - btle: - names: - - VX357A-BLE-V1.0 - - VX236A-BLE-V1.0 - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - rx: 0000ffe2-0000-1000-8000-00805f9b34fb - svakom-tarax: - defaults: - name: ToyCod Tara X - features: - - feature-type: Vibrate - description: Internal vibrator - actuator: - step-range: - - 0 - - 3 - messages: - - ScalarCmd - - feature-type: Vibrate - description: External pulsator - actuator: - step-range: - - 0 - - 3 - messages: - - ScalarCmd - communication: - - btle: - names: - - SX218A - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - rx: 0000ffe2-0000-1000-8000-00805f9b34fb - svakom-avaneo: - defaults: - name: Svakom Ava Neo - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 10 - messages: - - ScalarCmd - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 1 - messages: - - ScalarCmd - communication: - - btle: - names: - - Ava Neo - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - rx: 0000ffe2-0000-1000-8000-00805f9b34fb - svakom-barnard: - defaults: - name: Fantasy Cup Barnard - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 3 - messages: - - ScalarCmd - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 3 - messages: - - ScalarCmd - communication: - - btle: - names: - - DG239A - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - svakom-barney: - defaults: - name: Mutufun Barney - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 10 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 10 - messages: - - ScalarCmd - communication: - - btle: - names: - - DJ333A - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - rx: 0000ffe2-0000-1000-8000-00805f9b34fb - realov: - defaults: - name: Realov Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 50 - messages: - - ScalarCmd - communication: - - btle: - names: - - REALOV_VIBE - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - motorbunny: - defaults: - name: Motorbunny Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Rotate - actuator: - step-range: - - 0 - - 255 - messages: - - RotateCmd - configurations: - - identifier: - - MB Controller - name: Motorbunny Classic - - identifier: - - MB LINK 201 - name: Motorbunny Buck - communication: - - btle: - names: - - MB Controller - - MB LINK 201 - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff6-0000-1000-8000-00805f9b34fb - zalo: - defaults: - name: Zalo Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 8 - messages: - - ScalarCmd - configurations: - - identifier: - - ZALO-Queen - name: Zalo Queen - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 8 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 8 - messages: - - ScalarCmd - - identifier: - - ZALO-King - name: Zalo King - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 8 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 8 - messages: - - ScalarCmd - - identifier: - - ZALO-Jeanne - name: Zalo Jeanne - communication: - - btle: - names: - - ZALO-Queen - - ZALO-King - - ZALO-Jeanne - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff1-0000-1000-8000-00805f9b34fb - sayberx: - defaults: - name: SayberX Device - features: [] - configurations: - - identifier: - - SayberX - name: SayberX - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 4 - messages: - - ScalarCmd - - identifier: - - X-Ring - name: Sayber X-Ring - communication: - - btle: - names: - - SayberX - - X-Ring * - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff6-0000-1000-8000-00805f9b34fb - rx: 0000fff8-0000-1000-8000-00805f9b34fb - muse: - defaults: - name: Muse Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 9 - messages: - - ScalarCmd - configurations: - - identifier: - - WB-ZDB-WST - name: Dream Lover Archer 2 - - identifier: - - WB-TDD - name: Galaku Panty Vib - communication: - - btle: - names: - - WB-ZDB-WST - - WB-TDD - services: - 0000aaa0-0000-1000-8000-00805f9b34fb: - tx: 0000aaa1-0000-1000-8000-00805f9b34fb - lelo-f1s: - defaults: - name: Lelo F1s - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - communication: - - btle: - names: - - F1s - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff1-0000-1000-8000-00805f9b34fb - rx: 00000aa4-0000-1000-8000-00805f9b34fb - lelo-f1sv2: - defaults: - name: Lelo F1s V2 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - configurations: - - identifier: - - F1SV2A - - F1SV2X - name: Lelo F1s V2 - - identifier: - - F1SV3 - name: Lelo F1s V3 - communication: - - btle: - names: - - F1SV2A - - F1SV2X - - F1SV3 - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff1-0000-1000-8000-00805f9b34fb - whitelist: 00000a10-0000-1000-8000-00805f9b34fb - rx: 00000a04-0000-1000-8000-00805f9b34fb - txvibrate: 0000fff2-0000-1000-8000-00805f9b34fb - generic0: 00000a11-0000-1000-8000-00805f9b34fb - lelo-harmony: - defaults: - name: Lelo Tiani Harmony - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - configurations: - - identifier: - - IdaWave - - Ida Wave - name: Lelo Ida Wave - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Rotate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - TOR3 - name: Lelo Tor 3 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - Hugo2 - name: Lelo Hugo 2 - - identifier: - - DoubleSonic - name: Lelo Enigma Double Sonic - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Rotate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - GIGI3 - name: Lelo Gigi 3 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - LIV3 - name: Lelo Liv 3 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - communication: - - btle: - names: - - IdaWave - - Ida Wave - - TianiHarmony - - Tiani Harmony - - TOR3 - - Hugo2 - - DoubleSonic - - GIGI3 - - LIV3 - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - command: 0000fff1-0000-1000-8000-00805f9b34fb - tx: 0000fff2-0000-1000-8000-00805f9b34fb - whitelist: 00000a11-0000-1000-8000-00805f9b34fb - aneros: - defaults: - name: Aneros Vivi - features: - - feature-type: Vibrate - description: Perineum Vibrator - actuator: - step-range: - - 0 - - 127 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Internal Vibrator - actuator: - step-range: - - 0 - - 127 - messages: - - ScalarCmd - communication: - - btle: - names: - - Massage Demo - services: - 0000ff00-0000-1000-8000-00805f9b34fb: - tx: 0000ff01-0000-1000-8000-00805f9b34fb - lovehoney-desire: - defaults: - name: Lovehoney Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 127 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 127 - messages: - - ScalarCmd - configurations: - - identifier: - - PROSTATE VIBE - name: Lovehoney Desire Prostate Vibrator - - identifier: - - KNICKER VIBE - name: Lovehoney Desire Knicker Vibrator - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 127 - messages: - - ScalarCmd - - identifier: - - LOVE EGG - name: Lovehoney Desire Love Egg - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 127 - messages: - - ScalarCmd - communication: - - btle: - names: - - PROSTATE VIBE - - KNICKER VIBE - - LOVE EGG - services: - 0000ff00-0000-1000-8000-00805f9b34fb: - tx: 0000ff01-0000-1000-8000-00805f9b34fb - twerkingbutt: - defaults: - name: Twerking Butt - features: [] - communication: - - btle: - names: - - BODIKANG - - Twerking Butt - - TwerkingButt - services: - 00000a60-0000-1000-8000-00805f9b34fb: - tx: 00000a66-0000-1000-8000-00805f9b34fb - rx: 00000a67-0000-1000-8000-00805f9b34fb - maxpro: - defaults: - name: MaxPro 2 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - communication: - - btle: - names: - - M2 - services: - 6e400001-b5a3-f393-e0a9-e50e24dcca9e: - tx: 6e400002-b5a3-f393-e0a9-e50e24dcca9e - nobra: - defaults: - name: Nobra's Silicone Dreams Toy - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 15 - messages: - - ScalarCmd - communication: - - btle: - names: - - NobraControl* - services: - 0000abf0-0000-1000-8000-00805f9b34fb: - tx: 0000abf1-0000-1000-8000-00805f9b34fb - - serial: - port: default - baud-rate: 19200 - data-bits: 8 - parity: 'N' - stop-bits: 1 - thehandy: - defaults: - name: The Handy - features: - - feature-type: Position - actuator: - step-range: - - 0 - - 100 - messages: - - LinearCmd - communication: - - btle: - names: - - The Handy - services: - 1775244d-6b43-439b-877c-060f2d9bed07: - firmware: 1775ff51-6b43-439b-877c-060f2d9bed07 - tx: 1775ff55-6b43-439b-877c-060f2d9bed07 - cachito: - defaults: - name: Cachito Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 5 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - configurations: - - identifier: - - CCTSK - name: Cachito Lure Tao - - identifier: - - CCTXueGao - name: Cachito Ice Cream - communication: - - btle: - names: - - CCTSK - - CCTXueGao - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff2-0000-1000-8000-00805f9b34fb - jejoue: - defaults: - name: Je Joue Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 5 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 5 - messages: - - ScalarCmd - communication: - - btle: - names: - - Je Joue - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff1-0000-1000-8000-00805f9b34fb - lovenuts: - defaults: - name: Love Nut - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 15 - messages: - - ScalarCmd - communication: - - btle: - names: - - Love_Nuts - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff1-0000-1000-8000-00805f9b34fb - patoo: - defaults: - name: Patoo Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - configurations: - - identifier: - - PTVEA - name: Patoo Carrot - - identifier: - - PCS - name: Patoo Vibrator - - identifier: - - PHT - name: Patoo Bean Sprout - - identifier: - - PBT - name: Patoo Devil - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - communication: - - btle: - names: - - PTVEA* - - PBT* - - PCS* - - PHT* - services: - f000aa64-0451-4000-b000-000000000000: - txmode: f000aa65-0451-4000-b000-000000000000 - tx: f000aa68-0451-4000-b000-000000000000 - tcode-v03: - defaults: - name: TCode v0.3 (Single Linear Axis) - features: - - feature-type: Position - actuator: - step-range: - - 0 - - 100 - messages: - - LinearCmd - communication: - - serial: - port: default - baud-rate: 115200 - data-bits: 8 - parity: 'N' - stop-bits: 1 - fredorch: - defaults: - name: Fredorch Device - features: - - feature-type: Position - actuator: - step-range: - - 0 - - 150 - messages: - - LinearCmd - communication: - - btle: - names: - - YXlinksSPP - services: - 0000ffb0-0000-1000-8000-00805f9b34fb: - tx: 0000ffb1-0000-1000-8000-00805f9b34fb - rx: 0000ffb2-0000-1000-8000-00805f9b34fb - fredorch-rotary: - defaults: - name: Fredorch Rotary Device - features: - - feature-type: Oscillate - description: Fucking Machine Oscillation Speed - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - communication: - - btle: - names: - - M1_* - services: - 0000ae10-0000-1000-8000-00805f9b34fb: - tx: 0000ae01-0000-1000-8000-00805f9b34fb - rx: 0000ae02-0000-1000-8000-00805f9b34fb - mizzzee: - defaults: - name: Mizz Zee Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 68 - messages: - - ScalarCmd - communication: - - btle: - names: - - NFY008 - services: - 0000eea0-0000-1000-8000-00805f9b34fb: - tx: 0000eea1-0000-1000-8000-00805f9b34fb - mizzzee-v2: - defaults: - name: Mizz Zee Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 68 - messages: - - ScalarCmd - communication: - - btle: - names: - - XHT - services: - 0000eea0-0000-1000-8000-00805f9b34fb: - tx: 0000ee01-0000-1000-8000-00805f9b34fb - mizzzee-v3: - defaults: - name: Mizz Zee Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 1000 - messages: - - ScalarCmd - communication: - - btle: - names: - - XHTKJ - services: - 0000ff10-0000-1000-8000-00805f9b34fb: - tx: 0000ff12-0000-1000-8000-00805f9b34fb - htk_bm: - defaults: - name: HTK Breast Massager - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 1 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 1 - messages: - - ScalarCmd - communication: - - btle: - names: - - HTK-BLE-BM001 - services: - 0000180f-0000-1000-8000-00805f9b34fb: - rxblebattery: 00002a19-0000-1000-8000-00805f9b34fb - 00001802-0000-1000-8000-00805f9b34fb: - tx: 00002a06-0000-1000-8000-00805f9b34fb - ankni: - defaults: - name: Roselex Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 3 - messages: - - ScalarCmd - communication: - - btle: - names: - - DSJM - services: - 0000fe00-0000-1000-8000-00805f9b34fb: - tx: 0000fe01-0000-1000-8000-00805f9b34fb - 0000fffe-0000-1000-8000-00805f9b34fb: - tx: 0000fe02-0000-1000-8000-00805f9b34fb - 0000180a-0000-1000-8000-00805f9b34fb: - generic0: 00002a50-0000-1000-8000-00805f9b34fb - hgod: - defaults: - name: Hgod Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 10 - messages: - - ScalarCmd - communication: - - btle: - names: - - AMN NEO - services: - 0000ffe3-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - rx: 0000ffe2-0000-1000-8000-00805f9b34fb - lovedistance: - defaults: - name: Love Distance Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 121 - messages: - - ScalarCmd - configurations: - - identifier: - - REACH G - name: Love Distance Reach G - - identifier: - - REACH - name: Love Distance Reach - - identifier: - - MAG - name: Love Distance Mag - - identifier: - - SPAN - name: Love Distance Span - - identifier: - - RANGE - name: Love Distance Range - - identifier: - - ORBIT - name: Love Distance Range - - identifier: - - JOIN G - name: Love Distance Join G - - identifier: - - LINK - name: Love Distance Link - - identifier: - - GRASP - name: Love Distance Grasp - - identifier: - - RECEIVE - name: Love Distance Receive - communication: - - btle: - names: - - REACH G - - REACH - - MAG - - SPAN - - RANGE - - ORBIT - - JOIN G - - LINK - - GRASP - - RECEIVE - services: - 0000ff00-0000-1000-8000-00805f9b34fb: - tx: 0000ff01-0000-1000-8000-00805f9b34fb - rx: 0000ff02-0000-1000-8000-00805f9b34fb - satisfyer: - defaults: - name: Satisfyer Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - configurations: - - identifier: - - '10005' - name: Satisfyer Hot Spot - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10006' - name: Satisfyer Heated Affair - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10007' - name: Satisfyer Big Heat - - identifier: - - '10008' - name: Satisfyer Heated Thrill - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10009' - name: Satisfyer Hot Bunny - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10010' - name: Satisfyer Heat Climax - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10011' - name: Satisfyer Heat Climax+ - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10012' - name: Satisfyer Hot Passion - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10013' - name: Satisfyer Haute Couture+ - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10014' - name: Satisfyer High Fashion+ - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10015' - name: Satisfyer Prêt-à-porter+ - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10024' - - '10025' - name: Satisfyer Love Triangle - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10027' - - '10028' - name: Satisfyer Curvy 1+ - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10030' - - '10031' - name: Satisfyer Curvy 2+ - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10032' - name: Satisfyer Double Wand-er - - identifier: - - '10046' - - '10047' - - '10048' - name: Satisfyer Double Joy - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10049' - - '10050' - - '10051' - name: Satisfyer Double Fun - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10052' - - '10053' - - '10054' - name: Satisfyer Double Love - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10055' - name: Satisfyer Curvy 3+ - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10059' - - '10060' - - '10061' - name: Satisfyer Hot Lover - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10062' - - '10063' - - '10064' - name: Satisfyer Mono Flex - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10065' - - '10066' - - '10067' - - '10068' - name: Satisfyer Double Flex - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10069' - - '10070' - - '10071' - name: Satisfyer Heat Wave - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10072' - name: Satisfyer Little Secret - - identifier: - - '10073' - name: Satisfyer Sexy Secret - - identifier: - - '10074' - name: Satisfyer Strong One - - identifier: - - '10075' - name: Satisfyer Mighty One - - identifier: - - '10076' - name: Satisfyer Powerful One - - identifier: - - '10077' - name: Satisfyer Royal One - - identifier: - - '10078' - name: Satisfyer Signet Ring - - identifier: - - '10079' - - '10080' - name: Satisfyer Dual Love - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10081' - - '10082' - name: Satisfyer Dual Pleasure - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10090' - name: Satisfyer Hero+ - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10091' - name: Satisfyer Knight+ - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10092' - - '10093' - name: Satisfyer Newcomer+ - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10100' - - '10101' - name: Satisfyer Plug-ilicious 1 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10102' - - '10103' - - '10104' - name: Satisfyer Plug-ilicious 2 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10105' - name: Satisfyer E-Love Foreplay - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10108' - name: Satisfyer E-Love G-Hunter - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10109' - name: Satisfyer E-Love G-Hunter+ - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10110' - name: Satisfyer E-Love G-Spotter - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10111' - name: Satisfyer E-Love G-Spotter+ - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10112' - name: Satisfyer E-Love Story - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10119' - - '10120' - - '10182' - name: Satisfyer Love Birds 1 - - identifier: - - '10121' - - '10122' - - '10123' - name: Satisfyer Love Birds 2 - - identifier: - - '10124' - - '10125' - - '10126' - name: Satisfyer Love Birds Vary - - identifier: - - '10127' - - '10128' - - '10129' - - '10201' - name: Satisfyer Ribbed Petal - - identifier: - - '10130' - - '10131' - - '10132' - - '10133' - name: Satisfyer Shiny Petal - - identifier: - - '10134' - - '10135' - - '10136' - - '10202' - name: Satisfyer Smooth Petal - - identifier: - - '10140' - name: Satisfyer Men Vibration+ - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10141' - name: Satisfyer Power Plug - - identifier: - - '10142' - - '10143' - name: Satisfyer Rotator Plug 1+ - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10144' - - '10145' - name: Satisfyer Rotator Plug 2+ - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10146' - - '10147' - name: Satisfyer Deep Diver - - identifier: - - '10148' - - '10149' - name: Satisfyer Sweet Seal - - identifier: - - '10150' - - '10151' - name: Satisfyer Trendsetter - - identifier: - - '10154' - - '10155' - - '10156' - name: Satisfyer Twirling Joy - - identifier: - - '10157' - - '10158' - name: Satisfyer Ultra Power Bullet 8 - - identifier: - - '10160' - - '10161' - - '10162' - name: Satisfyer Double Desire - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10163' - - '10164' - - '10165' - - '10166' - name: Satisfyer Double Lust - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10167' - name: Satisfyer Epic Duo - - identifier: - - '10168' - name: Satisfyer Pleasure Wand+ - - identifier: - - '10169' - - '10170' - - '10171' - name: Satisfyer Top Secret - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10172' - - '10173' - - '10174' - name: Satisfyer Top Secret+ - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10175' - - '10176' - name: Satisfyer Bullseye - - identifier: - - '10177' - - '10178' - - '10179' - name: Satisfyer Sunray - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10180' - - '10181' - name: Satisfyer Curvy Trinity 5+ - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10183' - - '10184' - name: Satisfyer Intensity Plug - - identifier: - - '10185' - name: Satisfyer Power Masturbator - - identifier: - - '10186' - - '10187' - name: Satisfyer Hug me - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10188' - name: Satisfyer Air Pump Bunny 5+ - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10189' - name: Satisfyer Air Pump Vibrator 5+ - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10190' - - '10191' - name: Satisfyer Threesome 4 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10192' - name: Satisfyer G-Spot Flex 4+ - - identifier: - - '10193' - - '10194' - name: Satisfyer G-Spot Flex 5+ - - identifier: - - '10195' - name: Satisfyer Air Pump Booty 5+ - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10196' - name: Satisfyer Pro+ Wave 4 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10197' - - '10198' - name: Satisfyer Mini Wand-er+ - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10199' - - '10200' - name: Satisfyer Tropical Tip - - identifier: - - '10203' - - '10204' - name: Satisfyer Twirling Pro+ - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10205' - name: Satisfyer Perfect Pair 4 - - identifier: - - '10206' - - '10207' - - '10208' - name: Satisfyer Booty Absolute Beginners 5 - - identifier: - - '10241' - - '10242' - name: Satisfyer Rrrolling Sensation - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '10307' - - '10308' - - '10309' - name: Satisfyer Pro 2 Gen 3 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - communication: - - btle: - names: - - SF * - manufacturer-data: - - company: 93 - data: - - 0 - - 0 - - 39 - - company: 93 - data: - - 0 - - 0 - - 40 - services: - 0000180a-0000-1000-8000-00805f9b34fb: - rxblemodel: 00002a24-0000-1000-8000-00805f9b34fb - 51361500-c5e7-47c7-8a6e-47ebc99d80e8: - command: 51361501-c5e7-47c7-8a6e-47ebc99d80e8 - tx: 51361502-c5e7-47c7-8a6e-47ebc99d80e8 - mannuo: - defaults: - name: ManNuo Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 3 - messages: - - ScalarCmd - communication: - - btle: - names: - - Sex toys - - Sex Toys - - LXCDVP - - MANO PRODUCT - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff1-0000-1000-8000-00805f9b34fb - rx: 0000fff4-0000-1000-8000-00805f9b34fb - kgoal-boost: - defaults: - name: KGoal Boost - features: - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - communication: - - btle: - names: - - Boost - services: - 0000180f-0000-1000-8000-00805f9b34fb: - rxblebattery: 00002a19-0000-1000-8000-00805f9b34fb - 8e7c6065-7656-17ad-1b41-b53d1a548e0d: - rxpressure: 10c2be2d-d2d5-b7a8-5f42-e2468c9ebbf5 - meese: - defaults: - name: Meese Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 10 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 3 - messages: - - ScalarCmd - configurations: - - identifier: - - Meese-V389 - name: Meese Tera - - identifier: - - Meese-cd - name: Meese Modo - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 10 - messages: - - ScalarCmd - communication: - - btle: - names: - - Meese-V389 - - Meese-cd - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - hismith: - defaults: - name: Hismith device - features: - - feature-type: Oscillate - description: Fucking Machine Oscillation Speed - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - configurations: - - identifier: - - '1001' - name: Hismith Sex Machine - - identifier: - - '1002' - name: Hismith Pro Traveler - - identifier: - - '1003' - name: Hismith Capsule - - identifier: - - '2001' - name: Hismith Thrusting Cup - features: - - feature-type: Oscillate - description: Stroker Oscillation Speed - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 1 - messages: - - ScalarCmd - - identifier: - - '1006' - name: Hismith G011 - features: - - feature-type: Oscillate - description: Stroker Oscillation Speed - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 1 - messages: - - ScalarCmd - - identifier: - - '3001' - name: Wildolo Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - communication: - - btle: - names: - - HISMITH - - Wildolo - - "\aHISMITH" - services: - 0000ffe5-0000-1000-8000-00805f9b34fb: - tx: 0000ffe9-0000-1000-8000-00805f9b34fb - 0000ff90-0000-1000-8000-00805f9b34fb: - rxblemodel: 0000ff96-0000-1000-8000-00805f9b34fb - hismith-mini: - defaults: - name: Hismith Mini device - features: - - feature-type: Oscillate - description: Fucking Machine Oscillation Speed - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - configurations: - - identifier: - - '4001' - name: Auxfun Sex Machine - - identifier: - - '1005' - - '1102' - name: Hismith Sex Machine - - identifier: - - '1004' - name: Hismith Mini Sex Machine - - identifier: - - '1101' - name: Hismith Servo Sex Machine - - identifier: - - '1402' - name: Hismith Ukulele - - identifier: - - '1501' - name: Hismith PleasureDrive - - identifier: - - '2201' - name: Sinloli Automatic Sex Doll - features: - - feature-type: Constrict - description: Air Pump - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrator - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '3101' - name: Eropair Rabbit Vibrator - features: - - feature-type: Vibrate - description: Internal Vibrator - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: External Vibrator - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '3102' - name: Eropair Thrusting Vibrating Dildo - features: - - feature-type: Oscillate - description: Thruster - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrator - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '2101' - name: Eropair Cup - features: - - feature-type: Constrict - description: Air Pump - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrator - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '2204' - name: Sinloli Cosima - features: - - feature-type: Oscillate - description: Stroker Oscillation Speed - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Constrict - description: Air Pump - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '2202' - name: Sinloli Ethel - features: - - feature-type: Oscillate - description: Stroker Oscillation Speed - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrator - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - '2205' - name: Sinloli Aston - communication: - - btle: - names: - - Auxfun-Box - - Sinloli - - Sinloli-Sherry - - Eropair * - - HISMITH S1 - - HISMITH S2 # Servo - - HISMITH S3 - - Sinloli Cosima - - Sinloli-Ethel - - Sinloli Aston - services: - 0000ffe5-0000-1000-8000-00805f9b34fb: - tx: 0000ffe9-0000-1000-8000-00805f9b34fb - 0000ff90-0000-1000-8000-00805f9b34fb: - rxblemodel: 0000ff96-0000-1000-8000-00805f9b34fb - wetoy: - defaults: - name: WeToy MiNa - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 3 - messages: - - ScalarCmd - communication: - - btle: - names: - - WeToy - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff3-0000-1000-8000-00805f9b34fb - pink_punch: - defaults: - name: Pink Punch Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - configurations: - - identifier: - - Pink_Punch - name: Pink Punch Sunset Mushroom - - identifier: - - PinkPunch_Peachu - name: Pink Punch Peachu - - identifier: - - PinkPunch_DreamBunny - name: Pink Punch Dream Bunny - communication: - - btle: - names: - - Pink_Punch - - PinkPunch_Peachu - - PinkPunch_DreamBunny - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - sakuraneko: - defaults: - name: Sakuraneko Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - configurations: - - identifier: - - sakuraneko-01 - name: Sakuraneko Korokoro - - identifier: - - sakuraneko-02 - name: Sakuraneko Nukunuku - - identifier: - - sakuraneko-03 - name: Sakuraneko Dokidoki - - identifier: - - sakuraneko-04 - name: Sakuraneko Koikoi - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Rotate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - communication: - - btle: - names: - - sakuraneko-01 - - sakuraneko-02 - - sakuraneko-03 - - sakuraneko-04 - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - synchro: - defaults: - name: Synchro - features: - - feature-type: Rotate - actuator: - step-range: - - 0 - - 6 - messages: - - RotateCmd - configurations: - - identifier: - - synchro EX - name: Synchro Exchange - communication: - - btle: - names: - - Shinkuro - - synchro2 - - synchro EX - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - tryfun: - defaults: - name: TryFun Yuan Series - features: - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 9 - messages: - - ScalarCmd - - feature-type: Rotate - actuator: - step-range: - - 0 - - 9 - messages: - - ScalarCmd - configurations: - - identifier: - - TF-SPRAY - name: TryFun Surge Pro - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 4 - messages: - - ScalarCmd - communication: - - btle: - names: - - TRYFUN-ONE - - TF-SPRAY - services: - 0000ff10-0000-1000-8000-00805f9b34fb: - tx: 0000fff1-0000-1000-8000-00805f9b34fb - 0000ffac-0000-1000-8000-00805f9b34fb: - tx: 0000ffb5-0000-1000-8000-00805f9b34fb - tryfun-meta2: - defaults: - name: TryFun Meta 2 - features: - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Rotate - actuator: - step-range: - - 0 - - 100 - messages: - - RotateCmd - communication: - - btle: - names: - - TF-META2 - services: - 0000ffac-0000-1000-8000-00805f9b34fb: - tx: 0000ffb7-0000-1000-8000-00805f9b34fb - tryfun-blackhole: - defaults: - name: TryFun Black Hole Plus - features: - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - communication: - - btle: - names: - - TF-BHPLUS - services: - 0000ffac-0000-1000-8000-00805f9b34fb: - tx: 0000ffb7-0000-1000-8000-00805f9b34fb - metaxsire: - defaults: - name: metaXsire Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - configurations: - - identifier: - - Rex - name: metaXsire Rex - - identifier: - - Cali - - LY165A01 - name: metaXsire Cali - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Constrict - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - Olis - name: metaXsire Olis - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Rotate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - LY213A01 - name: metaXsire BuCUE - features: - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - communication: - - btle: - names: - - Rex - - Cali - - LY165A01 - - Olis - - LY213A01 - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - rx: 0000ffe2-0000-1000-8000-00805f9b34fb - metaxsire-repeat: - defaults: - name: Cooxer Bullet Vibe - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - configurations: - - identifier: - - LY199B01 - name: Cooxer Bullet Vibe - - identifier: - - LY234A01 - name: metaXsire Tadpole - - identifier: - - LY271A01 - name: metaXsire Upton - - identifier: - - LY270A01 - name: metaXsire Una - communication: - - btle: - names: - - LY199B01 - - LY234A01 - - LY271A01 - - LY270A01 - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - rx: 0000ffe2-0000-1000-8000-00805f9b34fb - metaxsire-v2: - defaults: - name: metaXsire Nolan - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - configurations: - - identifier: - - LB-W01 - name: Libo Miao - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - identifier: - - HH010 - name: metaXsire HH010 - features: - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - communication: - - btle: - names: - - LY272A01 - - LB-W01 - - HH010 - services: - 0000bae0-0000-1000-8000-00805f9b34fb: - tx: 0000bae1-0000-1000-8000-00805f9b34fb - metaxsire-v3: - defaults: - name: metaXsire Tay - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - configurations: - - identifier: - - TAY001 - name: metaXsire Tay 1 - - identifier: - - TAY009 - name: metaXsire Tay 9 - - identifier: - - TAY006 - name: metaXsire Tay 6 - - identifier: - - TA-S001A - name: metaXsire Zeus - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 20 - messages: - - ScalarCmd - communication: - - btle: - names: - - TAY001 - - TAY006 - - TAY009 - - TA-S001A - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fe02-0000-1000-8000-00805f9b34fb - metaxsire-v4: - defaults: - name: metaXsire G1 Vibrator - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 99 - messages: - - ScalarCmd - communication: - - btle: - names: - - CFG1 vibrator - services: - 0000cfa2-0000-1000-8000-00805f9b34fb: - tx: 0000cf21-0000-1000-8000-00805f9b34fb - sexverse-lg389: - defaults: - name: Sexverse LG389 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 3 - messages: - - ScalarCmd - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 10 - messages: - - ScalarCmd - communication: - - btle: - names: - - LG389 - services: - 0000bae0-0000-1000-8000-00805f9b34fb: - tx: 0000bae1-0000-1000-8000-00805f9b34fb - rx: 0000bae2-0000-1000-8000-00805f9b34fb - cowgirl: - defaults: - name: The Cowgirl Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Rotate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - configurations: - - identifier: - - THE COWGIRL - name: The Cowgirl - - identifier: - - THE UNICORN - name: The Unicorn - communication: - - btle: - names: - - THE COWGIRL - - THE UNICORN - services: - 0000fe00-0000-1000-8000-00805f9b34fb: - tx: 0000fe01-0000-1000-8000-00805f9b34fb - cowgirl-cone: - defaults: - name: The Cowgirl Cone - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 128 - messages: - - ScalarCmd - configurations: - - identifier: - - CG-CONE - name: The Cowgirl Cone - communication: - - btle: - names: - - CG-CONE - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - galaku-pump: - defaults: - name: Galaku Device - features: - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - configurations: - - identifier: - - V415 - name: Galaku Nebula - communication: - - btle: - names: - - V415 - services: - 00001000-0000-1000-8000-00805f9b34fb: - tx: 00001001-0000-1000-8000-00805f9b34fb - galaku: - defaults: - name: Galaku Device - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - configurations: - # Type 0 - - identifier: - - V415 - name: Galaku Nebula - - identifier: - - GX85 - name: Galaku Shana - - identifier: - - GX07 - name: Galaku Miya - - identifier: - - GX17 - name: Galaku Capsule lipstick - - identifier: - - GX21 - name: Galaku Vitality Cat - - identifier: - - GX22 - name: Galaku Phantom X - - identifier: - - GX16 - name: Galaku Vitality Strawberry - - identifier: - - GX29 - name: Galaku Little Magic Box - - identifier: - - GX23 - name: Galaku Little Whale - - identifier: - - GX25 - name: Galaku Happy Vibrator - - identifier: - - GX26 - name: Galaku Xiaobao Beans - - identifier: - - GK03 - name: Galaku Capsule Vibrator - - identifier: - - GX39 - name: Galaku Ice cone miniAV stick - - identifier: - - G321 - name: Galaku mini ice cream cone - - identifier: - - G304 - name: Galaku Shia's Collar - - identifier: - - G336 - name: Galaku The Second Generation of Vitality Bird - - identifier: - - G331 - name: Galaku Octopus glans massager - - identifier: - - G326 - name: Galaku Alice - - identifier: - - G335 - name: Galaku Unicorn Butt Plug - - identifier: - - G341 - name: Galaku Ace - - identifier: - - G355 - name: Galaku Little cute turtle - - identifier: - - G349 - name: Galaku Little Bullet - - identifier: - - G407 - name: Galaku Joy Vibrator - - identifier: - - G204 - name: Galaku Bowling - - identifier: - - G171 - name: Galaku Mixin Controller - - identifier: - - G12D - name: Galaku Hua Chao Brush - - identifier: - - G123 - name: Galaku 花sai - - identifier: - - G23A - name: Galaku Dream Vibration - - identifier: - - G336 - name: Galaku The Second Generation of Vitality Bird - - identifier: - - G23A - name: Galaku Dream Vibration - - identifier: - - A073 - name: Galaku Joy Vibrator - - identifier: - - GLMT - name: Galaku Rogue Rabbit - - identifier: - - G901 - name: Galaku Suck the vibrator - - identifier: - - G912 - name: Galaku Donut - - identifier: - - G901 - name: Galaku Suck the vibrator - - identifier: - - G20B - name: Galaku Ballet Vibrator - - identifier: - - K112 - name: Galaku Donut - - identifier: - - G202 - name: Galaku Flirting Pen - - identifier: - - K118 - name: Galaku Ball vibrator - - identifier: - - K107 - name: Galaku Cyberpunk Airplane Cup - - identifier: - - G203 - name: Galaku Vitality Cute Pet - - identifier: - - TXHL - name: Galaku Little gourd vibrating egg - - identifier: - - TXMM - name: Galaku little kitten - - identifier: - - TXKL - name: Galaku Little Dinosaur - - identifier: - - K108 - name: Galaku Bell sucking - - identifier: - - K109 - name: Galaku Ring vibration - - identifier: - - KWL2 - name: Galaku Erection Booster - - identifier: - - TFHL - name: Galaku Gyoyo-G (meaning Yue-little gourd) - - identifier: - - TFMM - name: Galaku Gyoyo (meaning joy) - - identifier: - - TFKL - name: Galaku Gyoyo (meaning joy) - - identifier: - - K120 - name: Galaku Pinky stick - - identifier: - - K12A - name: Galaku Little Turtle Stick - - identifier: - - K12C - name: Galaku Xiao Xian Wan - - identifier: - - LL18 - name: Galaku Mitang - - identifier: - - CYX2 - name: Secret Lover Simon - - identifier: - - RC31 - name: Secret Lover Betty - - identifier: - - MD19 - name: Secret Lover Kevin - # Type 1 - - identifier: - - G317 - name: Galaku Zaku Aircraft Cup - features: - - feature-type: Oscillate - description: Oscillate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - G312 - name: Galaku Mecha-Original Owner's Aircraft Cup - features: - - feature-type: Oscillate - description: Oscillate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - G302 - name: Galaku Little Devil - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - G320 - name: Galaku Athena - features: - - feature-type: Oscillate - description: Oscillate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - G314 - name: Galaku Vitality Octopus II - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - G228 - name: Galaku Little Dolphin - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - G315 - name: Galaku Unicorn - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - G307 - name: Galaku Queen Bee Gun - features: - - feature-type: Oscillate - description: Oscillate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - K311 - name: Galaku Freya - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - G339 - name: Galaku Rhino Prostate Massager - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - G354 - name: Galaku Double-A Aircraft Cup - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - G12B - name: Galaku Flower Season - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - G29C - name: Galaku Little Rubik's Cube - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - G29D - name: Galaku Small powder cake - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - GKML - name: Galaku Milly - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - G348 - name: Galaku Rhinoceros Back Court - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - G913 - name: Galaku Unicorn II - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - G213 - name: Galaku Phantom - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - TFF1 - name: Galaku F1 Aircraft Cup - features: - - feature-type: Oscillate - description: Oscillate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - G310 - name: Galaku Scepter AV Stick - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - K113 - name: Galaku Unicorn II - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - G228 - name: Galaku Little Dolphin - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - G310 - name: Galaku Scepter AV Stick - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - TFF1 - name: Galaku F1 Aircraft Cup - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - D358 - name: Galaku Classic vibration-absorbing AV state - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - G322 - name: Galaku Unicorn - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - D402 - name: Galaku New series of vibrators - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - G40A - name: Galaku New series of vibrators - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - G403 - name: Galaku New series of vibrators - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - G43A - name: Galaku New series of vibrators - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - K12B - name: Galaku Little Turtle Stick - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - QCVW - name: Kisstoy Lost (Vibrating) - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - QCSW - name: Kisstoy Lost (Sucking) - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - QCPW - name: Kisstoy Lost (Insertable) - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - # Type 2 - - identifier: - - TFG1 - name: Galaku Aurora Aircraft Cup - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Constrict - description: Suction Pump - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - GK27 - name: Galaku Cannon-GT - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - GK25 - name: Galaku Phantom PLUS - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - AC695X_1(BLE) - name: Galaku Vision - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - GX33 - name: Galaku Dimension No. 1 - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - - identifier: - - WSXK - name: Galaku Starry Sky CUP - features: - - feature-type: Vibrate - description: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Battery - description: Battery Level - sensor: - value-range: - - - 0 - - 100 - messages: - - SensorReadCmd - communication: - - btle: - names: - # Type 0 - # - V415 see galaku-pump - - GX85 - - GX07 - - GX17 - - GX21 - - GX22 - - GX16 - - GX29 - - GX23 - - GX25 - - GX26 - - GK03 - - GX39 - - G321 - - G304 - - G336 - - G331 - - G326 - - G335 - - G341 - - G355 - - G349 - - G407 - - G204 - - G171 - - G12D - - G123 - - G23A - - G336 - - G23A - - A073 - - GLMT - - G901 - - G912 - - G901 - - G20B - - K112 - - G202 - - K118 - - K107 - - G203 - - TXHL - - TXMM - - TXKL - - K108 - - K109 - - KWL2 - - TFHL - - TFMM - - TFKL - - K120 - - K12A - - K12C - - LL18 - - CYX2 # Secret Lover Simon - - RC31 # Secret Lover Betty - - MD19 # Secret Lover Kevin - # Type 1 - - G317 - - G312 - - G302 - - G320 - - G314 - - G228 - - G315 - - G307 - - K311 - - G339 - - G354 - - G12B - - G29C - - G29D - - GKML - - G348 - - G913 - - G213 - - TFF1 - - G310 - - K113 - - G228 - - G310 - - TFF1 - - D358 - - G322 - - D402 - - G40A - - G403 - - G43A - - K12B - - QCVW - - QCSW - - QCPW - # Type 2 - - TFG1 - - GK27 - - GK25 - - AC695X_1(BLE) - - GX33 - - WSXK - services: - 00001000-0000-1000-8000-00805f9b34fb: - tx: 00001001-0000-1000-8000-00805f9b34fb - rxblebattery: 00001002-0000-1000-8000-00805f9b34fb - xibao: - defaults: - name: Xibao Smart Masturbation Cup - features: - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 99 - messages: - - ScalarCmd - communication: - - btle: - names: - - CCYB_* - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff2-0000-1000-8000-00805f9b34fb - sensee: - defaults: - name: Sensee Diandou Rabbit - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - communication: - - btle: - names: - - CTY222S4 - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff5-0000-1000-8000-00805f9b34fb - sensee-v2: - defaults: - name: Sensee Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Constrict - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - configurations: - - identifier: - - CCPA10S2 - name: Sensee Capsule - - identifier: - - CCPA18S5 - name: Sensee Astronaut - - identifier: - - Easylive NO8 Cup - name: Sensee No8 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - CCP322S5 - name: Easylive Vader - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - CTY508S5 - name: Sensee Voice-Interactive Female Vibrator - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - PTYB22S2 - name: Sensee Moonlight - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Constrict - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - CTY823S5 - name: Sensee Little Seahorse - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Constrict - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - CTY916S4 - name: Sensee Dream Stick - features: - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - communication: - - btle: - names: - - CCPA10S2 - - CCPA18S5 - - Easylive NO8 Cup - - CTY508S5 - - CTY916S4 - - PTYB22S2 - - CCP322S5 - - CTY823S5 - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff5-0000-1000-8000-00805f9b34fb - rx: 0000fff4-0000-1000-8000-00805f9b34fb - fox: - defaults: - name: Fox Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 3 - messages: - - ScalarCmd - communication: - - btle: - names: - - FOX - - FOX M70 Pro - - FoxM70Pro - - FOX M70-2 - services: - 0000ae00-0000-1000-8000-00805f9b34fb: - tx: 0000ae01-0000-1000-8000-00805f9b34fb - kizuna: - defaults: - name: Kizuna Smart - features: - - feature-type: Rotate - actuator: - step-range: - - 0 - - 9 - messages: - - ScalarCmd - communication: - - serial: - port: default - baud-rate: 19200 - data-bits: 8 - parity: 'N' - stop-bits: 1 - xiuxiuda: - defaults: - name: Xiuxiuda Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 19 - messages: - - ScalarCmd - communication: - - btle: - names: - - XXD-Lush* - services: - 53300001-0023-4bd4-bbd5-a6920e4c5653: - tx: 53300003-0023-4bd4-bbd5-a6920e4c5653 - longlosttouch: - defaults: - name: Long Lost Touch Possible Kiss - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - communication: - - btle: - names: - - RS-KNW - services: - 0000cb60-0000-1000-8000-00805f9b34fb: - tx: 0000cb61-0000-1000-8000-00805f9b34fb - rx: 0000cb62-0000-1000-8000-00805f9b34fb - adrienlastic: - defaults: - name: Adrien Lastic Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 16 - messages: - - ScalarCmd - configurations: - - identifier: - - LVS-S001 - name: Adrien Lastic Palpitation - - identifier: - - LVS-S002 - name: Adrien Lastic Revelation - communication: - - btle: - names: - - >- - Placeholder to avoid conflict with bad attempt to clone a Lovense - Lush - advertised-services: - - 00001320-0000-1000-8000-00805f9b34fb - services: - 6e400001-b5a3-f393-e0a9-e50e24dcca9e: - tx: 6e400002-b5a3-f393-e0a9-e50e24dcca9e - nintendo-joycon: - defaults: - name: Nintendo Joycon - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 1000 - messages: - - ScalarCmd - communication: - - hid: - pairs: - - vendor-id: 1406 - product-id: 8199 - - vendor-id: 1406 - product-id: 8198 - - vendor-id: 1406 - product-id: 8201 - foreo: - defaults: - name: Foreo Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 10 - messages: - - ScalarCmd - configurations: - - identifier: - - FOFO - - LUNA fofo - - LUNA FOFO - - LUNA PLAY SMART - name: Foreo LUNA fofo - - identifier: - - LUNA PLAYSMART2 - - LUNA PLAY SMART2 - - LUNA play smart2 - - LUNA play smart 2 - name: Foreo LUNA play smart 2 - - identifier: - - LUNA 3 - - LUNA3 - name: Foreo LUNA 3 - - identifier: - - LUNA3PLUS - - LUNA3 PLUS - - LUNA 3 PLUS - - LUNA 3 plus - name: Foreo LUNA 3 plus - - identifier: - - LUNA 3 MEN - - LUNA3MEN - name: Foreo LUNA 3 men - - identifier: - - LUNA MINI3 - - LUNA MINI 3 - - LUNA mini 3 - name: Foreo LUNA 3 mini - - identifier: - - LUNA4 - - LUNA 4 - name: Foreo LUNA 4 - - identifier: - - LUNA4PLUS - - LUNA4 PLUS - - LUNA 4 plus - name: Foreo LUNA 4 plus - - identifier: - - LUNA4MEN - - LUNA 4 MEN - - LUNA 4 FOR MEN - name: Foreo LUNA 4 men - - identifier: - - LUNA MINI4 - - LUNA MINI 4 - - LUNA mini 4 - - LUNA 4 mini - name: Foreo LUNA 4 mini - - identifier: - - UFO - name: Foreo UFO - - identifier: - - UFO mini - - UFO MINI - - UFO MIN - name: Foreo UFO mini - - identifier: - - UFO2 - - UFO 2 - name: Foreo UFO 2 - - identifier: - - UFO3 - name: Foreo UFO 3 - - identifier: - - UFO3go - name: Foreo UFO 3 go - - identifier: - - UFO3eyes - name: Foreo UFO 3 led - - identifier: - - UFO3mini - name: Foreo UFO 3 mini - - identifier: - - UFOMINI2 - - UFO mini 2 - name: Foreo UFO mini 2 - - identifier: - - BEAR - name: Foreo BEAR - - identifier: - - BEAR_MINI - - BEAR MINI - - BEAR mini - name: Foreo BEAR mini - - identifier: - - BEAR2 - - BEAR 2 - name: Foreo BEAR 2 - - identifier: - - BEAR2go - name: Foreo BEAR 2 go - - identifier: - - BEAR2eyes - name: Foreo BEAR 2 eyes - - identifier: - - BEAR2body - name: Foreo BEAR 2 body - - identifier: - - KIWI - name: Foreo KIWI - - identifier: - - KIWI derma - name: Foreo KIWI derma - communication: - - btle: - names: - - FOFO - - LUNA fofo - - LUNA FOFO - - LUNA PLAY SMART - - LUNA PLAYSMART2 - - LUNA PLAY SMART2 - - LUNA play smart2 - - LUNA play smart 2 - - LUNA 3 - - LUNA3 - - LUNA3PLUS - - LUNA3 PLUS - - LUNA 3 PLUS - - LUNA 3 plus - - LUNA 3 MEN - - LUNA3MEN - - LUNA MINI3 - - LUNA MINI 3 - - LUNA mini 3 - - LUNA4PLUS - - LUNA4 - - LUNA 4 - - LUNA4PLUS - - LUNA4 PLUS - - LUNA 4 plus - - LUNA4MEN - - LUNA 4 MEN - - LUNA 4 FOR MEN - - LUNA MINI4 - - LUNA MINI 4 - - LUNA mini 4 - - LUNA 4 mini - - UFO - - UFO mini - - UFO MINI - - UFO MIN - - UFO2 - - UFO 2 - - UFOMINI2 - - UFO mini 2 - - UFO3 - - UFO3mini - - UFO3go - - UFO3led - - BEAR - - BEAR_MINI - - BEAR MINI - - BEAR mini - - BEAR2 - - BEAR 2 - - BEAR2go - - BEAR2body - - BEAR2eyes - - KIWI - - KIWI derma - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff1-0000-1000-8000-00805f9b34fb - monsterpub: - defaults: - name: Sistalk MonsterPub Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - configurations: - - identifier: - - MP2_JK_N_P1 - name: Sistalk MonsterPub 2 Doctor Whale - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - MP_MW_TL_P2 - name: Sistalk MonsterPub Magic Kiss - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - MP2_QC_TL_P1 - name: Sistalk MonsterPub 2 Mister Devil - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - MP_BABY_QC_N_P4 - name: Sistalk MonsterPub Baby Youth Health - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - MP_MXY_N_P1 - name: Sistalk MonsterPub KiniCat - - identifier: - - MP1N_QC_TL_P2 - name: Sistalk MonsterPub BeatHeart - - identifier: - - TDG_LIP_PT2 - name: Tracy's Dog Surreal - - identifier: - - MP1P_QC_TL_P6 - name: Sistalk MonsterPub 1P Mister Devil - - identifier: - - MPMB_QC_TL_P2 - name: Sistalk MonsterPub Sweet - - identifier: - - MPAV_QC_TL_P1 - name: Sistalk MonsterPub Amazing - - identifier: - - MH_TOR_TL_P5 - name: Sistalk MonsterHub Tornado - - identifier: - - MP_SUCKBANG_P5 - name: Sistalk MonsterPub Pop - features: - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - identifier: - - TDG_CRAYBIT_PT - name: Tracy's Dog Craybit Pro - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - communication: - - btle: - names: - - MonsterPub - - MonsterHub - - TracyDog - services: - 00006000-0000-1000-8000-00805f9b34fb: - tx: 00006001-0000-1000-8000-00805f9b34fb - txmode: 00006002-0000-1000-8000-00805f9b34fb - txvibrate: 00006003-0000-1000-8000-00805f9b34fb - generic0: 0000600a-0000-1000-8000-00805f9b34fb - 00006010-0000-1000-8000-00805f9b34fb: - rxblemodel: 00006014-0000-1000-8000-00805f9b34fb - 00008000-0000-1000-8000-00805f9b34fb: - rx: 00008001-0000-1000-8000-00805f9b34fb - joyhub: - defaults: - name: JoyHub Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - configurations: - - identifier: - - JOYHUB-ROSELLA2 - name: JoyHub Rosella 2 - - identifier: - - J-Velocity - name: JoyHub Velocity - - identifier: - - J-ElixirEgg - name: JoyHub ElixirEgg - - identifier: - - J-RetroGuard - name: JoyHub Retro Guard - - identifier: - - J-TrueForm3 - name: JoyHub TrueForm 3 - - identifier: - - J-TrueForm - name: JoyHub TrueForm - - identifier: - - J-Rhythmic2 - name: JoyHub Rhythmic 2 - - identifier: - - J-Rhythmic3 - name: JoyHub Rhythmic 3 - - identifier: - - J-Rainbow - name: JoyHub Rainbow - - identifier: - - J-BlackBull - name: JoyHub Black Bull - - identifier: - - J-Peacock - name: JoyHub Peacock - - identifier: - - J-Mace - name: JoyHub Mace - - identifier: - - J-Tarian - name: JoyHub Tarian - - identifier: - - J-Euphoric - name: JoyHub Euphoric - - identifier: - - J-Euphoric3 - name: JoyHub Euphoric3 - - identifier: - - J-Torrian - name: JoyHub Torrian - - identifier: - - J-Rayen - name: JoyHub Rayen - - identifier: - - J-Mackay - name: JoyHub Mackay - - identifier: - - J-Rowdy3 - name: JoyHub Rowdy 3 - - identifier: - - J-Eclipse - name: JoyHub Eclipse - - identifier: - - J-Scarlett - name: JoyHub Scarlett - - identifier: - - J-Tarik - name: JoyHub Tarik - - identifier: - - J-UricaGuard2 - name: JoyHub Urica Guard 2 - - identifier: - - J-Viva - name: JoyHub Viva - - identifier: - - J-Ryden - name: JoyHub Ryden - - identifier: - - J-Petalwish2 - name: JoyHub Petalwish 2 - features: - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-VortexTongue - name: JoyHub Vortex Tongue - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Constrict - description: Air Pump - actuator: - step-range: - - 0 - - 3 - messages: - - ScalarCmd - - feature-type: Rotate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-VibSiren - name: JoyHub VibSiren - features: - - feature-type: Vibrate - description: External vibrator - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Internal vibrator - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Mysticolor - name: JoyHub Mysticolor - features: - - feature-type: Rotate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Constrict - description: Air Pump - actuator: - step-range: - - 0 - - 7 - messages: - - ScalarCmd - - identifier: - - J-VividWings - name: JoyHub Vivid Wings - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Mariner - name: JoyHub Mariner - features: - - feature-type: Rotate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Constrict - description: Air Pump - actuator: - step-range: - - 0 - - 2 - messages: - - ScalarCmd - - identifier: - - J-MarsLion - name: JoyHub MarsLion - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Constrict - description: Air Pump - actuator: - step-range: - - 0 - - 5 - messages: - - ScalarCmd - - identifier: - - J-Pul - name: JoyHub Pul - features: - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-ROSELLA3 - name: JoyHub Rose Love - features: - - feature-type: Constrict - description: Air Pump - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-DukeDazzle2 - name: JoyHub Edasich - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Mars - name: JoyHub Mars - features: - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Martino - name: JoyHub Martino - features: - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-MarsLion2 - name: JoyHub Mars Lion 2 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Constrict - actuator: - step-range: - - 0 - - 5 - messages: - - ScalarCmd - - identifier: - - J-Myrna - name: JoyHub Myrna - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Constrict - actuator: - step-range: - - 0 - - 9 - messages: - - ScalarCmd - - identifier: - - J-Vase2 - name: JoyHub Vase 2 - features: - - feature-type: Vibrate - description: Biting lips - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Sideways flicker - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - communication: - - btle: - names: - - J-Petalwish2 - - J-VortexTongue - - J-Velocity - - JOYHUB-ROSELLA2 - - J-VibSiren - - J-ElixirEgg - - J-RetroGuard - - J-TrueForm - - J-TrueForm3 - - J-Rhythmic2 - - J-Rhythmic3 - - J-Mysticolor - - J-VividWings - - J-Rainbow - - J-BlackBull - - J-Peacock - - J-Mariner - - J-Mace - - J-MarsLion - - J-Tarian - - J-Pul - - J-Euphoric - - J-Euphoric3 - - J-Torrian - - J-Rayen - - J-ROSELLA3 - - J-Mackay - - J-Rowdy3 - - J-Eclipse - - J-DukeDazzle2 - - J-Scarlett - - J-Tarik - - J-UricaGuard2 - - J-Viva - - J-Ryden - - J-Mars - - J-MarsLion2 - - J-Myrna - - J-Vase2 - - J-Martino - services: - 0000ffa0-0000-1000-8000-00805f9b34fb: - tx: 0000ffa1-0000-1000-8000-00805f9b34fb - joyhub-v2: - defaults: - name: JoyHub Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - configurations: - - identifier: - - J-Pearlconch - name: JoyHub Pearlconch - features: - - feature-type: Rotate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Pearlconch - name: JoyHub Pearlconch - features: - - feature-type: Rotate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-PearlconchL - name: JoyHub Pearlconch L - features: - - feature-type: Rotate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Piet2 - name: JoyHub Piet 2 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Rotate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Panther - name: JoyHub Panther - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Rotate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-PetiteRose - name: JoyHub Petite Rose - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Rotate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-MoonHorn - name: JoyHub Moon Horn - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Constrict - description: Suction - actuator: - step-range: - - 0 - - 9 - messages: - - ScalarCmd - - identifier: - - J-Mecha - name: JoyHub Mecha - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Constrict - description: Suction - actuator: - step-range: - - 0 - - 7 - messages: - - ScalarCmd - - identifier: - - J-Lagoon - name: JoyHub Lagoon - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Constrict - description: Suction - actuator: - step-range: - - 0 - - 5 - messages: - - ScalarCmd - - identifier: - - J-VibTrefoil - name: JoyHub VibTrefoil - features: - - feature-type: Vibrate - description: External vibrator - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Internal vibrator - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Firedragon - name: JoyHub Firedragon - features: - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Dina - name: JoyHub Deena - features: - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Internal vibrator - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - description: External vibrator - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Vbarbie3f - name: JoyHub Cherly - features: - - feature-type: Vibrate - description: External vibrator - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Internal vibrator - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-CHERLY2c - name: JoyHub Cherly 2c - features: - - feature-type: Vibrate - description: Internal vibrator - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Internal Whip - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - description: External vibrator - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Pathfinder2 - name: JoyHub Pathfinder 2 - features: - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Pathfinder - name: JoyHub Pathfinder - features: - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-VibRipple - name: JoyHub Angela - features: - - feature-type: Vibrate - description: External vibrator - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Internal vibrator - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Verax - name: JoyHub Verax - features: - - feature-type: Vibrate - description: Internal Whip - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Internal vibrator - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Verax2 - name: JoyHub Verax 2 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Rotate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Euphoric2 - name: JoyHub Euphoric 2 - features: - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-ROSEBUD - name: JoyHub RoseBUD - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Rotate - description: Flicker - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Constrict - description: Suction - actuator: - step-range: - - 0 - - 5 - messages: - - ScalarCmd - - identifier: - - J-Morningbuds2 - name: JoyHub Morningbuds - features: - - feature-type: Rotate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Rhythmic4 - name: JoyHub Rhythmic 4 - features: - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Virtuoso2 - name: JoyHub Virtuoso 2 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Rotate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Constrict - description: Suction - actuator: - step-range: - - 0 - - 3 - messages: - - ScalarCmd - - identifier: - - J-Dyllis - name: JoyHub Dyllis - features: - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Flamewing - name: JoyHub PhoenixGP - features: - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Fabledragon - name: JoyHub Fable Dragon - features: - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Faunus - name: JoyHub Faunus - features: - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-VelvetRabbit - name: JoyHub Velvet Rabbit - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-VividPulse - name: JoyHub Vivid Pulse - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-VioletVine - name: JoyHub Violet Vine - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-VibSiren2 - name: JoyHub VibSiren 2 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Veemy - name: JoyHub Veemy - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Viball - name: JoyHub Viball - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Vase - name: JoyHub Vase - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Vortex2s - name: JoyHub Vortex 2s - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-VortexTongue2 - name: JoyHub Lips - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Rotate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Constrict - description: Air Pump - actuator: - step-range: - - 0 - - 3 - messages: - - ScalarCmd - - identifier: - - J-Torin - name: JoyHub Torin - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-VBarbiep - name: JoyHub VBarbie p - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Vbarbie - name: JoyHub VBarbie - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Royaleye - name: JoyHub Royaleye - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-VBarbie2t - name: JoyHub Norma - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Pau - name: JoyHub Pau - features: - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Petalwish3 - name: JoyHub Petalwish 3 - features: - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Marshal - name: JoyHub Marshal - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Constrict - description: Air Pump - actuator: - step-range: - - 0 - - 9 - messages: - - ScalarCmd - - identifier: - - J-Vince - name: JoyHub Vince - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Dallin - name: JoyHub Dallin - features: - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Mace2 - name: JoyHub Maynor - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Constrict - description: Air Pump - actuator: - step-range: - - 0 - - 9 - messages: - - ScalarCmd - - identifier: - - J-Verax4 - name: JoyHub Verax 4 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Palmyra - name: JoyHub Palmyra - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Xylia - name: JoyHub Xylia - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Maiden - name: JoyHub Maiden - features: - - feature-type: Rotate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Constrict - actuator: - step-range: - - 0 - - 5 - messages: - - ScalarCmd - - identifier: - - J-Viele3 - name: JoyHub Viele 3 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Rotate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Troi - name: JoyHub Troi - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Tanmouth - name: JoyHub Tanmouth - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Marcela - name: JoyHub Marcela - features: - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-Vita - name: JoyHub Vita - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Rotate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - identifier: - - J-LACH - name: JoyHub Lach - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Constrict - actuator: - step-range: - - 0 - - 5 - messages: - - ScalarCmd - - identifier: - - J-Markel - name: JoyHub Markel - features: - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Constrict - description: Suction - actuator: - step-range: - - 0 - - 9 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - communication: - - btle: - names: - - J-Pearlconch - - J-PearlconchL - - J-PetiteRose - - J-MoonHorn - - J-VibTrefoil - - J-Panther - - J-Mecha - - J-Lagoon - - J-Firedragon - - J-Dina - - J-Vbarbie3f - - J-CHERLY2c - - J-Pathfinder2 - - J-Pathfinder - - J-VibRipple - - J-Verax - - J-Verax2 - - J-Euphoric2 - - J-ROSEBUD - - J-Morningbuds2 - - J-Rhythmic4 - - J-Virtuoso2 - - J-Dyllis - - J-Flamewing - - J-VelvetRabbit - - J-VividPulse - - J-VioletVine - - J-VibSiren2 - - J-Veemy - - J-Fabledragon - - J-Faunus - - J-VortexTongue2 - - J-Torin - - J-VBarbiep - - J-Vbarbie - - J-Viball - - J-Vase - - J-Vortex2s - - J-Royaleye - - J-VBarbie2t - - J-Pau - - J-Petalwish3 - - J-Marshal - - J-Piet2 - - J-Vince - - J-Dallin - - J-Mace2 - - J-Verax4 - - J-Palmyra - - J-Maiden - - J-Viele3 - - J-Xylia - - J-Troi - - J-Tanmouth - - J-Marcela - - J-Vita - - J-LACH - - J-Markel - services: - 0000ffa0-0000-1000-8000-00805f9b34fb: - tx: 0000ffa1-0000-1000-8000-00805f9b34fb - joyhub-v3: - defaults: - name: JoyHub Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - configurations: - - identifier: - - J-Ringstar - name: JoyHub Starfish - - identifier: - - J-RapidTwist2 - name: JoyHub Resi Ring 2 - communication: - - btle: - names: - - J-Ringstar - - J-RapidTwist2 - services: - 0000ffa0-0000-1000-8000-00805f9b34fb: - tx: 0000ffa1-0000-1000-8000-00805f9b34fb - joyhub-v4: - defaults: - name: JoyHub Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Rotate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Constrict - description: Suction - actuator: - step-range: - - 0 - - 4 - messages: - - ScalarCmd - configurations: - - identifier: - - J-RoseLin - name: JoyHub RoseLin - - identifier: - - J-Viele - name: JoyHub Viele - features: - - feature-type: Rotate - description: Internal Simulator - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Internal Whip - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Internal Vibrator - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - communication: - - btle: - names: - - J-RoseLin - - J-Viele - services: - 0000ffa0-0000-1000-8000-00805f9b34fb: - tx: 0000ffa1-0000-1000-8000-00805f9b34fb - joyhub-v5: - defaults: - name: JoyHub Device - features: - - feature-type: Rotate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Constrict - description: Suction - actuator: - step-range: - - 0 - - 1 - messages: - - ScalarCmd - configurations: - - identifier: - - J-Virtuoso - name: JoyHub Virtuoso - - identifier: - - J-Pathfinder3 - name: JoyHub Pathfinder 3 - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - communication: - - btle: - names: - - J-Virtuoso - - J-Pathfinder3 - services: - 0000ffa0-0000-1000-8000-00805f9b34fb: - tx: 0000ffa1-0000-1000-8000-00805f9b34fb - joyhub-v6: - defaults: - name: JoyHub Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Constrict - description: Suction - actuator: - step-range: - - 0 - - 9 - messages: - - ScalarCmd - configurations: - - identifier: - - J-Melody - name: JoyHub Melody - communication: - - btle: - names: - - J-Melody - services: - 0000ffa0-0000-1000-8000-00805f9b34fb: - tx: 0000ffa1-0000-1000-8000-00805f9b34fb - itoys: - defaults: - name: iToys Seagull - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 3 - messages: - - ScalarCmd - communication: - - btle: - names: - - 26-021-B - services: - 0000ffa0-0000-1000-8000-00805f9b34fb: - tx: 0000ffa1-0000-1000-8000-00805f9b34fb - leten: - defaults: - name: Leten Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 25 - messages: - - ScalarCmd - communication: - - btle: - names: - - T528-LT - - F537-LT - - F520B-LT - - F520A-LT - services: - 0000fff0-0000-1000-8000-00805f9b34fb: - tx: 0000fff1-0000-1000-8000-00805f9b34fb - 0000ffe0-0000-1000-8000-00805f9b34fb: - rx: 0000ffe1-0000-1000-8000-00805f9b34fb - vibcrafter: - defaults: - name: VibCrafter Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 99 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 99 - messages: - - ScalarCmd - configurations: - - identifier: - - be gentle - name: VibCrafter Harlow - - identifier: - - Hayden - name: VibCrafter Hayden - - identifier: - - Nidalee - name: VibCrafter Nidalee - - identifier: - - Janna - name: VibCrafter Janna - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 99 - messages: - - ScalarCmd - communication: - - btle: - names: - - be gentle - - Janna - - Hayden - - Nidalee - services: - 53300051-0060-4bd4-bbe5-a6920e4c5663: - tx: 53300052-0060-4bd4-bbe5-a6920e4c5663 - rx: 53300053-0060-4bd4-bbe5-a6920e4c5663 - lioness: - defaults: - name: Lioness - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - communication: - - btle: - names: - - Lioness - - Lioness2 - services: - d973f2ed-b19e-11e2-9e96-0800200c9a66: - tx: d973f2f4-b19e-11e2-9e96-0800200c9a66 - d973f2e5-b19e-11e2-9e96-0800200c9a66: - rx: d973f2e6-b19e-11e2-9e96-0800200c9a66 - activejoy: - defaults: - name: IntoYou Remote Egg Vibrator - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - communication: - - btle: - names: - - SS-TD-YDTD-001 - services: - 0000f0b0-0000-1000-8000-00805f9b34fb: - tx: 0000f0b1-0000-1000-8000-00805f9b34fb - rx: 0000f0b2-0000-1000-8000-00805f9b34fb - cupido: - defaults: - name: Cupido Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - communication: - - btle: - names: - - MY2607-BLE-V1.0 - services: - 0000f0b0-0000-1000-8000-00805f9b34fb: - tx: 0000f0b1-0000-1000-8000-00805f9b34fb - rx: 0000f0b2-0000-1000-8000-00805f9b34fb - amorelie-joy: - defaults: - name: Amorelie Joy Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - configurations: - - identifier: - - 4D02 - name: Amorelie Joy Move - - identifier: - - 4D05 - name: Amorelie Joy Cha-Cha - - identifier: - - 4D06 - name: Amorelie Joy Boogie - - identifier: - - 4D01 - name: Amorelie Joy Shimmer - - identifier: - - 4D03 - name: Amorelie Joy Grow - - identifier: - - 4D04 - name: Amorelie Joy Shuffle - - identifier: - - 4D07 - name: Amorelie Joy Salsa - communication: - - btle: - names: - - 4D01 - - 4D02 - - 4D03 - - 4D04 - - 4D05 - - 4D06 - - 4D07 - - 4D08 - - 4D09 - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - rx: 0000ffe2-0000-1000-8000-00805f9b34fb - tx: 0000ffe3-0000-1000-8000-00805f9b34fb - feelingso: - defaults: - name: FeelingSo Flair Feel - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 19 - messages: - - ScalarCmd - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 19 - messages: - - ScalarCmd - communication: - - btle: - names: - - Flair Feel - services: - 42410001-0000-0101-0000-736278637a72: - tx: 42410002-0000-0101-0000-736278637a72 - rx: 42410003-0000-0101-0000-736278637a72 - deepsire: - defaults: - name: DeepSire Device - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - configurations: - - identifier: - - IMP 3 - name: Kuirkish Imp 3 - communication: - - btle: - names: - - IMP 3 - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - nextlevelracing: - defaults: - name: Next Level Racing HF8 Haptic Gaming Pad - features: - - feature-type: Vibrate - description: Right thigh - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Left thigh - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Right buttock - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Left buttock - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Right back - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Left back - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Right shoulder - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - description: Left shoulder - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - communication: - - serial: - port: default - baud-rate: 115200 - data-bits: 8 - parity: 'N' - stop-bits: 1 # Actually 1.5, but this is never actually used - xuanhuan: - defaults: - name: Xuanhuan Masturbator - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 10 - messages: - - ScalarCmd - communication: - - btle: - names: - - QUXIN - services: - 0000fffe-0000-1000-8000-00805f9b34fb: - tx: 0000fe02-0000-1000-8000-00805f9b34fb - serveu: - defaults: - name: ServeU - features: - # TODO ServeU has an explicit connection interval of 20ms, we should define this here somehow? - - feature-type: Position - actuator: - step-range: - - 0 - - 100 - messages: - - LinearCmd - communication: - - btle: - names: - - ServeU - services: - 31bb1111-33e3-4f3c-a7fb-104288e7cb77: - tx: 31bb2222-33e3-4f3c-a7fb-104288e7cb77 - fleshy-thrust: - defaults: - name: Fleshy Thrust Sync - features: - - feature-type: Position - actuator: - step-range: - - 0 - - 180 - messages: - - LinearCmd - communication: - - btle: - names: - - BT05 - services: - 0000ffe0-0000-1000-8000-00805f9b34fb: - tx: 0000ffe1-0000-1000-8000-00805f9b34fb - nexus-revo: - defaults: - name: Nexus Revo Stealth - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 10 - messages: - - ScalarCmd - - feature-type: Rotate - actuator: - step-range: - - 0 - - 2 - messages: - - RotateCmd - communication: - - btle: - names: - - XW-LW3 - services: - 0000c570-0000-1000-8000-00805f9b34fb: - tx: 0000c571-0000-1000-8000-00805f9b34fb - luvmazer: - defaults: - name: Luvmazer Finger Magic - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Rotate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - communication: - - btle: - names: - - TKLM-W001-BT - services: - 0000ffa0-0000-1000-8000-00805f9b34fb: - tx: 0000ffa1-0000-1000-8000-00805f9b34fb - loob: - defaults: - name: Joyroid Loob - features: - - feature-type: Position - actuator: - step-range: - - 0 - - 1000 - messages: - - LinearCmd - communication: - - btle: - names: - - LOOB - services: - b75c49d2-04a3-4071-a0b5-35853eb08307: - tx: ba5c49d2-04a3-4071-a0b5-35853eb08307 - bananasome: - defaults: - name: Bananasome Rocket X7 - features: - - feature-type: Oscillate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 255 - messages: - - ScalarCmd - communication: - - btle: - names: - - 火箭X7 - services: - 0000ae00-0000-1000-8000-00805f9b34fb: - tx: 0000ae01-0000-1000-8000-00805f9b34fb - omobo: - defaults: - name: Omobo ViVegg Vibrator - features: - - feature-type: Vibrate - actuator: - step-range: - - 0 - - 100 - messages: - - ScalarCmd - communication: - - btle: - names: - - S6 - services: - 0000ffb0-0000-1000-8000-00805f9b34fb: - tx: 0000ffb2-0000-1000-8000-00805f9b34fb \ No newline at end of file diff --git a/buttplug/buttplug-device-config/package.json b/buttplug/buttplug-device-config/package.json deleted file mode 100644 index d9c6f667d..000000000 --- a/buttplug/buttplug-device-config/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "buttplug-device-config", - "version": "54.0.0", - "description": "Buttplug Device Configuration File", - "main": "index.js", - "scripts": { - "build": "js-yaml device-config-v2/buttplug-device-config-v2.yml > build-config/buttplug-device-config-v2.json && ajv validate --strict=false -s device-config-v2/buttplug-device-config-schema-v2.json -d build-config/buttplug-device-config-v2.json", - "build:v3": "js-yaml device-config-v3/buttplug-device-config-v3.yml > build-config/buttplug-device-config-v3.json && ajv validate --strict=false -s device-config-v3/buttplug-device-config-schema-v3.json -d build-config/buttplug-device-config-v3.json", - "convert": "node ./convert-v2-to-v3.js" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/buttplugio/buttplug-device-config.git" - }, - "keywords": [ - "teledildonics", - "buttplug" - ], - "author": "Nonpolynomial Labs, LLC ", - "license": "BSD-3-Clause", - "bugs": { - "url": "https://github.com/buttplugio/buttplug-device-config/issues" - }, - "homepage": "https://github.com/buttplugio/buttplug-device-config#readme", - "devDependencies": { - "ajv": "^8.12.0", - "ajv-cli": "^5.0.0", - "js-yaml": "^4.1.0" - }, - "dependencies": { - "trash-cli": "^5.0.0" - } -} diff --git a/buttplug/buttplug-device-config/yarn.lock b/buttplug/buttplug-device-config/yarn.lock deleted file mode 100644 index ebc5cd6f5..000000000 --- a/buttplug/buttplug-device-config/yarn.lock +++ /dev/null @@ -1,1230 +0,0 @@ -# This file is generated by running "yarn install" inside your project. -# Manual changes might be lost - proceed with caution! - -__metadata: - version: 8 - cacheKey: 10c0 - -"@babel/code-frame@npm:^7.0.0": - version: 7.18.6 - resolution: "@babel/code-frame@npm:7.18.6" - dependencies: - "@babel/highlight": "npm:^7.18.6" - checksum: 10c0/e3966f2717b7ebd9610524730e10b75ee74154f62617e5e115c97dbbbabc5351845c9aa850788012cb4d9aee85c3dc59fe6bef36690f244e8dcfca34bd35e9c9 - languageName: node - linkType: hard - -"@babel/helper-validator-identifier@npm:^7.18.6": - version: 7.19.1 - resolution: "@babel/helper-validator-identifier@npm:7.19.1" - checksum: 10c0/f978ecfea840f65b64ab9e17fac380625a45f4fe1361eeb29867fcfd1c9eaa72abd7023f2f40ac3168587d7e5153660d16cfccb352a557be2efd347a051b4b20 - languageName: node - linkType: hard - -"@babel/highlight@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/highlight@npm:7.18.6" - dependencies: - "@babel/helper-validator-identifier": "npm:^7.18.6" - chalk: "npm:^2.0.0" - js-tokens: "npm:^4.0.0" - checksum: 10c0/a6a6928d25099ef04c337fcbb829fab8059bb67d31ac37212efd611bdbe247d0e71a5096c4524272cb56399f40251fac57c025e42d3bc924db0183a6435a60ac - languageName: node - linkType: hard - -"@sindresorhus/chunkify@npm:^0.2.0": - version: 0.2.0 - resolution: "@sindresorhus/chunkify@npm:0.2.0" - checksum: 10c0/e53a917fd7896d0a788c6701aa91e287757c186ee48a25c3d78b48f5284d9dc7db236e321bd023ee434858ffe976afaa590897465d76331ffee5982eca9bbd9c - languageName: node - linkType: hard - -"@sindresorhus/df@npm:^1.0.1": - version: 1.0.1 - resolution: "@sindresorhus/df@npm:1.0.1" - checksum: 10c0/71a4ffb1e698cda2042ea82617915b4377ae58f4b43a16adca7810fe6f78e075e49e873adad7cafd443ffe0c82951fe5789a62f5a32a11513893d1a0d760ad53 - languageName: node - linkType: hard - -"@sindresorhus/df@npm:^3.1.1": - version: 3.1.1 - resolution: "@sindresorhus/df@npm:3.1.1" - dependencies: - execa: "npm:^2.0.1" - checksum: 10c0/39570379856aea81b9c79649779295f87985897a389fde708c43ae91079d9e2721cd6f6423d1330d934343cd5aa35612ddf072e1438ca9529ee13945b924f575 - languageName: node - linkType: hard - -"@stroncium/procfs@npm:^1.2.1": - version: 1.2.1 - resolution: "@stroncium/procfs@npm:1.2.1" - checksum: 10c0/94421e19073905c98e619aaa9a2b6dc65b3cd706d8b0ef6fc0f242b0edb80e7ddd25cbd19ff0506d7d56546b2dc95a458523dc5dee058bc9b7749c02c0758102 - languageName: node - linkType: hard - -"@types/minimist@npm:^1.2.2": - version: 1.2.2 - resolution: "@types/minimist@npm:1.2.2" - checksum: 10c0/f220f57f682bbc3793dab4518f8e2180faa79d8e2589c79614fd777d7182be203ba399020c3a056a115064f5d57a065004a32b522b2737246407621681b24137 - languageName: node - linkType: hard - -"@types/normalize-package-data@npm:^2.4.0": - version: 2.4.1 - resolution: "@types/normalize-package-data@npm:2.4.1" - checksum: 10c0/c90b163741f27a1a4c3b1869d7d5c272adbd355eb50d5f060f9ce122ce4342cf35f5b0005f55ef780596cacfeb69b7eee54cd3c2e02d37f75e664945b6e75fc6 - languageName: node - linkType: hard - -"aggregate-error@npm:^4.0.0": - version: 4.0.1 - resolution: "aggregate-error@npm:4.0.1" - dependencies: - clean-stack: "npm:^4.0.0" - indent-string: "npm:^5.0.0" - checksum: 10c0/75fd739f5c4c60a667cce35ccaf0edf135e147ef0be9a029cab75de14ac9421779b15339d562e58d25b233ea0ef2bbd4c916f149fdbcb73c2b9a62209e611343 - languageName: node - linkType: hard - -"ajv-cli@npm:^5.0.0": - version: 5.0.0 - resolution: "ajv-cli@npm:5.0.0" - dependencies: - ajv: "npm:^8.0.0" - fast-json-patch: "npm:^2.0.0" - glob: "npm:^7.1.0" - js-yaml: "npm:^3.14.0" - json-schema-migrate: "npm:^2.0.0" - json5: "npm:^2.1.3" - minimist: "npm:^1.2.0" - peerDependencies: - ts-node: ">=9.0.0" - peerDependenciesMeta: - ts-node: - optional: true - bin: - ajv: dist/index.js - checksum: 10c0/91c70cb3997fefdb32157b20278643d9ccf6e4897a6b8babf42d6bdb55cb1b7df31524bd5589bf897be14378893b4cf21c366ba313708c3d618e0e976292d7f1 - languageName: node - linkType: hard - -"ajv@npm:^8.0.0": - version: 8.11.2 - resolution: "ajv@npm:8.11.2" - dependencies: - fast-deep-equal: "npm:^3.1.1" - json-schema-traverse: "npm:^1.0.0" - require-from-string: "npm:^2.0.2" - uri-js: "npm:^4.2.2" - checksum: 10c0/152450e03f45e6ff09dab02d9647340e7bf7bcffbe88047b1c5ad7518cc278aa812f1f41606958772a93861b06b8abc91ddb9e124626aab253a9efef875d8e2c - languageName: node - linkType: hard - -"ajv@npm:^8.12.0": - version: 8.12.0 - resolution: "ajv@npm:8.12.0" - dependencies: - fast-deep-equal: "npm:^3.1.1" - json-schema-traverse: "npm:^1.0.0" - require-from-string: "npm:^2.0.2" - uri-js: "npm:^4.2.2" - checksum: 10c0/ac4f72adf727ee425e049bc9d8b31d4a57e1c90da8d28bcd23d60781b12fcd6fc3d68db5df16994c57b78b94eed7988f5a6b482fd376dc5b084125e20a0a622e - languageName: node - linkType: hard - -"ansi-styles@npm:^3.2.1": - version: 3.2.1 - resolution: "ansi-styles@npm:3.2.1" - dependencies: - color-convert: "npm:^1.9.0" - checksum: 10c0/ece5a8ef069fcc5298f67e3f4771a663129abd174ea2dfa87923a2be2abf6cd367ef72ac87942da00ce85bd1d651d4cd8595aebdb1b385889b89b205860e977b - languageName: node - linkType: hard - -"argparse@npm:^1.0.7": - version: 1.0.10 - resolution: "argparse@npm:1.0.10" - dependencies: - sprintf-js: "npm:~1.0.2" - checksum: 10c0/b2972c5c23c63df66bca144dbc65d180efa74f25f8fd9b7d9a0a6c88ae839db32df3d54770dcb6460cf840d232b60695d1a6b1053f599d84e73f7437087712de - languageName: node - linkType: hard - -"argparse@npm:^2.0.1": - version: 2.0.1 - resolution: "argparse@npm:2.0.1" - checksum: 10c0/c5640c2d89045371c7cedd6a70212a04e360fd34d6edeae32f6952c63949e3525ea77dbec0289d8213a99bbaeab5abfa860b5c12cf88a2e6cf8106e90dd27a7e - languageName: node - linkType: hard - -"array-union@npm:^1.0.1": - version: 1.0.2 - resolution: "array-union@npm:1.0.2" - dependencies: - array-uniq: "npm:^1.0.1" - checksum: 10c0/18686767c0cfdae8dc4acf5ac119b0f0eacad82b7fcc0aa62cc41f93c5ad406d494b6a6e53d85e52e8f0349b67a4fec815feeb537e95c02510d747bc9a4157c7 - languageName: node - linkType: hard - -"array-uniq@npm:^1.0.1": - version: 1.0.3 - resolution: "array-uniq@npm:1.0.3" - checksum: 10c0/3acbaf9e6d5faeb1010e2db04ab171b8d265889e46c61762e502979bdc5e55656013726e9a61507de3c82d329a0dc1e8072630a3454b4f2b881cb19ba7fd8aa6 - languageName: node - linkType: hard - -"arrify@npm:^1.0.1": - version: 1.0.1 - resolution: "arrify@npm:1.0.1" - checksum: 10c0/c35c8d1a81bcd5474c0c57fe3f4bad1a4d46a5fa353cedcff7a54da315df60db71829e69104b859dff96c5d68af46bd2be259fe5e50dc6aa9df3b36bea0383ab - languageName: node - linkType: hard - -"balanced-match@npm:^1.0.0": - version: 1.0.2 - resolution: "balanced-match@npm:1.0.2" - checksum: 10c0/9308baf0a7e4838a82bbfd11e01b1cb0f0cf2893bc1676c27c2a8c0e70cbae1c59120c3268517a8ae7fb6376b4639ef81ca22582611dbee4ed28df945134aaee - languageName: node - linkType: hard - -"brace-expansion@npm:^1.1.7": - version: 1.1.11 - resolution: "brace-expansion@npm:1.1.11" - dependencies: - balanced-match: "npm:^1.0.0" - concat-map: "npm:0.0.1" - checksum: 10c0/695a56cd058096a7cb71fb09d9d6a7070113c7be516699ed361317aca2ec169f618e28b8af352e02ab4233fb54eb0168460a40dc320bab0034b36ab59aaad668 - languageName: node - linkType: hard - -"buttplug-device-config@workspace:.": - version: 0.0.0-use.local - resolution: "buttplug-device-config@workspace:." - dependencies: - ajv: "npm:^8.12.0" - ajv-cli: "npm:^5.0.0" - js-yaml: "npm:^4.1.0" - trash-cli: "npm:^5.0.0" - languageName: unknown - linkType: soft - -"camelcase-keys@npm:^7.0.0": - version: 7.0.2 - resolution: "camelcase-keys@npm:7.0.2" - dependencies: - camelcase: "npm:^6.3.0" - map-obj: "npm:^4.1.0" - quick-lru: "npm:^5.1.1" - type-fest: "npm:^1.2.1" - checksum: 10c0/ae86a51168643e9e8a2f2c7bfa17850729979ec3dafc5253056a7d97931cbb0e3ef5b4185e59d54b7a56c54405dee2874b0c82033498d8626e512ff9034cb05c - languageName: node - linkType: hard - -"camelcase@npm:^6.3.0": - version: 6.3.0 - resolution: "camelcase@npm:6.3.0" - checksum: 10c0/0d701658219bd3116d12da3eab31acddb3f9440790c0792e0d398f0a520a6a4058018e546862b6fba89d7ae990efaeb97da71e1913e9ebf5a8b5621a3d55c710 - languageName: node - linkType: hard - -"chalk@npm:^2.0.0": - version: 2.4.2 - resolution: "chalk@npm:2.4.2" - dependencies: - ansi-styles: "npm:^3.2.1" - escape-string-regexp: "npm:^1.0.5" - supports-color: "npm:^5.3.0" - checksum: 10c0/e6543f02ec877732e3a2d1c3c3323ddb4d39fbab687c23f526e25bd4c6a9bf3b83a696e8c769d078e04e5754921648f7821b2a2acfd16c550435fd630026e073 - languageName: node - linkType: hard - -"clean-stack@npm:^4.0.0": - version: 4.2.0 - resolution: "clean-stack@npm:4.2.0" - dependencies: - escape-string-regexp: "npm:5.0.0" - checksum: 10c0/2bdf981a0fef0a23c14255df693b30eb9ae27eedf212470d8c400a0c0b6fb82fbf1ff8c5216ccd5721e3670b700389c886b1dce5070776dc9fbcc040957758c0 - languageName: node - linkType: hard - -"color-convert@npm:^1.9.0": - version: 1.9.3 - resolution: "color-convert@npm:1.9.3" - dependencies: - color-name: "npm:1.1.3" - checksum: 10c0/5ad3c534949a8c68fca8fbc6f09068f435f0ad290ab8b2f76841b9e6af7e0bb57b98cb05b0e19fe33f5d91e5a8611ad457e5f69e0a484caad1f7487fd0e8253c - languageName: node - linkType: hard - -"color-name@npm:1.1.3": - version: 1.1.3 - resolution: "color-name@npm:1.1.3" - checksum: 10c0/566a3d42cca25b9b3cd5528cd7754b8e89c0eb646b7f214e8e2eaddb69994ac5f0557d9c175eb5d8f0ad73531140d9c47525085ee752a91a2ab15ab459caf6d6 - languageName: node - linkType: hard - -"concat-map@npm:0.0.1": - version: 0.0.1 - resolution: "concat-map@npm:0.0.1" - checksum: 10c0/c996b1cfdf95b6c90fee4dae37e332c8b6eb7d106430c17d538034c0ad9a1630cb194d2ab37293b1bdd4d779494beee7786d586a50bd9376fd6f7bcc2bd4c98f - languageName: node - linkType: hard - -"cross-spawn@npm:^7.0.0": - version: 7.0.6 - resolution: "cross-spawn@npm:7.0.6" - dependencies: - path-key: "npm:^3.1.0" - shebang-command: "npm:^2.0.0" - which: "npm:^2.0.1" - checksum: 10c0/053ea8b2135caff68a9e81470e845613e374e7309a47731e81639de3eaeb90c3d01af0e0b44d2ab9d50b43467223b88567dfeb3262db942dc063b9976718ffc1 - languageName: node - linkType: hard - -"decamelize-keys@npm:^1.1.0": - version: 1.1.1 - resolution: "decamelize-keys@npm:1.1.1" - dependencies: - decamelize: "npm:^1.1.0" - map-obj: "npm:^1.0.0" - checksum: 10c0/4ca385933127437658338c65fb9aead5f21b28d3dd3ccd7956eb29aab0953b5d3c047fbc207111672220c71ecf7a4d34f36c92851b7bbde6fca1a02c541bdd7d - languageName: node - linkType: hard - -"decamelize@npm:^1.1.0": - version: 1.2.0 - resolution: "decamelize@npm:1.2.0" - checksum: 10c0/85c39fe8fbf0482d4a1e224ef0119db5c1897f8503bcef8b826adff7a1b11414972f6fef2d7dec2ee0b4be3863cf64ac1439137ae9e6af23a3d8dcbe26a5b4b2 - languageName: node - linkType: hard - -"decamelize@npm:^5.0.0": - version: 5.0.1 - resolution: "decamelize@npm:5.0.1" - checksum: 10c0/3da71022bc1e85487810fa0833138effb599fa331ca21e179650e93a765d0c4dabeb1ecdd6ad1474fa0bacd2457953c63ea335afb6e53b35f2b4bf779514e2a3 - languageName: node - linkType: hard - -"dir-glob@npm:^2.0.0": - version: 2.2.2 - resolution: "dir-glob@npm:2.2.2" - dependencies: - path-type: "npm:^3.0.0" - checksum: 10c0/67575fd496df80ec90969f1a9f881f03b4ef614ca2c07139df81a12f9816250780dff906f482def0f897dd748d22fa13c076b52ac635e0024f7d434846077a3a - languageName: node - linkType: hard - -"end-of-stream@npm:^1.1.0": - version: 1.4.4 - resolution: "end-of-stream@npm:1.4.4" - dependencies: - once: "npm:^1.4.0" - checksum: 10c0/870b423afb2d54bb8d243c63e07c170409d41e20b47eeef0727547aea5740bd6717aca45597a9f2745525667a6b804c1e7bede41f856818faee5806dd9ff3975 - languageName: node - linkType: hard - -"error-ex@npm:^1.3.1": - version: 1.3.2 - resolution: "error-ex@npm:1.3.2" - dependencies: - is-arrayish: "npm:^0.2.1" - checksum: 10c0/ba827f89369b4c93382cfca5a264d059dfefdaa56ecc5e338ffa58a6471f5ed93b71a20add1d52290a4873d92381174382658c885ac1a2305f7baca363ce9cce - languageName: node - linkType: hard - -"escape-string-regexp@npm:5.0.0": - version: 5.0.0 - resolution: "escape-string-regexp@npm:5.0.0" - checksum: 10c0/6366f474c6f37a802800a435232395e04e9885919873e382b157ab7e8f0feb8fed71497f84a6f6a81a49aab41815522f5839112bd38026d203aea0c91622df95 - languageName: node - linkType: hard - -"escape-string-regexp@npm:^1.0.5": - version: 1.0.5 - resolution: "escape-string-regexp@npm:1.0.5" - checksum: 10c0/a968ad453dd0c2724e14a4f20e177aaf32bb384ab41b674a8454afe9a41c5e6fe8903323e0a1052f56289d04bd600f81278edf140b0fcc02f5cac98d0f5b5371 - languageName: node - linkType: hard - -"esprima@npm:^4.0.0": - version: 4.0.1 - resolution: "esprima@npm:4.0.1" - bin: - esparse: ./bin/esparse.js - esvalidate: ./bin/esvalidate.js - checksum: 10c0/ad4bab9ead0808cf56501750fd9d3fb276f6b105f987707d059005d57e182d18a7c9ec7f3a01794ebddcca676773e42ca48a32d67a250c9d35e009ca613caba3 - languageName: node - linkType: hard - -"execa@npm:^2.0.1": - version: 2.1.0 - resolution: "execa@npm:2.1.0" - dependencies: - cross-spawn: "npm:^7.0.0" - get-stream: "npm:^5.0.0" - is-stream: "npm:^2.0.0" - merge-stream: "npm:^2.0.0" - npm-run-path: "npm:^3.0.0" - onetime: "npm:^5.1.0" - p-finally: "npm:^2.0.0" - signal-exit: "npm:^3.0.2" - strip-final-newline: "npm:^2.0.0" - checksum: 10c0/6578db04a18a9d166a2de6f85be2f1130315fe5917d8163fdbbeaaec39f89cc20448e243dffe833f58b93c210fb3b19e3612c155c81853722497100b8230c34c - languageName: node - linkType: hard - -"fast-deep-equal@npm:^2.0.1": - version: 2.0.1 - resolution: "fast-deep-equal@npm:2.0.1" - checksum: 10c0/1602e0d6ed63493c865cc6b03f9070d6d3926e8cd086a123060b58f80a295f3f08b1ecfb479ae7c45b7fd45535202aea7cf5b49bc31bffb81c20b1502300be84 - languageName: node - linkType: hard - -"fast-deep-equal@npm:^3.1.1": - version: 3.1.3 - resolution: "fast-deep-equal@npm:3.1.3" - checksum: 10c0/40dedc862eb8992c54579c66d914635afbec43350afbbe991235fdcb4e3a8d5af1b23ae7e79bef7d4882d0ecee06c3197488026998fb19f72dc95acff1d1b1d0 - languageName: node - linkType: hard - -"fast-json-patch@npm:^2.0.0": - version: 2.2.1 - resolution: "fast-json-patch@npm:2.2.1" - dependencies: - fast-deep-equal: "npm:^2.0.1" - checksum: 10c0/3200148b8244081ac628e8044a3ba6c42bbe26542d1586b0e87221bff8d5ef58252a2dd846a709ff4683cf826e89123025c2708729933dde859430a40f0d321e - languageName: node - linkType: hard - -"find-up@npm:^5.0.0": - version: 5.0.0 - resolution: "find-up@npm:5.0.0" - dependencies: - locate-path: "npm:^6.0.0" - path-exists: "npm:^4.0.0" - checksum: 10c0/062c5a83a9c02f53cdd6d175a37ecf8f87ea5bbff1fdfb828f04bfa021441bc7583e8ebc0872a4c1baab96221fb8a8a275a19809fb93fbc40bd69ec35634069a - languageName: node - linkType: hard - -"fs.realpath@npm:^1.0.0": - version: 1.0.0 - resolution: "fs.realpath@npm:1.0.0" - checksum: 10c0/444cf1291d997165dfd4c0d58b69f0e4782bfd9149fd72faa4fe299e68e0e93d6db941660b37dd29153bf7186672ececa3b50b7e7249477b03fdf850f287c948 - languageName: node - linkType: hard - -"function-bind@npm:^1.1.1": - version: 1.1.1 - resolution: "function-bind@npm:1.1.1" - checksum: 10c0/60b74b2407e1942e1ed7f8c284f8ef714d0689dcfce5319985a5b7da3fc727f40b4a59ec72dc55aa83365ad7b8fa4fac3a30d93c850a2b452f29ae03dbc10a1e - languageName: node - linkType: hard - -"get-stream@npm:^5.0.0": - version: 5.2.0 - resolution: "get-stream@npm:5.2.0" - dependencies: - pump: "npm:^3.0.0" - checksum: 10c0/43797ffd815fbb26685bf188c8cfebecb8af87b3925091dd7b9a9c915993293d78e3c9e1bce125928ff92f2d0796f3889b92b5ec6d58d1041b574682132e0a80 - languageName: node - linkType: hard - -"glob@npm:^7.1.0, glob@npm:^7.1.2": - version: 7.1.7 - resolution: "glob@npm:7.1.7" - dependencies: - fs.realpath: "npm:^1.0.0" - inflight: "npm:^1.0.4" - inherits: "npm:2" - minimatch: "npm:^3.0.4" - once: "npm:^1.3.0" - path-is-absolute: "npm:^1.0.0" - checksum: 10c0/173245e6f9ccf904309eb7ef4a44a11f3bf68e9e341dff5a28b5db0dd7123b7506daf41497f3437a0710f57198187b758c2351eeaabce4d16935e956920da6a4 - languageName: node - linkType: hard - -"globby@npm:^7.1.1": - version: 7.1.1 - resolution: "globby@npm:7.1.1" - dependencies: - array-union: "npm:^1.0.1" - dir-glob: "npm:^2.0.0" - glob: "npm:^7.1.2" - ignore: "npm:^3.3.5" - pify: "npm:^3.0.0" - slash: "npm:^1.0.0" - checksum: 10c0/016d4dfac6069221b2db18ad6afb0011639899920dbec87492ddc048fcd433361e6c094b12451ab14cf062013a776f47ef21bb8289d5e09a2f23e81d5aec0f8e - languageName: node - linkType: hard - -"hard-rejection@npm:^2.1.0": - version: 2.1.0 - resolution: "hard-rejection@npm:2.1.0" - checksum: 10c0/febc3343a1ad575aedcc112580835b44a89a89e01f400b4eda6e8110869edfdab0b00cd1bd4c3bfec9475a57e79e0b355aecd5be46454b6a62b9a359af60e564 - languageName: node - linkType: hard - -"has-flag@npm:^3.0.0": - version: 3.0.0 - resolution: "has-flag@npm:3.0.0" - checksum: 10c0/1c6c83b14b8b1b3c25b0727b8ba3e3b647f99e9e6e13eb7322107261de07a4c1be56fc0d45678fc376e09772a3a1642ccdaf8fc69bdf123b6c086598397ce473 - languageName: node - linkType: hard - -"has@npm:^1.0.3": - version: 1.0.3 - resolution: "has@npm:1.0.3" - dependencies: - function-bind: "npm:^1.1.1" - checksum: 10c0/e1da0d2bd109f116b632f27782cf23182b42f14972ca9540e4c5aa7e52647407a0a4a76937334fddcb56befe94a3494825ec22b19b51f5e5507c3153fd1a5e1b - languageName: node - linkType: hard - -"hosted-git-info@npm:^4.0.1": - version: 4.1.0 - resolution: "hosted-git-info@npm:4.1.0" - dependencies: - lru-cache: "npm:^6.0.0" - checksum: 10c0/150fbcb001600336d17fdbae803264abed013548eea7946c2264c49ebe2ebd8c4441ba71dd23dd8e18c65de79d637f98b22d4760ba5fb2e0b15d62543d0fff07 - languageName: node - linkType: hard - -"ignore@npm:^3.3.5": - version: 3.3.10 - resolution: "ignore@npm:3.3.10" - checksum: 10c0/973e0ef3b3eaab8fc19014d80014ed11bcf3585de8088d9c7a5b5c4edefc55f4ecdc498144bdd0440b8e2ff22deb03f89c90300bfef2d1750d5920f997d0a600 - languageName: node - linkType: hard - -"indent-string@npm:^5.0.0": - version: 5.0.0 - resolution: "indent-string@npm:5.0.0" - checksum: 10c0/8ee77b57d92e71745e133f6f444d6fa3ed503ad0e1bcd7e80c8da08b42375c07117128d670589725ed07b1978065803fa86318c309ba45415b7fe13e7f170220 - languageName: node - linkType: hard - -"inflight@npm:^1.0.4": - version: 1.0.6 - resolution: "inflight@npm:1.0.6" - dependencies: - once: "npm:^1.3.0" - wrappy: "npm:1" - checksum: 10c0/7faca22584600a9dc5b9fca2cd5feb7135ac8c935449837b315676b4c90aa4f391ec4f42240178244b5a34e8bede1948627fda392ca3191522fc46b34e985ab2 - languageName: node - linkType: hard - -"inherits@npm:2": - version: 2.0.4 - resolution: "inherits@npm:2.0.4" - checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2 - languageName: node - linkType: hard - -"is-arrayish@npm:^0.2.1": - version: 0.2.1 - resolution: "is-arrayish@npm:0.2.1" - checksum: 10c0/e7fb686a739068bb70f860b39b67afc62acc62e36bb61c5f965768abce1873b379c563e61dd2adad96ebb7edf6651111b385e490cf508378959b0ed4cac4e729 - languageName: node - linkType: hard - -"is-core-module@npm:^2.5.0": - version: 2.11.0 - resolution: "is-core-module@npm:2.11.0" - dependencies: - has: "npm:^1.0.3" - checksum: 10c0/fd8f78ef4e243c295deafa809f89381d89aff5aaf38bb63266b17ee6e34b6a051baa5bdc2365456863336d56af6a59a4c1df1256b4eff7d6b4afac618586b004 - languageName: node - linkType: hard - -"is-path-inside@npm:^4.0.0": - version: 4.0.0 - resolution: "is-path-inside@npm:4.0.0" - checksum: 10c0/51188d7e2b1d907a9a5f7c18d99a90b60870b951ed87cf97595d9aaa429d4c010652c3350bcbf31182e7f4b0eab9a1860b43e16729b13cb1a44baaa6cdb64c46 - languageName: node - linkType: hard - -"is-plain-obj@npm:^1.1.0": - version: 1.1.0 - resolution: "is-plain-obj@npm:1.1.0" - checksum: 10c0/daaee1805add26f781b413fdf192fc91d52409583be30ace35c82607d440da63cc4cac0ac55136716688d6c0a2c6ef3edb2254fecbd1fe06056d6bd15975ee8c - languageName: node - linkType: hard - -"is-stream@npm:^2.0.0": - version: 2.0.1 - resolution: "is-stream@npm:2.0.1" - checksum: 10c0/7c284241313fc6efc329b8d7f08e16c0efeb6baab1b4cd0ba579eb78e5af1aa5da11e68559896a2067cd6c526bd29241dda4eb1225e627d5aa1a89a76d4635a5 - languageName: node - linkType: hard - -"isexe@npm:^2.0.0": - version: 2.0.0 - resolution: "isexe@npm:2.0.0" - checksum: 10c0/228cfa503fadc2c31596ab06ed6aa82c9976eec2bfd83397e7eaf06d0ccf42cd1dfd6743bf9aeb01aebd4156d009994c5f76ea898d2832c1fe342da923ca457d - languageName: node - linkType: hard - -"js-tokens@npm:^4.0.0": - version: 4.0.0 - resolution: "js-tokens@npm:4.0.0" - checksum: 10c0/e248708d377aa058eacf2037b07ded847790e6de892bbad3dac0abba2e759cb9f121b00099a65195616badcb6eca8d14d975cb3e89eb1cfda644756402c8aeed - languageName: node - linkType: hard - -"js-yaml@npm:^3.14.0": - version: 3.14.1 - resolution: "js-yaml@npm:3.14.1" - dependencies: - argparse: "npm:^1.0.7" - esprima: "npm:^4.0.0" - bin: - js-yaml: bin/js-yaml.js - checksum: 10c0/6746baaaeac312c4db8e75fa22331d9a04cccb7792d126ed8ce6a0bbcfef0cedaddd0c5098fade53db067c09fe00aa1c957674b4765610a8b06a5a189e46433b - languageName: node - linkType: hard - -"js-yaml@npm:^4.1.0": - version: 4.1.0 - resolution: "js-yaml@npm:4.1.0" - dependencies: - argparse: "npm:^2.0.1" - bin: - js-yaml: bin/js-yaml.js - checksum: 10c0/184a24b4eaacfce40ad9074c64fd42ac83cf74d8c8cd137718d456ced75051229e5061b8633c3366b8aada17945a7a356b337828c19da92b51ae62126575018f - languageName: node - linkType: hard - -"json-parse-even-better-errors@npm:^2.3.0": - version: 2.3.1 - resolution: "json-parse-even-better-errors@npm:2.3.1" - checksum: 10c0/140932564c8f0b88455432e0f33c4cb4086b8868e37524e07e723f4eaedb9425bdc2bafd71bd1d9765bd15fd1e2d126972bc83990f55c467168c228c24d665f3 - languageName: node - linkType: hard - -"json-schema-migrate@npm:^2.0.0": - version: 2.0.0 - resolution: "json-schema-migrate@npm:2.0.0" - dependencies: - ajv: "npm:^8.0.0" - checksum: 10c0/9d14970cd1cab496b0a5dc52c3a69a0346dc2bd4ce3135a3561739eddb46cb7696dc815ff9ddcd015c4c4532b56495a1ec5db4b141b77e6d2c9c7b492cd02c6a - languageName: node - linkType: hard - -"json-schema-traverse@npm:^1.0.0": - version: 1.0.0 - resolution: "json-schema-traverse@npm:1.0.0" - checksum: 10c0/71e30015d7f3d6dc1c316d6298047c8ef98a06d31ad064919976583eb61e1018a60a0067338f0f79cabc00d84af3fcc489bd48ce8a46ea165d9541ba17fb30c6 - languageName: node - linkType: hard - -"json5@npm:^2.1.3": - version: 2.2.0 - resolution: "json5@npm:2.2.0" - dependencies: - minimist: "npm:^1.2.5" - bin: - json5: lib/cli.js - checksum: 10c0/fbe021f69fa100f0a863e5ab9105ead3971ad5141e7c0dc5134c6148545dae98a69602fb8f9f4dd65af0db7ca00887bf5b35af60be34c10f58fb5fc1f2366a4e - languageName: node - linkType: hard - -"kind-of@npm:^6.0.3": - version: 6.0.3 - resolution: "kind-of@npm:6.0.3" - checksum: 10c0/61cdff9623dabf3568b6445e93e31376bee1cdb93f8ba7033d86022c2a9b1791a1d9510e026e6465ebd701a6dd2f7b0808483ad8838341ac52f003f512e0b4c4 - languageName: node - linkType: hard - -"lines-and-columns@npm:^1.1.6": - version: 1.2.4 - resolution: "lines-and-columns@npm:1.2.4" - checksum: 10c0/3da6ee62d4cd9f03f5dc90b4df2540fb85b352081bee77fe4bbcd12c9000ead7f35e0a38b8d09a9bb99b13223446dd8689ff3c4959807620726d788701a83d2d - languageName: node - linkType: hard - -"locate-path@npm:^6.0.0": - version: 6.0.0 - resolution: "locate-path@npm:6.0.0" - dependencies: - p-locate: "npm:^5.0.0" - checksum: 10c0/d3972ab70dfe58ce620e64265f90162d247e87159b6126b01314dd67be43d50e96a50b517bce2d9452a79409c7614054c277b5232377de50416564a77ac7aad3 - languageName: node - linkType: hard - -"lru-cache@npm:^6.0.0": - version: 6.0.0 - resolution: "lru-cache@npm:6.0.0" - dependencies: - yallist: "npm:^4.0.0" - checksum: 10c0/cb53e582785c48187d7a188d3379c181b5ca2a9c78d2bce3e7dee36f32761d1c42983da3fe12b55cb74e1779fa94cdc2e5367c028a9b35317184ede0c07a30a9 - languageName: node - linkType: hard - -"map-obj@npm:^1.0.0": - version: 1.0.1 - resolution: "map-obj@npm:1.0.1" - checksum: 10c0/ccca88395e7d38671ed9f5652ecf471ecd546924be2fb900836b9da35e068a96687d96a5f93dcdfa94d9a27d649d2f10a84595590f89a347fb4dda47629dcc52 - languageName: node - linkType: hard - -"map-obj@npm:^4.1.0": - version: 4.3.0 - resolution: "map-obj@npm:4.3.0" - checksum: 10c0/1c19e1c88513c8abdab25c316367154c6a0a6a0f77e3e8c391bb7c0e093aefed293f539d026dc013d86219e5e4c25f23b0003ea588be2101ccd757bacc12d43b - languageName: node - linkType: hard - -"meow@npm:^10.1.2": - version: 10.1.5 - resolution: "meow@npm:10.1.5" - dependencies: - "@types/minimist": "npm:^1.2.2" - camelcase-keys: "npm:^7.0.0" - decamelize: "npm:^5.0.0" - decamelize-keys: "npm:^1.1.0" - hard-rejection: "npm:^2.1.0" - minimist-options: "npm:4.1.0" - normalize-package-data: "npm:^3.0.2" - read-pkg-up: "npm:^8.0.0" - redent: "npm:^4.0.0" - trim-newlines: "npm:^4.0.2" - type-fest: "npm:^1.2.2" - yargs-parser: "npm:^20.2.9" - checksum: 10c0/a513849022edd5ddcc41d28c679d31978abe414d9db5bc457e95e537a4327b2910fd2f699cdd883293f9a5da8951a50939bf60fbd62f7fe12b9ddf96a84b1b27 - languageName: node - linkType: hard - -"merge-stream@npm:^2.0.0": - version: 2.0.0 - resolution: "merge-stream@npm:2.0.0" - checksum: 10c0/867fdbb30a6d58b011449b8885601ec1690c3e41c759ecd5a9d609094f7aed0096c37823ff4a7190ef0b8f22cc86beb7049196ff68c016e3b3c671d0dac91ce5 - languageName: node - linkType: hard - -"mimic-fn@npm:^2.1.0": - version: 2.1.0 - resolution: "mimic-fn@npm:2.1.0" - checksum: 10c0/b26f5479d7ec6cc2bce275a08f146cf78f5e7b661b18114e2506dd91ec7ec47e7a25bf4360e5438094db0560bcc868079fb3b1fb3892b833c1ecbf63f80c95a4 - languageName: node - linkType: hard - -"min-indent@npm:^1.0.1": - version: 1.0.1 - resolution: "min-indent@npm:1.0.1" - checksum: 10c0/7e207bd5c20401b292de291f02913230cb1163abca162044f7db1d951fa245b174dc00869d40dd9a9f32a885ad6a5f3e767ee104cf278f399cb4e92d3f582d5c - languageName: node - linkType: hard - -"minimatch@npm:^3.0.4": - version: 3.1.2 - resolution: "minimatch@npm:3.1.2" - dependencies: - brace-expansion: "npm:^1.1.7" - checksum: 10c0/0262810a8fc2e72cca45d6fd86bd349eee435eb95ac6aa45c9ea2180e7ee875ef44c32b55b5973ceabe95ea12682f6e3725cbb63d7a2d1da3ae1163c8b210311 - languageName: node - linkType: hard - -"minimist-options@npm:4.1.0": - version: 4.1.0 - resolution: "minimist-options@npm:4.1.0" - dependencies: - arrify: "npm:^1.0.1" - is-plain-obj: "npm:^1.1.0" - kind-of: "npm:^6.0.3" - checksum: 10c0/7871f9cdd15d1e7374e5b013e2ceda3d327a06a8c7b38ae16d9ef941e07d985e952c589e57213f7aa90a8744c60aed9524c0d85e501f5478382d9181f2763f54 - languageName: node - linkType: hard - -"minimist@npm:^1.2.0, minimist@npm:^1.2.5": - version: 1.2.8 - resolution: "minimist@npm:1.2.8" - checksum: 10c0/19d3fcdca050087b84c2029841a093691a91259a47def2f18222f41e7645a0b7c44ef4b40e88a1e58a40c84d2ef0ee6047c55594d298146d0eb3f6b737c20ce6 - languageName: node - linkType: hard - -"mount-point@npm:^3.0.0": - version: 3.0.0 - resolution: "mount-point@npm:3.0.0" - dependencies: - "@sindresorhus/df": "npm:^1.0.1" - pify: "npm:^2.3.0" - pinkie-promise: "npm:^2.0.1" - checksum: 10c0/1837afa180c55f9d6b01f04680e4aae4561ef94f1e7db8747f60c6130e4932e8e6cf6c42febd15502985761726e1f3ad989ac9a5ab86eca61b9577935d64d222 - languageName: node - linkType: hard - -"move-file@npm:^3.0.0": - version: 3.0.0 - resolution: "move-file@npm:3.0.0" - dependencies: - path-exists: "npm:^5.0.0" - checksum: 10c0/c91ab16367db25faf84586ffc7e3cca24ba2bcc59d69166cd45170e170b56fad1721520412eb91dd14265b44f00feac886a4bbed8073f5fd84fca5269d3950a4 - languageName: node - linkType: hard - -"normalize-package-data@npm:^3.0.2": - version: 3.0.3 - resolution: "normalize-package-data@npm:3.0.3" - dependencies: - hosted-git-info: "npm:^4.0.1" - is-core-module: "npm:^2.5.0" - semver: "npm:^7.3.4" - validate-npm-package-license: "npm:^3.0.1" - checksum: 10c0/e5d0f739ba2c465d41f77c9d950e291ea4af78f8816ddb91c5da62257c40b76d8c83278b0d08ffbcd0f187636ebddad20e181e924873916d03e6e5ea2ef026be - languageName: node - linkType: hard - -"npm-run-path@npm:^3.0.0": - version: 3.1.0 - resolution: "npm-run-path@npm:3.1.0" - dependencies: - path-key: "npm:^3.0.0" - checksum: 10c0/8399f01239e9a5bf5a10bddbc71ecac97e0b7890e5b78abe9731fc759db48865b0686cc86ec079cd254a98ba119a3fa08f1b23f9de1a5428c19007bbc7b5a728 - languageName: node - linkType: hard - -"once@npm:^1.3.0, once@npm:^1.3.1, once@npm:^1.4.0": - version: 1.4.0 - resolution: "once@npm:1.4.0" - dependencies: - wrappy: "npm:1" - checksum: 10c0/5d48aca287dfefabd756621c5dfce5c91a549a93e9fdb7b8246bc4c4790aa2ec17b34a260530474635147aeb631a2dcc8b32c613df0675f96041cbb8244517d0 - languageName: node - linkType: hard - -"onetime@npm:^5.1.0": - version: 5.1.2 - resolution: "onetime@npm:5.1.2" - dependencies: - mimic-fn: "npm:^2.1.0" - checksum: 10c0/ffcef6fbb2692c3c40749f31ea2e22677a876daea92959b8a80b521d95cca7a668c884d8b2045d1d8ee7d56796aa405c405462af112a1477594cc63531baeb8f - languageName: node - linkType: hard - -"os-homedir@npm:^1.0.0": - version: 1.0.2 - resolution: "os-homedir@npm:1.0.2" - checksum: 10c0/6be4aa67317ee247b8d46142e243fb4ef1d2d65d3067f54bfc5079257a2f4d4d76b2da78cba7af3cb3f56dbb2e4202e0c47f26171d11ca1ed4008d842c90363f - languageName: node - linkType: hard - -"p-finally@npm:^2.0.0": - version: 2.0.1 - resolution: "p-finally@npm:2.0.1" - checksum: 10c0/a4ee34179f5e0eb5417462ca5afbca4b6b537b051ea87c8ec7649ffb2b60a8e82a06441792fe496ab0d0156c4060a3dfd707973915a1b8369b00f2531e3eab94 - languageName: node - linkType: hard - -"p-limit@npm:^3.0.2": - version: 3.1.0 - resolution: "p-limit@npm:3.1.0" - dependencies: - yocto-queue: "npm:^0.1.0" - checksum: 10c0/9db675949dbdc9c3763c89e748d0ef8bdad0afbb24d49ceaf4c46c02c77d30db4e0652ed36d0a0a7a95154335fab810d95c86153105bb73b3a90448e2bb14e1a - languageName: node - linkType: hard - -"p-locate@npm:^5.0.0": - version: 5.0.0 - resolution: "p-locate@npm:5.0.0" - dependencies: - p-limit: "npm:^3.0.2" - checksum: 10c0/2290d627ab7903b8b70d11d384fee714b797f6040d9278932754a6860845c4d3190603a0772a663c8cb5a7b21d1b16acb3a6487ebcafa9773094edc3dfe6009a - languageName: node - linkType: hard - -"p-map@npm:^5.1.0": - version: 5.5.0 - resolution: "p-map@npm:5.5.0" - dependencies: - aggregate-error: "npm:^4.0.0" - checksum: 10c0/410bce846b1e3db6bb2ccab6248372ecf4e635fc2b31331c8f56478e73fec9e146e8b4547585e635703160a3d252a6a65b8f855834aebc2c3408eb5789630cc4 - languageName: node - linkType: hard - -"parse-json@npm:^5.2.0": - version: 5.2.0 - resolution: "parse-json@npm:5.2.0" - dependencies: - "@babel/code-frame": "npm:^7.0.0" - error-ex: "npm:^1.3.1" - json-parse-even-better-errors: "npm:^2.3.0" - lines-and-columns: "npm:^1.1.6" - checksum: 10c0/77947f2253005be7a12d858aedbafa09c9ae39eb4863adf330f7b416ca4f4a08132e453e08de2db46459256fb66afaac5ee758b44fe6541b7cdaf9d252e59585 - languageName: node - linkType: hard - -"path-exists@npm:^4.0.0": - version: 4.0.0 - resolution: "path-exists@npm:4.0.0" - checksum: 10c0/8c0bd3f5238188197dc78dced15207a4716c51cc4e3624c44fc97acf69558f5ebb9a2afff486fe1b4ee148e0c133e96c5e11a9aa5c48a3006e3467da070e5e1b - languageName: node - linkType: hard - -"path-exists@npm:^5.0.0": - version: 5.0.0 - resolution: "path-exists@npm:5.0.0" - checksum: 10c0/b170f3060b31604cde93eefdb7392b89d832dfbc1bed717c9718cbe0f230c1669b7e75f87e19901da2250b84d092989a0f9e44d2ef41deb09aa3ad28e691a40a - languageName: node - linkType: hard - -"path-is-absolute@npm:^1.0.0": - version: 1.0.1 - resolution: "path-is-absolute@npm:1.0.1" - checksum: 10c0/127da03c82172a2a50099cddbf02510c1791fc2cc5f7713ddb613a56838db1e8168b121a920079d052e0936c23005562059756d653b7c544c53185efe53be078 - languageName: node - linkType: hard - -"path-key@npm:^3.0.0, path-key@npm:^3.1.0": - version: 3.1.1 - resolution: "path-key@npm:3.1.1" - checksum: 10c0/748c43efd5a569c039d7a00a03b58eecd1d75f3999f5a28303d75f521288df4823bc057d8784eb72358b2895a05f29a070bc9f1f17d28226cc4e62494cc58c4c - languageName: node - linkType: hard - -"path-type@npm:^3.0.0": - version: 3.0.0 - resolution: "path-type@npm:3.0.0" - dependencies: - pify: "npm:^3.0.0" - checksum: 10c0/1332c632f1cac15790ebab8dd729b67ba04fc96f81647496feb1c2975d862d046f41e4b975dbd893048999b2cc90721f72924ad820acc58c78507ba7141a8e56 - languageName: node - linkType: hard - -"pify@npm:^2.3.0": - version: 2.3.0 - resolution: "pify@npm:2.3.0" - checksum: 10c0/551ff8ab830b1052633f59cb8adc9ae8407a436e06b4a9718bcb27dc5844b83d535c3a8512b388b6062af65a98c49bdc0dd523d8b2617b188f7c8fee457158dc - languageName: node - linkType: hard - -"pify@npm:^3.0.0": - version: 3.0.0 - resolution: "pify@npm:3.0.0" - checksum: 10c0/fead19ed9d801f1b1fcd0638a1ac53eabbb0945bf615f2f8806a8b646565a04a1b0e7ef115c951d225f042cca388fdc1cd3add46d10d1ed6951c20bd2998af10 - languageName: node - linkType: hard - -"pinkie-promise@npm:^2.0.1": - version: 2.0.1 - resolution: "pinkie-promise@npm:2.0.1" - dependencies: - pinkie: "npm:^2.0.0" - checksum: 10c0/11b5e5ce2b090c573f8fad7b517cbca1bb9a247587306f05ae71aef6f9b2cd2b923c304aa9663c2409cfde27b367286179f1379bc4ec18a3fbf2bb0d473b160a - languageName: node - linkType: hard - -"pinkie@npm:^2.0.0": - version: 2.0.4 - resolution: "pinkie@npm:2.0.4" - checksum: 10c0/25228b08b5597da42dc384221aa0ce56ee0fbf32965db12ba838e2a9ca0193c2f0609c45551ee077ccd2060bf109137fdb185b00c6d7e0ed7e35006d20fdcbc6 - languageName: node - linkType: hard - -"pump@npm:^3.0.0": - version: 3.0.0 - resolution: "pump@npm:3.0.0" - dependencies: - end-of-stream: "npm:^1.1.0" - once: "npm:^1.3.1" - checksum: 10c0/bbdeda4f747cdf47db97428f3a135728669e56a0ae5f354a9ac5b74556556f5446a46f720a8f14ca2ece5be9b4d5d23c346db02b555f46739934cc6c093a5478 - languageName: node - linkType: hard - -"punycode@npm:^2.1.0": - version: 2.1.1 - resolution: "punycode@npm:2.1.1" - checksum: 10c0/83815ca9b9177f055771f31980cbec7ffaef10257d50a95ab99b4a30f0404846e85fa6887ee1bbc0aaddb7bad6d96e2fa150a016051ff0f6b92be4ad613ddca8 - languageName: node - linkType: hard - -"quick-lru@npm:^5.1.1": - version: 5.1.1 - resolution: "quick-lru@npm:5.1.1" - checksum: 10c0/a24cba5da8cec30d70d2484be37622580f64765fb6390a928b17f60cd69e8dbd32a954b3ff9176fa1b86d86ff2ba05252fae55dc4d40d0291c60412b0ad096da - languageName: node - linkType: hard - -"read-pkg-up@npm:^8.0.0": - version: 8.0.0 - resolution: "read-pkg-up@npm:8.0.0" - dependencies: - find-up: "npm:^5.0.0" - read-pkg: "npm:^6.0.0" - type-fest: "npm:^1.0.1" - checksum: 10c0/cf3905ccbe5cd602f23192cc7ca65ed17561bab117eadb9aed817441d5bfc6b9a11215c2a3e9505f501d046818f3c4180dbea61fa83c42083e0b4e407d5cc745 - languageName: node - linkType: hard - -"read-pkg@npm:^6.0.0": - version: 6.0.0 - resolution: "read-pkg@npm:6.0.0" - dependencies: - "@types/normalize-package-data": "npm:^2.4.0" - normalize-package-data: "npm:^3.0.2" - parse-json: "npm:^5.2.0" - type-fest: "npm:^1.0.1" - checksum: 10c0/b51ee5eed75324f4fac34c9a40b5e4b403de4c532242be01959c9bbdb1ff9db1c6c2aefaba569622fec49d1ead866e97ba856ab145f6e11039b11f7bec1318ba - languageName: node - linkType: hard - -"redent@npm:^4.0.0": - version: 4.0.0 - resolution: "redent@npm:4.0.0" - dependencies: - indent-string: "npm:^5.0.0" - strip-indent: "npm:^4.0.0" - checksum: 10c0/a9b640c8f4b2b5b26a1a908706475ff404dd50a97d6f094bc3c59717be922622927cc7d601d4ae2857d897ad243fd979bd76d751a0481cee8be7024e5fb4c662 - languageName: node - linkType: hard - -"require-from-string@npm:^2.0.2": - version: 2.0.2 - resolution: "require-from-string@npm:2.0.2" - checksum: 10c0/aaa267e0c5b022fc5fd4eef49d8285086b15f2a1c54b28240fdf03599cbd9c26049fee3eab894f2e1f6ca65e513b030a7c264201e3f005601e80c49fb2937ce2 - languageName: node - linkType: hard - -"semver@npm:^7.3.4": - version: 7.3.8 - resolution: "semver@npm:7.3.8" - dependencies: - lru-cache: "npm:^6.0.0" - bin: - semver: bin/semver.js - checksum: 10c0/7e581d679530db31757301c2117721577a2bb36a301a443aac833b8efad372cda58e7f2a464fe4412ae1041cc1f63a6c1fe0ced8c57ce5aca1e0b57bb0d627b9 - languageName: node - linkType: hard - -"shebang-command@npm:^2.0.0": - version: 2.0.0 - resolution: "shebang-command@npm:2.0.0" - dependencies: - shebang-regex: "npm:^3.0.0" - checksum: 10c0/a41692e7d89a553ef21d324a5cceb5f686d1f3c040759c50aab69688634688c5c327f26f3ecf7001ebfd78c01f3c7c0a11a7c8bfd0a8bc9f6240d4f40b224e4e - languageName: node - linkType: hard - -"shebang-regex@npm:^3.0.0": - version: 3.0.0 - resolution: "shebang-regex@npm:3.0.0" - checksum: 10c0/1dbed0726dd0e1152a92696c76c7f06084eb32a90f0528d11acd764043aacf76994b2fb30aa1291a21bd019d6699164d048286309a278855ee7bec06cf6fb690 - languageName: node - linkType: hard - -"signal-exit@npm:^3.0.2": - version: 3.0.7 - resolution: "signal-exit@npm:3.0.7" - checksum: 10c0/25d272fa73e146048565e08f3309d5b942c1979a6f4a58a8c59d5fa299728e9c2fcd1a759ec870863b1fd38653670240cd420dad2ad9330c71f36608a6a1c912 - languageName: node - linkType: hard - -"slash@npm:^1.0.0": - version: 1.0.0 - resolution: "slash@npm:1.0.0" - checksum: 10c0/3944659885d905480f98810542fd314f3e1006eaad25ec78227a7835a469d9ed66fc3dd90abc7377dd2e71f4b5473e8f766bd08198fdd25152a80792e9ed464c - languageName: node - linkType: hard - -"spdx-correct@npm:^3.0.0": - version: 3.1.1 - resolution: "spdx-correct@npm:3.1.1" - dependencies: - spdx-expression-parse: "npm:^3.0.0" - spdx-license-ids: "npm:^3.0.0" - checksum: 10c0/25909eecc4024963a8e398399dbdd59ddb925bd7dbecd9c9cf6df0d75c29b68cd30b82123564acc51810eb02cfc4b634a2e16e88aa982433306012e318849249 - languageName: node - linkType: hard - -"spdx-exceptions@npm:^2.1.0": - version: 2.3.0 - resolution: "spdx-exceptions@npm:2.3.0" - checksum: 10c0/83089e77d2a91cb6805a5c910a2bedb9e50799da091f532c2ba4150efdef6e53f121523d3e2dc2573a340dc0189e648b03157097f65465b3a0c06da1f18d7e8a - languageName: node - linkType: hard - -"spdx-expression-parse@npm:^3.0.0": - version: 3.0.1 - resolution: "spdx-expression-parse@npm:3.0.1" - dependencies: - spdx-exceptions: "npm:^2.1.0" - spdx-license-ids: "npm:^3.0.0" - checksum: 10c0/6f8a41c87759fa184a58713b86c6a8b028250f158159f1d03ed9d1b6ee4d9eefdc74181c8ddc581a341aa971c3e7b79e30b59c23b05d2436d5de1c30bdef7171 - languageName: node - linkType: hard - -"spdx-license-ids@npm:^3.0.0": - version: 3.0.12 - resolution: "spdx-license-ids@npm:3.0.12" - checksum: 10c0/b749db2fdecf4ac1893b8e4c435c3bfe5247af9cb412a3cd8375c8bc5a24ad7f3c4263dfe0fc04701f98613f189787700f1deac3e9272c96dfaffc01826c2d0f - languageName: node - linkType: hard - -"sprintf-js@npm:~1.0.2": - version: 1.0.3 - resolution: "sprintf-js@npm:1.0.3" - checksum: 10c0/ecadcfe4c771890140da5023d43e190b7566d9cf8b2d238600f31bec0fc653f328da4450eb04bd59a431771a8e9cc0e118f0aa3974b683a4981b4e07abc2a5bb - languageName: node - linkType: hard - -"strip-final-newline@npm:^2.0.0": - version: 2.0.0 - resolution: "strip-final-newline@npm:2.0.0" - checksum: 10c0/bddf8ccd47acd85c0e09ad7375409d81653f645fda13227a9d459642277c253d877b68f2e5e4d819fe75733b0e626bac7e954c04f3236f6d196f79c94fa4a96f - languageName: node - linkType: hard - -"strip-indent@npm:^4.0.0": - version: 4.0.0 - resolution: "strip-indent@npm:4.0.0" - dependencies: - min-indent: "npm:^1.0.1" - checksum: 10c0/6b1fb4e22056867f5c9e7a6f3f45922d9a2436cac758607d58aeaac0d3b16ec40b1c43317de7900f1b8dd7a4107352fa47fb960f2c23566538c51e8585c8870e - languageName: node - linkType: hard - -"supports-color@npm:^5.3.0": - version: 5.5.0 - resolution: "supports-color@npm:5.5.0" - dependencies: - has-flag: "npm:^3.0.0" - checksum: 10c0/6ae5ff319bfbb021f8a86da8ea1f8db52fac8bd4d499492e30ec17095b58af11f0c55f8577390a749b1c4dde691b6a0315dab78f5f54c9b3d83f8fb5905c1c05 - languageName: node - linkType: hard - -"trash-cli@npm:^5.0.0": - version: 5.0.0 - resolution: "trash-cli@npm:5.0.0" - dependencies: - meow: "npm:^10.1.2" - trash: "npm:^8.0.0" - bin: - trash: cli.js - checksum: 10c0/59c9524d7b8d2c4e5da413981d740619f4d8e429ddc09b565dd4e6c6caa3e31ed9f123e8ff97d247554b39dc0650a36903a3c58960e673b612cb753986ab0a5f - languageName: node - linkType: hard - -"trash@npm:^8.0.0": - version: 8.1.0 - resolution: "trash@npm:8.1.0" - dependencies: - "@sindresorhus/chunkify": "npm:^0.2.0" - "@stroncium/procfs": "npm:^1.2.1" - globby: "npm:^7.1.1" - is-path-inside: "npm:^4.0.0" - move-file: "npm:^3.0.0" - p-map: "npm:^5.1.0" - uuid: "npm:^8.3.2" - xdg-trashdir: "npm:^3.1.0" - checksum: 10c0/6445443bd6f2b0e9355477acb9cd657649d0a0dbbecc26228d66e6b4f1656c037ce405914aa4cf22b944902ff04fd60fa5b85bb48128fbdd88d3a6a28ccd8f52 - languageName: node - linkType: hard - -"trim-newlines@npm:^4.0.2": - version: 4.0.2 - resolution: "trim-newlines@npm:4.0.2" - checksum: 10c0/48d022e9d14f27cf8b71983691af61cd8ce511d159ed0962452d2fa23f58298398d905e1ff982566f9034f93df3ef676868c1c14d13bcd849e7500dbfbd6101b - languageName: node - linkType: hard - -"type-fest@npm:^1.0.1, type-fest@npm:^1.2.1, type-fest@npm:^1.2.2": - version: 1.4.0 - resolution: "type-fest@npm:1.4.0" - checksum: 10c0/a3c0f4ee28ff6ddf800d769eafafcdeab32efa38763c1a1b8daeae681920f6e345d7920bf277245235561d8117dab765cb5f829c76b713b4c9de0998a5397141 - languageName: node - linkType: hard - -"uri-js@npm:^4.2.2": - version: 4.4.1 - resolution: "uri-js@npm:4.4.1" - dependencies: - punycode: "npm:^2.1.0" - checksum: 10c0/4ef57b45aa820d7ac6496e9208559986c665e49447cb072744c13b66925a362d96dd5a46c4530a6b8e203e5db5fe849369444440cb22ecfc26c679359e5dfa3c - languageName: node - linkType: hard - -"user-home@npm:^2.0.0": - version: 2.0.0 - resolution: "user-home@npm:2.0.0" - dependencies: - os-homedir: "npm:^1.0.0" - checksum: 10c0/cbcb251c64f0dce8f3a598049afa5dadd42c928f9834c8720227ee17ededa819296582f9964d963974787f00a4d4cd68e90fd69bc5d8df528d666a6882f84b0c - languageName: node - linkType: hard - -"uuid@npm:^8.3.2": - version: 8.3.2 - resolution: "uuid@npm:8.3.2" - bin: - uuid: dist/bin/uuid - checksum: 10c0/bcbb807a917d374a49f475fae2e87fdca7da5e5530820ef53f65ba1d12131bd81a92ecf259cc7ce317cbe0f289e7d79fdfebcef9bfa3087c8c8a2fa304c9be54 - languageName: node - linkType: hard - -"validate-npm-package-license@npm:^3.0.1": - version: 3.0.4 - resolution: "validate-npm-package-license@npm:3.0.4" - dependencies: - spdx-correct: "npm:^3.0.0" - spdx-expression-parse: "npm:^3.0.0" - checksum: 10c0/7b91e455a8de9a0beaa9fe961e536b677da7f48c9a493edf4d4d4a87fd80a7a10267d438723364e432c2fcd00b5650b5378275cded362383ef570276e6312f4f - languageName: node - linkType: hard - -"which@npm:^2.0.1": - version: 2.0.2 - resolution: "which@npm:2.0.2" - dependencies: - isexe: "npm:^2.0.0" - bin: - node-which: ./bin/node-which - checksum: 10c0/66522872a768b60c2a65a57e8ad184e5372f5b6a9ca6d5f033d4b0dc98aff63995655a7503b9c0a2598936f532120e81dd8cc155e2e92ed662a2b9377cc4374f - languageName: node - linkType: hard - -"wrappy@npm:1": - version: 1.0.2 - resolution: "wrappy@npm:1.0.2" - checksum: 10c0/56fece1a4018c6a6c8e28fbc88c87e0fbf4ea8fd64fc6c63b18f4acc4bd13e0ad2515189786dd2c30d3eec9663d70f4ecf699330002f8ccb547e4a18231fc9f0 - languageName: node - linkType: hard - -"xdg-basedir@npm:^4.0.0": - version: 4.0.0 - resolution: "xdg-basedir@npm:4.0.0" - checksum: 10c0/1b5d70d58355af90363a4e0a51c992e77fc5a1d8de5822699c7d6e96a6afea9a1e048cb93312be6870f338ca45ebe97f000425028fa149c1e87d1b5b8b212a06 - languageName: node - linkType: hard - -"xdg-trashdir@npm:^3.1.0": - version: 3.1.0 - resolution: "xdg-trashdir@npm:3.1.0" - dependencies: - "@sindresorhus/df": "npm:^3.1.1" - mount-point: "npm:^3.0.0" - user-home: "npm:^2.0.0" - xdg-basedir: "npm:^4.0.0" - checksum: 10c0/8f3da0aa890faa0a30fb5ad127110e8c8f129b19751239a29d916cd9632a3376f77e51f843157c6b95c06042b45f30b5aff1991358f4b93f7d3cc7dab5f564a1 - languageName: node - linkType: hard - -"yallist@npm:^4.0.0": - version: 4.0.0 - resolution: "yallist@npm:4.0.0" - checksum: 10c0/2286b5e8dbfe22204ab66e2ef5cc9bbb1e55dfc873bbe0d568aa943eb255d131890dfd5bf243637273d31119b870f49c18fcde2c6ffbb7a7a092b870dc90625a - languageName: node - linkType: hard - -"yargs-parser@npm:^20.2.9": - version: 20.2.9 - resolution: "yargs-parser@npm:20.2.9" - checksum: 10c0/0685a8e58bbfb57fab6aefe03c6da904a59769bd803a722bb098bd5b0f29d274a1357762c7258fb487512811b8063fb5d2824a3415a0a4540598335b3b086c72 - languageName: node - linkType: hard - -"yocto-queue@npm:^0.1.0": - version: 0.1.0 - resolution: "yocto-queue@npm:0.1.0" - checksum: 10c0/dceb44c28578b31641e13695d200d34ec4ab3966a5729814d5445b194933c096b7ced71494ce53a0e8820685d1d010df8b2422e5bf2cdea7e469d97ffbea306f - languageName: node - linkType: hard diff --git a/buttplug/buttplug-schema/.bookignore b/buttplug/buttplug-schema/.bookignore deleted file mode 100644 index 6ec4099f2..000000000 --- a/buttplug/buttplug-schema/.bookignore +++ /dev/null @@ -1 +0,0 @@ -\.#* \ No newline at end of file diff --git a/buttplug/buttplug-schema/.gitignore b/buttplug/buttplug-schema/.gitignore deleted file mode 100644 index 9daa8247d..000000000 --- a/buttplug/buttplug-schema/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -.DS_Store -node_modules diff --git a/buttplug/buttplug-schema/.travis.yml b/buttplug/buttplug-schema/.travis.yml deleted file mode 100644 index 0fe652c20..000000000 --- a/buttplug/buttplug-schema/.travis.yml +++ /dev/null @@ -1,4 +0,0 @@ -language: node_js -node_js: - - "7" -script: "npm run test" diff --git a/buttplug/buttplug-schema/README.md b/buttplug/buttplug-schema/README.md deleted file mode 100644 index 1734e3ad2..000000000 --- a/buttplug/buttplug-schema/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# Buttplug Spec JSON Message Schema - -[![Patreon donate button](https://img.shields.io/badge/patreon-donate-yellow.svg)](https://www.patreon.com/qdot) -[![Github donate button](https://img.shields.io/badge/github-donate-ff69b4.svg)](https://www.github.com/sponsors/qdot) -[![Discourse Forums](https://img.shields.io/discourse/status?label=buttplug.io%20forums&server=https%3A%2F%2Fdiscuss.buttplug.io)](https://discuss.buttplug.io) -[![Discord](https://img.shields.io/discord/353303527587708932.svg?logo=discord)](https://discord.buttplug.io) -[![Twitter](https://img.shields.io/twitter/follow/buttplugio.svg?style=social&logo=twitter)](https://twitter.com/buttplugio) - -The JSON schema for buttplug messages. Mainly used for the Rust Buttplug Server to verify messages -going in/out of the system. - -While you *can* integrate schema checking in client implementations (and can at least be handy for -testing), it's not really needed. You can trust that the server will reject your message if it's -invalid. - -## License - -buttplug is BSD 3-Clause licensed. - -```text - -Copyright (c) 2016-2022, Nonpolynomial, LLC -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of buttplug nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -``` \ No newline at end of file diff --git a/buttplug/buttplug-schema/package.json b/buttplug/buttplug-schema/package.json deleted file mode 100644 index 992029b0d..000000000 --- a/buttplug/buttplug-schema/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "buttplug-json-schema", - "version": "0.0.1", - "description": "Buttplug JSON Schema Repo", - "main": "index.js", - "directories": { - "doc": "docs" - }, - "scripts": { - "test": "ajv compile -s schema/buttplug-schema.json && node tests/jsontest.js" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/metafetish/buttplug-schema.git" - }, - "author": "Kyle Machulis ", - "license": "BSD-3-Clause", - "bugs": { - "url": "https://github.com/metafetish/buttplug-schema/issues" - }, - "homepage": "https://github.com/metafetish/buttplug-schema#readme", - "devDependencies": { - "ajv-cli": "^5.0.0" - } -} diff --git a/buttplug/buttplug-schema/tests/jsontest.js b/buttplug/buttplug-schema/tests/jsontest.js deleted file mode 100644 index bf613deab..000000000 --- a/buttplug/buttplug-schema/tests/jsontest.js +++ /dev/null @@ -1,31 +0,0 @@ -const ajv = require("ajv"); -const fs = require('fs').promises; -const assert = require('assert'); - -async function test() { - const tests = JSON.parse(await fs.readFile("./tests/schema-test.json", "utf-8")); - const schema = JSON.parse(await fs.readFile("./schema/buttplug-schema.json", "utf-8")); - const validator = new ajv(); - validator.addMetaSchema(require("ajv/lib/refs/json-schema-draft-06.json")); - const jsonValidator = validator.compile(schema); - for (const test of tests) { - console.log("Running " + test["Description"]); - for (const testName of test["Tests"]) { - if (testName === "ShouldPassParse") { - assert(jsonValidator(test["Messages"]), jsonValidator.errors ? jsonValidator.errors.map((error) => error.message).join("; ") : "No errors"); - } - else if (testName === "ShouldFailParse") { - assert(!jsonValidator(test["Messages"]), `Test ${test["Description"]} passed parsing when it should have failed`); - } - else if (testName === "ShouldFailOnExtraField") { - let obj = {... test["Message"]}; - obj["ExtraField"] = "I'm a useless extra field"; - assert(!jsonValidator(obj), `Test ${test["Description"]} passed with extra field when it should have failed`); - } else { - assert(false, `Test name ${testName} not valid`); - } - } - } -} - -test().then(() => { console.log("Done"); }).catch((err) => { console.log(`failure: ${err}`); }); diff --git a/buttplug/buttplug-schema/tests/schema-test.json b/buttplug/buttplug-schema/tests/schema-test.json deleted file mode 100644 index 927b03639..000000000 --- a/buttplug/buttplug-schema/tests/schema-test.json +++ /dev/null @@ -1,290 +0,0 @@ -[ - { - "Description": "Ok Message", - "Messages": [ - { - "Ok": { - "Id": 1 - } - } - ], - "Tests": [ - "ShouldPassParse", - "ShouldFailOnExtraField" - ] - }, - { - "Description": "Error Message", - "Messages": [ - { - "Error": { - "Id": 0, - "ErrorMessage": "Server received invalid JSON.", - "ErrorCode": 3 - } - } - ], - "Tests": [ - "ShouldPassParse", - "ShouldFailOnExtraField" - ] - }, - { - "Description": "Ping Message", - "Messages": [ - { - "Ping": { - "Id": 5 - } - } - ], - "Tests": [ - "ShouldPassParse", - "ShouldFailOnExtraField" - ] - }, - { - "Description": "Test Message", - "Messages": [ - { - "Test": { - "Id": 5, - "TestString": "Moo" - } - } - ], - "Tests": [ - "ShouldPassParse", - "ShouldFailOnExtraField" - ] - }, - { - "Description": "RequestLog Message", - "Messages": [ - { - "RequestLog": { - "Id": 1, - "LogLevel": "Warn" - } - } - ], - "Tests": [ - "ShouldPassParse", - "ShouldFailOnExtraField" - ] - }, - { - "Description": "RequestLog Message with invalid level", - "Messages": [ - { - "RequestLog": { - "Id": 1, - "LogLevel": "NotALevel" - } - } - ], - "Tests": [ - "ShouldFailParse" - ] - }, - { - "Description": "Log Message", - "Messages": [ - { - "Log": { - "Id": 0, - "LogLevel": "Trace", - "LogMessage": "This is a Log Message." - } - } - ], - "Tests": [ - "ShouldPassParse", - "ShouldFailOnExtraField" - ] - }, - { - "Description": "Array with no elements", - "Messages": [], - "Tests": [ - "ShouldFailParse" - ] - }, - { - "Description": "Array with null object", - "Messages": [{}], - "Tests": [ - "ShouldFailParse" - ] - }, - { - "Description": "DeviceAdded should Pass with FeatureCount and StepCount", - "Messages": [ - { - "DeviceAdded": { - "Id": 0, - "DeviceName": "TestDevice 1", - "DeviceIndex": 0, - "DeviceMessages": { - "SingleMotorVibrateCmd": {}, - "VibrateCmd": { "FeatureCount": 2, "StepCount": [20, 20] }, - "StopDeviceCmd": {} - } - } - } - ], - "Tests": [ - "ShouldPassParse" - ] - }, - { - "Description": "RawReadCmd", - "Messages": [ - { - "RawReadCmd": { - "Id": 0, - "DeviceIndex": 0, - "Endpoint": "rx", - "Length": 0, - "WaitForData": false - } - } - ], - "Tests": [ - "ShouldPassParse", - "ShouldFailOnExtraField" - ] - }, - { - "Description": "RawWriteCmd", - "Messages": [ - { - "RawWriteCmd": { - "Id": 0, - "DeviceIndex": 0, - "Endpoint": "rx", - "Data": [0, 0, 0] - } - } - ], - "Tests": [ - "ShouldPassParse", - "ShouldFailOnExtraField" - ] - }, - { - "Description": "RawSubscribeCmd", - "Messages": [ - { - "RawSubscribeCmd": { - "Id": 0, - "DeviceIndex": 0, - "Endpoint": "rx" - } - } - ], - "Tests": [ - "ShouldPassParse", - "ShouldFailOnExtraField" - ] - }, - { - "Description": "RawUnsubscribeCmd", - "Messages": [ - { - "RawUnsubscribeCmd": { - "Id": 0, - "DeviceIndex": 0, - "Endpoint": "rx" - } - } - ], - "Tests": [ - "ShouldPassParse", - "ShouldFailOnExtraField" - ] - }, - { - "Description": "RawReading", - "Messages": [ - { - "RawReading": { - "Id": 0, - "DeviceIndex": 0, - "Endpoint": "rx", - "Data": [0, 0, 0] - } - } - ], - "Tests": [ - "ShouldPassParse", - "ShouldFailOnExtraField" - ] - }, - { - "Description": "PatternPlaybackCmd", - "Messages": [ - { - "PatternPlaybackCmd": { - "Id": 0, - "DeviceIndex": 0, - "Patterns": [ - { - "Index": 0, - "Pattern": "Wave", - "Strength": 1.0 - } - ] - } - } - ], - "Tests": [ - "ShouldPassParse", - "ShouldFailOnExtraField" - ] - }, - { - "Description": "ShockCmd", - "Messages": [ - { - "ShockCmd": { - "Id": 0, - "DeviceIndex": 0, - "Shocks": [ - { - "Index": 0, - "Duration": 500, - "Strength": 0.5 - } - ] - } - } - ], - "Tests": [ - "ShouldPassParse", - "ShouldFailOnExtraField" - ] - }, - { - "Description": "ToneEmitterCmd", - "Messages": [ - { - "ToneEmitterCmd": { - "Id": 0, - "DeviceIndex": 0, - "Tones": [ - { - "Index": 0, - "Duration": 500, - "Volume": 0.5 - } - ] - } - } - ], - "Tests": [ - "ShouldPassParse", - "ShouldFailOnExtraField" - ] - } -] diff --git a/buttplug/buttplug-schema/yarn.lock b/buttplug/buttplug-schema/yarn.lock deleted file mode 100644 index 6b249c13b..000000000 --- a/buttplug/buttplug-schema/yarn.lock +++ /dev/null @@ -1,179 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -ajv-cli@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/ajv-cli/-/ajv-cli-5.0.0.tgz#78956ed2934e6dde4c9e696b587be1c2998862e8" - integrity sha512-LY4m6dUv44HTyhV+u2z5uX4EhPYTM38Iv1jdgDJJJCyOOuqB8KtZEGjPZ2T+sh5ZIJrXUfgErYx/j3gLd3+PlQ== - dependencies: - ajv "^8.0.0" - fast-json-patch "^2.0.0" - glob "^7.1.0" - js-yaml "^3.14.0" - json-schema-migrate "^2.0.0" - json5 "^2.1.3" - minimist "^1.2.0" - -ajv@^8.0.0: - version "8.6.1" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.1.tgz#ae65764bf1edde8cd861281cda5057852364a295" - integrity sha512-42VLtQUOLefAvKFAQIxIZDaThq6om/PrfP0CYk3/vn+y4BMNkKnbli8ON2QCiHov4KkzOSJ/xSoBJdayiiYvVQ== - dependencies: - fast-deep-equal "^3.1.1" - json-schema-traverse "^1.0.0" - require-from-string "^2.0.2" - uri-js "^4.2.2" - -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -fast-deep-equal@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" - integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= - -fast-deep-equal@^3.1.1: - version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-json-patch@^2.0.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/fast-json-patch/-/fast-json-patch-2.2.1.tgz#18150d36c9ab65c7209e7d4eb113f4f8eaabe6d9" - integrity sha512-4j5uBaTnsYAV5ebkidvxiLUYOwjQ+JSFljeqfTxCrH9bDmlCQaOJFS84oDJ2rAXZq2yskmk3ORfoP9DCwqFNig== - dependencies: - fast-deep-equal "^2.0.1" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -glob@^7.1.0: - version "7.1.7" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" - integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= - -js-yaml@^3.14.0: - version "3.14.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" - integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -json-schema-migrate@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/json-schema-migrate/-/json-schema-migrate-2.0.0.tgz#335ef5218cd32fcc96c1ddce66c71ba586224496" - integrity sha512-r38SVTtojDRp4eD6WsCqiE0eNDt4v1WalBXb9cyZYw9ai5cGtBwzRNWjHzJl38w6TxFkXAIA7h+fyX3tnrAFhQ== - dependencies: - ajv "^8.0.0" - -json-schema-traverse@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" - integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== - -json5@^2.1.3: - version "2.2.2" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.2.tgz#64471c5bdcc564c18f7c1d4df2e2297f2457c5ab" - integrity sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ== - -minimatch@^3.0.4: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimist@^1.2.0: - version "1.2.6" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" - integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -punycode@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -require-from-string@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" - integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= - -uri-js@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" - integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== - dependencies: - punycode "^2.1.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= diff --git a/buttplug/src/core/message/battery_level_cmd.rs b/buttplug/src/core/message/battery_level_cmd.rs deleted file mode 100644 index 38241bc52..000000000 --- a/buttplug/src/core/message/battery_level_cmd.rs +++ /dev/null @@ -1,35 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 super::*; -#[cfg(feature = "serialize-json")] -use serde::{Deserialize, Serialize}; - -/// Battery level request -#[derive(Debug, ButtplugDeviceMessage, ButtplugMessageFinalizer, PartialEq, Eq, Clone)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct BatteryLevelCmdV2 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] - device_index: u32, -} - -impl BatteryLevelCmdV2 { - pub fn new(device_index: u32) -> Self { - Self { - id: 1, - device_index, - } - } -} - -impl ButtplugMessageValidator for BatteryLevelCmdV2 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_not_system_id(self.id) - } -} diff --git a/buttplug/src/core/message/client_device_message_attributes.rs b/buttplug/src/core/message/client_device_message_attributes.rs deleted file mode 100644 index 496a39783..000000000 --- a/buttplug/src/core/message/client_device_message_attributes.rs +++ /dev/null @@ -1,695 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::core::{ - errors::ButtplugDeviceError, - message::{ButtplugDeviceMessageType, Endpoint}, -}; -use getset::{Getters, MutGetters, Setters}; -use serde::{ser::SerializeSeq, Deserialize, Serialize, Serializer}; -use std::ops::RangeInclusive; - -use super::{ - ButtplugActuatorFeatureMessageType, - ButtplugSensorFeatureMessageType, - DeviceFeature, - FeatureType, -}; - -#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum ActuatorType { - Unknown, - Vibrate, - // Single Direction Rotation Speed - Rotate, - Oscillate, - Constrict, - Inflate, - // For instances where we specify a position to move to ASAP. Usually servos, probably for the - // OSR-2/SR-6. - Position, -} - -impl TryFrom for ActuatorType { - type Error = String; - fn try_from(value: FeatureType) -> Result { - match value { - FeatureType::Unknown => Ok(ActuatorType::Unknown), - FeatureType::Vibrate => Ok(ActuatorType::Vibrate), - FeatureType::Rotate => Ok(ActuatorType::Rotate), - FeatureType::Oscillate => Ok(ActuatorType::Oscillate), - FeatureType::Constrict => Ok(ActuatorType::Constrict), - FeatureType::Inflate => Ok(ActuatorType::Inflate), - FeatureType::Position => Ok(ActuatorType::Position), - _ => Err(format!( - "Feature type {value} not valid for ActuatorType conversion" - )), - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display)] -pub enum SensorType { - Unknown, - Battery, - RSSI, - Button, - Pressure, - // Temperature, - // Accelerometer, - // Gyro, -} - -impl TryFrom for SensorType { - type Error = String; - fn try_from(value: FeatureType) -> Result { - match value { - FeatureType::Unknown => Ok(SensorType::Unknown), - FeatureType::Battery => Ok(SensorType::Battery), - FeatureType::RSSI => Ok(SensorType::RSSI), - FeatureType::Button => Ok(SensorType::Button), - FeatureType::Pressure => Ok(SensorType::Pressure), - _ => Err(format!( - "Feature type {value} not valid for SensorType conversion" - )), - } - } -} - -// This will look almost exactly like ServerDeviceMessageAttributes. However, it will only contain -// information we want the client to know, i.e. step counts versus specific step ranges. This is -// what will be sent to the client as part of DeviceAdded/DeviceList messages. It should not be used -// for outside configuration/serialization, rather it should be a subset of that information. -// -// 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. -#[derive(Clone, Debug, Default, PartialEq, Eq, Getters, MutGetters, Setters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct ClientDeviceMessageAttributesV3 { - // Generic commands - #[getset(get = "pub", get_mut = "pub(super)")] - #[serde(rename = "ScalarCmd")] - #[serde(skip_serializing_if = "Option::is_none")] - scalar_cmd: Option>, - #[getset(get = "pub", get_mut = "pub(super)")] - #[serde(rename = "RotateCmd")] - #[serde(skip_serializing_if = "Option::is_none")] - rotate_cmd: Option>, - #[getset(get = "pub", get_mut = "pub(super)")] - #[serde(rename = "LinearCmd")] - #[serde(skip_serializing_if = "Option::is_none")] - linear_cmd: Option>, - - // Sensor Messages - #[getset(get = "pub")] - #[serde(rename = "SensorReadCmd")] - #[serde(skip_serializing_if = "Option::is_none")] - sensor_read_cmd: Option>, - #[getset(get = "pub")] - #[serde(rename = "SensorSubscribeCmd")] - #[serde(skip_serializing_if = "Option::is_none")] - sensor_subscribe_cmd: Option>, - - // StopDeviceCmd always exists - #[getset(get = "pub")] - #[serde(rename = "StopDeviceCmd")] - #[serde(skip_deserializing)] - stop_device_cmd: NullDeviceMessageAttributesV1, - - // Raw commands are only added post-serialization - #[getset(get = "pub")] - #[serde(rename = "RawReadCmd")] - #[serde(skip_deserializing)] - #[serde(skip_serializing_if = "Option::is_none")] - raw_read_cmd: Option, - // Raw commands are only added post-serialization - #[getset(get = "pub")] - #[serde(rename = "RawWriteCmd")] - #[serde(skip_deserializing)] - #[serde(skip_serializing_if = "Option::is_none")] - raw_write_cmd: Option, - // Raw commands are only added post-serialization - #[getset(get = "pub")] - #[serde(rename = "RawSubscribeCmd")] - #[serde(skip_deserializing)] - #[serde(skip_serializing_if = "Option::is_none")] - raw_subscribe_cmd: Option, - - // Needed to load from config for fallback, but unused here. - #[getset(get = "pub")] - #[serde(rename = "FleshlightLaunchFW12Cmd")] - #[serde(skip_serializing)] - fleshlight_launch_fw12_cmd: Option, - #[getset(get = "pub")] - #[serde(rename = "VorzeA10CycloneCmd")] - #[serde(skip_serializing)] - vorze_a10_cyclone_cmd: Option, -} - -impl From> for ClientDeviceMessageAttributesV3 { - fn from(features: Vec) -> Self { - let actuator_filter = |message_type| { - let attrs: Vec = features - .iter() - .filter(|x| { - if let Some(actuator) = x.actuator() { - actuator.messages().contains(message_type) - } else { - false - } - }) - .map(|x| x.clone().try_into().unwrap()) - .collect(); - if !attrs.is_empty() { - Some(attrs) - } else { - None - } - }; - - let sensor_filter = |message_type| { - let attrs: Vec = features - .iter() - .filter(|x| { - if let Some(sensor) = x.sensor() { - sensor.messages().contains(message_type) - } else { - false - } - }) - .map(|x| x.clone().try_into().unwrap()) - .collect(); - if !attrs.is_empty() { - Some(attrs) - } else { - None - } - }; - - // Raw messages - let raw_attrs = if let Some(raw_feature) = features.iter().find(|f| f.raw().is_some()) { - Some(RawDeviceMessageAttributesV2::new( - raw_feature.raw().as_ref().unwrap().endpoints(), - )) - } else { - None - }; - - Self { - scalar_cmd: actuator_filter(&ButtplugActuatorFeatureMessageType::ScalarCmd), - rotate_cmd: actuator_filter(&ButtplugActuatorFeatureMessageType::RotateCmd), - linear_cmd: actuator_filter(&ButtplugActuatorFeatureMessageType::LinearCmd), - sensor_read_cmd: sensor_filter(&ButtplugSensorFeatureMessageType::SensorReadCmd), - sensor_subscribe_cmd: sensor_filter(&ButtplugSensorFeatureMessageType::SensorSubscribeCmd), - raw_read_cmd: raw_attrs.clone(), - raw_write_cmd: raw_attrs.clone(), - raw_subscribe_cmd: raw_attrs.clone(), - ..Default::default() - } - } -} - -impl ClientDeviceMessageAttributesV3 { - pub fn raw_unsubscribe_cmd(&self) -> &Option { - self.raw_subscribe_cmd() - } - - pub fn message_allowed(&self, message_type: &ButtplugDeviceMessageType) -> bool { - match message_type { - ButtplugDeviceMessageType::ScalarCmd => self.scalar_cmd.is_some(), - // VibrateCmd and SingleMotorVibrateCmd will derive from Scalars, so errors will be thrown in - // the scalar parser if the actuator isn't correct. - ButtplugDeviceMessageType::VibrateCmd => self.scalar_cmd.is_some(), - ButtplugDeviceMessageType::SingleMotorVibrateCmd => self.scalar_cmd.is_some(), - ButtplugDeviceMessageType::SensorReadCmd => self.sensor_read_cmd.is_some(), - ButtplugDeviceMessageType::SensorSubscribeCmd => self.sensor_subscribe_cmd.is_some(), - ButtplugDeviceMessageType::SensorUnsubscribeCmd => self.sensor_subscribe_cmd.is_some(), - ButtplugDeviceMessageType::LinearCmd => self.linear_cmd.is_some(), - ButtplugDeviceMessageType::RotateCmd => self.rotate_cmd.is_some(), - ButtplugDeviceMessageType::BatteryLevelCmd => { - if let Some(sensor_info) = &self.sensor_read_cmd { - sensor_info - .iter() - .any(|x| *x.sensor_type() == SensorType::Battery) - } else { - false - } - } - ButtplugDeviceMessageType::FleshlightLaunchFW12Cmd => { - self.fleshlight_launch_fw12_cmd.is_some() - } - ButtplugDeviceMessageType::RSSILevelCmd => { - if let Some(sensor_info) = &self.sensor_read_cmd { - sensor_info - .iter() - .any(|x| *x.sensor_type() == SensorType::RSSI) - } else { - false - } - } - ButtplugDeviceMessageType::RawReadCmd => self.raw_read_cmd.is_some(), - ButtplugDeviceMessageType::RawSubscribeCmd => self.raw_subscribe_cmd.is_some(), - ButtplugDeviceMessageType::RawUnsubscribeCmd => self.raw_subscribe_cmd.is_some(), - ButtplugDeviceMessageType::RawWriteCmd => self.raw_write_cmd.is_some(), - ButtplugDeviceMessageType::VorzeA10CycloneCmd => self.vorze_a10_cyclone_cmd.is_some(), - ButtplugDeviceMessageType::StopDeviceCmd => true, - ButtplugDeviceMessageType::KiirooCmd => false, - ButtplugDeviceMessageType::LovenseCmd => false, - } - } - - pub fn finalize(&mut self) { - if let Some(scalar_attrs) = &mut self.scalar_cmd { - for (i, attr) in scalar_attrs.iter_mut().enumerate() { - attr.index = i as u32; - } - } - if let Some(sensor_read_attrs) = &mut self.sensor_read_cmd { - for (i, attr) in sensor_read_attrs.iter_mut().enumerate() { - attr.index = i as u32; - } - } - if let Some(sensor_subscribe_attrs) = &mut self.sensor_subscribe_cmd { - for (i, attr) in sensor_subscribe_attrs.iter_mut().enumerate() { - attr.index = i as u32; - } - } - } -} - -#[derive(Default)] -pub struct ClientDeviceMessageAttributesV3Builder { - attrs: ClientDeviceMessageAttributesV3, -} - -impl ClientDeviceMessageAttributesV3Builder { - pub fn scalar_cmd(&mut self, attrs: &[ClientGenericDeviceMessageAttributesV3]) -> &Self { - self.attrs.scalar_cmd = Some(attrs.to_vec()); - self - } - - pub fn rotate_cmd(&mut self, attrs: &[ClientGenericDeviceMessageAttributesV3]) -> &Self { - self.attrs.rotate_cmd = Some(attrs.to_vec()); - self - } - - pub fn linear_cmd(&mut self, attrs: &[ClientGenericDeviceMessageAttributesV3]) -> &Self { - self.attrs.linear_cmd = Some(attrs.to_vec()); - self - } - - pub fn sensor_read_cmd(&mut self, attrs: &[SensorDeviceMessageAttributesV3]) -> &Self { - self.attrs.sensor_read_cmd = Some(attrs.to_vec()); - self - } - - pub fn sensor_subscribe_cmd(&mut self, attrs: &[SensorDeviceMessageAttributesV3]) -> &Self { - self.attrs.sensor_subscribe_cmd = Some(attrs.to_vec()); - self - } - - pub fn raw_read_cmd(&mut self, endpoints: &[Endpoint]) -> &Self { - self.attrs.raw_read_cmd = Some(RawDeviceMessageAttributesV2::new(endpoints)); - self - } - - pub fn raw_write_cmd(&mut self, endpoints: &[Endpoint]) -> &Self { - self.attrs.raw_write_cmd = Some(RawDeviceMessageAttributesV2::new(endpoints)); - self - } - - pub fn raw_subscribe_cmd(&mut self, endpoints: &[Endpoint]) -> &Self { - self.attrs.raw_subscribe_cmd = Some(RawDeviceMessageAttributesV2::new(endpoints)); - self - } - - pub fn finish(&mut self) -> ClientDeviceMessageAttributesV3 { - self.attrs.finalize(); - self.attrs.clone() - } -} - -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct NullDeviceMessageAttributesV1 {} - -fn unspecified_feature() -> String { - "N/A".to_string() -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Getters, Setters)] -pub struct ClientGenericDeviceMessageAttributesV3 { - #[getset(get = "pub")] - #[serde(rename = "FeatureDescriptor")] - #[serde(default = "unspecified_feature")] - feature_descriptor: String, - #[getset(get = "pub")] - #[serde(rename = "ActuatorType")] - actuator_type: ActuatorType, - #[serde(rename = "StepCount")] - #[getset(get = "pub")] - step_count: u32, - // TODO This needs to actually be part of the device info relayed to the client in spec v4. - #[getset(get = "pub")] - #[serde(skip, default)] - index: u32, -} - -impl TryFrom for ClientGenericDeviceMessageAttributesV3 { - type Error = String; - fn try_from(value: DeviceFeature) -> Result { - if let Some(actuator) = value.actuator() { - let actuator_type = (*value.feature_type()).try_into()?; - let step_limit = actuator.step_limit(); - let step_count = step_limit.end() - step_limit.start(); - let attrs = Self { - feature_descriptor: value.description().to_owned(), - actuator_type, - step_count: step_count, - index: 0, - }; - Ok(attrs) - } else { - Err(format!( - "Cannot produce a GenericDeviceMessageAttribute from a feature with no actuator member" - )) - } - } -} - -impl ClientGenericDeviceMessageAttributesV3 { - pub fn new(feature_descriptor: &str, step_count: u32, actuator_type: ActuatorType) -> Self { - Self { - feature_descriptor: feature_descriptor.to_owned(), - actuator_type, - step_count, - index: 0, - } - } - - // This is created out of already verified server device message attributes, so we'll assume it's - // fine. - pub fn is_valid(&self, _: &ButtplugDeviceMessageType) -> Result<(), ButtplugDeviceError> { - Ok(()) - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, Getters, Setters)] -pub struct RawDeviceMessageAttributesV2 { - #[getset(get = "pub")] - #[serde(rename = "Endpoints")] - endpoints: Vec, -} - -impl RawDeviceMessageAttributesV2 { - pub fn new(endpoints: &[Endpoint]) -> Self { - Self { - endpoints: endpoints.to_vec(), - } - } -} - -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()])?; - } - seq.end() -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Getters, Setters)] -pub struct SensorDeviceMessageAttributesV3 { - #[getset(get = "pub")] - #[serde(rename = "FeatureDescriptor")] - feature_descriptor: String, - #[getset(get = "pub")] - #[serde(rename = "SensorType")] - sensor_type: SensorType, - #[getset(get = "pub")] - #[serde(rename = "SensorRange", serialize_with = "range_sequence_serialize")] - sensor_range: Vec>, - // TODO This needs to actually be part of the device info relayed to the client in spec v4. - #[getset(get = "pub")] - #[serde(skip, default)] - index: u32, -} - -impl TryFrom for SensorDeviceMessageAttributesV3 { - type Error = String; - fn try_from(value: DeviceFeature) -> Result { - if let Some(sensor) = value.sensor() { - Ok(Self { - feature_descriptor: value.description().to_owned(), - sensor_type: (*value.feature_type()).try_into()?, - sensor_range: sensor.value_range().clone(), - index: 0, - }) - } else { - Err("Device Feature does not expose a sensor.".to_owned()) - } - } -} - -/* -impl SensorDeviceMessageAttributes { - pub fn new(feature_descriptor: &str, sensor_type: SensorType) -> Self { - Self { feature_descriptor: feature_descriptor.to_owned(), sensor_type } - } -} - */ - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Getters, Setters)] -pub struct ClientDeviceMessageAttributesV2 { - // Generic commands - #[getset(get = "pub")] - #[serde(rename = "VibrateCmd")] - #[serde(skip_serializing_if = "Option::is_none")] - vibrate_cmd: Option, - #[getset(get = "pub")] - #[serde(rename = "RotateCmd")] - #[serde(skip_serializing_if = "Option::is_none")] - rotate_cmd: Option, - #[getset(get = "pub")] - #[serde(rename = "LinearCmd")] - #[serde(skip_serializing_if = "Option::is_none")] - linear_cmd: Option, - #[getset(get = "pub")] - #[serde(rename = "BatteryLevelCmd")] - #[serde(skip_serializing_if = "Option::is_none")] - battery_level_cmd: Option, - - // RSSILevel is added post-serialization (only for bluetooth devices) - #[getset(get = "pub")] - #[serde(rename = "RSSILevelCmd")] - #[serde(skip_serializing_if = "Option::is_none")] - rssi_level_cmd: Option, - - // StopDeviceCmd always exists - #[getset(get = "pub")] - #[serde(rename = "StopDeviceCmd")] - stop_device_cmd: NullDeviceMessageAttributesV1, - - // Raw commands are only added post-serialization - #[getset(get = "pub")] - #[serde(rename = "RawReadCmd")] - #[serde(skip_serializing_if = "Option::is_none")] - raw_read_cmd: Option, - #[getset(get = "pub")] - #[serde(rename = "RawWriteCmd")] - #[serde(skip_serializing_if = "Option::is_none")] - raw_write_cmd: Option, - #[getset(get = "pub")] - #[serde(rename = "RawSubscribeCmd")] - #[serde(skip_serializing_if = "Option::is_none")] - raw_subscribe_cmd: Option, - #[getset(get = "pub")] - #[serde(rename = "RawUnsubscribeCmd")] - #[serde(skip_serializing_if = "Option::is_none")] - raw_unsubscribe_cmd: Option, - - // Needed to load from config for fallback, but unused here. - #[getset(get = "pub")] - #[serde(rename = "FleshlightLaunchFW12Cmd")] - #[serde(skip)] - fleshlight_launch_fw12_cmd: Option, - #[getset(get = "pub")] - #[serde(rename = "VorzeA10CycloneCmd")] - #[serde(skip)] - vorze_a10_cyclone_cmd: Option, -} - -impl From for ClientDeviceMessageAttributesV2 { - fn from(other: ClientDeviceMessageAttributesV3) -> Self { - Self { - vibrate_cmd: other - .scalar_cmd() - .as_ref() - .map(|x| GenericDeviceMessageAttributesV2::vibrate_cmd_from_scalar_cmd(x)) - .filter(|x| x.feature_count != 0), - rotate_cmd: other - .rotate_cmd() - .as_ref() - .map(|x| GenericDeviceMessageAttributesV2::from(x.clone())), - linear_cmd: other - .linear_cmd() - .as_ref() - .map(|x| GenericDeviceMessageAttributesV2::from(x.clone())), - battery_level_cmd: { - if let Some(sensor_info) = &other.sensor_read_cmd { - if sensor_info - .iter() - .any(|x| *x.sensor_type() == SensorType::Battery) - { - Some(NullDeviceMessageAttributesV1::default()) - } else { - None - } - } else { - None - } - }, - rssi_level_cmd: { - if let Some(sensor_info) = &other.sensor_read_cmd { - if sensor_info - .iter() - .any(|x| *x.sensor_type() == SensorType::RSSI) - { - Some(NullDeviceMessageAttributesV1::default()) - } else { - None - } - } else { - None - } - }, - stop_device_cmd: other.stop_device_cmd().clone(), - raw_read_cmd: other.raw_read_cmd().clone(), - raw_write_cmd: other.raw_write_cmd().clone(), - raw_subscribe_cmd: other.raw_subscribe_cmd().clone(), - raw_unsubscribe_cmd: other.raw_subscribe_cmd().clone(), - fleshlight_launch_fw12_cmd: other.fleshlight_launch_fw12_cmd().clone(), - vorze_a10_cyclone_cmd: other.vorze_a10_cyclone_cmd().clone(), - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Getters, Setters)] -pub struct GenericDeviceMessageAttributesV2 { - #[getset(get = "pub")] - #[serde(rename = "FeatureCount")] - feature_count: u32, - #[getset(get = "pub")] - #[serde(rename = "StepCount")] - step_count: Vec, -} - -impl GenericDeviceMessageAttributesV2 { - pub fn vibrate_cmd_from_scalar_cmd( - attributes_vec: &[ClientGenericDeviceMessageAttributesV3], - ) -> Self { - let mut feature_count = 0u32; - let mut step_count = vec![]; - for attr in attributes_vec { - if *attr.actuator_type() == ActuatorType::Vibrate { - feature_count += 1; - step_count.push(*attr.step_count()); - } - } - Self { - feature_count, - step_count, - } - } -} - -impl From> for GenericDeviceMessageAttributesV2 { - fn from(attributes_vec: Vec) -> Self { - Self { - feature_count: attributes_vec.len() as u32, - step_count: attributes_vec.iter().map(|x| *x.step_count()).collect(), - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Getters, Setters)] -pub struct ClientDeviceMessageAttributesV1 { - // Generic commands - #[getset(get = "pub")] - #[serde(rename = "VibrateCmd")] - #[serde(skip_serializing_if = "Option::is_none")] - vibrate_cmd: Option, - #[getset(get = "pub")] - #[serde(rename = "RotateCmd")] - #[serde(skip_serializing_if = "Option::is_none")] - rotate_cmd: Option, - #[getset(get = "pub")] - #[serde(rename = "LinearCmd")] - #[serde(skip_serializing_if = "Option::is_none")] - linear_cmd: Option, - - // StopDeviceCmd always exists - #[getset(get = "pub")] - stop_device_cmd: NullDeviceMessageAttributesV1, - - // Obsolete commands are only added post-serialization - #[getset(get = "pub")] - #[serde(skip_serializing_if = "Option::is_none")] - single_motor_vibrate_cmd: Option, - #[getset(get = "pub")] - #[serde(skip_serializing_if = "Option::is_none")] - fleshlight_launch_fw12_cmd: Option, - #[getset(get = "pub")] - #[serde(skip_serializing_if = "Option::is_none")] - vorze_a10_cyclone_cmd: Option, -} - -impl From for ClientDeviceMessageAttributesV1 { - fn from(other: ClientDeviceMessageAttributesV2) -> Self { - Self { - vibrate_cmd: other - .vibrate_cmd() - .as_ref() - .map(|x| GenericDeviceMessageAttributesV1::from(x.clone())), - rotate_cmd: other - .rotate_cmd() - .as_ref() - .map(|x| GenericDeviceMessageAttributesV1::from(x.clone())), - linear_cmd: other - .linear_cmd() - .as_ref() - .map(|x| GenericDeviceMessageAttributesV1::from(x.clone())), - stop_device_cmd: other.stop_device_cmd().clone(), - fleshlight_launch_fw12_cmd: other.fleshlight_launch_fw12_cmd().clone(), - vorze_a10_cyclone_cmd: other.vorze_a10_cyclone_cmd().clone(), - single_motor_vibrate_cmd: if other.vibrate_cmd().is_some() { - Some(NullDeviceMessageAttributesV1::default()) - } else { - None - }, - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Getters, Setters)] -pub struct GenericDeviceMessageAttributesV1 { - #[serde(rename = "FeatureCount")] - feature_count: u32, -} - -impl From for GenericDeviceMessageAttributesV1 { - fn from(attributes: GenericDeviceMessageAttributesV2) -> Self { - Self { - feature_count: *attributes.feature_count(), - } - } -} diff --git a/buttplug/src/core/message/device_added.rs b/buttplug/src/core/message/device_added.rs deleted file mode 100644 index 8ead2a0c3..000000000 --- a/buttplug/src/core/message/device_added.rs +++ /dev/null @@ -1,283 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 super::device_message_info::{DeviceMessageInfoV0, DeviceMessageInfoV1, DeviceMessageInfoV2}; -use super::*; - -use getset::{CopyGetters, Getters}; - -#[cfg(feature = "serialize-json")] -use serde::{Deserialize, Serialize}; - -/// Notification that a device has been found and connected to the server. -#[derive(ButtplugMessage, Clone, Debug, PartialEq, Eq, Getters, CopyGetters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct DeviceAddedV4 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - // DeviceAdded is not considered a device message because it only notifies of existence and is not - // a command (and goes from server to client), therefore we have to define the getter ourselves. - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] - #[getset(get_copy = "pub")] - device_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceName"))] - #[getset(get = "pub")] - device_name: String, - #[cfg_attr( - feature = "serialize-json", - serde(rename = "DeviceDisplayName", skip_serializing_if = "Option::is_none") - )] - #[getset(get = "pub")] - device_display_name: Option, - #[cfg_attr( - feature = "serialize-json", - serde( - rename = "DeviceMessageTimingGap", - skip_serializing_if = "Option::is_none" - ) - )] - #[getset(get = "pub")] - device_message_timing_gap: Option, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceFeatures"))] - #[getset(get = "pub")] - device_features: Vec, -} - -impl DeviceAddedV4 { - pub fn new( - device_index: u32, - device_name: &str, - device_display_name: &Option, - device_message_timing_gap: &Option, - device_features: &Vec, - ) -> Self { - let mut obj = Self { - id: 0, - device_index, - device_name: device_name.to_string(), - device_display_name: device_display_name.clone(), - device_message_timing_gap: *device_message_timing_gap, - device_features: device_features.clone(), - }; - obj.finalize(); - obj - } -} - -impl ButtplugMessageValidator for DeviceAddedV4 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_system_id(self.id) - } -} - -impl ButtplugMessageFinalizer for DeviceAddedV4 { - fn finalize(&mut self) { - } -} - -impl From for DeviceAddedV3 { - fn from(value: DeviceAddedV4) -> Self { - let mut da3 = DeviceAddedV3::new( - value.device_index(), - &value.device_name(), - &value.device_display_name(), - &None, - &value.device_features().clone().into(), - ); - da3.set_id(value.id); - da3 - } -} - -/// Notification that a device has been found and connected to the server. -#[derive(ButtplugMessage, Clone, Debug, PartialEq, Eq, Getters, CopyGetters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct DeviceAddedV3 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - // DeviceAdded is not considered a device message because it only notifies of existence and is not - // a command (and goes from server to client), therefore we have to define the getter ourselves. - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] - #[getset(get_copy = "pub")] - device_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceName"))] - #[getset(get = "pub")] - device_name: String, - #[cfg_attr( - feature = "serialize-json", - serde(rename = "DeviceDisplayName", skip_serializing_if = "Option::is_none") - )] - #[getset(get = "pub")] - device_display_name: Option, - #[cfg_attr( - feature = "serialize-json", - serde( - rename = "DeviceMessageTimingGap", - skip_serializing_if = "Option::is_none" - ) - )] - #[getset(get = "pub")] - device_message_timing_gap: Option, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceMessages"))] - #[getset(get = "pub")] - device_messages: ClientDeviceMessageAttributesV3, -} - -impl DeviceAddedV3 { - pub fn new( - device_index: u32, - device_name: &str, - device_display_name: &Option, - device_message_timing_gap: &Option, - device_messages: &ClientDeviceMessageAttributesV3, - ) -> Self { - let mut obj = Self { - id: 0, - device_index, - device_name: device_name.to_string(), - device_display_name: device_display_name.clone(), - device_message_timing_gap: *device_message_timing_gap, - device_messages: device_messages.clone(), - }; - obj.finalize(); - obj - } -} - -impl ButtplugMessageValidator for DeviceAddedV3 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_system_id(self.id) - } -} - -impl ButtplugMessageFinalizer for DeviceAddedV3 { - fn finalize(&mut self) { - self.device_messages.finalize(); - } -} - -#[derive(ButtplugMessage, Clone, Debug, PartialEq, Eq, Getters, CopyGetters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct DeviceAddedV2 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] - #[getset(get_copy = "pub")] - device_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceName"))] - #[getset(get = "pub")] - device_name: String, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceMessages"))] - #[getset(get = "pub")] - device_messages: ClientDeviceMessageAttributesV2, -} - -impl From for DeviceAddedV2 { - fn from(msg: DeviceAddedV3) -> Self { - let id = msg.id(); - let dmi = DeviceMessageInfoV3::from(msg); - let dmiv1 = DeviceMessageInfoV2::from(dmi); - - Self { - id, - device_index: dmiv1.device_index(), - device_name: dmiv1.device_name().clone(), - device_messages: dmiv1.device_messages().clone(), - } - } -} - -impl ButtplugMessageValidator for DeviceAddedV2 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_system_id(self.id) - } -} - -impl ButtplugMessageFinalizer for DeviceAddedV2 { -} - -#[derive(ButtplugMessage, Clone, Debug, PartialEq, Eq, Getters, CopyGetters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct DeviceAddedV1 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] - #[getset(get_copy = "pub")] - device_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceName"))] - #[getset(get = "pub")] - device_name: String, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceMessages"))] - #[getset(get = "pub")] - device_messages: ClientDeviceMessageAttributesV1, -} - -impl From for DeviceAddedV1 { - fn from(msg: DeviceAddedV2) -> Self { - let id = msg.id(); - let dmiv2 = DeviceMessageInfoV2::from(msg); - let dmiv1 = DeviceMessageInfoV1::from(dmiv2); - - Self { - id, - device_index: dmiv1.device_index(), - device_name: dmiv1.device_name().clone(), - device_messages: dmiv1.device_messages().clone(), - } - } -} - -impl ButtplugMessageValidator for DeviceAddedV1 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_system_id(self.id) - } -} - -impl ButtplugMessageFinalizer for DeviceAddedV1 { -} - -#[derive(Default, ButtplugMessage, Clone, Debug, PartialEq, Eq, Getters, CopyGetters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct DeviceAddedV0 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] - #[getset(get_copy = "pub")] - device_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceName"))] - #[getset(get = "pub")] - device_name: String, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceMessages"))] - #[getset(get = "pub")] - device_messages: Vec, -} - -impl From for DeviceAddedV0 { - fn from(msg: DeviceAddedV1) -> Self { - let id = msg.id(); - let dmiv1 = DeviceMessageInfoV1::from(msg); - let dmiv0 = DeviceMessageInfoV0::from(dmiv1); - - Self { - id, - device_index: dmiv0.device_index(), - device_name: dmiv0.device_name().clone(), - device_messages: dmiv0.device_messages().clone(), - } - } -} - -impl ButtplugMessageValidator for DeviceAddedV0 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_system_id(self.id) - } -} - -impl ButtplugMessageFinalizer for DeviceAddedV0 { -} - -// TODO Test repeated message type in attributes in JSON diff --git a/buttplug/src/core/message/device_feature.rs b/buttplug/src/core/message/device_feature.rs deleted file mode 100644 index f23299780..000000000 --- a/buttplug/src/core/message/device_feature.rs +++ /dev/null @@ -1,291 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::core::{ - errors::ButtplugDeviceError, - message::{ButtplugDeviceMessageType, Endpoint}, -}; -use getset::{Getters, MutGetters, Setters}; -use serde::{ser::SerializeSeq, Deserialize, Serialize, Serializer}; -use std::{collections::HashSet, ops::RangeInclusive}; - -use super::{ - ActuatorType, - ButtplugActuatorFeatureMessageType, - ButtplugSensorFeatureMessageType, - SensorType, -}; - -#[derive(Debug, Default, Display, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum FeatureType { - #[default] - Unknown, - Vibrate, - // Single Direction Rotation Speed - Rotate, - Oscillate, - Constrict, - Inflate, - // For instances where we specify a position to move to ASAP. Usually servos, probably for the - // OSR-2/SR-6. - Position, - // Sensor Types - Battery, - RSSI, - Button, - Pressure, - // Currently unused but possible sensor features: - // Temperature, - // Accelerometer, - // Gyro, - // - // Raw Feature, for when raw messages are on - Raw, -} - -impl From for FeatureType { - fn from(value: ActuatorType) -> Self { - match value { - ActuatorType::Unknown => FeatureType::Unknown, - ActuatorType::Vibrate => FeatureType::Vibrate, - ActuatorType::Rotate => FeatureType::Rotate, - ActuatorType::Oscillate => FeatureType::Oscillate, - ActuatorType::Constrict => FeatureType::Constrict, - ActuatorType::Inflate => FeatureType::Inflate, - ActuatorType::Position => FeatureType::Position, - } - } -} - -impl From for FeatureType { - fn from(value: SensorType) -> Self { - match value { - SensorType::Unknown => FeatureType::Unknown, - SensorType::Battery => FeatureType::Battery, - SensorType::RSSI => FeatureType::RSSI, - SensorType::Button => FeatureType::Button, - SensorType::Pressure => FeatureType::Pressure, - } - } -} - -// 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 -// what will be sent to the client as part of DeviceAdded/DeviceList messages. It should not be used -// for outside configuration/serialization, rather it should be a subset of that information. -// -// 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. -#[derive( - Clone, Debug, Default, PartialEq, Eq, Getters, MutGetters, Setters, Serialize, Deserialize, -)] -pub struct DeviceFeature { - #[getset(get = "pub", get_mut = "pub(super)")] - #[serde(default)] - description: String, - #[getset(get = "pub")] - #[serde(rename = "feature-type")] - feature_type: FeatureType, - #[getset(get = "pub")] - #[serde(skip_serializing_if = "Option::is_none")] - #[serde(rename = "actuator")] - actuator: Option, - #[getset(get = "pub")] - #[serde(skip_serializing_if = "Option::is_none")] - #[serde(rename = "sensor")] - sensor: Option, - #[getset(get = "pub")] - #[serde(skip)] - raw: Option, -} - -impl DeviceFeature { - pub fn new( - description: &str, - feature_type: FeatureType, - actuator: &Option, - sensor: &Option, - ) -> Self { - Self { - description: description.to_owned(), - feature_type, - actuator: actuator.clone(), - sensor: sensor.clone(), - raw: None, - } - } - - pub fn is_valid(&self) -> Result<(), ButtplugDeviceError> { - if let Some(actuator) = &self.actuator { - actuator.is_valid()?; - } - Ok(()) - } - - pub fn new_raw_feature(endpoints: &[Endpoint]) -> Self { - Self { - description: "Raw Endpoints".to_owned(), - feature_type: FeatureType::Raw, - actuator: None, - sensor: None, - raw: Some(DeviceFeatureRaw::new(endpoints)), - } - } -} - -fn range_serialize(range: &RangeInclusive, serializer: S) -> Result -where - S: Serializer, -{ - let mut seq = serializer.serialize_seq(Some(2))?; - seq.serialize_element(&range.start())?; - seq.serialize_element(&range.end())?; - seq.end() -} - -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()])?; - } - seq.end() -} - -#[derive(Clone, Debug, PartialEq, Eq, Getters, MutGetters, Setters, Serialize, Deserialize)] -pub struct DeviceFeatureActuatorSerialized { - #[getset(get = "pub")] - #[serde(rename = "step-range")] - #[serde(serialize_with = "range_serialize")] - step_range: RangeInclusive, - // This doesn't exist in base configs, so when we load these from the base config file, we'll just - // copy the step_range value. - #[getset(get = "pub")] - #[serde(rename = "step-limit")] - #[serde(default)] - step_limit: Option>, - #[getset(get = "pub")] - #[serde(rename = "messages")] - messages: HashSet, -} - -#[derive(Clone, Debug, PartialEq, Eq, Getters, MutGetters, Setters, Serialize, Deserialize)] -#[serde(from = "DeviceFeatureActuatorSerialized")] -pub struct DeviceFeatureActuator { - #[getset(get = "pub")] - #[serde(rename = "step-range")] - #[serde(serialize_with = "range_serialize")] - step_range: RangeInclusive, - // This doesn't exist in base configs, so when we load these from the base config file, we'll just - // copy the step_range value. - #[getset(get = "pub")] - #[serde(rename = "step-limit")] - #[serde(serialize_with = "range_serialize")] - step_limit: RangeInclusive, - #[getset(get = "pub")] - #[serde(rename = "messages")] - messages: HashSet, -} - -impl From for DeviceFeatureActuator { - fn from(value: DeviceFeatureActuatorSerialized) -> Self { - Self { - step_range: value.step_range.clone(), - step_limit: value.step_limit.unwrap_or(value.step_range), - messages: value.messages, - } - } -} - -impl DeviceFeatureActuator { - pub fn new( - step_range: &RangeInclusive, - step_limit: &RangeInclusive, - messages: &HashSet, - ) -> Self { - Self { - step_range: step_range.clone(), - step_limit: step_limit.clone(), - messages: messages.clone(), - } - } - - pub fn is_valid(&self) -> Result<(), ButtplugDeviceError> { - if self.step_range.is_empty() || self.step_range.start() > self.step_range.end() { - Err(ButtplugDeviceError::DeviceConfigurationError(format!( - "Step range out of order, must be start <= x <= end." - ))) - } else if self.step_limit.is_empty() || self.step_limit.start() > self.step_limit.end() { - Err(ButtplugDeviceError::DeviceConfigurationError(format!( - "Step limit out of order, must be start <= x <= end." - ))) - } else { - Ok(()) - } - } -} - -#[derive( - Clone, Debug, Default, PartialEq, Eq, Getters, MutGetters, Setters, Serialize, Deserialize, -)] -pub struct DeviceFeatureSensor { - #[getset(get = "pub", get_mut = "pub(super)")] - #[serde(rename = "value-range")] - #[serde(serialize_with = "range_sequence_serialize")] - value_range: Vec>, - #[getset(get = "pub")] - #[serde(rename = "messages")] - messages: HashSet, -} - -impl DeviceFeatureSensor { - pub fn new( - value_range: &Vec>, - messages: &HashSet, - ) -> Self { - Self { - value_range: value_range.clone(), - messages: messages.clone(), - } - } -} - -#[derive( - Clone, Debug, Default, PartialEq, Eq, Getters, MutGetters, Setters, Serialize, Deserialize, -)] -pub struct DeviceFeatureRaw { - #[getset(get = "pub")] - #[serde(rename = "Endpoints")] - endpoints: Vec, - #[getset(get = "pub")] - #[serde(rename = "Messages")] - messages: HashSet, -} - -impl DeviceFeatureRaw { - pub fn new(endpoints: &[Endpoint]) -> Self { - Self { - endpoints: endpoints.into(), - messages: HashSet::from_iter( - [ - ButtplugDeviceMessageType::RawReadCmd, - ButtplugDeviceMessageType::RawWriteCmd, - ButtplugDeviceMessageType::RawSubscribeCmd, - ButtplugDeviceMessageType::RawUnsubscribeCmd, - ] - .iter() - .cloned(), - ), - } - } -} diff --git a/buttplug/src/core/message/device_list.rs b/buttplug/src/core/message/device_list.rs deleted file mode 100644 index 655c899e6..000000000 --- a/buttplug/src/core/message/device_list.rs +++ /dev/null @@ -1,178 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 super::device_message_info::{DeviceMessageInfoV0, DeviceMessageInfoV1, DeviceMessageInfoV2}; -use super::*; -use device_message_info::DeviceMessageInfoV4; -use getset::Getters; -#[cfg(feature = "serialize-json")] -use serde::{Deserialize, Serialize}; - -/// List of all devices currently connected to the server. -#[derive(Default, Clone, Debug, PartialEq, Eq, ButtplugMessage, Getters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct DeviceListV4 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "Devices"))] - #[getset(get = "pub")] - devices: Vec, -} - -impl DeviceListV4 { - pub fn new(devices: Vec) -> Self { - Self { id: 1, devices } - } -} - -impl ButtplugMessageValidator for DeviceListV4 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_not_system_id(self.id) - } -} - -impl ButtplugMessageFinalizer for DeviceListV4 { - fn finalize(&mut self) { - } -} - -impl From for DeviceListV3 { - fn from(value: DeviceListV4) -> Self { - let mut dl3 = DeviceListV3::new(value.devices().iter().map(|x| x.clone().into()).collect()); - dl3.set_id(value.id); - dl3 - } -} - -/// List of all devices currently connected to the server. -#[derive(Default, Clone, Debug, PartialEq, Eq, ButtplugMessage, Getters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct DeviceListV3 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "Devices"))] - #[getset(get = "pub")] - devices: Vec, -} - -impl DeviceListV3 { - pub fn new(devices: Vec) -> Self { - Self { id: 1, devices } - } -} - -impl ButtplugMessageValidator for DeviceListV3 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_not_system_id(self.id) - } -} - -impl ButtplugMessageFinalizer for DeviceListV3 { - fn finalize(&mut self) { - for device in &mut self.devices { - device.device_messages_mut().finalize(); - } - } -} - -#[derive(Default, Clone, Debug, PartialEq, Eq, ButtplugMessage, Getters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct DeviceListV2 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "Devices"))] - #[getset(get = "pub")] - devices: Vec, -} - -impl From for DeviceListV2 { - fn from(msg: DeviceListV3) -> Self { - let mut devices = vec![]; - for d in msg.devices { - devices.push(DeviceMessageInfoV2::from(d)); - } - Self { - id: msg.id, - devices, - } - } -} - -impl ButtplugMessageValidator for DeviceListV2 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_not_system_id(self.id) - } -} - -impl ButtplugMessageFinalizer for DeviceListV2 { -} - -#[derive(Default, Clone, Debug, PartialEq, Eq, ButtplugMessage, Getters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct DeviceListV1 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "Devices"))] - #[getset(get = "pub")] - devices: Vec, -} - -impl From for DeviceListV1 { - fn from(msg: DeviceListV2) -> Self { - let mut devices = vec![]; - for d in msg.devices { - let dmiv2 = DeviceMessageInfoV2::from(d); - devices.push(DeviceMessageInfoV1::from(dmiv2)); - } - Self { - id: msg.id, - devices, - } - } -} - -impl ButtplugMessageValidator for DeviceListV1 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_not_system_id(self.id) - } -} - -impl ButtplugMessageFinalizer for DeviceListV1 { -} - -#[derive(Default, Clone, Debug, PartialEq, Eq, ButtplugMessage, Getters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct DeviceListV0 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "Devices"))] - #[getset(get = "pub")] - devices: Vec, -} - -impl From for DeviceListV0 { - fn from(msg: DeviceListV1) -> Self { - let mut devices = vec![]; - for d in msg.devices { - let dmiv1 = DeviceMessageInfoV1::from(d); - devices.push(DeviceMessageInfoV0::from(dmiv1)); - } - Self { - id: msg.id, - devices, - } - } -} - -impl ButtplugMessageValidator for DeviceListV0 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_not_system_id(self.id) - } -} - -impl ButtplugMessageFinalizer for DeviceListV0 { -} diff --git a/buttplug/src/core/message/device_message_info.rs b/buttplug/src/core/message/device_message_info.rs deleted file mode 100644 index 15a75c55b..000000000 --- a/buttplug/src/core/message/device_message_info.rs +++ /dev/null @@ -1,285 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 super::*; -use device_added::DeviceAddedV4; -use getset::{CopyGetters, Getters, MutGetters}; -#[cfg(feature = "serialize-json")] -use serde::{Deserialize, Serialize}; - -/// Substructure of device messages, used for attribute information (name, messages supported, etc...) -#[derive(Clone, Debug, PartialEq, Eq, MutGetters, Getters, CopyGetters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct DeviceMessageInfoV4 { - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] - #[getset(get_copy = "pub")] - device_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceName"))] - #[getset(get = "pub")] - device_name: String, - #[cfg_attr( - feature = "serialize-json", - serde(rename = "DeviceDisplayName", skip_serializing_if = "Option::is_none") - )] - #[getset(get = "pub")] - device_display_name: Option, - #[cfg_attr( - feature = "serialize-json", - serde( - rename = "DeviceMessageTimingGap", - skip_serializing_if = "Option::is_none" - ) - )] - #[getset(get = "pub")] - device_message_timing_gap: Option, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceFeatures"))] - #[getset(get = "pub", get_mut = "pub(super)")] - device_features: Vec, -} - -impl DeviceMessageInfoV4 { - pub fn new( - device_index: u32, - device_name: &str, - device_display_name: &Option, - device_message_timing_gap: &Option, - device_features: Vec, - ) -> Self { - Self { - device_index, - device_name: device_name.to_owned(), - device_display_name: device_display_name.clone(), - device_message_timing_gap: *device_message_timing_gap, - device_features, - } - } -} - -impl From for DeviceMessageInfoV4 { - fn from(device_added: DeviceAddedV4) -> Self { - Self { - device_index: device_added.device_index(), - device_name: device_added.device_name().clone(), - device_display_name: device_added.device_display_name().clone(), - device_message_timing_gap: *device_added.device_message_timing_gap(), - device_features: device_added.device_features().clone(), - } - } -} - -impl From for DeviceMessageInfoV3 { - fn from(value: DeviceMessageInfoV4) -> Self { - DeviceMessageInfoV3::new( - value.device_index(), - &value.device_name(), - &value.device_display_name(), - &None, - value.device_features().clone().into(), - ) - } -} - -/// Substructure of device messages, used for attribute information (name, messages supported, etc...) -#[derive(Clone, Debug, PartialEq, Eq, MutGetters, Getters, CopyGetters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct DeviceMessageInfoV3 { - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] - #[getset(get_copy = "pub")] - device_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceName"))] - #[getset(get = "pub")] - device_name: String, - #[cfg_attr( - feature = "serialize-json", - serde(rename = "DeviceDisplayName", skip_serializing_if = "Option::is_none") - )] - #[getset(get = "pub")] - device_display_name: Option, - #[cfg_attr( - feature = "serialize-json", - serde( - rename = "DeviceMessageTimingGap", - skip_serializing_if = "Option::is_none" - ) - )] - #[getset(get = "pub")] - device_message_timing_gap: Option, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceMessages"))] - #[getset(get = "pub", get_mut = "pub(super)")] - device_messages: ClientDeviceMessageAttributesV3, -} - -impl DeviceMessageInfoV3 { - pub fn new( - device_index: u32, - device_name: &str, - device_display_name: &Option, - device_message_timing_gap: &Option, - device_messages: ClientDeviceMessageAttributesV3, - ) -> Self { - Self { - device_index, - device_name: device_name.to_owned(), - device_display_name: device_display_name.clone(), - device_message_timing_gap: *device_message_timing_gap, - device_messages, - } - } -} - -impl From for DeviceMessageInfoV3 { - fn from(device_added: DeviceAddedV3) -> Self { - Self { - device_index: device_added.device_index(), - device_name: device_added.device_name().clone(), - device_display_name: device_added.device_display_name().clone(), - device_message_timing_gap: *device_added.device_message_timing_gap(), - device_messages: device_added.device_messages().clone(), - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Getters, CopyGetters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct DeviceMessageInfoV2 { - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] - #[getset(get_copy = "pub")] - device_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceName"))] - #[getset(get = "pub")] - device_name: String, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceMessages"))] - #[getset(get = "pub")] - device_messages: ClientDeviceMessageAttributesV2, -} - -impl From for DeviceMessageInfoV2 { - fn from(device_added: DeviceAddedV3) -> Self { - let dmi = DeviceMessageInfoV3::from(device_added); - DeviceMessageInfoV2::from(dmi) - } -} - -impl From for DeviceMessageInfoV2 { - fn from(device_added: DeviceAddedV2) -> Self { - // No structural difference, it's all content changes - Self { - device_index: device_added.device_index(), - device_name: device_added.device_name().clone(), - device_messages: device_added.device_messages().clone(), - } - } -} - -impl From for DeviceMessageInfoV2 { - fn from(device_message_info: DeviceMessageInfoV3) -> Self { - // No structural difference, it's all content changes - Self { - device_index: device_message_info.device_index, - device_name: device_message_info.device_name, - device_messages: device_message_info.device_messages.into(), - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Getters, CopyGetters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct DeviceMessageInfoV1 { - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] - #[getset(get_copy = "pub")] - device_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceName"))] - #[getset(get = "pub")] - device_name: String, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceMessages"))] - #[getset(get = "pub")] - device_messages: ClientDeviceMessageAttributesV1, -} - -impl From for DeviceMessageInfoV1 { - fn from(device_added: DeviceAddedV1) -> Self { - Self { - device_index: device_added.device_index(), - device_name: device_added.device_name().clone(), - device_messages: device_added.device_messages().clone(), - } - } -} - -impl From for DeviceMessageInfoV1 { - fn from(device_message_info: DeviceMessageInfoV2) -> Self { - // No structural difference, it's all content changes - Self { - device_index: device_message_info.device_index, - device_name: device_message_info.device_name, - device_messages: device_message_info.device_messages.into(), - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Getters, CopyGetters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct DeviceMessageInfoV0 { - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] - #[getset(get_copy = "pub")] - device_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceName"))] - #[getset(get = "pub")] - device_name: String, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceMessages"))] - #[getset(get = "pub")] - device_messages: Vec, -} - -impl From for DeviceMessageInfoV0 { - fn from(device_added: DeviceAddedV3) -> Self { - let dmi = DeviceMessageInfoV3::from(device_added); - let dmi_v2: DeviceMessageInfoV2 = dmi.into(); - let dmi_v1: DeviceMessageInfoV1 = dmi_v2.into(); - dmi_v1.into() - } -} - -impl From for DeviceMessageInfoV0 { - fn from(device_message_info: DeviceMessageInfoV1) -> Self { - // Convert to array of message types. - let mut device_messages: Vec = vec![]; - - device_messages.push(ButtplugDeviceMessageType::StopDeviceCmd); - if device_message_info - .device_messages - .single_motor_vibrate_cmd() - .is_some() - { - device_messages.push(ButtplugDeviceMessageType::SingleMotorVibrateCmd); - } - if device_message_info - .device_messages - .fleshlight_launch_fw12_cmd() - .is_some() - { - device_messages.push(ButtplugDeviceMessageType::FleshlightLaunchFW12Cmd); - } - if device_message_info - .device_messages - .vorze_a10_cyclone_cmd() - .is_some() - { - device_messages.push(ButtplugDeviceMessageType::VorzeA10CycloneCmd); - } - - device_messages.sort(); - - // SingleMotorVibrateCmd is added as part of the V1 conversion, so we - // can expect we'll have it here. - Self { - device_name: device_message_info.device_name, - device_index: device_message_info.device_index, - device_messages, - } - } -} diff --git a/buttplug/src/core/message/kiiroo_cmd.rs b/buttplug/src/core/message/kiiroo_cmd.rs deleted file mode 100644 index 484b25d06..000000000 --- a/buttplug/src/core/message/kiiroo_cmd.rs +++ /dev/null @@ -1,40 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 super::*; -use getset::Getters; -#[cfg(feature = "serialize-json")] -use serde::{Deserialize, Serialize}; - -/// Kiiroo Command (Version 0 Message, Deprecated in spec) -#[derive(Debug, ButtplugDeviceMessage, ButtplugMessageFinalizer, PartialEq, Eq, Clone, Getters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct KiirooCmdV0 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] - device_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "Command"))] - #[getset(get = "pub")] - command: String, -} - -impl KiirooCmdV0 { - pub fn new(device_index: u32, command: &str) -> Self { - Self { - id: 1, - device_index, - command: command.to_owned(), - } - } -} - -impl ButtplugMessageValidator for KiirooCmdV0 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_not_system_id(self.id) - } -} diff --git a/buttplug/src/core/message/linear_cmd.rs b/buttplug/src/core/message/linear_cmd.rs deleted file mode 100644 index e74f18b5a..000000000 --- a/buttplug/src/core/message/linear_cmd.rs +++ /dev/null @@ -1,133 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 super::*; -use getset::{CopyGetters, Getters}; -#[cfg(feature = "serialize-json")] -use serde::{Deserialize, Serialize}; - -/// Move device to a certain position in a certain amount of time -#[derive(Debug, PartialEq, Clone, CopyGetters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -#[getset(get_copy = "pub")] -pub struct VectorSubcommandV4 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Index"))] - feature_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "Duration"))] - duration: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "Position"))] - position: f64, -} - -impl VectorSubcommandV4 { - pub fn new(feature_index: u32, duration: u32, position: f64) -> Self { - Self { - feature_index, - duration, - position, - } - } -} - -#[derive(Debug, ButtplugDeviceMessage, ButtplugMessageFinalizer, PartialEq, Clone, Getters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct LinearCmdV4 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] - device_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "Vectors"))] - #[getset(get = "pub")] - vectors: Vec, -} - -impl LinearCmdV4 { - pub fn new(device_index: u32, vectors: Vec) -> Self { - Self { - id: 1, - device_index, - vectors, - } - } -} - -impl ButtplugMessageValidator for LinearCmdV4 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_not_system_id(self.id)?; - for vec in &self.vectors { - self.is_in_command_range( - vec.position, - format!( - "VectorSubcommand position {} for index {} is invalid, should be between 0.0 and 1.0", - vec.position, vec.feature_index - ), - )?; - } - Ok(()) - } -} - -/// Move device to a certain position in a certain amount of time -#[derive(Debug, PartialEq, Clone, CopyGetters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -#[getset(get_copy = "pub")] -pub struct VectorSubcommandV1 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Index"))] - index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "Duration"))] - duration: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "Position"))] - position: f64, -} - -impl VectorSubcommandV1 { - pub fn new(index: u32, duration: u32, position: f64) -> Self { - Self { - index, - duration, - position, - } - } -} - -#[derive(Debug, ButtplugDeviceMessage, ButtplugMessageFinalizer, PartialEq, Clone, Getters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct LinearCmdV1 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] - device_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "Vectors"))] - #[getset(get = "pub")] - vectors: Vec, -} - -impl LinearCmdV1 { - pub fn new(device_index: u32, vectors: Vec) -> Self { - Self { - id: 1, - device_index, - vectors, - } - } -} - -impl ButtplugMessageValidator for LinearCmdV1 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_not_system_id(self.id)?; - for vec in &self.vectors { - self.is_in_command_range( - vec.position, - format!( - "VectorSubcommand position {} for index {} is invalid, should be between 0.0 and 1.0", - vec.position, vec.index - ), - )?; - } - Ok(()) - } -} diff --git a/buttplug/src/core/message/log.rs b/buttplug/src/core/message/log.rs deleted file mode 100644 index c8b9dc51a..000000000 --- a/buttplug/src/core/message/log.rs +++ /dev/null @@ -1,43 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 super::*; -use getset::{CopyGetters, Getters}; -#[cfg(feature = "serialize-json")] -use serde::{Deserialize, Serialize}; - -/// Log message received from server (Version 1 Message, Deprecated) -#[derive( - Debug, ButtplugMessage, ButtplugMessageFinalizer, PartialEq, Eq, Clone, Getters, CopyGetters, -)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct LogV0 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "LogLevel"))] - #[getset(get_copy = "pub")] - log_level: LogLevel, - #[cfg_attr(feature = "serialize-json", serde(rename = "LogMessage"))] - #[getset(get = "pub")] - log_message: String, -} - -impl LogV0 { - pub fn new(log_level: LogLevel, log_message: &str) -> Self { - Self { - id: 0, - log_level, - log_message: log_message.to_owned(), - } - } -} - -impl ButtplugMessageValidator for LogV0 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_system_id(self.id) - } -} diff --git a/buttplug/src/core/message/log_level.rs b/buttplug/src/core/message/log_level.rs deleted file mode 100644 index 37e48521d..000000000 --- a/buttplug/src/core/message/log_level.rs +++ /dev/null @@ -1,55 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 Nonpolynomial Labs LLC. All rights reserved. -// -// Licensed under the BSD 3-Clause license. See LICENSE file in the project root -// for full license information. - -#[cfg(feature = "serialize-json")] -use serde::{Deserialize, Serialize}; -use std::cmp::Ord; -use tracing::Level; - -/// Log Levels (Version 1 Message, Deprecated) -#[derive(Debug, PartialEq, Clone, Ord, PartialOrd, Eq, Copy)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub enum LogLevel { - Off = 0, - Fatal, - Error, - Warn, - Info, - Debug, - Trace, -} - -impl From for LogLevel { - fn from(level: Level) -> Self { - match level { - Level::ERROR => LogLevel::Error, - Level::WARN => LogLevel::Warn, - Level::INFO => LogLevel::Info, - Level::DEBUG => LogLevel::Debug, - Level::TRACE => LogLevel::Trace, - } - } -} - -impl From for Level { - fn from(level: LogLevel) -> Level { - match level { - // Rust doesn't have a Fatal level, and we never use it in code, so - // just convert to Error. - LogLevel::Fatal => Level::ERROR, - LogLevel::Error => Level::ERROR, - LogLevel::Warn => Level::WARN, - LogLevel::Info => Level::INFO, - LogLevel::Debug => Level::DEBUG, - LogLevel::Trace => Level::TRACE, - LogLevel::Off => { - error!("Log messages with a log level of Off are not allowed"); - Level::ERROR - } - } - } -} diff --git a/buttplug/src/core/message/lovense_cmd.rs b/buttplug/src/core/message/lovense_cmd.rs deleted file mode 100644 index 1d8ebab23..000000000 --- a/buttplug/src/core/message/lovense_cmd.rs +++ /dev/null @@ -1,43 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 super::*; -use getset::Getters; -#[cfg(feature = "serialize-json")] -use serde::{Deserialize, Serialize}; - -/// Lovense specific commands (Version 0 Message, **Deprecated**) -// As this message is considered deprecated and is not actually implemented for -// Lovense devices even on spec v1 connections, we can put a null validator on -// it. -#[derive(Debug, ButtplugDeviceMessage, ButtplugMessageFinalizer, PartialEq, Eq, Clone, Getters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct LovenseCmdV0 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] - device_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "Command"))] - #[getset(get = "pub")] - command: String, -} - -impl LovenseCmdV0 { - pub fn new(device_index: u32, command: &str) -> Self { - Self { - id: 1, - device_index, - command: command.to_owned(), - } - } -} - -impl ButtplugMessageValidator for LovenseCmdV0 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_not_system_id(self.id) - } -} diff --git a/buttplug/src/core/message/mod.rs b/buttplug/src/core/message/mod.rs deleted file mode 100644 index 45f65f53e..000000000 --- a/buttplug/src/core/message/mod.rs +++ /dev/null @@ -1,942 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 Nonpolynomial Labs LLC. All rights reserved. -// -// Licensed under the BSD 3-Clause license. See LICENSE file in the project root -// for full license information. - -//! Representations of low level [Buttplug Protocol](https://buttplug-spec.docs.buttplug.io) -//! messages -//! -//! The core communication types for the Buttplug protocol. There are structs for each message type, -//! sometimes with multiple versions of the same message relating to different spec versions. There -//! are also enum types that are used to classify messages into categories, for instance, messages -//! that only should be sent by a client or server. - -mod battery_level_cmd; -mod battery_level_reading; -mod client_device_message_attributes; -mod device_added; -mod device_feature; -mod device_list; -mod device_message_info; -mod device_removed; -mod endpoint; -mod error; -mod fleshlight_launch_fw12_cmd; -mod kiiroo_cmd; -mod linear_cmd; -mod log; -mod log_level; -mod lovense_cmd; -mod ok; -mod ping; -mod raw_read_cmd; -mod raw_reading; -mod raw_subscribe_cmd; -mod raw_unsubscribe_cmd; -mod raw_write_cmd; -mod request_device_list; -mod request_log; -mod request_server_info; -mod rotate_cmd; -mod rssi_level_cmd; -mod rssi_level_reading; -mod scalar_cmd; -mod scanning_finished; -mod sensor_read_cmd; -mod sensor_reading; -mod sensor_subscribe_cmd; -mod sensor_unsubscribe_cmd; -pub mod serializer; -mod server_info; -mod single_motor_vibrate_cmd; -mod start_scanning; -mod stop_all_devices; -mod stop_device_cmd; -mod stop_scanning; -mod test; -mod vibrate_cmd; -mod vorze_a10_cyclone_cmd; - -pub use self::log::LogV0; -pub use battery_level_cmd::BatteryLevelCmdV2; -pub use battery_level_reading::BatteryLevelReadingV2; -pub use client_device_message_attributes::{ - ActuatorType, - ClientDeviceMessageAttributesV1, - ClientDeviceMessageAttributesV2, - ClientDeviceMessageAttributesV3, - ClientDeviceMessageAttributesV3Builder, - ClientGenericDeviceMessageAttributesV3, - NullDeviceMessageAttributesV1, - RawDeviceMessageAttributesV2, - SensorDeviceMessageAttributesV3, - SensorType, -}; -pub use device_added::{DeviceAddedV0, DeviceAddedV1, DeviceAddedV2, DeviceAddedV3, DeviceAddedV4}; -pub use device_feature::{ - DeviceFeature, - DeviceFeatureActuator, - DeviceFeatureRaw, - DeviceFeatureSensor, - FeatureType, -}; -pub use device_list::{DeviceListV0, DeviceListV1, DeviceListV2, DeviceListV3, DeviceListV4}; -pub use device_message_info::{ - DeviceMessageInfoV0, - DeviceMessageInfoV1, - DeviceMessageInfoV2, - DeviceMessageInfoV3, - DeviceMessageInfoV4, -}; -pub use device_removed::DeviceRemovedV0; -pub use endpoint::Endpoint; -pub use error::{ErrorCode, ErrorV0}; -pub use fleshlight_launch_fw12_cmd::FleshlightLaunchFW12CmdV0; -pub use kiiroo_cmd::KiirooCmdV0; -pub use linear_cmd::{LinearCmdV1, LinearCmdV4, VectorSubcommandV1, VectorSubcommandV4}; -pub use log_level::LogLevel; -pub use lovense_cmd::LovenseCmdV0; -pub use ok::OkV0; -pub use ping::PingV0; -pub use raw_read_cmd::RawReadCmdV2; -pub use raw_reading::RawReadingV2; -pub use raw_subscribe_cmd::RawSubscribeCmdV2; -pub use raw_unsubscribe_cmd::RawUnsubscribeCmdV2; -pub use raw_write_cmd::RawWriteCmdV2; -pub use request_device_list::RequestDeviceListV0; -pub use request_log::RequestLogV0; -pub use request_server_info::RequestServerInfoV1; -pub use rotate_cmd::{RotateCmdV1, RotateCmdV4, RotationSubcommandV1, RotationSubcommandV4}; -pub use rssi_level_cmd::RSSILevelCmdV2; -pub use rssi_level_reading::RSSILevelReadingV2; -pub use scalar_cmd::{ScalarCmdV3, ScalarCmdV4, ScalarSubcommandV3, ScalarSubcommandV4}; -pub use scanning_finished::ScanningFinishedV0; -pub use sensor_read_cmd::{SensorReadCmdV3, SensorReadCmdV4}; -pub use sensor_reading::{SensorReadingV3, SensorReadingV4}; -pub use sensor_subscribe_cmd::{SensorSubscribeCmdV3, SensorSubscribeCmdV4}; -pub use sensor_unsubscribe_cmd::{SensorUnsubscribeCmdV3, SensorUnsubscribeCmdV4}; -pub use server_info::{ServerInfoV0, ServerInfoV2}; -pub use single_motor_vibrate_cmd::SingleMotorVibrateCmdV0; -pub use start_scanning::StartScanningV0; -pub use stop_all_devices::StopAllDevicesV0; -pub use stop_device_cmd::StopDeviceCmdV0; -pub use stop_scanning::StopScanningV0; -pub use test::TestV0; -pub use vibrate_cmd::{VibrateCmdV1, VibrateSubcommandV1}; -pub use vorze_a10_cyclone_cmd::VorzeA10CycloneCmdV0; - -use crate::core::errors::ButtplugMessageError; -use serde::{Deserialize, Serialize}; -#[cfg(feature = "serialize-json")] -use serde_repr::{Deserialize_repr, Serialize_repr}; -use std::cmp::Ordering; -use std::convert::TryFrom; - -use super::errors::ButtplugError; - -/// Enum of possible [Buttplug Message -/// Spec](https://buttplug-spec.docs.buttplug.io) versions. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Display)] -#[repr(u32)] -#[cfg_attr(feature = "serialize-json", derive(Serialize_repr, Deserialize_repr))] -pub enum ButtplugMessageSpecVersion { - Version0 = 0, - Version1 = 1, - Version2 = 2, - Version3 = 3, - Version4 = 4, -} - -impl TryFrom for ButtplugMessageSpecVersion { - type Error = ButtplugError; - - // There's probably another crate to make this easier but eh. - fn try_from(value: i32) -> Result { - match value { - 0 => Ok(ButtplugMessageSpecVersion::Version0), - 1 => Ok(ButtplugMessageSpecVersion::Version1), - 2 => Ok(ButtplugMessageSpecVersion::Version2), - 3 => Ok(ButtplugMessageSpecVersion::Version3), - 4 => Ok(ButtplugMessageSpecVersion::Version4), - _ => Err( - ButtplugMessageError::InvalidMessageContents(format!( - "Message spec version {} is not valid", - value - )) - .into(), - ), - } - } -} - -/// Message Id for events sent from the server, which are not in response to a -/// client request. -pub const BUTTPLUG_SERVER_EVENT_ID: u32 = 0; - -/// The current latest version of the spec implemented by the library. -pub const BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION: ButtplugMessageSpecVersion = - ButtplugMessageSpecVersion::Version3; - -pub trait ButtplugMessageFinalizer { - fn finalize(&mut self) { - } -} - -/// Base trait for all Buttplug Protocol Message Structs. Handles management of -/// message ids, as well as implementing conveinence functions for converting -/// between message structs and various message enums, serialization, etc... -pub trait ButtplugMessage: - ButtplugMessageValidator + ButtplugMessageFinalizer + Send + Sync + Clone -{ - /// Returns the id number of the message - fn id(&self) -> u32; - /// Sets the id number of the message. - fn set_id(&mut self, id: u32); - /// True if the message is an event (message id of 0) from the server. - fn is_server_event(&self) -> bool { - self.id() == BUTTPLUG_SERVER_EVENT_ID - } -} - -/// Validation function for message contents. Can be run before message is -/// transmitted, as message may be formed and mutated at multiple points in the -/// library, or may need to be checked after deserialization. Message enums will -/// run this on whatever their variant is. -pub trait ButtplugMessageValidator { - /// Returns () if the message is valid, otherwise returns a message error. - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - // By default, return Ok, as many messages won't have any checks. - Ok(()) - } - - fn is_system_id(&self, id: u32) -> Result<(), ButtplugMessageError> { - if id == 0 { - Ok(()) - } else { - Err(ButtplugMessageError::InvalidMessageContents( - "Message should have id of 0, as it is a system message.".to_string(), - )) - } - } - - fn is_not_system_id(&self, id: u32) -> Result<(), ButtplugMessageError> { - if id == 0 { - Err(ButtplugMessageError::InvalidMessageContents( - "Message should not have 0 for an Id. Id of 0 is reserved for system messages.".to_string(), - )) - } else { - Ok(()) - } - } - - fn is_in_command_range(&self, value: f64, error_msg: String) -> Result<(), ButtplugMessageError> { - if !(0.0..=1.0).contains(&value) { - Err(ButtplugMessageError::InvalidMessageContents(error_msg)) - } else { - Ok(()) - } - } -} - -/// Adds device index handling to the [ButtplugMessage] trait. -pub trait ButtplugDeviceMessage: ButtplugMessage { - fn device_index(&self) -> u32; - fn set_device_index(&mut self, id: u32); -} - -/// Used in [MessageAttributes][crate::core::messages::DeviceMessageAttributes] for denoting message -/// capabilties. -#[derive(Copy, Debug, Clone, PartialEq, Eq, Hash, Display, Serialize, Deserialize)] -pub enum ButtplugDeviceMessageType { - VibrateCmd, - LinearCmd, - RotateCmd, - StopDeviceCmd, - RawWriteCmd, - RawReadCmd, - RawSubscribeCmd, - RawUnsubscribeCmd, - BatteryLevelCmd, - RSSILevelCmd, - ScalarCmd, - SensorReadCmd, - SensorSubscribeCmd, - SensorUnsubscribeCmd, - // Deprecated generic commands - SingleMotorVibrateCmd, - // Deprecated device specific commands - FleshlightLaunchFW12Cmd, - LovenseCmd, - KiirooCmd, - VorzeA10CycloneCmd, -} - -// Ordering for ButtplugDeviceMessageType should be lexicographic, for -// serialization reasons. -impl PartialOrd for ButtplugDeviceMessageType { - fn partial_cmp(&self, other: &ButtplugDeviceMessageType) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for ButtplugDeviceMessageType { - fn cmp(&self, other: &ButtplugDeviceMessageType) -> Ordering { - self.to_string().cmp(&other.to_string()) - } -} - -#[derive(Copy, Debug, Clone, Hash, Display, PartialEq, Eq, Serialize, Deserialize)] -pub enum ButtplugActuatorFeatureMessageType { - ScalarCmd, - RotateCmd, - LinearCmd, -} - -impl From for ButtplugDeviceMessageType { - fn from(value: ButtplugActuatorFeatureMessageType) -> Self { - match value { - ButtplugActuatorFeatureMessageType::LinearCmd => ButtplugDeviceMessageType::LinearCmd, - ButtplugActuatorFeatureMessageType::RotateCmd => ButtplugDeviceMessageType::RotateCmd, - ButtplugActuatorFeatureMessageType::ScalarCmd => ButtplugDeviceMessageType::ScalarCmd, - } - } -} - -impl TryFrom for ButtplugActuatorFeatureMessageType { - type Error = (); - - fn try_from(value: ButtplugDeviceMessageType) -> Result { - match value { - ButtplugDeviceMessageType::LinearCmd => Ok(ButtplugActuatorFeatureMessageType::LinearCmd), - ButtplugDeviceMessageType::RotateCmd => Ok(ButtplugActuatorFeatureMessageType::RotateCmd), - ButtplugDeviceMessageType::ScalarCmd => Ok(ButtplugActuatorFeatureMessageType::ScalarCmd), - _ => Err(()), - } - } -} - -#[derive(Copy, Debug, Clone, Hash, Display, PartialEq, Eq, Serialize, Deserialize)] -pub enum ButtplugSensorFeatureMessageType { - SensorReadCmd, - SensorSubscribeCmd, -} - -impl From for ButtplugDeviceMessageType { - fn from(value: ButtplugSensorFeatureMessageType) -> Self { - match value { - ButtplugSensorFeatureMessageType::SensorReadCmd => ButtplugDeviceMessageType::SensorReadCmd, - ButtplugSensorFeatureMessageType::SensorSubscribeCmd => { - ButtplugDeviceMessageType::SensorSubscribeCmd - } - } - } -} - -impl TryFrom for ButtplugSensorFeatureMessageType { - type Error = (); - - fn try_from(value: ButtplugDeviceMessageType) -> Result { - match value { - ButtplugDeviceMessageType::SensorReadCmd => { - Ok(ButtplugSensorFeatureMessageType::SensorReadCmd) - } - ButtplugDeviceMessageType::SensorSubscribeCmd => { - Ok(ButtplugSensorFeatureMessageType::SensorSubscribeCmd) - } - _ => Err(()), - } - } -} - -#[derive(Copy, Debug, Clone, Hash, Display, PartialEq, Eq, Serialize, Deserialize)] -pub enum ButtplugRawFeatureMessageType { - RawReadCmd, - RawWriteCmd, - RawSubscribeCmd, -} - -impl From for ButtplugDeviceMessageType { - fn from(value: ButtplugRawFeatureMessageType) -> Self { - match value { - ButtplugRawFeatureMessageType::RawReadCmd => ButtplugDeviceMessageType::RawReadCmd, - ButtplugRawFeatureMessageType::RawWriteCmd => ButtplugDeviceMessageType::RawWriteCmd, - ButtplugRawFeatureMessageType::RawSubscribeCmd => ButtplugDeviceMessageType::RawSubscribeCmd, - } - } -} - -impl TryFrom for ButtplugRawFeatureMessageType { - type Error = (); - - fn try_from(value: ButtplugDeviceMessageType) -> Result { - match value { - ButtplugDeviceMessageType::RawReadCmd => Ok(ButtplugRawFeatureMessageType::RawReadCmd), - ButtplugDeviceMessageType::RawWriteCmd => Ok(ButtplugRawFeatureMessageType::RawWriteCmd), - ButtplugDeviceMessageType::RawSubscribeCmd => { - Ok(ButtplugRawFeatureMessageType::RawSubscribeCmd) - } - _ => Err(()), - } - } -} - -#[derive( - Debug, Clone, PartialEq, ButtplugMessage, ButtplugMessageFinalizer, ButtplugMessageValidator, -)] -pub enum ButtplugClientMessageVariant { - V0(ButtplugClientMessageV0), - V1(ButtplugClientMessageV1), - V2(ButtplugClientMessageV2), - V3(ButtplugClientMessageV3), - V4(ButtplugClientMessageV4), -} - -impl ButtplugClientMessageVariant { - pub fn version(&self) -> ButtplugMessageSpecVersion { - match self { - Self::V0(_) => ButtplugMessageSpecVersion::Version0, - Self::V1(_) => ButtplugMessageSpecVersion::Version1, - Self::V2(_) => ButtplugMessageSpecVersion::Version2, - Self::V3(_) => ButtplugMessageSpecVersion::Version3, - Self::V4(_) => ButtplugMessageSpecVersion::Version4, - } - } -} - -impl From for ButtplugClientMessageVariant { - fn from(value: ButtplugClientMessageV0) -> Self { - ButtplugClientMessageVariant::V0(value) - } -} - -impl From for ButtplugClientMessageVariant { - fn from(value: ButtplugClientMessageV1) -> Self { - ButtplugClientMessageVariant::V1(value) - } -} - -impl From for ButtplugClientMessageVariant { - fn from(value: ButtplugClientMessageV2) -> Self { - ButtplugClientMessageVariant::V2(value) - } -} - -impl From for ButtplugClientMessageVariant { - fn from(value: ButtplugClientMessageV3) -> Self { - ButtplugClientMessageVariant::V3(value) - } -} - -impl From for ButtplugClientMessageVariant { - fn from(value: ButtplugClientMessageV4) -> Self { - ButtplugClientMessageVariant::V4(value) - } -} - -#[derive( - Debug, Clone, PartialEq, ButtplugMessage, ButtplugMessageFinalizer, ButtplugMessageValidator, -)] -pub enum ButtplugServerMessageVariant { - V0(ButtplugServerMessageV0), - V1(ButtplugServerMessageV1), - V2(ButtplugServerMessageV2), - V3(ButtplugServerMessageV3), - V4(ButtplugServerMessageV4), -} - -impl ButtplugServerMessageVariant { - pub fn version(&self) -> ButtplugMessageSpecVersion { - match self { - Self::V0(_) => ButtplugMessageSpecVersion::Version0, - Self::V1(_) => ButtplugMessageSpecVersion::Version1, - Self::V2(_) => ButtplugMessageSpecVersion::Version2, - Self::V3(_) => ButtplugMessageSpecVersion::Version3, - Self::V4(_) => ButtplugMessageSpecVersion::Version4, - } - } -} - -impl From for ButtplugServerMessageVariant { - fn from(value: ButtplugServerMessageV0) -> Self { - ButtplugServerMessageVariant::V0(value) - } -} - -impl From for ButtplugServerMessageVariant { - fn from(value: ButtplugServerMessageV1) -> Self { - ButtplugServerMessageVariant::V1(value) - } -} - -impl From for ButtplugServerMessageVariant { - fn from(value: ButtplugServerMessageV2) -> Self { - ButtplugServerMessageVariant::V2(value) - } -} - -impl From for ButtplugServerMessageVariant { - fn from(value: ButtplugServerMessageV3) -> Self { - ButtplugServerMessageVariant::V3(value) - } -} - -impl From for ButtplugServerMessageVariant { - fn from(value: ButtplugServerMessageV4) -> Self { - ButtplugServerMessageVariant::V4(value) - } -} - -/// Represents all possible messages a [ButtplugServer][crate::server::ButtplugServer] can send to a -/// [ButtplugClient][crate::client::ButtplugClient] that denote an EVENT from a device. These are -/// only used in notifications, so read requests will not need to be added here, only messages that -/// will require Id of 0. -#[derive( - Debug, - Clone, - PartialEq, - Eq, - ButtplugMessage, - ButtplugMessageValidator, - ButtplugMessageFinalizer, - FromSpecificButtplugMessage, -)] -pub enum ButtplugServerDeviceMessage { - // Generic commands - RawReading(RawReadingV2), - // Generic Sensor Reading Messages - SensorReading(SensorReadingV4), -} - -impl From for ButtplugServerMessageV4 { - fn from(other: ButtplugServerDeviceMessage) -> Self { - match other { - ButtplugServerDeviceMessage::RawReading(msg) => ButtplugServerMessageV4::RawReading(msg), - ButtplugServerDeviceMessage::SensorReading(msg) => { - ButtplugServerMessageV4::SensorReading(msg) - } - } - } -} - -/// Type alias for the latest version of client-to-server messages. -pub type ButtplugClientMessageCurrent = ButtplugClientMessageV3; -/// Type alias for the latest version of server-to-client messages. -pub type ButtplugServerMessageCurrent = ButtplugServerMessageV3; - -/// Represents all client-to-server messages in v3 of the Buttplug Spec -#[derive( - Debug, - Clone, - PartialEq, - ButtplugMessage, - ButtplugMessageValidator, - ButtplugMessageFinalizer, - FromSpecificButtplugMessage, -)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub enum ButtplugClientMessageV4 { - // Handshake messages - RequestServerInfo(RequestServerInfoV1), - Ping(PingV0), - // Device enumeration messages - StartScanning(StartScanningV0), - StopScanning(StopScanningV0), - RequestDeviceList(RequestDeviceListV0), - // Generic commands - StopDeviceCmd(StopDeviceCmdV0), - StopAllDevices(StopAllDevicesV0), - ScalarCmd(ScalarCmdV4), - LinearCmd(LinearCmdV4), - RotateCmd(RotateCmdV4), - RawWriteCmd(RawWriteCmdV2), - RawReadCmd(RawReadCmdV2), - RawSubscribeCmd(RawSubscribeCmdV2), - RawUnsubscribeCmd(RawUnsubscribeCmdV2), - // Sensor commands - SensorReadCmd(SensorReadCmdV4), - SensorSubscribeCmd(SensorSubscribeCmdV4), - SensorUnsubscribeCmd(SensorUnsubscribeCmdV4), -} - -/// Represents all server-to-client messages in v3 of the Buttplug Spec -#[derive( - Debug, Clone, PartialEq, ButtplugMessage, ButtplugMessageValidator, FromSpecificButtplugMessage, -)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub enum ButtplugServerMessageV4 { - // Status messages - Ok(OkV0), - Error(ErrorV0), - // Handshake messages - ServerInfo(ServerInfoV2), - // Device enumeration messages - DeviceList(DeviceListV4), - DeviceAdded(DeviceAddedV4), - DeviceRemoved(DeviceRemovedV0), - ScanningFinished(ScanningFinishedV0), - // Generic commands - RawReading(RawReadingV2), - // Sensor commands - SensorReading(SensorReadingV4), -} - -impl ButtplugMessageFinalizer for ButtplugServerMessageV4 { - fn finalize(&mut self) { - match self { - ButtplugServerMessageV4::DeviceAdded(da) => da.finalize(), - ButtplugServerMessageV4::DeviceList(dl) => dl.finalize(), - _ => return, - } - } -} - -/// Represents all client-to-server messages in v3 of the Buttplug Spec -#[derive( - Debug, - Clone, - PartialEq, - ButtplugMessage, - ButtplugMessageValidator, - ButtplugMessageFinalizer, - FromSpecificButtplugMessage, -)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub enum ButtplugClientMessageV3 { - // Handshake messages - RequestServerInfo(RequestServerInfoV1), - Ping(PingV0), - // Device enumeration messages - StartScanning(StartScanningV0), - StopScanning(StopScanningV0), - RequestDeviceList(RequestDeviceListV0), - // Generic commands - StopAllDevices(StopAllDevicesV0), - VibrateCmd(VibrateCmdV1), - LinearCmd(LinearCmdV1), - RotateCmd(RotateCmdV1), - RawWriteCmd(RawWriteCmdV2), - RawReadCmd(RawReadCmdV2), - StopDeviceCmd(StopDeviceCmdV0), - RawSubscribeCmd(RawSubscribeCmdV2), - RawUnsubscribeCmd(RawUnsubscribeCmdV2), - ScalarCmd(ScalarCmdV3), - // Sensor commands - SensorReadCmd(SensorReadCmdV3), - SensorSubscribeCmd(SensorSubscribeCmdV3), - SensorUnsubscribeCmd(SensorUnsubscribeCmdV3), -} - -/// Represents all server-to-client messages in v3 of the Buttplug Spec -#[derive( - Debug, Clone, PartialEq, ButtplugMessage, ButtplugMessageValidator, FromSpecificButtplugMessage, -)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub enum ButtplugServerMessageV3 { - // Status messages - Ok(OkV0), - Error(ErrorV0), - // Handshake messages - ServerInfo(ServerInfoV2), - // Device enumeration messages - DeviceList(DeviceListV3), - DeviceAdded(DeviceAddedV3), - DeviceRemoved(DeviceRemovedV0), - ScanningFinished(ScanningFinishedV0), - // Generic commands - RawReading(RawReadingV2), - // Sensor commands - SensorReading(SensorReadingV3), -} - -impl ButtplugMessageFinalizer for ButtplugServerMessageV3 { - fn finalize(&mut self) { - match self { - ButtplugServerMessageV3::DeviceAdded(da) => da.finalize(), - ButtplugServerMessageV3::DeviceList(dl) => dl.finalize(), - _ => return, - } - } -} - -/// Represents all client-to-server messages in v2 of the Buttplug Spec -#[derive( - Debug, - Clone, - PartialEq, - ButtplugMessage, - ButtplugMessageValidator, - ButtplugMessageFinalizer, - FromSpecificButtplugMessage, -)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub enum ButtplugClientMessageV2 { - // Handshake messages - RequestServerInfo(RequestServerInfoV1), - Ping(PingV0), - // Device enumeration messages - StartScanning(StartScanningV0), - StopScanning(StopScanningV0), - RequestDeviceList(RequestDeviceListV0), - // Generic commands - StopAllDevices(StopAllDevicesV0), - VibrateCmd(VibrateCmdV1), - LinearCmd(LinearCmdV1), - RotateCmd(RotateCmdV1), - RawWriteCmd(RawWriteCmdV2), - RawReadCmd(RawReadCmdV2), - StopDeviceCmd(StopDeviceCmdV0), - RawSubscribeCmd(RawSubscribeCmdV2), - RawUnsubscribeCmd(RawUnsubscribeCmdV2), - // Sensor commands - BatteryLevelCmd(BatteryLevelCmdV2), - RSSILevelCmd(RSSILevelCmdV2), -} - -/// Represents all server-to-client messages in v2 of the Buttplug Spec -#[derive( - Debug, - Clone, - PartialEq, - ButtplugMessage, - ButtplugMessageValidator, - ButtplugMessageFinalizer, - FromSpecificButtplugMessage, -)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub enum ButtplugServerMessageV2 { - // Status messages - Ok(OkV0), - Error(ErrorV0), - // Handshake messages - ServerInfo(ServerInfoV2), - // Device enumeration messages - DeviceList(DeviceListV2), - DeviceAdded(DeviceAddedV2), - DeviceRemoved(DeviceRemovedV0), - ScanningFinished(ScanningFinishedV0), - // Generic commands - RawReading(RawReadingV2), - // Sensor commands - BatteryLevelReading(BatteryLevelReadingV2), - RSSILevelReading(RSSILevelReadingV2), -} - -/// Represents all client-to-server messages in v1 of the Buttplug Spec -#[derive( - Debug, - Clone, - PartialEq, - ButtplugMessage, - ButtplugMessageValidator, - ButtplugMessageFinalizer, - FromSpecificButtplugMessage, -)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub enum ButtplugClientMessageV1 { - // Handshake and server messages - RequestServerInfo(RequestServerInfoV1), - Ping(PingV0), - RequestLog(RequestLogV0), - // Device enumeration messages - StartScanning(StartScanningV0), - StopScanning(StopScanningV0), - RequestDeviceList(RequestDeviceListV0), - // Generic commands - StopAllDevices(StopAllDevicesV0), - VibrateCmd(VibrateCmdV1), - LinearCmd(LinearCmdV1), - RotateCmd(RotateCmdV1), - StopDeviceCmd(StopDeviceCmdV0), - // Deprecated generic commands (not removed until v2) - SingleMotorVibrateCmd(SingleMotorVibrateCmdV0), - // Deprecated device specific commands (not removed until v2) - FleshlightLaunchFW12Cmd(FleshlightLaunchFW12CmdV0), - LovenseCmd(LovenseCmdV0), - KiirooCmd(KiirooCmdV0), - VorzeA10CycloneCmd(VorzeA10CycloneCmdV0), -} - -/// Represents all server-to-client messages in v2 of the Buttplug Spec -#[derive( - Debug, - Clone, - PartialEq, - ButtplugMessage, - ButtplugMessageValidator, - ButtplugMessageFinalizer, - FromSpecificButtplugMessage, -)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub enum ButtplugServerMessageV1 { - // Status messages - Ok(OkV0), - Error(ErrorV0), - Log(LogV0), - // Handshake messages - ServerInfo(ServerInfoV0), - // Device enumeration messages - DeviceList(DeviceListV1), - DeviceAdded(DeviceAddedV1), - DeviceRemoved(DeviceRemovedV0), - ScanningFinished(ScanningFinishedV0), -} - -/// Represents all client-to-server messages in v0 of the Buttplug Spec -#[derive( - Debug, - Clone, - PartialEq, - ButtplugMessage, - ButtplugMessageValidator, - ButtplugMessageFinalizer, - FromSpecificButtplugMessage, -)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub enum ButtplugClientMessageV0 { - RequestLog(RequestLogV0), - Ping(PingV0), - // Handshake messages - // - // We use RequestServerInfoV1 here, as the only difference between v0 and v1 was passing the spec - // version. If the spec version doesn't exist, we automatically set the spec version to 0. - RequestServerInfo(RequestServerInfoV1), - // Device enumeration messages - StartScanning(StartScanningV0), - StopScanning(StopScanningV0), - RequestDeviceList(RequestDeviceListV0), - // Generic commands - StopAllDevices(StopAllDevicesV0), - StopDeviceCmd(StopDeviceCmdV0), - // Deprecated generic commands - SingleMotorVibrateCmd(SingleMotorVibrateCmdV0), - // Deprecated device specific commands - FleshlightLaunchFW12Cmd(FleshlightLaunchFW12CmdV0), - LovenseCmd(LovenseCmdV0), - KiirooCmd(KiirooCmdV0), - VorzeA10CycloneCmd(VorzeA10CycloneCmdV0), -} - -/// Represents all server-to-client messages in v0 of the Buttplug Spec -#[derive( - Debug, Clone, PartialEq, ButtplugMessage, ButtplugMessageValidator, ButtplugMessageFinalizer, -)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub enum ButtplugServerMessageV0 { - // Status messages - Ok(OkV0), - Error(ErrorV0), - Log(LogV0), - // Handshake messages - ServerInfo(ServerInfoV0), - // Device enumeration messages - DeviceList(DeviceListV0), - DeviceAdded(DeviceAddedV0), - DeviceRemoved(DeviceRemovedV0), - ScanningFinished(ScanningFinishedV0), -} - -/// Represents messages that should go to the -/// [DeviceManager][crate::server::device_manager::DeviceManager] of a -/// [ButtplugServer](crate::server::ButtplugServer) -#[derive( - Debug, - Clone, - PartialEq, - Eq, - ButtplugMessage, - ButtplugMessageValidator, - ButtplugMessageFinalizer, - FromSpecificButtplugMessage, -)] -pub enum ButtplugDeviceManagerMessageUnion { - RequestDeviceList(RequestDeviceListV0), - StopAllDevices(StopAllDevicesV0), - StartScanning(StartScanningV0), - StopScanning(StopScanningV0), -} - -impl TryFrom for ButtplugDeviceManagerMessageUnion { - type Error = (); - - fn try_from(value: ButtplugClientMessageV4) -> Result { - match value { - ButtplugClientMessageV4::RequestDeviceList(m) => { - Ok(ButtplugDeviceManagerMessageUnion::RequestDeviceList(m)) - } - ButtplugClientMessageV4::StopAllDevices(m) => { - Ok(ButtplugDeviceManagerMessageUnion::StopAllDevices(m)) - } - ButtplugClientMessageV4::StartScanning(m) => { - Ok(ButtplugDeviceManagerMessageUnion::StartScanning(m)) - } - ButtplugClientMessageV4::StopScanning(m) => { - Ok(ButtplugDeviceManagerMessageUnion::StopScanning(m)) - } - _ => Err(()), - } - } -} - -/// Represents all possible device command message types. -#[derive( - Debug, - Clone, - PartialEq, - ButtplugDeviceMessage, - ButtplugMessageValidator, - ButtplugMessageFinalizer, - FromSpecificButtplugMessage, -)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub enum ButtplugDeviceCommandMessageUnion { - StopDeviceCmd(StopDeviceCmdV0), - LinearCmd(LinearCmdV4), - RotateCmd(RotateCmdV4), - ScalarCmd(ScalarCmdV4), - SensorReadCmd(SensorReadCmdV4), - SensorSubscribeCmd(SensorSubscribeCmdV4), - SensorUnsubscribeCmd(SensorUnsubscribeCmdV4), - RawWriteCmd(RawWriteCmdV2), - RawReadCmd(RawReadCmdV2), - RawSubscribeCmd(RawSubscribeCmdV2), - RawUnsubscribeCmd(RawUnsubscribeCmdV2), -} - -impl TryFrom for ButtplugDeviceCommandMessageUnion { - type Error = (); - - fn try_from(value: ButtplugClientMessageV4) -> Result { - match value { - ButtplugClientMessageV4::StopDeviceCmd(m) => { - Ok(ButtplugDeviceCommandMessageUnion::StopDeviceCmd(m)) - } - ButtplugClientMessageV4::LinearCmd(m) => Ok(ButtplugDeviceCommandMessageUnion::LinearCmd(m)), - ButtplugClientMessageV4::RotateCmd(m) => Ok(ButtplugDeviceCommandMessageUnion::RotateCmd(m)), - ButtplugClientMessageV4::ScalarCmd(m) => Ok(ButtplugDeviceCommandMessageUnion::ScalarCmd(m)), - ButtplugClientMessageV4::SensorReadCmd(m) => { - Ok(ButtplugDeviceCommandMessageUnion::SensorReadCmd(m)) - } - ButtplugClientMessageV4::SensorSubscribeCmd(m) => { - Ok(ButtplugDeviceCommandMessageUnion::SensorSubscribeCmd(m)) - } - ButtplugClientMessageV4::SensorUnsubscribeCmd(m) => { - Ok(ButtplugDeviceCommandMessageUnion::SensorUnsubscribeCmd(m)) - } - ButtplugClientMessageV4::RawWriteCmd(m) => { - Ok(ButtplugDeviceCommandMessageUnion::RawWriteCmd(m)) - } - ButtplugClientMessageV4::RawReadCmd(m) => { - Ok(ButtplugDeviceCommandMessageUnion::RawReadCmd(m)) - } - ButtplugClientMessageV4::RawSubscribeCmd(m) => { - Ok(ButtplugDeviceCommandMessageUnion::RawSubscribeCmd(m)) - } - ButtplugClientMessageV4::RawUnsubscribeCmd(m) => { - Ok(ButtplugDeviceCommandMessageUnion::RawUnsubscribeCmd(m)) - } - _ => Err(()), - } - } -} diff --git a/buttplug/src/core/message/raw_read_cmd.rs b/buttplug/src/core/message/raw_read_cmd.rs deleted file mode 100644 index 3d95d9ab1..000000000 --- a/buttplug/src/core/message/raw_read_cmd.rs +++ /dev/null @@ -1,50 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 super::*; -use getset::CopyGetters; -#[cfg(feature = "serialize-json")] -use serde::{Deserialize, Serialize}; - -#[derive( - Debug, ButtplugDeviceMessage, ButtplugMessageFinalizer, PartialEq, Eq, Clone, CopyGetters, -)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct RawReadCmdV2 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] - device_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "Endpoint"))] - #[getset(get_copy = "pub")] - endpoint: Endpoint, - #[cfg_attr(feature = "serialize-json", serde(rename = "ExpectedLength"))] - #[getset(get_copy = "pub")] - expected_length: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "Timeout"))] - #[getset(get_copy = "pub")] - timeout: u32, -} - -impl RawReadCmdV2 { - pub fn new(device_index: u32, endpoint: Endpoint, expected_length: u32, timeout: u32) -> Self { - Self { - id: 1, - device_index, - endpoint, - expected_length, - timeout, - } - } -} - -impl ButtplugMessageValidator for RawReadCmdV2 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_not_system_id(self.id) - // TODO Should expected_length always be > 0? - } -} diff --git a/buttplug/src/core/message/raw_reading.rs b/buttplug/src/core/message/raw_reading.rs deleted file mode 100644 index b4edee271..000000000 --- a/buttplug/src/core/message/raw_reading.rs +++ /dev/null @@ -1,77 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 super::*; -use getset::{CopyGetters, Getters}; -#[cfg(feature = "serialize-json")] -use serde::{Deserialize, Serialize}; - -// This message can have an Id of 0, as it can be emitted as part of a -// subscription and won't have a matching task Id in that case. -#[derive( - Debug, - ButtplugDeviceMessage, - ButtplugMessageValidator, - ButtplugMessageFinalizer, - PartialEq, - Eq, - Clone, - Getters, - CopyGetters, -)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct RawReadingV2 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] - device_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "Endpoint"))] - #[getset(get_copy = "pub")] - endpoint: Endpoint, - #[cfg_attr(feature = "serialize-json", serde(rename = "Data"))] - #[getset(get = "pub")] - data: Vec, -} - -impl RawReadingV2 { - pub fn new(device_index: u32, endpoint: Endpoint, data: Vec) -> Self { - Self { - id: 0, - device_index, - endpoint, - data, - } - } -} - -#[cfg(feature = "serialize-json")] -#[cfg(test)] -mod test { - use crate::core::message::{ButtplugServerMessageCurrent, Endpoint, RawReadingV2}; - - #[test] - fn test_endpoint_deserialize() { - let endpoint_str = - "{\"RawReading\":{\"Id\":0,\"DeviceIndex\":0,\"Endpoint\":\"tx\",\"Data\":[0]}}"; - let union: ButtplugServerMessageCurrent = - serde_json::from_str(endpoint_str).expect("Infallible deserialization."); - assert_eq!( - ButtplugServerMessageCurrent::RawReading(RawReadingV2::new(0, Endpoint::Tx, vec!(0))), - union - ); - } - - #[test] - fn test_endpoint_serialize() { - let union = - ButtplugServerMessageCurrent::RawReading(RawReadingV2::new(0, Endpoint::Tx, vec![0])); - let js = serde_json::to_string(&union).expect("Infallible serialization."); - let endpoint_str = - "{\"RawReading\":{\"Id\":0,\"DeviceIndex\":0,\"Endpoint\":\"tx\",\"Data\":[0]}}"; - assert_eq!(js, endpoint_str); - } -} diff --git a/buttplug/src/core/message/raw_subscribe_cmd.rs b/buttplug/src/core/message/raw_subscribe_cmd.rs deleted file mode 100644 index 9b38fce0c..000000000 --- a/buttplug/src/core/message/raw_subscribe_cmd.rs +++ /dev/null @@ -1,41 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 super::*; -use getset::CopyGetters; -#[cfg(feature = "serialize-json")] -use serde::{Deserialize, Serialize}; - -#[derive( - Debug, ButtplugDeviceMessage, ButtplugMessageFinalizer, PartialEq, Eq, Clone, CopyGetters, -)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct RawSubscribeCmdV2 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] - device_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "Endpoint"))] - #[getset(get_copy = "pub")] - endpoint: Endpoint, -} - -impl RawSubscribeCmdV2 { - pub fn new(device_index: u32, endpoint: Endpoint) -> Self { - Self { - id: 1, - device_index, - endpoint, - } - } -} - -impl ButtplugMessageValidator for RawSubscribeCmdV2 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_not_system_id(self.id) - } -} diff --git a/buttplug/src/core/message/raw_unsubscribe_cmd.rs b/buttplug/src/core/message/raw_unsubscribe_cmd.rs deleted file mode 100644 index 06439d5b4..000000000 --- a/buttplug/src/core/message/raw_unsubscribe_cmd.rs +++ /dev/null @@ -1,41 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 super::*; -use getset::CopyGetters; -#[cfg(feature = "serialize-json")] -use serde::{Deserialize, Serialize}; - -#[derive( - Debug, ButtplugDeviceMessage, ButtplugMessageFinalizer, PartialEq, Eq, Clone, CopyGetters, -)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct RawUnsubscribeCmdV2 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] - device_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "Endpoint"))] - #[getset(get_copy = "pub")] - endpoint: Endpoint, -} - -impl RawUnsubscribeCmdV2 { - pub fn new(device_index: u32, endpoint: Endpoint) -> Self { - Self { - id: 1, - device_index, - endpoint, - } - } -} - -impl ButtplugMessageValidator for RawUnsubscribeCmdV2 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_not_system_id(self.id) - } -} diff --git a/buttplug/src/core/message/raw_write_cmd.rs b/buttplug/src/core/message/raw_write_cmd.rs deleted file mode 100644 index 736b8d343..000000000 --- a/buttplug/src/core/message/raw_write_cmd.rs +++ /dev/null @@ -1,54 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 super::*; -use getset::{CopyGetters, Getters}; -#[cfg(feature = "serialize-json")] -use serde::{Deserialize, Serialize}; - -#[derive( - Debug, ButtplugDeviceMessage, ButtplugMessageFinalizer, PartialEq, Eq, Clone, Getters, CopyGetters, -)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct RawWriteCmdV2 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] - device_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "Endpoint"))] - #[getset(get_copy = "pub")] - endpoint: Endpoint, - #[cfg_attr(feature = "serialize-json", serde(rename = "Data"))] - #[getset(get = "pub")] - data: Vec, - #[cfg_attr(feature = "serialize-json", serde(rename = "WriteWithResponse"))] - #[getset(get_copy = "pub")] - write_with_response: bool, -} - -impl RawWriteCmdV2 { - pub fn new( - device_index: u32, - endpoint: Endpoint, - data: &[u8], - write_with_response: bool, - ) -> Self { - Self { - id: 1, - device_index, - endpoint, - data: data.to_vec(), - write_with_response, - } - } -} - -impl ButtplugMessageValidator for RawWriteCmdV2 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_not_system_id(self.id) - } -} diff --git a/buttplug/src/core/message/request_log.rs b/buttplug/src/core/message/request_log.rs deleted file mode 100644 index 6ca6d96f1..000000000 --- a/buttplug/src/core/message/request_log.rs +++ /dev/null @@ -1,33 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 super::*; -use getset::CopyGetters; -#[cfg(feature = "serialize-json")] -use serde::{Deserialize, Serialize}; - -#[derive(Debug, ButtplugMessage, ButtplugMessageFinalizer, PartialEq, Eq, Clone, CopyGetters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct RequestLogV0 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "LogLevel"))] - #[getset(get_copy = "pub")] - log_level: LogLevel, -} - -impl RequestLogV0 { - pub fn new(log_level: LogLevel) -> Self { - Self { id: 1, log_level } - } -} - -impl ButtplugMessageValidator for RequestLogV0 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_not_system_id(self.id) - } -} diff --git a/buttplug/src/core/message/rotate_cmd.rs b/buttplug/src/core/message/rotate_cmd.rs deleted file mode 100644 index 48955bbb0..000000000 --- a/buttplug/src/core/message/rotate_cmd.rs +++ /dev/null @@ -1,133 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 super::*; -pub use getset::{CopyGetters, Getters}; -#[cfg(feature = "serialize-json")] -use serde::{Deserialize, Serialize}; - -#[derive(Debug, PartialEq, Clone, CopyGetters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -#[getset(get_copy = "pub")] -pub struct RotationSubcommandV4 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Index"))] - feature_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "Speed"))] - speed: f64, - #[cfg_attr(feature = "serialize-json", serde(rename = "Clockwise"))] - clockwise: bool, -} - -impl RotationSubcommandV4 { - pub fn new(feature_index: u32, speed: f64, clockwise: bool) -> Self { - Self { - feature_index, - speed, - clockwise, - } - } -} - -#[derive(Debug, ButtplugDeviceMessage, ButtplugMessageFinalizer, PartialEq, Clone, Getters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct RotateCmdV4 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] - device_index: u32, - #[getset(get = "pub")] - #[cfg_attr(feature = "serialize-json", serde(rename = "Rotations"))] - #[getset(get = "pub")] - rotations: Vec, -} - -impl RotateCmdV4 { - pub fn new(device_index: u32, rotations: Vec) -> Self { - Self { - id: 1, - device_index, - rotations, - } - } -} - -impl ButtplugMessageValidator for RotateCmdV4 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_not_system_id(self.id)?; - for rotation in &self.rotations { - self.is_in_command_range( - rotation.speed, - format!( - "Speed {} for RotateCmd index {} is invalid. Speed should be a value between 0.0 and 1.0", - rotation.speed, rotation.feature_index - ), - )?; - } - Ok(()) - } -} - -#[derive(Debug, PartialEq, Clone, CopyGetters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -#[getset(get_copy = "pub")] -pub struct RotationSubcommandV1 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Index"))] - index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "Speed"))] - speed: f64, - #[cfg_attr(feature = "serialize-json", serde(rename = "Clockwise"))] - clockwise: bool, -} - -impl RotationSubcommandV1 { - pub fn new(index: u32, speed: f64, clockwise: bool) -> Self { - Self { - index, - speed, - clockwise, - } - } -} - -#[derive(Debug, ButtplugDeviceMessage, ButtplugMessageFinalizer, PartialEq, Clone, Getters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct RotateCmdV1 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] - device_index: u32, - #[getset(get = "pub")] - #[cfg_attr(feature = "serialize-json", serde(rename = "Rotations"))] - #[getset(get = "pub")] - rotations: Vec, -} - -impl RotateCmdV1 { - pub fn new(device_index: u32, rotations: Vec) -> Self { - Self { - id: 1, - device_index, - rotations, - } - } -} - -impl ButtplugMessageValidator for RotateCmdV1 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_not_system_id(self.id)?; - for rotation in &self.rotations { - self.is_in_command_range( - rotation.speed, - format!( - "Speed {} for RotateCmd index {} is invalid. Speed should be a value between 0.0 and 1.0", - rotation.speed, rotation.index - ), - )?; - } - Ok(()) - } -} diff --git a/buttplug/src/core/message/rssi_level_cmd.rs b/buttplug/src/core/message/rssi_level_cmd.rs deleted file mode 100644 index 3b838040a..000000000 --- a/buttplug/src/core/message/rssi_level_cmd.rs +++ /dev/null @@ -1,34 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 super::*; -#[cfg(feature = "serialize-json")] -use serde::{Deserialize, Serialize}; - -#[derive(Debug, ButtplugDeviceMessage, ButtplugMessageFinalizer, PartialEq, Eq, Clone)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct RSSILevelCmdV2 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] - device_index: u32, -} - -impl RSSILevelCmdV2 { - pub fn new(device_index: u32) -> Self { - Self { - id: 1, - device_index, - } - } -} - -impl ButtplugMessageValidator for RSSILevelCmdV2 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_not_system_id(self.id) - } -} diff --git a/buttplug/src/core/message/rssi_level_reading.rs b/buttplug/src/core/message/rssi_level_reading.rs deleted file mode 100644 index 9cf074022..000000000 --- a/buttplug/src/core/message/rssi_level_reading.rs +++ /dev/null @@ -1,49 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 super::*; -use getset::CopyGetters; -#[cfg(feature = "serialize-json")] -use serde::{Deserialize, Serialize}; - -#[derive( - Debug, ButtplugDeviceMessage, ButtplugMessageFinalizer, PartialEq, Eq, Clone, CopyGetters, -)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct RSSILevelReadingV2 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] - device_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "RSSILevel"))] - #[getset(get_copy = "pub")] - rssi_level: i32, -} - -impl RSSILevelReadingV2 { - pub fn new(device_index: u32, rssi_level: i32) -> Self { - Self { - id: 1, - device_index, - rssi_level, - } - } -} - -impl ButtplugMessageValidator for RSSILevelReadingV2 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_not_system_id(self.id)?; - if self.rssi_level > 0 { - Err(ButtplugMessageError::InvalidMessageContents(format!( - "RSSI level {} is invalid. RSSI Levels are always negative.", - self.rssi_level - ))) - } else { - Ok(()) - } - } -} diff --git a/buttplug/src/core/message/scalar_cmd.rs b/buttplug/src/core/message/scalar_cmd.rs deleted file mode 100644 index dd0fbd365..000000000 --- a/buttplug/src/core/message/scalar_cmd.rs +++ /dev/null @@ -1,137 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 super::*; -use getset::{CopyGetters, Getters}; -#[cfg(feature = "serialize-json")] -use serde::{Deserialize, Serialize}; - -/// Generic command for setting a level (single magnitude value) of a device feature. -#[derive(Debug, PartialEq, Clone, CopyGetters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -#[getset(get_copy = "pub")] -pub struct ScalarSubcommandV4 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Index"))] - feature_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "Scalar"))] - scalar: f64, - #[cfg_attr(feature = "serialize-json", serde(rename = "ActuatorType"))] - actuator_type: ActuatorType, -} - -impl ScalarSubcommandV4 { - pub fn new(feature_index: u32, scalar: f64, actuator_type: ActuatorType) -> Self { - Self { - feature_index, - scalar, - actuator_type, - } - } -} - -#[derive( - Debug, Default, ButtplugDeviceMessage, ButtplugMessageFinalizer, PartialEq, Clone, Getters, -)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct ScalarCmdV4 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] - device_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "Scalars"))] - #[getset(get = "pub")] - scalars: Vec, -} - -impl ScalarCmdV4 { - pub fn new(device_index: u32, scalars: Vec) -> Self { - Self { - id: 1, - device_index, - scalars, - } - } -} - -impl ButtplugMessageValidator for ScalarCmdV4 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_not_system_id(self.id)?; - for level in &self.scalars { - self.is_in_command_range( - level.scalar, - format!( - "Level {} for ScalarCmd feature index {} is invalid. Level should be a value between 0.0 and 1.0", - level.scalar, level.feature_index - ), - )?; - } - Ok(()) - } -} - -/// Generic command for setting a level (single magnitude value) of a device feature. -#[derive(Debug, PartialEq, Clone, CopyGetters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -#[getset(get_copy = "pub")] -pub struct ScalarSubcommandV3 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Index"))] - index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "Scalar"))] - scalar: f64, - #[cfg_attr(feature = "serialize-json", serde(rename = "ActuatorType"))] - actuator_type: ActuatorType, -} - -impl ScalarSubcommandV3 { - pub fn new(index: u32, scalar: f64, actuator_type: ActuatorType) -> Self { - Self { - index, - scalar, - actuator_type, - } - } -} - -#[derive( - Debug, Default, ButtplugDeviceMessage, ButtplugMessageFinalizer, PartialEq, Clone, Getters, -)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct ScalarCmdV3 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] - device_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "Scalars"))] - #[getset(get = "pub")] - scalars: Vec, -} - -impl ScalarCmdV3 { - pub fn new(device_index: u32, scalars: Vec) -> Self { - Self { - id: 1, - device_index, - scalars, - } - } -} - -impl ButtplugMessageValidator for ScalarCmdV3 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_not_system_id(self.id)?; - for level in &self.scalars { - self.is_in_command_range( - level.scalar, - format!( - "Level {} for ScalarCmd index {} is invalid. Level should be a value between 0.0 and 1.0", - level.scalar, level.index - ), - )?; - } - Ok(()) - } -} diff --git a/buttplug/src/core/message/sensor_read_cmd.rs b/buttplug/src/core/message/sensor_read_cmd.rs deleted file mode 100644 index bcccba6de..000000000 --- a/buttplug/src/core/message/sensor_read_cmd.rs +++ /dev/null @@ -1,81 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 super::*; -use getset::{CopyGetters, Getters}; -#[cfg(feature = "serialize-json")] -use serde::{Deserialize, Serialize}; - -#[derive( - Debug, ButtplugDeviceMessage, ButtplugMessageFinalizer, PartialEq, Eq, Clone, Getters, CopyGetters, -)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct SensorReadCmdV4 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] - device_index: u32, - #[getset(get = "pub")] - #[cfg_attr(feature = "serialize-json", serde(rename = "FeatureIndex"))] - feature_index: u32, - #[getset(get = "pub")] - #[cfg_attr(feature = "serialize-json", serde(rename = "SensorType"))] - sensor_type: SensorType, -} - -impl SensorReadCmdV4 { - pub fn new(device_index: u32, feature_index: u32, sensor_type: SensorType) -> Self { - Self { - id: 1, - device_index, - feature_index, - sensor_type, - } - } -} - -impl ButtplugMessageValidator for SensorReadCmdV4 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_not_system_id(self.id) - // TODO Should expected_length always be > 0? - } -} - -#[derive( - Debug, ButtplugDeviceMessage, ButtplugMessageFinalizer, PartialEq, Eq, Clone, Getters, CopyGetters, -)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct SensorReadCmdV3 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] - device_index: u32, - #[getset(get = "pub")] - #[cfg_attr(feature = "serialize-json", serde(rename = "SensorIndex"))] - sensor_index: u32, - #[getset(get = "pub")] - #[cfg_attr(feature = "serialize-json", serde(rename = "SensorType"))] - sensor_type: SensorType, -} - -impl SensorReadCmdV3 { - pub fn new(device_index: u32, sensor_index: u32, sensor_type: SensorType) -> Self { - Self { - id: 1, - device_index, - sensor_index, - sensor_type, - } - } -} - -impl ButtplugMessageValidator for SensorReadCmdV3 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_not_system_id(self.id) - // TODO Should expected_length always be > 0? - } -} diff --git a/buttplug/src/core/message/sensor_reading.rs b/buttplug/src/core/message/sensor_reading.rs deleted file mode 100644 index 4f1beb2da..000000000 --- a/buttplug/src/core/message/sensor_reading.rs +++ /dev/null @@ -1,105 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 super::*; -use getset::{CopyGetters, Getters}; -#[cfg(feature = "serialize-json")] -use serde::{Deserialize, Serialize}; - -// This message can have an Id of 0, as it can be emitted as part of a -// subscription and won't have a matching task Id in that case. -#[derive( - Debug, - ButtplugDeviceMessage, - ButtplugMessageValidator, - ButtplugMessageFinalizer, - Clone, - Getters, - CopyGetters, - PartialEq, - Eq, -)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct SensorReadingV4 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] - device_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "FeatureIndex"))] - #[getset[get_copy="pub"]] - feature_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "SensorType"))] - #[getset[get_copy="pub"]] - sensor_type: SensorType, - #[cfg_attr(feature = "serialize-json", serde(rename = "Data"))] - #[getset[get="pub"]] - data: Vec, -} - -impl SensorReadingV4 { - pub fn new( - device_index: u32, - feature_index: u32, - sensor_type: SensorType, - data: Vec, - ) -> Self { - Self { - id: 0, - device_index, - feature_index, - sensor_type, - data, - } - } -} - -// This message can have an Id of 0, as it can be emitted as part of a -// subscription and won't have a matching task Id in that case. -#[derive( - Debug, - ButtplugDeviceMessage, - ButtplugMessageValidator, - ButtplugMessageFinalizer, - Clone, - Getters, - CopyGetters, - PartialEq, - Eq, -)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct SensorReadingV3 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] - device_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "SensorIndex"))] - #[getset[get_copy="pub"]] - sensor_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "SensorType"))] - #[getset[get_copy="pub"]] - sensor_type: SensorType, - #[cfg_attr(feature = "serialize-json", serde(rename = "Data"))] - #[getset[get="pub"]] - data: Vec, -} - -impl SensorReadingV3 { - pub fn new( - device_index: u32, - sensor_index: u32, - sensor_type: SensorType, - data: Vec, - ) -> Self { - Self { - id: 0, - device_index, - sensor_index, - sensor_type, - data, - } - } -} diff --git a/buttplug/src/core/message/sensor_subscribe_cmd.rs b/buttplug/src/core/message/sensor_subscribe_cmd.rs deleted file mode 100644 index 926390a43..000000000 --- a/buttplug/src/core/message/sensor_subscribe_cmd.rs +++ /dev/null @@ -1,75 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 super::*; -use getset::Getters; -#[cfg(feature = "serialize-json")] -use serde::{Deserialize, Serialize}; - -#[derive(Debug, ButtplugDeviceMessage, ButtplugMessageFinalizer, PartialEq, Eq, Clone, Getters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct SensorSubscribeCmdV4 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] - device_index: u32, - #[getset(get = "pub")] - #[cfg_attr(feature = "serialize-json", serde(rename = "FeatureIndex"))] - feature_index: u32, - #[getset(get = "pub")] - #[cfg_attr(feature = "serialize-json", serde(rename = "SensorType"))] - sensor_type: SensorType, -} - -impl SensorSubscribeCmdV4 { - pub fn new(device_index: u32, feature_index: u32, sensor_type: SensorType) -> Self { - Self { - id: 1, - device_index, - feature_index, - sensor_type, - } - } -} - -impl ButtplugMessageValidator for SensorSubscribeCmdV4 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_not_system_id(self.id) - } -} - -#[derive(Debug, ButtplugDeviceMessage, ButtplugMessageFinalizer, PartialEq, Eq, Clone, Getters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct SensorSubscribeCmdV3 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] - device_index: u32, - #[getset(get = "pub")] - #[cfg_attr(feature = "serialize-json", serde(rename = "SensorIndex"))] - sensor_index: u32, - #[getset(get = "pub")] - #[cfg_attr(feature = "serialize-json", serde(rename = "SensorType"))] - sensor_type: SensorType, -} - -impl SensorSubscribeCmdV3 { - pub fn new(device_index: u32, sensor_index: u32, sensor_type: SensorType) -> Self { - Self { - id: 1, - device_index, - sensor_index, - sensor_type, - } - } -} - -impl ButtplugMessageValidator for SensorSubscribeCmdV3 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_not_system_id(self.id) - } -} diff --git a/buttplug/src/core/message/sensor_unsubscribe_cmd.rs b/buttplug/src/core/message/sensor_unsubscribe_cmd.rs deleted file mode 100644 index 948d47be0..000000000 --- a/buttplug/src/core/message/sensor_unsubscribe_cmd.rs +++ /dev/null @@ -1,75 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 super::*; -use getset::Getters; -#[cfg(feature = "serialize-json")] -use serde::{Deserialize, Serialize}; - -#[derive(Debug, ButtplugDeviceMessage, ButtplugMessageFinalizer, PartialEq, Eq, Clone, Getters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct SensorUnsubscribeCmdV4 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] - device_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "SensorIndex"))] - #[getset(get = "pub")] - feature_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "SensorType"))] - #[getset(get = "pub")] - sensor_type: SensorType, -} - -impl SensorUnsubscribeCmdV4 { - pub fn new(device_index: u32, feature_index: u32, sensor_type: SensorType) -> Self { - Self { - id: 1, - device_index, - feature_index, - sensor_type, - } - } -} - -impl ButtplugMessageValidator for SensorUnsubscribeCmdV4 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_not_system_id(self.id) - } -} - -#[derive(Debug, ButtplugDeviceMessage, ButtplugMessageFinalizer, PartialEq, Eq, Clone, Getters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct SensorUnsubscribeCmdV3 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] - device_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "SensorIndex"))] - #[getset(get = "pub")] - sensor_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "SensorType"))] - #[getset(get = "pub")] - sensor_type: SensorType, -} - -impl SensorUnsubscribeCmdV3 { - pub fn new(device_index: u32, sensor_index: u32, sensor_type: SensorType) -> Self { - Self { - id: 1, - device_index, - sensor_index, - sensor_type, - } - } -} - -impl ButtplugMessageValidator for SensorUnsubscribeCmdV3 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_not_system_id(self.id) - } -} diff --git a/buttplug/src/core/message/serializer/json_serializer.rs b/buttplug/src/core/message/serializer/json_serializer.rs deleted file mode 100644 index 4219a0e55..000000000 --- a/buttplug/src/core/message/serializer/json_serializer.rs +++ /dev/null @@ -1,545 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 super::{ButtplugMessageSerializer, ButtplugSerializedMessage, ButtplugSerializerError}; -use crate::core::{ - errors::{ButtplugError, ButtplugHandshakeError, ButtplugMessageError}, - message::{ - self, - ButtplugClientMessageCurrent, - ButtplugClientMessageV0, - ButtplugClientMessageV1, - ButtplugClientMessageV2, - ButtplugClientMessageV3, - ButtplugClientMessageV4, - ButtplugClientMessageVariant, - ButtplugMessage, - ButtplugMessageFinalizer, - ButtplugMessageSpecVersion, - ButtplugServerMessageCurrent, - ButtplugServerMessageV0, - ButtplugServerMessageV1, - ButtplugServerMessageV2, - ButtplugServerMessageV3, - ButtplugServerMessageV4, - ButtplugServerMessageVariant, - }, -}; -use jsonschema::Validator; -use once_cell::sync::OnceCell; -use serde::{Deserialize, Serialize}; -use serde_json::{Deserializer, Value}; -use std::fmt::Debug; - -static MESSAGE_JSON_SCHEMA: &str = - include_str!("../../../../buttplug-schema/schema/buttplug-schema.json"); - -/// Creates a [jsonschema::JSONSchema] validator using the built in buttplug message schema. -pub fn create_message_validator() -> Validator { - let schema: serde_json::Value = - serde_json::from_str(MESSAGE_JSON_SCHEMA).expect("Built in schema better be valid"); - Validator::new(&schema).expect("Built in schema better be valid") -} -pub struct ButtplugServerJSONSerializer { - pub(super) message_version: OnceCell, - validator: Validator, -} - -impl Default for ButtplugServerJSONSerializer { - fn default() -> Self { - Self { - message_version: OnceCell::new(), - validator: create_message_validator(), - } - } -} - -impl ButtplugServerJSONSerializer { - pub fn force_message_version(&self, version: &ButtplugMessageSpecVersion) { - self - .message_version - .set(*version) - .expect("This should only ever be called once."); - } -} - -/// Returns the message as a string in Buttplug JSON Protocol format. -pub fn msg_to_protocol_json(msg: T) -> String -where - T: ButtplugMessage + Serialize + Deserialize<'static>, -{ - serde_json::to_string(&[&msg]).expect("Infallible serialization") -} - -pub fn vec_to_protocol_json(msg: &[T]) -> String -where - T: ButtplugMessage + Serialize + Deserialize<'static>, -{ - serde_json::to_string(msg).expect("Infallible serialization") -} - -pub fn deserialize_to_message( - validator: &Validator, - msg_str: &str, -) -> Result, ButtplugSerializerError> -where - T: serde::de::DeserializeOwned + ButtplugMessageFinalizer + Clone + Debug, -{ - // TODO This assumes that we've gotten a full JSON string to deserialize, which may not be the - // case. - let stream = Deserializer::from_str(msg_str).into_iter::(); - - let mut result = vec![]; - - for msg in stream { - match msg { - Ok(json_msg) => { - if validator.is_valid(&json_msg) { - match serde_json::from_value::>(json_msg) { - Ok(mut msg_vec) => { - for msg in msg_vec.iter_mut() { - msg.finalize(); - } - result.append(&mut msg_vec); - //Ok(msg_vec) - } - Err(e) => { - return Err(ButtplugSerializerError::JsonSerializerError(format!( - "Message: {} - Error: {:?}", - msg_str, e - ))) - } - } - } else { - // If is_valid fails, re-run validation to get our error message. - let e = validator - .validate(&json_msg) - .expect_err("We can't get here without validity checks failing."); - return Err(ButtplugSerializerError::JsonSerializerError(format!( - "Error during JSON Schema Validation - Message: {} - Error: {:?}", - json_msg, e - ))); - } - } - Err(e) => { - return Err(ButtplugSerializerError::JsonSerializerError(format!( - "Message: {} - Error: {:?}", - msg_str, e - ))) - } - } - } - Ok(result) -} - -impl ButtplugMessageSerializer for ButtplugServerJSONSerializer { - type Inbound = ButtplugClientMessageVariant; - type Outbound = ButtplugServerMessageVariant; - - fn deserialize( - &self, - serialized_msg: &ButtplugSerializedMessage, - ) -> Result, ButtplugSerializerError> { - let msg = if let ButtplugSerializedMessage::Text(text_msg) = serialized_msg { - text_msg - } else { - return Err(ButtplugSerializerError::BinaryDeserializationError); - }; - - if let Some(version) = self.message_version.get() { - return Ok(match version { - ButtplugMessageSpecVersion::Version0 => { - deserialize_to_message::(&self.validator, msg)? - .iter() - .cloned() - .map(|m| m.into()) - .collect() - } - ButtplugMessageSpecVersion::Version1 => { - deserialize_to_message::(&self.validator, msg)? - .iter() - .cloned() - .map(|m| m.into()) - .collect() - } - ButtplugMessageSpecVersion::Version2 => { - deserialize_to_message::(&self.validator, msg)? - .iter() - .cloned() - .map(|m| m.into()) - .collect() - } - ButtplugMessageSpecVersion::Version3 => { - deserialize_to_message::(&self.validator, msg)? - .iter() - .cloned() - .map(|m| m.into()) - .collect() - } - ButtplugMessageSpecVersion::Version4 => { - deserialize_to_message::(&self.validator, msg)? - .iter() - .cloned() - .map(|m| m.into()) - .collect() - } - }); - } - // If we don't have a message version yet, we need to parse this as a RequestServerInfo message - // to get the version. RequestServerInfo can always be parsed as the latest message version, as - // we keep it compatible across versions via serde options. - let msg_union = deserialize_to_message::(&self.validator, msg)?; - // If the message is malformed, just return an spec version not received error. - if msg_union.is_empty() { - return Err(ButtplugSerializerError::MessageSpecVersionNotReceived); - } - if let ButtplugClientMessageV4::RequestServerInfo(rsi) = &msg_union[0] { - info!( - "Setting JSON Wrapper message version to {}", - rsi.message_version() - ); - self - .message_version - .set(rsi.message_version()) - .expect("This should only ever be called once."); - } else { - return Err(ButtplugSerializerError::MessageSpecVersionNotReceived); - } - // Now that we know our version, parse the message again. - self.deserialize(serialized_msg) - } - - fn serialize(&self, msgs: &[ButtplugServerMessageVariant]) -> ButtplugSerializedMessage { - if let Some(version) = self.message_version.get() { - ButtplugSerializedMessage::Text(match version { - ButtplugMessageSpecVersion::Version0 => { - let msg_vec: Vec = msgs - .iter() - .map(|msg| match msg { - ButtplugServerMessageVariant::V0(msgv0) => msgv0.clone(), - _ => ButtplugServerMessageV0::Error( - message::ErrorV0::from(ButtplugError::from( - ButtplugMessageError::MessageConversionError(format!( - "Message {:?} not in Spec V0! This is a server bug.", - msg - )), - )) - .into(), - ), - }) - .collect(); - vec_to_protocol_json(&msg_vec) - } - ButtplugMessageSpecVersion::Version1 => { - let msg_vec: Vec = msgs - .iter() - .map(|msg| match msg { - ButtplugServerMessageVariant::V1(msgv1) => msgv1.clone(), - _ => ButtplugServerMessageV1::Error( - message::ErrorV0::from(ButtplugError::from( - ButtplugMessageError::MessageConversionError(format!( - "Message {:?} not in Spec V1! This is a server bug.", - msg - )), - )) - .into(), - ), - }) - .collect(); - vec_to_protocol_json(&msg_vec) - } - ButtplugMessageSpecVersion::Version2 => { - let msg_vec: Vec = msgs - .iter() - .map(|msg| match msg { - ButtplugServerMessageVariant::V2(msgv2) => msgv2.clone(), - _ => ButtplugServerMessageV2::Error( - message::ErrorV0::from(ButtplugError::from( - ButtplugMessageError::MessageConversionError(format!( - "Message {:?} not in Spec V2! This is a server bug.", - msg - )), - )) - .into(), - ), - }) - .collect(); - vec_to_protocol_json(&msg_vec) - } - ButtplugMessageSpecVersion::Version3 => { - let msg_vec: Vec = msgs - .iter() - .map(|msg| match msg { - ButtplugServerMessageVariant::V3(msgv3) => msgv3.clone(), - _ => ButtplugServerMessageV3::Error( - message::ErrorV0::from(ButtplugError::from( - ButtplugMessageError::MessageConversionError(format!( - "Message {:?} not in Spec V3! This is a server bug.", - msg - )), - )) - .into(), - ), - }) - .collect(); - vec_to_protocol_json(&msg_vec) - } - ButtplugMessageSpecVersion::Version4 => { - let msg_vec: Vec = msgs - .iter() - .map(|msg| match msg { - ButtplugServerMessageVariant::V4(msgv4) => msgv4.clone(), - _ => ButtplugServerMessageV4::Error( - message::ErrorV0::from(ButtplugError::from( - ButtplugMessageError::MessageConversionError(format!( - "Message {:?} not in Spec V4! This is a server bug.", - msg - )), - )) - .into(), - ), - }) - .collect(); - vec_to_protocol_json(&msg_vec) - } - }) - } else { - // If we don't even have enough info to know which message - // version to convert to, consider this a handshake error. - ButtplugSerializedMessage::Text(msg_to_protocol_json(ButtplugServerMessageCurrent::Error( - ButtplugError::from(ButtplugHandshakeError::RequestServerInfoExpected).into(), - ))) - } - } -} - -pub struct ButtplugClientJSONSerializerImpl { - validator: Validator, -} - -impl Default for ButtplugClientJSONSerializerImpl { - fn default() -> Self { - Self { - validator: create_message_validator(), - } - } -} - -impl ButtplugClientJSONSerializerImpl { - pub fn deserialize( - &self, - msg: &ButtplugSerializedMessage, - ) -> Result, ButtplugSerializerError> - where - T: serde::de::DeserializeOwned + ButtplugMessageFinalizer + Clone + Debug, - { - if let ButtplugSerializedMessage::Text(text_msg) = msg { - deserialize_to_message::(&self.validator, text_msg) - } else { - Err(ButtplugSerializerError::BinaryDeserializationError) - } - } - - pub fn serialize(&self, msg: &[T]) -> ButtplugSerializedMessage - where - T: ButtplugMessage + Serialize + Deserialize<'static>, - { - ButtplugSerializedMessage::Text(vec_to_protocol_json(msg)) - } -} - -#[derive(Default)] -pub struct ButtplugClientJSONSerializer { - serializer_impl: ButtplugClientJSONSerializerImpl, -} - -impl ButtplugMessageSerializer for ButtplugClientJSONSerializer { - type Inbound = ButtplugServerMessageCurrent; - type Outbound = ButtplugClientMessageCurrent; - - fn deserialize( - &self, - msg: &ButtplugSerializedMessage, - ) -> Result, ButtplugSerializerError> { - self.serializer_impl.deserialize(msg) - } - - fn serialize(&self, msg: &[Self::Outbound]) -> ButtplugSerializedMessage { - self.serializer_impl.serialize(msg) - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::core::message::{RequestServerInfoV1, BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION}; - - #[test] - fn test_correct_message_version() { - let json = r#"[{ - "RequestServerInfo": { - "Id": 1, - "ClientName": "Test Client", - "MessageVersion": 2 - } - }]"#; - let serializer = ButtplugServerJSONSerializer::default(); - serializer - .deserialize(&ButtplugSerializedMessage::Text(json.to_owned())) - .expect("Infallible deserialization"); - assert_eq!( - *serializer.message_version.get().unwrap(), - ButtplugMessageSpecVersion::Version2 - ); - } - - #[test] - fn test_wrong_message_version() { - let json = r#"[{ - "RequestServerInfo": { - "Id": 1, - "ClientName": "Test Client", - "MessageVersion": 100 - } - }]"#; - let serializer = ButtplugServerJSONSerializer::default(); - let msg = serializer.deserialize(&ButtplugSerializedMessage::Text(json.to_owned())); - assert!(msg.is_err()); - } - - #[test] - fn test_message_array() { - let json = r#"[ - { - "RequestServerInfo": { - "Id": 1, - "ClientName": "Test Client", - "MessageVersion": 3 - } - }, - { - "RequestServerInfo": { - "Id": 1, - "ClientName": "Test Client", - "MessageVersion": 3 - } - }, - { - "RequestServerInfo": { - "Id": 1, - "ClientName": "Test Client", - "MessageVersion": 3 - } - }]"#; - let serializer = ButtplugServerJSONSerializer::default(); - let messages = serializer - .deserialize(&ButtplugSerializedMessage::Text(json.to_owned())) - .expect("Infallible deserialization"); - assert_eq!(messages.len(), 3); - } - - #[test] - fn test_streamed_message_array() { - let json = r#"[ - { - "RequestServerInfo": { - "Id": 1, - "ClientName": "Test Client", - "MessageVersion": 3 - } - }] - [{ - "RequestServerInfo": { - "Id": 1, - "ClientName": "Test Client", - "MessageVersion": 3 - } - }] - [{ - "RequestServerInfo": { - "Id": 1, - "ClientName": "Test Client", - "MessageVersion": 3 - } - }] - "#; - let serializer = ButtplugServerJSONSerializer::default(); - let messages = serializer - .deserialize(&ButtplugSerializedMessage::Text(json.to_owned())) - .expect("Infallible deserialization"); - assert_eq!(messages.len(), 3); - } - - #[test] - fn test_invalid_streamed_message_array() { - // Missing a } in the second message. - let json = r#"[ - { - "RequestServerInfo": { - "Id": 1, - "ClientName": "Test Client", - "MessageVersion": 3 - } - }] - [{ - "RequestServerInfo": { - "Id": 1, - "ClientName": "Test Client", - "MessageVersion": 3 - }] - [{ - "RequestServerInfo": { - "Id": 1, - "ClientName": "Test Client", - "MessageVersion": 3 - } - }] - "#; - let serializer = ButtplugServerJSONSerializer::default(); - assert!(matches!( - serializer.deserialize(&ButtplugSerializedMessage::Text(json.to_owned())), - Err(_) - )); - } - - #[test] - fn test_client_incorrect_messages() { - let incorrect_incoming_messages = vec![ - // Not valid JSON - "not a json message", - // Valid json object but no contents - "{}", - // Valid json but not an object - "[]", - // Not a message type - "[{\"NotAMessage\":{}}]", - // Valid json and message type but not in correct format - "[{\"Ok\":[]}]", - // Valid json and message type but not in correct format - "[{\"Ok\":{}}]", - // Valid json and message type but not an array. - "{\"Ok\":{\"Id\":0}}", - // Valid json and message type but not an array. - "[{\"Ok\":{\"Id\":0}}]", - // Valid json and message type but with extra content - "[{\"Ok\":{\"NotAField\":\"NotAValue\",\"Id\":1}}]", - ]; - let serializer = ButtplugClientJSONSerializer::default(); - let _ = serializer.serialize(&vec![RequestServerInfoV1::new( - "test client", - BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION, - ) - .into()]); - for msg in incorrect_incoming_messages { - let res = serializer.deserialize(&ButtplugSerializedMessage::Text(msg.to_owned())); - assert!(res.is_err(), "{} should be an error", msg); - if let Err(ButtplugSerializerError::MessageSpecVersionNotReceived) = res { - assert!(false, "Wrong error!"); - } - } - } -} diff --git a/buttplug/src/core/message/server_info.rs b/buttplug/src/core/message/server_info.rs deleted file mode 100644 index f374f7000..000000000 --- a/buttplug/src/core/message/server_info.rs +++ /dev/null @@ -1,109 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 super::*; -use getset::{CopyGetters, Getters}; -#[cfg(feature = "serialize-json")] -use serde::{Deserialize, Serialize}; - -#[derive( - Debug, ButtplugMessage, ButtplugMessageFinalizer, PartialEq, Eq, Clone, Getters, CopyGetters, -)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct ServerInfoV2 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "MessageVersion"))] - #[getset(get_copy = "pub")] - message_version: ButtplugMessageSpecVersion, - #[cfg_attr(feature = "serialize-json", serde(rename = "MaxPingTime"))] - #[getset(get_copy = "pub")] - max_ping_time: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "ServerName"))] - #[getset(get = "pub")] - server_name: String, -} - -impl ServerInfoV2 { - pub fn new( - server_name: &str, - message_version: ButtplugMessageSpecVersion, - max_ping_time: u32, - ) -> Self { - Self { - id: 1, - message_version, - max_ping_time, - server_name: server_name.to_string(), - } - } -} - -impl ButtplugMessageValidator for ServerInfoV2 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_not_system_id(self.id) - } -} - -#[derive( - Debug, ButtplugMessage, ButtplugMessageFinalizer, PartialEq, Eq, Clone, Getters, CopyGetters, -)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] -pub struct ServerInfoV0 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] - id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "MajorVersion"))] - #[getset(get_copy = "pub")] - major_version: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "MinorVersion"))] - #[getset(get_copy = "pub")] - minor_version: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "BuildVersion"))] - #[getset(get_copy = "pub")] - build_version: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "MessageVersion"))] - #[getset(get_copy = "pub")] - message_version: ButtplugMessageSpecVersion, - #[cfg_attr(feature = "serialize-json", serde(rename = "MaxPingTime"))] - #[getset(get_copy = "pub")] - max_ping_time: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "ServerName"))] - #[getset(get = "pub")] - server_name: String, -} - -impl ServerInfoV0 { - pub fn new( - server_name: &str, - message_version: ButtplugMessageSpecVersion, - max_ping_time: u32, - ) -> Self { - Self { - id: 1, - major_version: 0, - minor_version: 0, - build_version: 0, - message_version, - max_ping_time, - server_name: server_name.to_string(), - } - } -} - -impl From for ServerInfoV0 { - fn from(msg: ServerInfoV2) -> Self { - let mut out_msg = Self::new(&msg.server_name, msg.message_version, msg.max_ping_time); - out_msg.set_id(msg.id()); - out_msg - } -} - -impl ButtplugMessageValidator for ServerInfoV0 { - fn is_valid(&self) -> Result<(), ButtplugMessageError> { - self.is_system_id(self.id) - } -} diff --git a/buttplug/src/lib.rs b/buttplug/src/lib.rs deleted file mode 100644 index 1447d916c..000000000 --- a/buttplug/src/lib.rs +++ /dev/null @@ -1,47 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 Nonpolynomial Labs LLC. All rights reserved. -// -// Licensed under the BSD 3-Clause license. See LICENSE file in the project root -// for full license information. - -#![crate_type = "lib"] -#![crate_name = "buttplug"] -#![doc = include_str!("../README.md")] - -//! # An Overview of Buttplug's Module System -//! -//! Buttplug is broken up into the following modules: -//! -//! - [Core](crate::core) -//! - Generic portions of the library code that are used by the other modules. This includes -//! message classes, serializers, connectors, and errors. -//! - [Client](crate::client) -//! - The public facing API for applications. This module is what most programs will use to talk -//! to Buttplug servers, either directly through Rust, or through our [FFI -//! Layer](https://github.com/buttplugio/buttplug-rs-ffi) for other languages. -//! - [Server](crate::server) -//! - Handles actual hardware connections and communication. If you want to add new devices or -//! protocols to Buttplug, or change how the system access devices, this is the module you'll be -//! working in. -//! - [Util](crate::util) -//! - Utilities for all portions of the library that may not be specifically related to sex toy -//! functionality. This includes managers for different async runtimes, configuration file -//! loading, utilities for streams and futures, etc... - -#[macro_use] -extern crate buttplug_derive; -#[macro_use] -extern crate strum_macros; -#[cfg(any(feature = "client", feature = "server"))] -#[macro_use] -extern crate futures; -#[macro_use] -extern crate tracing; - -#[cfg(feature = "client")] -pub mod client; -pub mod core; -#[cfg(feature = "server")] -pub mod server; -pub mod util; diff --git a/buttplug/src/server/device/configuration/device_definitions.rs b/buttplug/src/server/device/configuration/device_definitions.rs deleted file mode 100644 index 4bf367928..000000000 --- a/buttplug/src/server/device/configuration/device_definitions.rs +++ /dev/null @@ -1,130 +0,0 @@ -use getset::{CopyGetters, Getters, MutGetters, Setters}; -use serde::{Deserialize, Serialize}; - -use crate::core::message::{ - ButtplugActuatorFeatureMessageType, - ButtplugDeviceMessageType, - ButtplugRawFeatureMessageType, - ButtplugSensorFeatureMessageType, - DeviceFeature, - Endpoint, -}; - -#[derive(Debug, Clone, Getters)] -#[getset(get = "pub")] -pub struct BaseDeviceDefinition { - /// Given name of the device this instance represents. - name: String, - /// Message attributes for this device instance. - features: Vec, -} - -impl BaseDeviceDefinition { - /// Create a new instance - pub fn new(name: &str, features: &[DeviceFeature]) -> Self { - Self { - name: name.to_owned(), - features: features.into(), - } - } -} - -#[derive(Serialize, Deserialize, Debug, Getters, CopyGetters, Default, Clone)] -pub struct UserDeviceCustomization { - #[serde(skip_serializing_if = "Option::is_none")] - #[serde(default)] - #[serde(rename = "display-name")] - #[getset(get = "pub")] - display_name: Option, - #[serde(default)] - #[getset(get_copy = "pub")] - allow: bool, - #[serde(default)] - #[getset(get_copy = "pub")] - deny: bool, - #[getset(get_copy = "pub")] - index: u32, -} - -impl UserDeviceCustomization { - pub fn new(display_name: &Option, allow: bool, deny: bool, index: u32) -> Self { - Self { - display_name: display_name.clone(), - allow, - deny, - index, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, Getters, Setters, MutGetters)] -#[getset(get = "pub", set = "pub", get_mut = "pub")] -pub struct UserDeviceDefinition { - /// Given name of the device this instance represents. - name: String, - /// Message attributes for this device instance. - features: Vec, - /// Per-user configurations specific to this device instance. - #[serde(rename = "user-config")] - user_config: UserDeviceCustomization, -} - -impl UserDeviceDefinition { - /// Create a new instance - pub fn new( - name: &str, - features: &[DeviceFeature], - user_config: &UserDeviceCustomization, - ) -> Self { - Self { - name: name.to_owned(), - features: features.into(), - user_config: user_config.clone(), - } - } - - pub fn new_from_base_definition(def: &BaseDeviceDefinition, index: u32) -> Self { - Self { - name: def.name().clone(), - features: def.features().clone(), - user_config: UserDeviceCustomization { - index: index, - ..Default::default() - }, - } - } - - pub fn add_raw_messages(&mut self, endpoints: &[Endpoint]) { - self - .features - .push(DeviceFeature::new_raw_feature(endpoints)); - } - - // Return true if any feature on this device handles this message. We'll deal with the actual - // feature indexing when the message itself is handled. - pub fn allows_message(&self, msg_type: &ButtplugDeviceMessageType) -> bool { - for feature in &self.features { - if let Ok(actuator_msg_type) = ButtplugActuatorFeatureMessageType::try_from(msg_type.clone()) - { - if let Some(actuator) = feature.actuator() { - if actuator.messages().contains(&actuator_msg_type) { - return true; - } - } - } else if let Ok(sensor_msg_type) = - ButtplugSensorFeatureMessageType::try_from(msg_type.clone()) - { - if let Some(sensor) = feature.sensor() { - if sensor.messages().contains(&sensor_msg_type) { - return true; - } - } - } else if let Ok(_) = ButtplugRawFeatureMessageType::try_from(msg_type.clone()) { - if let Some(_) = feature.raw() { - return true; - } - } - } - false - } -} diff --git a/buttplug/src/server/device/hardware/communication/lovense_dongle/lovense_serial_dongle_comm_manager.rs b/buttplug/src/server/device/hardware/communication/lovense_dongle/lovense_serial_dongle_comm_manager.rs deleted file mode 100644 index 7dd081b1d..000000000 --- a/buttplug/src/server/device/hardware/communication/lovense_dongle/lovense_serial_dongle_comm_manager.rs +++ /dev/null @@ -1,327 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 super::{ - lovense_dongle_messages::{ - LovenseDeviceCommand, - LovenseDongleIncomingMessage, - OutgoingLovenseData, - }, - lovense_dongle_state_machine::create_lovense_dongle_machine, -}; -use crate::{ - core::ButtplugResultFuture, - server::device::hardware::communication::{ - HardwareCommunicationManager, - HardwareCommunicationManagerBuilder, - HardwareCommunicationManagerEvent, - }, - util::async_manager, -}; -use futures::FutureExt; -use serde_json::Deserializer; -use serialport::{available_ports, SerialPort, SerialPortType}; -use std::{ - io::ErrorKind, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, - thread, - time::Duration, -}; -use tokio::{ - runtime, - sync::{ - mpsc::{channel, Receiver, Sender}, - Mutex, - }, -}; -use tokio_util::sync::CancellationToken; -use tracing_futures::Instrument; - -fn serial_write_thread( - mut port: Box, - mut receiver: Receiver, - token: CancellationToken, -) { - let rt = runtime::Builder::new_current_thread() - .build() - .expect("Should always build"); - let _guard = rt.enter(); - - let mut port_write = |mut data: String| { - data += "\r\n"; - debug!("Writing message: {}", data); - - // TODO WRITE SHOULD ALWAYS BE FOLLOWED BY A READ UNLESS "EAGER" IS USED - // - // We should check this on the outgoing message. Otherwise we will run into - // all sorts of trouble. - if let Err(e) = port.write_all(&data.into_bytes()) { - error!("Cannot write to port: {}", e); - } - }; - while let Some(data) = async_manager::block_on(async { - select! { - _ = token.cancelled().fuse() => None, - data = receiver.recv().fuse() => data - } - }) { - match data { - OutgoingLovenseData::Raw(s) => { - port_write(s); - } - OutgoingLovenseData::Message(m) => { - port_write( - serde_json::to_string(&m).expect("We create these packets so they'll always serialize."), - ); - } - } - } - debug!("Exiting lovense dongle write thread."); -} - -fn serial_read_thread( - mut port: Box, - sender: Sender, - token: CancellationToken, -) { - let mut data: String = String::default(); - while !token.is_cancelled() { - let mut buf: [u8; 1024] = [0; 1024]; - match port.read(&mut buf) { - Ok(len) => { - debug!("Got {} serial bytes", len); - data += std::str::from_utf8(&buf[0..len]) - .expect("We should always get valid data from the port."); - if data.contains('\n') { - debug!("Serial Buffer: {}", data); - - let sender_clone = sender.clone(); - let stream = Deserializer::from_str(&data).into_iter::(); - for msg in stream { - match msg { - Ok(m) => { - debug!("Read message: {:?}", m); - async_manager::block_on(async { - sender_clone - .send(m) - .await - .expect("Thread shouldn't be running if we don't have a listener.") - }); - } - Err(e) => { - error!("Error reading: {:?}", e); - /* - sender_clone - .send(IncomingLovenseData::Raw(incoming.clone().to_string())) - .await; - */ - } - } - } - - // TODO We don't seem to have an extra coming through at the moment, - // but might need this later? - data = String::default(); - } - } - Err(e) => { - if e.kind() == ErrorKind::TimedOut { - continue; - } - error!("{:?}", e); - break; - } - } - } - debug!("Exiting lovense dongle read thread."); -} - -#[derive(Default, Clone)] -pub struct LovenseSerialDongleCommunicationManagerBuilder {} - -impl HardwareCommunicationManagerBuilder for LovenseSerialDongleCommunicationManagerBuilder { - fn finish( - &mut self, - sender: Sender, - ) -> Box { - Box::new(LovenseSerialDongleCommunicationManager::new(sender)) - } -} - -pub struct LovenseSerialDongleCommunicationManager { - machine_sender: Sender, - //port: Arc>>>, - read_thread: Arc>>>, - write_thread: Arc>>>, - is_scanning: Arc, - thread_cancellation_token: CancellationToken, - dongle_available: Arc, -} - -impl LovenseSerialDongleCommunicationManager { - fn new(event_sender: Sender) -> Self { - trace!("Lovense dongle serial port created"); - let (machine_sender, machine_receiver) = channel(256); - let dongle_available = Arc::new(AtomicBool::new(false)); - let mgr = Self { - machine_sender, - read_thread: Arc::new(Mutex::new(None)), - write_thread: Arc::new(Mutex::new(None)), - is_scanning: Arc::new(AtomicBool::new(false)), - thread_cancellation_token: CancellationToken::new(), - dongle_available, - }; - let dongle_fut = mgr.find_dongle(); - // TODO If we don't find a dongle before scanning, what happens? - async_manager::spawn(async move { - if let Err(err) = dongle_fut.await { - error!("Error finding serial dongle: {:?}", err); - } - }); - let mut machine = - create_lovense_dongle_machine(event_sender, machine_receiver, mgr.is_scanning.clone()); - async_manager::spawn( - async move { - while let Some(next) = machine.transition().await { - machine = next; - } - } - .instrument(tracing::info_span!( - parent: tracing::Span::current(), - "Lovense Serial Dongle State Machine" - )), - ); - mgr - } - - fn find_dongle(&self) -> ButtplugResultFuture { - // First off, see if we can actually find a Lovense dongle. If we already - // have one, skip on to scanning. If we can't find one, send message to log - // and stop scanning. - - let machine_sender_clone = self.machine_sender.clone(); - let held_read_thread = self.read_thread.clone(); - let held_write_thread = self.write_thread.clone(); - let token = self.thread_cancellation_token.child_token(); - let dongle_available = self.dongle_available.clone(); - async move { - // TODO Does this block? Should it run in one of our threads? - let found_dongle = false; - match available_ports() { - Ok(ports) => { - debug!("Got {} serial ports back", ports.len()); - for p in ports { - if let SerialPortType::UsbPort(usb_info) = p.port_type { - // Hardcode the dongle VID/PID for now. We can't really do protocol - // detection here because this is a comm bus to us, not a device. - if usb_info.vid == 0x1a86 && usb_info.pid == 0x7523 { - // We've found a dongle. - info!("Found lovense dongle, connecting"); - let serial_port = - serialport::new(&p.port_name, 115200).timeout(Duration::from_millis(500)); - match serial_port.open() { - Ok(dongle_port) => { - let read_token = token.child_token(); - let write_token = token.child_token(); - let (writer_sender, writer_receiver) = channel(256); - let (reader_sender, reader_receiver) = channel(256); - let read_port = (*dongle_port) - .try_clone() - .expect("USB port should always clone."); - let read_thread = thread::Builder::new() - .name("Serial Reader Thread".to_string()) - .spawn(move || { - serial_read_thread(read_port, reader_sender, read_token); - }) - .expect("Thread should always create"); - let write_port = (*dongle_port) - .try_clone() - .expect("USB port should always clone."); - let write_thread = thread::Builder::new() - .name("Serial Writer Thread".to_string()) - .spawn(move || { - serial_write_thread(write_port, writer_receiver, write_token); - }) - .expect("Thread should always create"); - *(held_read_thread.lock().await) = Some(read_thread); - *(held_write_thread.lock().await) = Some(write_thread); - dongle_available.store(true, Ordering::SeqCst); - machine_sender_clone - .send(LovenseDeviceCommand::DongleFound( - writer_sender, - reader_receiver, - )) - .await - .expect("Machine exists if we got here."); - } - Err(e) => error!("{:?}", e), - }; - } - } - } - } - Err(_) => { - info!("No serial ports found"); - } - } - if !found_dongle { - warn!("Cannot find Lovense Serial dongle."); - } - Ok(()) - } - .instrument(tracing::info_span!("Lovense Serial Dongle Finder")) - .boxed() - } -} - -impl HardwareCommunicationManager for LovenseSerialDongleCommunicationManager { - fn name(&self) -> &'static str { - "LovenseSerialDongleCommunicationManager" - } - - fn start_scanning(&mut self) -> ButtplugResultFuture { - debug!("Lovense Dongle Manager scanning for devices."); - let sender = self.machine_sender.clone(); - async move { - sender - .send(LovenseDeviceCommand::StartScanning) - .await - .expect("If we're getting scan requests, we should a task to throw it at."); - Ok(()) - } - .boxed() - } - - fn stop_scanning(&mut self) -> ButtplugResultFuture { - let sender = self.machine_sender.clone(); - async move { - sender - .send(LovenseDeviceCommand::StopScanning) - .await - .expect("If we're getting scan requests, we should a task to throw it at."); - Ok(()) - } - .boxed() - } - - fn scanning_status(&self) -> bool { - self.is_scanning.load(Ordering::SeqCst) - } - - fn can_scan(&self) -> bool { - self.dongle_available.load(Ordering::SeqCst) - } -} - -impl Drop for LovenseSerialDongleCommunicationManager { - fn drop(&mut self) { - self.thread_cancellation_token.cancel(); - } -} diff --git a/buttplug/src/server/device/protocol/activejoy.rs b/buttplug/src/server/device/protocol/activejoy.rs deleted file mode 100644 index 1126b0a06..000000000 --- a/buttplug/src/server/device/protocol/activejoy.rs +++ /dev/null @@ -1,46 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, -}; - -generic_protocol_setup!(ActiveJoy, "activejoy"); - -#[derive(Default)] -pub struct ActiveJoy {} - -impl ProtocolHandler for ActiveJoy { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( - &self, - index: u32, - scalar: u32, - ) -> Result, ButtplugDeviceError> { - Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - [ - 0xb0, // static header - 0x01, // mode: 1=vibe, 5=shock, 6=thrust, 7=suction, 8=rotation, 16=swing, - 0x00, // strong mode = 1 (thrust, suction, swing, rotate) - index as u8, // 0 unless vibe2 - if scalar == 0 { 0x00 } else { 0x01 }, - scalar as u8, - ] - .to_vec(), - false, - ) - .into()]) - } -} diff --git a/buttplug/src/server/device/protocol/actuator_command_manager.rs b/buttplug/src/server/device/protocol/actuator_command_manager.rs deleted file mode 100644 index 30b65351b..000000000 --- a/buttplug/src/server/device/protocol/actuator_command_manager.rs +++ /dev/null @@ -1,568 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::core::{ - errors::{ButtplugDeviceError, ButtplugError}, - message::{ - ActuatorType, - ButtplugActuatorFeatureMessageType, - ButtplugDeviceCommandMessageUnion, - DeviceFeature, - DeviceFeatureActuator, - RotateCmdV4, - RotationSubcommandV4, - ScalarCmdV4, - ScalarSubcommandV4, - }, -}; -use ahash::{HashMap, HashMapExt}; -use getset::Getters; -use std::{ - collections::HashSet, - sync::atomic::{AtomicBool, AtomicU32, Ordering::Relaxed}, -}; - -// As of the last rewrite of the command manager, we're currently only tracking values of scalar and -// rotation commands. We can just use the rotation (AtomicU32, AtomicBool) pair for storage, and -// ignore the direction bool for Scalars. -#[derive(Getters)] -#[getset(get = "pub")] -struct FeatureStatus { - actuator_type: ActuatorType, - actuator: DeviceFeatureActuator, - sent: AtomicBool, - value: (AtomicU32, AtomicBool), -} - -impl FeatureStatus { - pub fn new(actuator_type: &ActuatorType, actuator: &DeviceFeatureActuator) -> Self { - Self { - actuator_type: *actuator_type, - actuator: actuator.clone(), - sent: AtomicBool::new(false), - value: (AtomicU32::new(0), AtomicBool::new(false)), - } - } - - pub fn current(&self) -> (ActuatorType, (u32, bool)) { - ( - self.actuator_type, - (self.value.0.load(Relaxed), self.value.1.load(Relaxed)), - ) - } - - pub fn messages(&self) -> &HashSet { - self.actuator.messages() - } - - pub fn update(&self, value: &(f64, bool)) -> Option<(u32, bool)> { - let mut result = None; - let range_start = *self.actuator.step_limit().start(); - let range = self.actuator.step_limit().end() - range_start; - let scalar_modifier = value.0 * range as f64; - let scalar = if scalar_modifier < 0.0001 { - 0 - } else { - // When calculating speeds, round up. This follows how we calculated - // things in buttplug-js and buttplug-csharp, so it's more for history - // than anything, but it's what users will expect. - ((scalar_modifier + range_start as f64).ceil() as u32).clamp( - *self.actuator.step_range().start(), - *self.actuator.step_range().end(), - ) - }; - trace!( - "{:?} {:?} {} {} {}", - self.actuator.step_range(), - self.actuator.step_limit(), - range, - scalar_modifier, - scalar - ); - // If we've already sent commands, we don't want to send them again, - // because some of our communication busses are REALLY slow. Make sure - // these values get None in our return vector. - let current = self.value.0.load(Relaxed); - let clockwise = self.value.1.load(Relaxed); - let sent = self.sent.load(Relaxed); - if !sent || scalar != current || clockwise != value.1 { - self.value.0.store(scalar, Relaxed); - self.value.1.store(value.1, Relaxed); - if !sent { - self.sent.store(true, Relaxed); - } - result = Some((scalar, value.1)); - } - result - } -} - -// In order to make our lives easier, we make some assumptions about what's internally mutable in -// the ActuatorCommandManager (ACM). Once the ACM is configured for a device, it won't change sizes, -// because we don't support things like adding motors to devices randomly while Buttplug is running. -// Therefore we know that we'll just be storing values like vibration/rotation speeds. We can assume -// our storage of those can stay immutable (the vec sizes won't change) and make their internals -// mutable. While this could be RefCell'd or whatever, they're also always atomic types (until the -// horrible day some sex toy decides to use floats in its protocol), so we can just use atomics and -// call it done. -pub struct ActuatorCommandManager { - feature_status: Vec, - stop_commands: Vec, -} - -impl ActuatorCommandManager { - pub fn new(features: &Vec) -> Self { - let mut stop_commands = vec![]; - - let mut statuses = vec![]; - let mut scalar_subcommands = vec![]; - let mut rotate_subcommands = vec![]; - for (index, feature) in features.iter().enumerate() { - if let Some(actuator) = feature.actuator() { - let actuator_type: ActuatorType = feature.feature_type().clone().try_into().unwrap(); - statuses.push(FeatureStatus::new(&actuator_type, actuator)); - if actuator - .messages() - .contains(&crate::core::message::ButtplugActuatorFeatureMessageType::RotateCmd) - { - rotate_subcommands.push(RotationSubcommandV4::new(index as u32, 0.0, false)); - } else if actuator - .messages() - .contains(&crate::core::message::ButtplugActuatorFeatureMessageType::ScalarCmd) - { - scalar_subcommands.push(ScalarSubcommandV4::new(index as u32, 0.0, actuator_type)); - } - } - } - if !scalar_subcommands.is_empty() { - stop_commands.push(ScalarCmdV4::new(0, scalar_subcommands).into()); - } - if !rotate_subcommands.is_empty() { - stop_commands.push(RotateCmdV4::new(0, rotate_subcommands).into()); - } - - Self { - feature_status: statuses, - stop_commands, - } - } - - fn update( - &self, - msg_type: ButtplugActuatorFeatureMessageType, - commands: &Vec<(u32, ActuatorType, (f64, bool))>, - match_all: bool, - ) -> Result, ButtplugError> { - // Convert from the generic 0.0-1.0 range to the StepCount attribute given by the device config. - - // If we've already sent commands before, we should check against our old values. Otherwise, we - // should always send whatever command we're going to send. - let mut result: Vec<(u32, ActuatorType, (u32, bool))> = vec![]; - - for command in commands { - if command.0 >= self.feature_status.len().try_into().unwrap() { - return Err( - ButtplugDeviceError::ProtocolRequirementError(format!( - "Command requests feature index {}, which does not exist.", - command.0, - )) - .into(), - ); - } - } - - for (index, cmd) in self.feature_status.iter().enumerate() { - let u32_index: u32 = index.try_into().unwrap(); - if let Some((_, cmd_actuator, cmd_value)) = commands.iter().find(|x| x.0 == u32_index) { - // By this point, we should have already checked whether the feature takes the message type. - if let Some(updated_value) = self.feature_status[index].update(cmd_value) { - result.push((u32_index, *cmd_actuator, updated_value)); - } else if match_all { - result.push((u32_index, *cmd.actuator_type(), cmd.current().1)); - } - } else if match_all { - if cmd.messages().contains(&msg_type) { - result.push((u32_index, *cmd.actuator_type(), cmd.current().1)); - } - } - } - // Return the command vector for the protocol to turn into proprietary commands - Ok(result) - } - - pub fn update_scalar( - &self, - msg: &ScalarCmdV4, - match_all: bool, - ) -> Result>, ButtplugError> { - // First, make sure this is a valid command, that contains at least one - // subcommand. - if msg.scalars().is_empty() { - return Err( - ButtplugDeviceError::ProtocolRequirementError( - "ScalarCmd has 0 commands, will not do anything.".to_owned(), - ) - .into(), - ); - } - - let mut idxs = HashMap::new(); - for (i, x) in self.feature_status.iter().enumerate() { - if x - .messages() - .contains(&ButtplugActuatorFeatureMessageType::ScalarCmd) - { - idxs.insert(i, idxs.len()); - } - } - - let mut final_result: Vec> = vec![None; idxs.len()]; - - let mut commands: Vec<(u32, ActuatorType, (f64, bool))> = vec![]; - msg - .scalars() - .iter() - .for_each(|x| commands.push((x.feature_index(), x.actuator_type(), (x.scalar(), false)))); - let mut result = self.update( - ButtplugActuatorFeatureMessageType::ScalarCmd, - &commands, - match_all, - )?; - result.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); - result.iter().for_each(|(index, actuator, value)| { - final_result[idxs[&(*index as usize)]] = Some((*actuator, value.0)) - }); - Ok(final_result) - } - - pub fn update_rotation( - &self, - msg: &RotateCmdV4, - match_all: bool, - ) -> Result>, ButtplugError> { - // First, make sure this is a valid command, that contains at least one - // command. - if msg.rotations().is_empty() { - return Err( - ButtplugDeviceError::ProtocolRequirementError( - "RotateCmd has 0 commands, will not do anything.".to_owned(), - ) - .into(), - ); - } - - let mut final_result: Vec> = vec![ - None; - self - .feature_status - .iter() - .filter(|x| x - .messages() - .contains(&ButtplugActuatorFeatureMessageType::RotateCmd)) - .count() - ]; - - let mut commands: Vec<(u32, ActuatorType, (f64, bool))> = vec![]; - msg.rotations().iter().for_each(|x| { - commands.push(( - x.feature_index(), - ActuatorType::Rotate, - (x.speed(), x.clockwise()), - )) - }); - let mut result = self.update( - ButtplugActuatorFeatureMessageType::RotateCmd, - &commands, - match_all, - )?; - result.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); - result - .iter() - .enumerate() - .for_each(|(array_index, (_, _, value))| final_result[array_index] = Some(*value)); - Ok(final_result) - } - - pub fn stop_commands(&self) -> Vec { - self.stop_commands.clone() - } -} -/* -#[cfg(test)] -mod test { - use super::{GenericCommandManager, ProtocolDeviceAttributes}; - use crate::{ - core::message::{ActuatorType, RotateCmd, RotationSubcommand, ScalarCmd, ScalarSubcommand}, - server::device::configuration::{ - ServerDeviceMessageAttributesBuilder, - ServerGenericDeviceMessageAttributes, - }, - }; - use std::ops::RangeInclusive; - - #[test] - pub fn test_command_generator_vibration() { - let scalar_attrs = ServerGenericDeviceMessageAttributes::new( - "Test", - &RangeInclusive::new(0, 20), - ActuatorType::Vibrate, - ); - let scalar_attributes = ServerDeviceMessageAttributesBuilder::default() - .scalar_cmd(&vec![scalar_attrs.clone(), scalar_attrs]) - .finish(); - let device_attributes = ProtocolDeviceAttributes::new("Whatever", &None, &scalar_attributes); - let mgr = GenericCommandManager::new(&device_attributes); - let vibrate_msg = ScalarCmd::new( - 0, - vec![ - ScalarSubcommand::new(0, 0.5, ActuatorType::Vibrate), - ScalarSubcommand::new(1, 0.5, ActuatorType::Vibrate), - ], - ); - assert_eq!( - mgr - .update_scalar(&vibrate_msg, false) - .expect("Test, assuming infallible"), - vec![ - Some((ActuatorType::Vibrate, 10)), - Some((ActuatorType::Vibrate, 10)) - ] - ); - assert_eq!( - mgr - .update_scalar(&vibrate_msg, false) - .expect("Test, assuming infallible"), - vec![] - ); - let vibrate_msg_2 = ScalarCmd::new( - 0, - vec![ - ScalarSubcommand::new(0, 0.5, ActuatorType::Vibrate), - ScalarSubcommand::new(1, 0.75, ActuatorType::Vibrate), - ], - ); - assert_eq!( - mgr - .update_scalar(&vibrate_msg_2, false) - .expect("Test, assuming infallible"), - vec![None, Some((ActuatorType::Vibrate, 15))] - ); - let vibrate_msg_invalid = ScalarCmd::new( - 0, - vec![ScalarSubcommand::new(2, 0.5, ActuatorType::Vibrate)], - ); - assert!(mgr.update_scalar(&vibrate_msg_invalid, false).is_err()); - - assert_eq!( - mgr.scalars(), - vec![ - Some((ActuatorType::Vibrate, 10)), - Some((ActuatorType::Vibrate, 15)) - ] - ); - } - - #[test] - pub fn test_command_generator_vibration_match_all() { - let scalar_attrs = ServerGenericDeviceMessageAttributes::new( - "Test", - &RangeInclusive::new(0, 20), - ActuatorType::Vibrate, - ); - let scalar_attributes = ServerDeviceMessageAttributesBuilder::default() - .scalar_cmd(&vec![scalar_attrs.clone(), scalar_attrs]) - .finish(); - let device_attributes = ProtocolDeviceAttributes::new("Whatever", &None, &scalar_attributes); - let mgr = GenericCommandManager::new(&device_attributes); - let vibrate_msg = ScalarCmd::new( - 0, - vec![ - ScalarSubcommand::new(0, 0.5, ActuatorType::Vibrate), - ScalarSubcommand::new(1, 0.5, ActuatorType::Vibrate), - ], - ); - assert_eq!( - mgr - .update_scalar(&vibrate_msg, true) - .expect("Test, assuming infallible"), - vec![ - Some((ActuatorType::Vibrate, 10)), - Some((ActuatorType::Vibrate, 10)) - ] - ); - assert_eq!( - mgr - .update_scalar(&vibrate_msg, true) - .expect("Test, assuming infallible"), - vec![] - ); - let vibrate_msg_2 = ScalarCmd::new( - 0, - vec![ - ScalarSubcommand::new(0, 0.5, ActuatorType::Vibrate), - ScalarSubcommand::new(1, 0.75, ActuatorType::Vibrate), - ], - ); - assert_eq!( - mgr - .update_scalar(&vibrate_msg_2, true) - .expect("Test, assuming infallible"), - vec![ - Some((ActuatorType::Vibrate, 10)), - Some((ActuatorType::Vibrate, 15)) - ] - ); - let vibrate_msg_invalid = ScalarCmd::new( - 0, - vec![ScalarSubcommand::new(2, 0.5, ActuatorType::Vibrate)], - ); - assert!(mgr.update_scalar(&vibrate_msg_invalid, false).is_err()); - - assert_eq!( - mgr.scalars(), - vec![ - Some((ActuatorType::Vibrate, 10)), - Some((ActuatorType::Vibrate, 15)) - ] - ); - } - - #[test] - pub fn test_command_generator_vibration_step_range() { - let mut vibrate_attrs_1 = ServerGenericDeviceMessageAttributes::new( - "Test", - &RangeInclusive::new(0, 20), - ActuatorType::Vibrate, - ); - vibrate_attrs_1.set_step_range(RangeInclusive::new(10, 15)); - let mut vibrate_attrs_2 = ServerGenericDeviceMessageAttributes::new( - "Test", - &RangeInclusive::new(0, 20), - ActuatorType::Vibrate, - ); - vibrate_attrs_2.set_step_range(RangeInclusive::new(10, 20)); - - let vibrate_attributes = ServerDeviceMessageAttributesBuilder::default() - .scalar_cmd(&vec![vibrate_attrs_1, vibrate_attrs_2]) - .finish(); - let device_attributes = ProtocolDeviceAttributes::new("Whatever", &None, &vibrate_attributes); - let mgr = GenericCommandManager::new(&device_attributes); - let vibrate_msg = ScalarCmd::new( - 0, - vec![ - ScalarSubcommand::new(0, 0.5, ActuatorType::Vibrate), - ScalarSubcommand::new(1, 0.5, ActuatorType::Vibrate), - ], - ); - assert_eq!( - mgr - .update_scalar(&vibrate_msg, false) - .expect("Test, assuming infallible"), - vec![ - Some((ActuatorType::Vibrate, 13)), - Some((ActuatorType::Vibrate, 15)) - ] - ); - assert_eq!( - mgr - .update_scalar(&vibrate_msg, false) - .expect("Test, assuming infallible"), - vec![] - ); - let vibrate_msg_2 = ScalarCmd::new( - 0, - vec![ - ScalarSubcommand::new(0, 0.5, ActuatorType::Vibrate), - ScalarSubcommand::new(1, 0.75, ActuatorType::Vibrate), - ], - ); - assert_eq!( - mgr - .update_scalar(&vibrate_msg_2, false) - .expect("Test, assuming infallible"), - vec![None, Some((ActuatorType::Vibrate, 18))] - ); - let vibrate_msg_invalid = ScalarCmd::new( - 0, - vec![ScalarSubcommand::new(2, 0.5, ActuatorType::Vibrate)], - ); - assert!(mgr.update_scalar(&vibrate_msg_invalid, false).is_err()); - - assert_eq!( - mgr.scalars(), - vec![ - Some((ActuatorType::Vibrate, 13)), - Some((ActuatorType::Vibrate, 18)) - ] - ); - } - - #[test] - pub fn test_command_generator_rotation() { - let rotate_attrs = ServerGenericDeviceMessageAttributes::new( - "Test", - &RangeInclusive::new(0, 20), - ActuatorType::Rotate, - ); - - let rotate_attributes = ServerDeviceMessageAttributesBuilder::default() - .rotate_cmd(&vec![rotate_attrs.clone(), rotate_attrs]) - .finish(); - let device_attributes = ProtocolDeviceAttributes::new("Whatever", &None, &rotate_attributes); - let mgr = GenericCommandManager::new(&device_attributes); - - let rotate_msg = RotateCmd::new( - 0, - vec![ - RotationSubcommand::new(0, 0.5, true), - RotationSubcommand::new(1, 0.5, true), - ], - ); - assert_eq!( - mgr - .update_rotation(&rotate_msg, false) - .expect("Test, assuming infallible"), - vec![Some((10, true)), Some((10, true))] - ); - assert_eq!( - mgr - .update_rotation(&rotate_msg, false) - .expect("Test, assuming infallible"), - vec![None, None] - ); - let rotate_msg_2 = RotateCmd::new( - 0, - vec![ - RotationSubcommand::new(0, 0.5, true), - RotationSubcommand::new(1, 0.75, false), - ], - ); - assert_eq!( - mgr - .update_rotation(&rotate_msg_2, false) - .expect("Test, assuming infallible"), - vec![None, Some((15, false))] - ); - let rotate_msg_3 = RotateCmd::new( - 0, - vec![ - RotationSubcommand::new(0, 0.75, false), - RotationSubcommand::new(1, 0.75, false), - ], - ); - assert_eq!( - mgr - .update_rotation(&rotate_msg_3, true) - .expect("Test, assuming infallible"), - vec![Some((15, false)), Some((15, false))] - ); - let rotate_msg_invalid = RotateCmd::new(0, vec![RotationSubcommand::new(2, 0.5, true)]); - assert!(mgr.update_rotation(&rotate_msg_invalid, false).is_err()); - } - // TODO Write test for vibration stop generator -} -*/ diff --git a/buttplug/src/server/device/protocol/bananasome.rs b/buttplug/src/server/device/protocol/bananasome.rs deleted file mode 100644 index 3b58cbc3a..000000000 --- a/buttplug/src/server/device/protocol/bananasome.rs +++ /dev/null @@ -1,61 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2025 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 crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ - ActuatorType, - ActuatorType::{Oscillate, Vibrate}, - Endpoint, - }, - }, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, -}; - -generic_protocol_setup!(Bananasome, "bananasome"); - -#[derive(Default)] -pub struct Bananasome {} - -impl ProtocolHandler for Bananasome { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn needs_full_command_set(&self) -> bool { - true - } - fn handle_scalar_cmd( - &self, - commands: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - vec![ - 0xa0, - 0x03, - commands[0].unwrap_or((Oscillate, 0)).1 as u8, - if commands.len() > 1 { - commands[1].unwrap_or((Vibrate, 0)).1 - } else { - 0 - } as u8, - if commands.len() > 2 { - commands[2].unwrap_or((Vibrate, 0)).1 - } else { - 0 - } as u8, - ], - false, - ) - .into()]) - } -} diff --git a/buttplug/src/server/device/protocol/buttplug_passthru.rs b/buttplug/src/server/device/protocol/buttplug_passthru.rs deleted file mode 100644 index 2172f4b33..000000000 --- a/buttplug/src/server/device/protocol/buttplug_passthru.rs +++ /dev/null @@ -1,47 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ButtplugDeviceCommandMessageUnion, Endpoint}, - }, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, -}; - -generic_protocol_setup!(ButtplugPassthru, "buttplug-passthru"); - -#[derive(Default)] -struct ButtplugPassthru {} - -impl ProtocolHandler for ButtplugPassthru { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn has_handle_message(&self) -> bool { - true - } - - fn handle_message( - &self, - command_message: &ButtplugDeviceCommandMessageUnion, - ) -> Result, ButtplugDeviceError> { - Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - serde_json::to_string(&command_message) - .expect("Type is always serializable") - .as_bytes() - .to_vec(), - false, - ) - .into()]) - } -} diff --git a/buttplug/src/server/device/protocol/cowgirl.rs b/buttplug/src/server/device/protocol/cowgirl.rs deleted file mode 100644 index a516031a0..000000000 --- a/buttplug/src/server/device/protocol/cowgirl.rs +++ /dev/null @@ -1,77 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::core::errors::ButtplugDeviceError::ProtocolSpecificError; -use crate::core::message::ActuatorType; -use crate::core::message::ActuatorType::{Rotate, Vibrate}; -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, -}; - -generic_protocol_setup!(Cowgirl, "cowgirl"); - -#[derive(Default)] -pub struct Cowgirl {} - -impl ProtocolHandler for Cowgirl { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn needs_full_command_set(&self) -> bool { - true - } - - fn handle_scalar_cmd( - &self, - commands: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - let mut data: Vec = vec![0x00, 0x01]; - if commands.len() != 2 { - return Err(ProtocolSpecificError( - "cowgirl".to_owned(), - format!("Expected 2 attributes, got {}", commands.len()), - )); - } - - if let Some(cmd) = commands[0] { - if cmd.0 != Vibrate { - return Err(ProtocolSpecificError( - "cowgirl".to_owned(), - format!("Expected Vibrate attribute, got {:?}", cmd.0), - )); - } - data.push(cmd.1 as u8); - } else { - return Err(ProtocolSpecificError( - "cowgirl".to_owned(), - "Attribute 0 is None".to_owned(), - )); - } - - if let Some(cmd) = commands[1] { - if cmd.0 != Rotate { - return Err(ProtocolSpecificError( - "cowgirl".to_owned(), - format!("Expected Rotate attribute, got {:?}", cmd.0), - )); - } - data.push(cmd.1 as u8); - } else { - return Err(ProtocolSpecificError( - "cowgirl".to_owned(), - "Attribute 1 is None".to_owned(), - )); - } - - Ok(vec![HardwareWriteCmd::new(Endpoint::Tx, data, true).into()]) - } -} diff --git a/buttplug/src/server/device/protocol/feelingso.rs b/buttplug/src/server/device/protocol/feelingso.rs deleted file mode 100644 index fae8d10b8..000000000 --- a/buttplug/src/server/device/protocol/feelingso.rs +++ /dev/null @@ -1,60 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, - }, - generic_protocol_setup, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::ProtocolHandler, - }, -}; - -generic_protocol_setup!(FeelingSo, "feelingso"); - -#[derive(Default)] -pub struct FeelingSo {} - -impl ProtocolHandler for FeelingSo { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn needs_full_command_set(&self) -> bool { - true - } - - fn handle_scalar_cmd( - &self, - commands: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - let cmd1 = commands[0]; - let cmd2 = if commands.len() > 1 { - commands[1] - } else { - None - }; - - Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - vec![ - 0xaa, - 0x40, - 0x03, - cmd1.unwrap_or((ActuatorType::Vibrate, 0)).1 as u8, - cmd2.unwrap_or((ActuatorType::Oscillate, 0)).1 as u8, - 0x14, // Oscillate range: 1 to 4 - 0x19, // Checksum? - ], - false, - ) - .into()]) - } -} diff --git a/buttplug/src/server/device/protocol/galaku_pump.rs b/buttplug/src/server/device/protocol/galaku_pump.rs deleted file mode 100644 index 0750aa37f..000000000 --- a/buttplug/src/server/device/protocol/galaku_pump.rs +++ /dev/null @@ -1,75 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2023 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 crate::core::errors::ButtplugDeviceError::ProtocolSpecificError; -use crate::core::message::ActuatorType; -use crate::core::message::ActuatorType::{Oscillate, Vibrate}; -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, -}; -use std::num::Wrapping; - -static KEY_TAB: [[u8; 12]; 4] = [ - [0, 24, 0x98, 0xf7, 0xa5, 61, 13, 41, 37, 80, 68, 70], - [0, 69, 110, 106, 111, 120, 32, 83, 45, 49, 46, 55], - [0, 101, 120, 32, 84, 111, 121, 115, 10, 0x8e, 0x9d, 0xa3], - [0, 0xc5, 0xd6, 0xe7, 0xf8, 10, 50, 32, 111, 98, 13, 10], -]; - -generic_protocol_setup!(GalakuPump, "galaku-pump"); - -#[derive(Default)] -pub struct GalakuPump {} - -impl ProtocolHandler for GalakuPump { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn needs_full_command_set(&self) -> bool { - true - } - - fn handle_scalar_cmd( - &self, - commands: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - if commands.len() != 2 { - return Err(ProtocolSpecificError( - "galaku-pump".to_owned(), - format!("Expected 2 attributes, got {}", commands.len()), - )); - } - - let mut data: Vec = vec![ - 0x23, - 0x5a, - 0x00, - 0x00, - 0x01, - 0x60, - 0x03, - commands[0].unwrap_or((Oscillate, 0)).1 as u8, - commands[1].unwrap_or((Vibrate, 0)).1 as u8, - 0x00, - 0x00, - ]; - data.push(data.iter().fold(0u8, |c, b| (Wrapping(c) + Wrapping(*b)).0)); - - let mut data2: Vec = vec![0x23]; - for i in 1..data.len() { - let k = KEY_TAB[(data2[i - 1] & 3) as usize][i]; - data2.push((Wrapping((k ^ 0x23) ^ data[i]) + Wrapping(k)).0); - } - - Ok(vec![HardwareWriteCmd::new(Endpoint::Tx, data2, true).into()]) - } -} diff --git a/buttplug/src/server/device/protocol/htk_bm.rs b/buttplug/src/server/device/protocol/htk_bm.rs deleted file mode 100644 index ec7f8bd11..000000000 --- a/buttplug/src/server/device/protocol/htk_bm.rs +++ /dev/null @@ -1,49 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, - }, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, -}; - -generic_protocol_setup!(HtkBm, "htk_bm"); - -#[derive(Default)] -pub struct HtkBm {} - -impl ProtocolHandler for HtkBm { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_cmd( - &self, - cmds: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - let mut cmd_vec = vec![]; - if cmds.len() == 2 { - let mut data: u8 = 15; - let left = cmds[0].unwrap_or((ActuatorType::Vibrate, 0)).1; - let right = cmds[1].unwrap_or((ActuatorType::Vibrate, 0)).1; - if left != 0 && right != 0 { - data = 11 // both (normal mode) - } else if left != 0 { - data = 12 // left only - } else if right != 0 { - data = 13 // right only - } - cmd_vec.push(HardwareWriteCmd::new(Endpoint::Tx, vec![data], false).into()); - } - Ok(cmd_vec) - } -} diff --git a/buttplug/src/server/device/protocol/itoys.rs b/buttplug/src/server/device/protocol/itoys.rs deleted file mode 100644 index 804f6b5cc..000000000 --- a/buttplug/src/server/device/protocol/itoys.rs +++ /dev/null @@ -1,38 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, -}; - -generic_protocol_setup!(IToys, "itoys"); - -#[derive(Default)] -pub struct IToys {} - -impl ProtocolHandler for IToys { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( - &self, - _index: u32, - scalar: u32, - ) -> Result, ButtplugDeviceError> { - Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - vec![0xa0, 0x01, 0x00, 0x00, scalar as u8, 0xff], - false, - ) - .into()]) - } -} diff --git a/buttplug/src/server/device/protocol/jejoue.rs b/buttplug/src/server/device/protocol/jejoue.rs deleted file mode 100644 index 5d91cc266..000000000 --- a/buttplug/src/server/device/protocol/jejoue.rs +++ /dev/null @@ -1,61 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, - }, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, -}; - -generic_protocol_setup!(JeJoue, "jejoue"); - -#[derive(Default)] -pub struct JeJoue {} - -impl ProtocolHandler for JeJoue { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_cmd( - &self, - cmds: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - // Store off result before the match, so we drop the lock ASAP. - // Default to both vibes - let mut pattern: u8 = 1; - - // Use vibe 1 as speed - let mut speed = cmds[0].unwrap_or((ActuatorType::Vibrate, 0)).1 as u8; - - // Unless it's zero, then five vibe 2 a chance - if speed == 0 { - speed = cmds[1].unwrap_or((ActuatorType::Vibrate, 0)).1 as u8; - - // If we've vibing on 2 only, then change the pattern - if speed != 0 { - pattern = 3; - } - } - - // If we've vibing on 1 only, then change the pattern - if pattern == 1 && speed != 0 && cmds[1].unwrap_or((ActuatorType::Vibrate, 0)).1 == 0 { - pattern = 2; - } - Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - vec![pattern, speed], - false, - ) - .into()]) - } -} diff --git a/buttplug/src/server/device/protocol/joyhub.rs b/buttplug/src/server/device/protocol/joyhub.rs deleted file mode 100644 index e0b8ff813..000000000 --- a/buttplug/src/server/device/protocol/joyhub.rs +++ /dev/null @@ -1,193 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::server::device::configuration::ProtocolCommunicationSpecifier; -use crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, - }, - generic_protocol_initializer_setup, - server::device::{ - configuration::{UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ProtocolHandler, ProtocolIdentifier, ProtocolInitializer}, - }, - util::{async_manager, sleep}, -}; -use async_trait::async_trait; -use std::sync::{Arc, RwLock}; -use std::time::Duration; - -generic_protocol_initializer_setup!(JoyHub, "joyhub"); - -async fn delayed_constrict_handler(device: Arc, scalar: u8) { - sleep(Duration::from_millis(25)).await; - let res = device - .write_value(&HardwareWriteCmd::new( - Endpoint::Tx, - vec![ - 0xa0, - 0x07, - if scalar == 0 { 0x00 } else { 0x01 }, - 0x00, - scalar, - 0xff, - ], - false, - )) - .await; - if res.is_err() { - error!("Delayed JoyHub Constrict command error: {:?}", res.err()); - } -} - -fn vibes_changed( - old_commands_lock: &RwLock>>, - new_commands: &[Option<(ActuatorType, u32)>], - exclude: Vec, -) -> bool { - let old_commands = old_commands_lock.read().expect("locks should work"); - if old_commands.len() != new_commands.len() { - return true; - } - - for i in 0..old_commands.len() { - if exclude.contains(&i) { - continue; - } - if let Some(ocmd) = old_commands[i] { - if let Some(ncmd) = new_commands[i] { - if ocmd.1 != ncmd.1 { - return true; - } - } - } - } - false -} - -fn scalar_changed( - old_commands_lock: &RwLock>>, - new_commands: &[Option<(ActuatorType, u32)>], - index: usize, -) -> bool { - let old_commands = old_commands_lock.read().expect("locks should work"); - if old_commands.len() != new_commands.len() { - return true; - } - - if index < old_commands.len() { - if let Some(ocmd) = old_commands[index] { - if let Some(ncmd) = new_commands[index] { - if ocmd.1 != ncmd.1 { - return true; - } - } - } - } - false -} - -#[derive(Default)] -pub struct JoyHubInitializer {} - -#[async_trait] -impl ProtocolInitializer for JoyHubInitializer { - async fn initialize( - &mut self, - hardware: Arc, - _: &UserDeviceDefinition, - ) -> Result, ButtplugDeviceError> { - Ok(Arc::new(JoyHub::new(hardware))) - } -} - -pub struct JoyHub { - device: Arc, - last_cmds: RwLock>>, -} - -impl JoyHub { - fn new(device: Arc) -> Self { - let last_cmds = RwLock::new(vec![]); - Self { device, last_cmds } - } -} - -impl ProtocolHandler for JoyHub { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn needs_full_command_set(&self) -> bool { - true - } - - fn handle_scalar_cmd( - &self, - commands: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - let cmd1 = commands[0]; - let mut cmd2 = if commands.len() > 1 { - commands[1] - } else { - None - }; - let cmd3 = if commands.len() > 2 { - commands[2] - } else { - None - }; - - if let Some(cmd) = cmd2 { - if cmd.0 == ActuatorType::Constrict { - cmd2 = None; - if !scalar_changed(&self.last_cmds, commands, 1usize) { - // no-op - } else if vibes_changed(&self.last_cmds, commands, vec![1usize]) { - let dev = self.device.clone(); - async_manager::spawn(async move { delayed_constrict_handler(dev, cmd.1 as u8).await }); - } else { - let mut command_writer = self.last_cmds.write().expect("Locks should work"); - *command_writer = commands.to_vec(); - - return Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - vec![ - 0xa0, - 0x07, - if cmd.1 == 0 { 0x00 } else { 0x01 }, - 0x00, - cmd.1 as u8, - 0xff, - ], - false, - ) - .into()]); - } - } - } - - let mut command_writer = self.last_cmds.write().expect("Locks should work"); - *command_writer = commands.to_vec(); - Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - vec![ - 0xa0, - 0x03, - cmd1.unwrap_or((ActuatorType::Oscillate, 0)).1 as u8, - cmd3.unwrap_or((ActuatorType::Rotate, 0)).1 as u8, - cmd2.unwrap_or((ActuatorType::Oscillate, 0)).1 as u8, - 0x00, - 0xaa, - ], - false, - ) - .into()]) - } -} diff --git a/buttplug/src/server/device/protocol/joyhub_v2.rs b/buttplug/src/server/device/protocol/joyhub_v2.rs deleted file mode 100644 index c61e23996..000000000 --- a/buttplug/src/server/device/protocol/joyhub_v2.rs +++ /dev/null @@ -1,201 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::server::device::configuration::ProtocolCommunicationSpecifier; -use crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, - }, - generic_protocol_initializer_setup, - server::device::{ - configuration::{UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ProtocolHandler, ProtocolIdentifier, ProtocolInitializer}, - }, - util::{async_manager, sleep}, -}; -use async_trait::async_trait; -use std::sync::{Arc, RwLock}; -use std::time::Duration; - -generic_protocol_initializer_setup!(JoyHubV2, "joyhub-v2"); - -async fn delayed_constrict_handler(device: Arc, scalar: u8) { - sleep(Duration::from_millis(50)).await; - let res = device - .write_value(&HardwareWriteCmd::new( - Endpoint::Tx, - vec![0xa0, 0x0d, 0x00, 0x00, scalar, 0xff], - false, - )) - .await; - if res.is_err() { - error!("Delayed JoyHub Constrict command error: {:?}", res.err()); - } -} - -fn vibes_changed( - old_commands_lock: &RwLock>>, - new_commands: &[Option<(ActuatorType, u32)>], - exclude: Vec, -) -> bool { - let old_commands = old_commands_lock.read().expect("locks should work"); - if old_commands.len() != new_commands.len() { - return true; - } - - for i in 0..old_commands.len() { - if exclude.contains(&i) { - continue; - } - if let Some(ocmd) = old_commands[i] { - if let Some(ncmd) = new_commands[i] { - if ocmd.1 != ncmd.1 { - return true; - } - } - } - } - false -} - -fn scalar_changed( - old_commands_lock: &RwLock>>, - new_commands: &[Option<(ActuatorType, u32)>], - index: usize, -) -> bool { - let old_commands = old_commands_lock.read().expect("locks should work"); - if old_commands.len() != new_commands.len() { - return true; - } - - if index < old_commands.len() { - if let Some(ocmd) = old_commands[index] { - if let Some(ncmd) = new_commands[index] { - if ocmd.1 != ncmd.1 { - return true; - } - } - } - } - false -} - -#[derive(Default)] -pub struct JoyHubV2Initializer {} - -#[async_trait] -impl ProtocolInitializer for JoyHubV2Initializer { - async fn initialize( - &mut self, - hardware: Arc, - _: &UserDeviceDefinition, - ) -> Result, ButtplugDeviceError> { - Ok(Arc::new(JoyHubV2::new(hardware))) - } -} - -pub struct JoyHubV2 { - device: Arc, - last_cmds: RwLock>>, -} - -impl JoyHubV2 { - fn new(device: Arc) -> Self { - let last_cmds = RwLock::new(vec![]); - Self { device, last_cmds } - } -} - -impl ProtocolHandler for JoyHubV2 { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn needs_full_command_set(&self) -> bool { - true - } - - fn handle_scalar_cmd( - &self, - commands: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - let cmd1 = commands[0]; - let mut cmd2 = if commands.len() > 1 { - commands[1] - } else { - None - }; - let mut cmd3 = if commands.len() > 2 { - commands[2] - } else { - None - }; - - if let Some(cmd) = cmd2 { - if cmd.0 == ActuatorType::Constrict { - cmd2 = None; - if !scalar_changed(&self.last_cmds, commands, 1usize) { - // no-op - } else if vibes_changed(&self.last_cmds, commands, vec![1usize]) { - let dev = self.device.clone(); - async_manager::spawn(async move { delayed_constrict_handler(dev, cmd.1 as u8).await }); - } else { - let mut command_writer = self.last_cmds.write().expect("Locks should work"); - *command_writer = commands.to_vec(); - - return Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - vec![0xa0, 0x0d, 0x00, 0x00, cmd.1 as u8, 0xff], - false, - ) - .into()]); - } - } - } - - if let Some(cmd) = cmd3 { - if cmd.0 == ActuatorType::Constrict { - cmd3 = None; - if !scalar_changed(&self.last_cmds, commands, 2usize) { - // no-op - } else if vibes_changed(&self.last_cmds, commands, vec![2usize]) { - let dev = self.device.clone(); - async_manager::spawn(async move { delayed_constrict_handler(dev, cmd.1 as u8).await }); - } else { - let mut command_writer = self.last_cmds.write().expect("Locks should work"); - *command_writer = commands.to_vec(); - - return Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - vec![0xa0, 0x0d, 0x00, 0x00, cmd.1 as u8, 0xff], - false, - ) - .into()]); - } - } - } - - let mut command_writer = self.last_cmds.write().expect("Locks should work"); - *command_writer = commands.to_vec(); - Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - vec![ - 0xa0, - 0x03, - cmd1.unwrap_or((ActuatorType::Vibrate, 0)).1 as u8, - cmd2.unwrap_or((ActuatorType::Rotate, 0)).1 as u8, - cmd3.unwrap_or((ActuatorType::Oscillate, 0)).1 as u8, - 0x00, - 0xaa, - ], - false, - ) - .into()]) - } -} diff --git a/buttplug/src/server/device/protocol/joyhub_v3.rs b/buttplug/src/server/device/protocol/joyhub_v3.rs deleted file mode 100644 index 177ad0354..000000000 --- a/buttplug/src/server/device/protocol/joyhub_v3.rs +++ /dev/null @@ -1,54 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, - }, - generic_protocol_setup, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::ProtocolHandler, - }, -}; - -generic_protocol_setup!(JoyHubV3, "joyhub-v3"); - -#[derive(Default)] -pub struct JoyHubV3 {} - -impl ProtocolHandler for JoyHubV3 { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn needs_full_command_set(&self) -> bool { - true - } - - fn handle_scalar_cmd( - &self, - commands: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - let cmd1 = commands[0]; - Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - vec![ - 0xa0, - 0x03, - 0x00, - 0x00, - 0x00, - cmd1.unwrap_or((ActuatorType::Vibrate, 0)).1 as u8, - 0xaa, - ], - false, - ) - .into()]) - } -} diff --git a/buttplug/src/server/device/protocol/joyhub_v4.rs b/buttplug/src/server/device/protocol/joyhub_v4.rs deleted file mode 100644 index 945b938ac..000000000 --- a/buttplug/src/server/device/protocol/joyhub_v4.rs +++ /dev/null @@ -1,193 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, - }, - generic_protocol_initializer_setup, - server::device::{ - configuration::UserDeviceIdentifier, - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ProtocolHandler, ProtocolIdentifier, ProtocolInitializer}, - }, - util::{async_manager, sleep}, -}; -use async_trait::async_trait; -use std::sync::{Arc, RwLock}; -use std::time::Duration; - -generic_protocol_initializer_setup!(JoyHubV4, "joyhub-v4"); - -async fn delayed_constrict_handler(device: Arc, scalar: u8) { - sleep(Duration::from_millis(25)).await; - let res = device - .write_value(&HardwareWriteCmd::new( - Endpoint::Tx, - vec![ - 0xa0, - 0x07, - if scalar == 0 { 0x00 } else { 0x01 }, - 0x00, - scalar, - 0xff, - ], - false, - )) - .await; - if res.is_err() { - error!("Delayed JoyHub Constrict command error: {:?}", res.err()); - } -} -fn vibes_changed( - old_commands_lock: &RwLock>>, - new_commands: &[Option<(ActuatorType, u32)>], - exclude: Vec, -) -> bool { - let old_commands = old_commands_lock.read().expect("locks should work"); - if old_commands.len() != new_commands.len() { - return true; - } - - for i in 0..old_commands.len() { - if exclude.contains(&i) { - continue; - } - if let Some(ocmd) = old_commands[i] { - if let Some(ncmd) = new_commands[i] { - if ocmd.1 != ncmd.1 { - return true; - } - } - } - } - false -} - -fn scalar_changed( - old_commands_lock: &RwLock>>, - new_commands: &[Option<(ActuatorType, u32)>], - index: usize, -) -> bool { - let old_commands = old_commands_lock.read().expect("locks should work"); - if old_commands.len() != new_commands.len() { - return true; - } - - if index < old_commands.len() { - if let Some(ocmd) = old_commands[index] { - if let Some(ncmd) = new_commands[index] { - if ocmd.1 != ncmd.1 { - return true; - } - } - } - } - false -} - -#[derive(Default)] -pub struct JoyHubV4Initializer {} - -#[async_trait] -impl ProtocolInitializer for JoyHubV4Initializer { - async fn initialize( - &mut self, - hardware: Arc, - _: &UserDeviceDefinition, - ) -> Result, ButtplugDeviceError> { - Ok(Arc::new(JoyHubV4::new(hardware))) - } -} - -pub struct JoyHubV4 { - device: Arc, - last_cmds: RwLock>>, -} - -impl JoyHubV4 { - fn new(device: Arc) -> Self { - let last_cmds = RwLock::new(vec![]); - Self { device, last_cmds } - } -} - -impl ProtocolHandler for JoyHubV4 { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn needs_full_command_set(&self) -> bool { - true - } - - fn handle_scalar_cmd( - &self, - commands: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - let cmd1 = commands[0]; - let cmd2 = if commands.len() > 1 { - commands[1] - } else { - None - }; - let mut cmd3 = if commands.len() > 2 { - commands[2] - } else { - None - }; - - if let Some(cmd) = cmd3 { - if cmd.0 == ActuatorType::Constrict { - cmd3 = None; - if !scalar_changed(&self.last_cmds, commands, 2usize) { - // no-op - } else if vibes_changed(&self.last_cmds, commands, vec![2usize]) { - let dev = self.device.clone(); - async_manager::spawn(async move { delayed_constrict_handler(dev, cmd.1 as u8).await }); - } else { - let mut command_writer = self.last_cmds.write().expect("Locks should work"); - *command_writer = commands.to_vec(); - - return Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - vec![ - 0xa0, - 0x07, - if cmd.1 == 0 { 0x00 } else { 0x01 }, - 0x00, - cmd.1 as u8, - 0xff, - ], - false, - ) - .into()]); - } - } - } - - let mut command_writer = self.last_cmds.write().expect("Locks should work"); - *command_writer = commands.to_vec(); - - Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - vec![ - 0xa0, - 0x03, - cmd1.unwrap_or((ActuatorType::Vibrate, 0)).1 as u8, - 0x00, - cmd3.unwrap_or((ActuatorType::Vibrate, 0)).1 as u8, - cmd2.unwrap_or((ActuatorType::Rotate, 0)).1 as u8, - 0xaa, - ], - false, - ) - .into()]) - } -} diff --git a/buttplug/src/server/device/protocol/joyhub_v5.rs b/buttplug/src/server/device/protocol/joyhub_v5.rs deleted file mode 100644 index 184e9e779..000000000 --- a/buttplug/src/server/device/protocol/joyhub_v5.rs +++ /dev/null @@ -1,189 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, - }, - generic_protocol_initializer_setup, - server::device::{ - configuration::UserDeviceIdentifier, - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ProtocolHandler, ProtocolIdentifier, ProtocolInitializer}, - }, - util::{async_manager, sleep}, -}; -use async_trait::async_trait; -use std::sync::{Arc, RwLock}; -use std::time::Duration; - -generic_protocol_initializer_setup!(JoyHubV5, "joyhub-v5"); - -async fn delayed_constrict_handler(device: Arc, scalar: u8) { - sleep(Duration::from_millis(25)).await; - let res = device - .write_value(&HardwareWriteCmd::new( - Endpoint::Tx, - vec![ - 0xa0, - 0x07, - if scalar == 0 { 0x00 } else { 0x01 }, - 0x00, - scalar, - 0xff, - ], - false, - )) - .await; - if res.is_err() { - error!("Delayed JoyHub Constrict command error: {:?}", res.err()); - } -} - -fn vibes_changed( - old_commands_lock: &RwLock>>, - new_commands: &[Option<(ActuatorType, u32)>], - exclude: Vec, -) -> bool { - let old_commands = old_commands_lock.read().expect("locks should work"); - if old_commands.len() != new_commands.len() { - return true; - } - - for i in 0..old_commands.len() { - if exclude.contains(&i) { - continue; - } - if let Some(ocmd) = old_commands[i] { - if let Some(ncmd) = new_commands[i] { - if ocmd.1 != ncmd.1 { - return true; - } - } - } - } - false -} - -fn scalar_changed( - old_commands_lock: &RwLock>>, - new_commands: &[Option<(ActuatorType, u32)>], - index: usize, -) -> bool { - let old_commands = old_commands_lock.read().expect("locks should work"); - if old_commands.len() != new_commands.len() { - return true; - } - - if index < old_commands.len() { - if let Some(ocmd) = old_commands[index] { - if let Some(ncmd) = new_commands[index] { - if ocmd.1 != ncmd.1 { - return true; - } - } - } - } - false -} - -#[derive(Default)] -pub struct JoyHubV5Initializer {} - -#[async_trait] -impl ProtocolInitializer for JoyHubV5Initializer { - async fn initialize( - &mut self, - hardware: Arc, - _: &UserDeviceDefinition, - ) -> Result, ButtplugDeviceError> { - Ok(Arc::new(JoyHubV5::new(hardware))) - } -} - -pub struct JoyHubV5 { - device: Arc, - last_cmds: RwLock>>, -} - -impl JoyHubV5 { - fn new(device: Arc) -> Self { - let last_cmds = RwLock::new(vec![]); - Self { device, last_cmds } - } -} - -impl ProtocolHandler for JoyHubV5 { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn needs_full_command_set(&self) -> bool { - true - } - - fn handle_scalar_cmd( - &self, - commands: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - let cmd1 = commands[0]; - let mut cmd2 = if commands.len() > 1 { - commands[1] - } else { - None - }; - - if let Some(cmd) = cmd2 { - if cmd.0 == ActuatorType::Constrict { - cmd2 = None; - if !scalar_changed(&self.last_cmds, commands, 1usize) { - // no-op - } else if vibes_changed(&self.last_cmds, commands, vec![1usize]) { - let dev = self.device.clone(); - async_manager::spawn(async move { delayed_constrict_handler(dev, cmd.1 as u8).await }); - } else { - let mut command_writer = self.last_cmds.write().expect("Locks should work"); - *command_writer = commands.to_vec(); - - return Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - vec![ - 0xa0, - 0x07, - if cmd.1 == 0 { 0x00 } else { 0x01 }, - 0x00, - cmd.1 as u8, - 0xff, - ], - false, - ) - .into()]); - } - } - } - - let mut command_writer = self.last_cmds.write().expect("Locks should work"); - *command_writer = commands.to_vec(); - - Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - vec![ - 0xa0, - 0x03, - cmd2.unwrap_or((ActuatorType::Vibrate, 0)).1 as u8, - 0x00, - cmd1.unwrap_or((ActuatorType::Vibrate, 0)).1 as u8, - 0x00, - 0xaa, - ], - false, - ) - .into()]) - } -} diff --git a/buttplug/src/server/device/protocol/joyhub_v6.rs b/buttplug/src/server/device/protocol/joyhub_v6.rs deleted file mode 100644 index 6455190d3..000000000 --- a/buttplug/src/server/device/protocol/joyhub_v6.rs +++ /dev/null @@ -1,201 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::server::device::configuration::ProtocolCommunicationSpecifier; -use crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, - }, - generic_protocol_initializer_setup, - server::device::{ - configuration::{UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ProtocolHandler, ProtocolIdentifier, ProtocolInitializer}, - }, - util::{async_manager, sleep}, -}; -use async_trait::async_trait; -use std::sync::{Arc, RwLock}; -use std::time::Duration; - -generic_protocol_initializer_setup!(JoyHubV6, "joyhub-v6"); - -async fn delayed_constrict_handler(device: Arc, scalar: u8) { - sleep(Duration::from_millis(50)).await; - let res = device - .write_value(&HardwareWriteCmd::new( - Endpoint::Tx, - vec![0xa0, 0x0d, 0x00, 0x00, scalar, 0xff], - false, - )) - .await; - if res.is_err() { - error!("Delayed JoyHub Constrict command error: {:?}", res.err()); - } -} - -fn vibes_changed( - old_commands_lock: &RwLock>>, - new_commands: &[Option<(ActuatorType, u32)>], - exclude: Vec, -) -> bool { - let old_commands = old_commands_lock.read().expect("locks should work"); - if old_commands.len() != new_commands.len() { - return true; - } - - for i in 0..old_commands.len() { - if exclude.contains(&i) { - continue; - } - if let Some(ocmd) = old_commands[i] { - if let Some(ncmd) = new_commands[i] { - if ocmd.1 != ncmd.1 { - return true; - } - } - } - } - false -} - -fn scalar_changed( - old_commands_lock: &RwLock>>, - new_commands: &[Option<(ActuatorType, u32)>], - index: usize, -) -> bool { - let old_commands = old_commands_lock.read().expect("locks should work"); - if old_commands.len() != new_commands.len() { - return true; - } - - if index < old_commands.len() { - if let Some(ocmd) = old_commands[index] { - if let Some(ncmd) = new_commands[index] { - if ocmd.1 != ncmd.1 { - return true; - } - } - } - } - false -} - -#[derive(Default)] -pub struct JoyHubV6Initializer {} - -#[async_trait] -impl ProtocolInitializer for JoyHubV6Initializer { - async fn initialize( - &mut self, - hardware: Arc, - _: &UserDeviceDefinition, - ) -> Result, ButtplugDeviceError> { - Ok(Arc::new(JoyHubV6::new(hardware))) - } -} - -pub struct JoyHubV6 { - device: Arc, - last_cmds: RwLock>>, -} - -impl JoyHubV6 { - fn new(device: Arc) -> Self { - let last_cmds = RwLock::new(vec![]); - Self { device, last_cmds } - } -} - -impl ProtocolHandler for JoyHubV6 { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn needs_full_command_set(&self) -> bool { - true - } - - fn handle_scalar_cmd( - &self, - commands: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - let cmd1 = commands[0]; - let mut cmd2 = if commands.len() > 1 { - commands[1] - } else { - None - }; - let mut cmd3 = if commands.len() > 2 { - commands[2] - } else { - None - }; - - if let Some(cmd) = cmd2 { - if cmd.0 == ActuatorType::Constrict { - cmd2 = None; - if !scalar_changed(&self.last_cmds, commands, 1usize) { - // no-op - } else if vibes_changed(&self.last_cmds, commands, vec![1usize]) { - let dev = self.device.clone(); - async_manager::spawn(async move { delayed_constrict_handler(dev, cmd.1 as u8).await }); - } else { - let mut command_writer = self.last_cmds.write().expect("Locks should work"); - *command_writer = commands.to_vec(); - - return Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - vec![0xa0, 0x0d, 0x00, 0x00, cmd.1 as u8, 0xff], - false, - ) - .into()]); - } - } - } - - if let Some(cmd) = cmd3 { - if cmd.0 == ActuatorType::Constrict { - cmd3 = None; - if !scalar_changed(&self.last_cmds, commands, 2usize) { - // no-op - } else if vibes_changed(&self.last_cmds, commands, vec![2usize]) { - let dev = self.device.clone(); - async_manager::spawn(async move { delayed_constrict_handler(dev, cmd.1 as u8).await }); - } else { - let mut command_writer = self.last_cmds.write().expect("Locks should work"); - *command_writer = commands.to_vec(); - - return Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - vec![0xa0, 0x0d, 0x00, 0x00, cmd.1 as u8, 0xff], - false, - ) - .into()]); - } - } - } - - let mut command_writer = self.last_cmds.write().expect("Locks should work"); - *command_writer = commands.to_vec(); - Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - vec![ - 0xa0, - 0x03, - cmd2.unwrap_or((ActuatorType::Rotate, 0)).1 as u8, - cmd1.unwrap_or((ActuatorType::Vibrate, 0)).1 as u8, - cmd3.unwrap_or((ActuatorType::Oscillate, 0)).1 as u8, - 0x00, - 0xaa, - ], - false, - ) - .into()]) - } -} diff --git a/buttplug/src/server/device/protocol/kiiroo_v2.rs b/buttplug/src/server/device/protocol/kiiroo_v2.rs deleted file mode 100644 index 8c2f98045..000000000 --- a/buttplug/src/server/device/protocol/kiiroo_v2.rs +++ /dev/null @@ -1,89 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::{ - core::{ - errors::ButtplugDeviceError, - message::{self, Endpoint}, - }, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ - fleshlight_launch_helper::calculate_speed, - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, - }, -}; -use async_trait::async_trait; -use std::sync::{ - atomic::{AtomicU8, Ordering}, - Arc, -}; - -generic_protocol_initializer_setup!(KiirooV2, "kiiroo-v2"); - -#[derive(Default)] -pub struct KiirooV2Initializer {} - -#[async_trait] -impl ProtocolInitializer for KiirooV2Initializer { - async fn initialize( - &mut self, - hardware: Arc, - _: &UserDeviceDefinition, - ) -> Result, ButtplugDeviceError> { - let msg = HardwareWriteCmd::new(Endpoint::Firmware, vec![0x0u8], true); - hardware.write_value(&msg).await?; - Ok(Arc::new(KiirooV2::default())) - } -} - -#[derive(Default)] -pub struct KiirooV2 { - previous_position: Arc, -} - -impl ProtocolHandler for KiirooV2 { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_linear_cmd( - &self, - message: message::LinearCmdV4, - ) -> Result, ButtplugDeviceError> { - let v = message.vectors()[0].clone(); - // In the protocol, we know max speed is 99, so convert here. We have to - // use AtomicU8 because there's no AtomicF64 yet. - let previous_position = self.previous_position.load(Ordering::SeqCst); - let distance = (previous_position as f64 - (v.position() * 99f64)).abs() / 99f64; - let fl_cmd = message::FleshlightLaunchFW12CmdV0::new( - 0, - (v.position() * 99f64) as u8, - (calculate_speed(distance, v.duration()) * 99f64) as u8, - ); - self.handle_fleshlight_launch_fw12_cmd(fl_cmd) - } - - fn handle_fleshlight_launch_fw12_cmd( - &self, - message: message::FleshlightLaunchFW12CmdV0, - ) -> Result, ButtplugDeviceError> { - let position = message.position(); - self.previous_position.store(position, Ordering::SeqCst); - Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - [message.position(), message.speed()].to_vec(), - false, - ) - .into()]) - } -} diff --git a/buttplug/src/server/device/protocol/kiiroo_v2_vibrator.rs b/buttplug/src/server/device/protocol/kiiroo_v2_vibrator.rs deleted file mode 100644 index 6756f1dd4..000000000 --- a/buttplug/src/server/device/protocol/kiiroo_v2_vibrator.rs +++ /dev/null @@ -1,56 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, - }, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, -}; - -generic_protocol_setup!(KiirooV2Vibrator, "kiiroo-v2-vibrator"); - -#[derive(Default)] -pub struct KiirooV2Vibrator {} - -impl ProtocolHandler for KiirooV2Vibrator { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_cmd( - &self, - cmds: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - vec![ - cmds - .get(0) - .unwrap_or(&None) - .unwrap_or((ActuatorType::Vibrate, 0)) - .1 as u8, - cmds - .get(1) - .unwrap_or(&None) - .unwrap_or((ActuatorType::Vibrate, 0)) - .1 as u8, - cmds - .get(2) - .unwrap_or(&None) - .unwrap_or((ActuatorType::Vibrate, 0)) - .1 as u8, - ], - false, - ) - .into()]) - } -} diff --git a/buttplug/src/server/device/protocol/lelof1s.rs b/buttplug/src/server/device/protocol/lelof1s.rs deleted file mode 100644 index 8a2c23959..000000000 --- a/buttplug/src/server/device/protocol/lelof1s.rs +++ /dev/null @@ -1,73 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, - }, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareSubscribeCmd, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, - }, -}; -use async_trait::async_trait; -use std::sync::Arc; - -generic_protocol_initializer_setup!(LeloF1s, "lelo-f1s"); - -#[derive(Default)] -pub struct LeloF1sInitializer {} - -#[async_trait] -impl ProtocolInitializer for LeloF1sInitializer { - async fn initialize( - &mut self, - hardware: Arc, - _: &UserDeviceDefinition, - ) -> Result, ButtplugDeviceError> { - // The Lelo F1s needs you to hit the power button after connection - // before it'll accept any commands. Unless we listen for event on - // the button, this is more likely to turn the device off. - hardware - .subscribe(&HardwareSubscribeCmd::new(Endpoint::Rx)) - .await?; - Ok(Arc::new(LeloF1s::default())) - } -} - -#[derive(Default)] -pub struct LeloF1s {} - -impl ProtocolHandler for LeloF1s { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn needs_full_command_set(&self) -> bool { - true - } - - fn handle_scalar_cmd( - &self, - cmds: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - let mut cmd_vec = vec![0x1]; - for cmd in cmds.iter() { - cmd_vec.push(cmd.expect("LeloF1s should always send all values").1 as u8); - } - Ok(vec![ - HardwareWriteCmd::new(Endpoint::Tx, cmd_vec, false).into() - ]) - } -} diff --git a/buttplug/src/server/device/protocol/leten.rs b/buttplug/src/server/device/protocol/leten.rs deleted file mode 100644 index e0eb879dc..000000000 --- a/buttplug/src/server/device/protocol/leten.rs +++ /dev/null @@ -1,107 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, - }, - util::{async_manager, sleep}, -}; -use async_trait::async_trait; -use std::sync::{ - atomic::{AtomicU8, Ordering}, - Arc, -}; -use std::time::Duration; - -generic_protocol_initializer_setup!(Leten, "leten"); -#[derive(Default)] -pub struct LetenInitializer {} - -#[async_trait] -impl ProtocolInitializer for LetenInitializer { - async fn initialize( - &mut self, - hardware: Arc, - _: &UserDeviceDefinition, - ) -> Result, ButtplugDeviceError> { - // There's a more complex auth flow that the app "sometimes" goes through where it - // sends [0x04, 0x00] and waits for [0x01] on Rx before calling [0x04, 0x01] - hardware - .write_value(&HardwareWriteCmd::new(Endpoint::Tx, vec![0x04, 0x01], true)) - .await?; - // Sometimes sending this causes Rx to receive [0x0a] - Ok(Arc::new(Leten::new(hardware))) - } -} - -const LETEN_COMMAND_DELAY_MS: u64 = 1000; - -async fn command_update_handler(device: Arc, command_holder: Arc) { - trace!("Entering Leten keep-alive loop"); - let mut current_command = command_holder.load(Ordering::Relaxed); - while device - .write_value(&HardwareWriteCmd::new( - Endpoint::Tx, - vec![0x02, current_command], - true, - )) - .await - .is_ok() - { - sleep(Duration::from_millis(LETEN_COMMAND_DELAY_MS)).await; - current_command = command_holder.load(Ordering::Relaxed); - trace!("Leten Command: {:?}", current_command); - } - trace!("Leten keep-alive loop exiting, most likely due to device disconnection."); -} - -pub struct Leten { - current_command: Arc, -} - -impl Leten { - fn new(device: Arc) -> Self { - let current_command = Arc::new(AtomicU8::new(0)); - let current_command_clone = current_command.clone(); - async_manager::spawn( - async move { command_update_handler(device, current_command_clone).await }, - ); - Self { current_command } - } -} - -impl ProtocolHandler for Leten { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - // Leten keepalive is shorter - super::ProtocolKeepaliveStrategy::NoStrategy - } - - fn handle_scalar_vibrate_cmd( - &self, - _index: u32, - scalar: u32, - ) -> Result, ButtplugDeviceError> { - let current_command = self.current_command.clone(); - current_command.store(scalar as u8, Ordering::Relaxed); - - Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - vec![0x02, scalar as u8], - true, - ) - .into()]) - } -} diff --git a/buttplug/src/server/device/protocol/libo_shark.rs b/buttplug/src/server/device/protocol/libo_shark.rs deleted file mode 100644 index 640769c9e..000000000 --- a/buttplug/src/server/device/protocol/libo_shark.rs +++ /dev/null @@ -1,45 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, - }, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, -}; - -generic_protocol_setup!(LiboShark, "libo-shark"); - -#[derive(Default)] -pub struct LiboShark {} - -impl ProtocolHandler for LiboShark { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_cmd( - &self, - cmds: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - // Store off result before the match, so we drop the lock ASAP. - let mut data = 0u8; - if let Some((_, speed)) = cmds[0] { - data |= (speed as u8) << 4; - } - if let Some((_, speed)) = cmds[1] { - data |= speed as u8; - } - Ok(vec![ - HardwareWriteCmd::new(Endpoint::Tx, vec![data], false).into() - ]) - } -} diff --git a/buttplug/src/server/device/protocol/libo_vibes.rs b/buttplug/src/server/device/protocol/libo_vibes.rs deleted file mode 100644 index b178371f5..000000000 --- a/buttplug/src/server/device/protocol/libo_vibes.rs +++ /dev/null @@ -1,50 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, - }, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, -}; - -generic_protocol_setup!(LiboVibes, "libo-vibes"); - -#[derive(Default)] -pub struct LiboVibes {} - -impl ProtocolHandler for LiboVibes { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_cmd( - &self, - cmds: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - let mut msg_vec = vec![]; - for (index, cmd) in cmds.iter().enumerate() { - if let Some((_, speed)) = cmd { - if index == 0 { - msg_vec.push(HardwareWriteCmd::new(Endpoint::Tx, vec![*speed as u8], false).into()); - - // If this is a single vibe device, we need to send stop to TxMode too - if *speed as u8 == 0 && cmds.len() == 1 { - msg_vec.push(HardwareWriteCmd::new(Endpoint::TxMode, vec![0u8], false).into()); - } - } else if index == 1 { - msg_vec.push(HardwareWriteCmd::new(Endpoint::TxMode, vec![*speed as u8], false).into()); - } - } - } - Ok(msg_vec) - } -} diff --git a/buttplug/src/server/device/protocol/longlosttouch.rs b/buttplug/src/server/device/protocol/longlosttouch.rs deleted file mode 100644 index ff69458cb..000000000 --- a/buttplug/src/server/device/protocol/longlosttouch.rs +++ /dev/null @@ -1,155 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2023 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 crate::util::async_manager; -use crate::{ - core::{errors::ButtplugDeviceError, message, message::Endpoint}, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, - }, - util::sleep, -}; -use async_trait::async_trait; -use std::sync::atomic::{AtomicU8, Ordering}; -use std::sync::Arc; -use std::time::Duration; - -generic_protocol_initializer_setup!(LongLostTouch, "longlosttouch"); - -#[derive(Default)] -pub struct LongLostTouchInitializer {} - -#[async_trait] -impl ProtocolInitializer for LongLostTouchInitializer { - async fn initialize( - &mut self, - hardware: Arc, - _: &UserDeviceDefinition, - ) -> Result, ButtplugDeviceError> { - Ok(Arc::new(LongLostTouch::new(hardware))) - } -} - -pub struct LongLostTouch { - last_command: Arc>, -} - -fn form_commands(data: Arc>, force: Option>) -> Vec> { - let mut cmds: Vec> = Vec::new(); - if data.len() != 2 { - return cmds; - } - - let mut skip = vec![false; data.len()]; - let mut zero = vec![false; data.len()]; - if let Some(f) = force { - if f.len() != 2 { - return cmds; - } - for (i, force) in f.iter().enumerate() { - if !force { - skip[i] = true; - } else { - zero[i] = true; - } - } - } - - if data[0].load(Ordering::SeqCst) == data[1].load(Ordering::SeqCst) { - if zero[0] || zero[1] || data[0].load(Ordering::SeqCst) != 0 { - cmds.push(vec![ - 0xAA, - 0x02, - 0x00, - 0x00, - 0x00, - data[0].load(Ordering::SeqCst), - ]) - } - return cmds; - } - - (0..2).for_each(|i| { - if !skip[i as usize] && (zero[i as usize] || data[i as usize].load(Ordering::SeqCst) != 0) { - cmds.push(vec![ - 0xAA, - 0x02, - i + 1 as u8, - 0x00, - 0x00, - data[i as usize].load(Ordering::SeqCst), - ]) - } - }); - return cmds; -} - -async fn send_longlosttouch_updates(device: Arc, data: Arc>) { - loop { - let cmds = form_commands(data.clone(), None); - for cmd in cmds { - if let Err(e) = device - .write_value(&HardwareWriteCmd::new(Endpoint::Tx, cmd, true).into()) - .await - { - error!( - "Got an error from a long lost touch device, exiting control loop: {:?}", - e - ); - break; - } - } - sleep(Duration::from_millis(2500)).await; - } -} - -impl LongLostTouch { - fn new(hardware: Arc) -> Self { - let last_command = Arc::new((0..2).map(|_| AtomicU8::new(0)).collect::>()); - let last_command_clone = last_command.clone(); - async_manager::spawn(async move { - send_longlosttouch_updates(hardware, last_command_clone).await; - }); - - Self { last_command } - } -} - -impl ProtocolHandler for LongLostTouch { - fn handle_scalar_cmd( - &self, - commands: &[Option<(message::ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - if commands.len() != 2 { - return Err(ButtplugDeviceError::DeviceFeatureCountMismatch( - 2, - commands.len() as u32, - )); - } - for (i, item) in commands.iter().enumerate() { - if let Some(command) = item { - self.last_command[i].store(command.1 as u8, Ordering::SeqCst); - } - } - Ok( - form_commands( - self.last_command.clone(), - Some(commands.iter().map(|i| i.is_some()).collect()), - ) - .iter() - .map(|data| HardwareWriteCmd::new(Endpoint::Tx, data.clone(), true).into()) - .collect(), - ) - } -} diff --git a/buttplug/src/server/device/protocol/loob.rs b/buttplug/src/server/device/protocol/loob.rs deleted file mode 100644 index 8f598af6a..000000000 --- a/buttplug/src/server/device/protocol/loob.rs +++ /dev/null @@ -1,66 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2025 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 crate::{ - core::{ - errors::ButtplugDeviceError, - message::{self, Endpoint}, - }, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, - }, -}; -use async_trait::async_trait; -use std::cmp::{max, min}; -use std::sync::Arc; - -generic_protocol_initializer_setup!(Loob, "loob"); - -#[derive(Default)] -pub struct LoobInitializer {} - -#[async_trait] -impl ProtocolInitializer for LoobInitializer { - async fn initialize( - &mut self, - hardware: Arc, - _: &UserDeviceDefinition, - ) -> Result, ButtplugDeviceError> { - let msg = HardwareWriteCmd::new(Endpoint::Tx, vec![0x00, 0x01, 0x01, 0xf4], true); - hardware.write_value(&msg).await?; - Ok(Arc::new(Loob::default())) - } -} - -#[derive(Default)] -pub struct Loob {} - -impl ProtocolHandler for Loob { - fn handle_linear_cmd( - &self, - message: message::LinearCmdV4, - ) -> Result, ButtplugDeviceError> { - if let Some(vec) = message.vectors().get(0) { - let pos: u16 = max(min((vec.position() * 1000.0) as u16, 1000), 1); - let time: u16 = max(vec.duration() as u16, 1); - let mut data = pos.to_be_bytes().to_vec(); - for b in time.to_be_bytes() { - data.push(b); - } - Ok(vec![HardwareWriteCmd::new(Endpoint::Tx, data, false).into()]) - } else { - Ok(vec![]) - } - } -} diff --git a/buttplug/src/server/device/protocol/lovehoney_desire.rs b/buttplug/src/server/device/protocol/lovehoney_desire.rs deleted file mode 100644 index e4d7ad924..000000000 --- a/buttplug/src/server/device/protocol/lovehoney_desire.rs +++ /dev/null @@ -1,78 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, - }, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, -}; - -generic_protocol_setup!(LovehoneyDesire, "lovehoney-desire"); - -#[derive(Default)] -pub struct LovehoneyDesire {} - -impl ProtocolHandler for LovehoneyDesire { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn needs_full_command_set(&self) -> bool { - true - } - - fn handle_scalar_cmd( - &self, - cmds: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - // The Lovehoney Desire has 2 types of commands - // - // - Set both motors with one command - // - Set each motor separately - // - // We'll need to check what we got back and write our - // commands accordingly. - // - // Neat way of checking if everything is the same via - // https://sts10.github.io/2019/06/06/is-all-equal-function.html. - // - // Just make sure we're not matching on None, 'cause if - // that's the case we ain't got shit to do. - let mut msg_vec = vec![]; - if cmds[0].is_some() && cmds.windows(2).all(|w| w[0] == w[1]) { - msg_vec.push( - HardwareWriteCmd::new( - Endpoint::Tx, - vec![ - 0xF3, - 0, - cmds[0].expect("Already checked value existence").1 as u8, - ], - true, - ) - .into(), - ); - } else { - // We have differing values. Set each motor separately. - let mut i = 1; - - for cmd in cmds { - if let Some((_, speed)) = cmd { - msg_vec - .push(HardwareWriteCmd::new(Endpoint::Tx, vec![0xF3, i, *speed as u8], true).into()); - } - i += 1; - } - } - Ok(msg_vec) - } -} diff --git a/buttplug/src/server/device/protocol/lovense.rs b/buttplug/src/server/device/protocol/lovense.rs deleted file mode 100644 index 0db53761c..000000000 --- a/buttplug/src/server/device/protocol/lovense.rs +++ /dev/null @@ -1,501 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::{ - core::{ - errors::ButtplugDeviceError, - message::{self, ActuatorType, ButtplugDeviceMessage, Endpoint, FeatureType, SensorReadingV4}, - }, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareEvent, HardwareSubscribeCmd, HardwareWriteCmd}, - protocol::{ProtocolHandler, ProtocolIdentifier, ProtocolInitializer}, - }, - util::{async_manager, sleep}, -}; -use async_trait::async_trait; -use futures::{future::BoxFuture, FutureExt}; -use regex::Regex; -use std::{ - sync::{ - atomic::{AtomicBool, AtomicU32, AtomicU8, Ordering}, - Arc, - }, - time::Duration, -}; - -// Constants for dealing with the Lovense subscript/write race condition. The -// timeout needs to be VERY long, otherwise this trips up old lovense serial -// adapters. -// -// Just buy new adapters, people. -const LOVENSE_COMMAND_TIMEOUT_MS: u64 = 500; -const LOVENSE_COMMAND_RETRY: u64 = 5; - -pub mod setup { - use crate::server::device::protocol::{ProtocolIdentifier, ProtocolIdentifierFactory}; - #[derive(Default)] - pub struct LovenseIdentifierFactory {} - - impl ProtocolIdentifierFactory for LovenseIdentifierFactory { - fn identifier(&self) -> &str { - "lovense" - } - - fn create(&self) -> Box { - Box::new(super::LovenseIdentifier::default()) - } - } -} - -#[derive(Default)] -pub struct LovenseIdentifier {} - -fn lovense_model_resolver(type_response: String) -> String { - let parts = type_response.split(':').collect::>(); - if parts.len() < 2 { - warn!( - "Lovense Device returned invalid DeviceType info: {}", - type_response - ); - return "lovense".to_string(); - } - - let identifier = parts[0].to_owned(); - let version = parts[1].to_owned().parse::().unwrap_or(0); - - info!("Identified device type {} version {}", identifier, version); - - // Flexer: version must be 3+ to control actuators separately - if identifier == "EI" && version >= 3 { - return "EI-FW3".to_string(); - } - - identifier -} - -#[async_trait] -impl ProtocolIdentifier for LovenseIdentifier { - async fn identify( - &mut self, - hardware: Arc, - _: ProtocolCommunicationSpecifier, - ) -> Result<(UserDeviceIdentifier, Box), ButtplugDeviceError> { - let mut event_receiver = hardware.event_stream(); - let mut count = 0; - hardware - .subscribe(&HardwareSubscribeCmd::new(Endpoint::Rx)) - .await?; - - loop { - let msg = HardwareWriteCmd::new(Endpoint::Tx, b"DeviceType;".to_vec(), false); - hardware.write_value(&msg).await?; - - select! { - event = event_receiver.recv().fuse() => { - if let Ok(HardwareEvent::Notification(_, _, n)) = event { - let type_response = std::str::from_utf8(&n).map_err(|_| ButtplugDeviceError::ProtocolSpecificError("lovense".to_owned(), "Lovense device init got back non-UTF8 string.".to_owned()))?.to_owned(); - debug!("Lovense Device Type Response: {}", type_response); - let ident = lovense_model_resolver(type_response); - return Ok((UserDeviceIdentifier::new(hardware.address(), "lovense", &Some(ident.clone())), Box::new(LovenseInitializer::new(ident)))); - } else { - return Err( - ButtplugDeviceError::ProtocolSpecificError( - "Lovense".to_owned(), - "Lovense Device disconnected while getting DeviceType info.".to_owned(), - ), - ); - } - } - _ = sleep(Duration::from_millis(LOVENSE_COMMAND_TIMEOUT_MS)).fuse() => { - count += 1; - if count > LOVENSE_COMMAND_RETRY { - warn!("Lovense Device timed out while getting DeviceType info. ({} retries)", LOVENSE_COMMAND_RETRY); - let re = Regex::new(r"LVS-([A-Z]+)\d+").expect("Static regex shouldn't fail"); - if let Some(caps) = re.captures(hardware.name()) { - info!("Lovense Device identified by BLE name"); - return Ok((UserDeviceIdentifier::new(hardware.address(), "lovense", &Some(caps[1].to_string())), Box::new(LovenseInitializer::new(caps[1].to_string())))); - }; - return Ok((UserDeviceIdentifier::new(hardware.address(), "lovense", &None), Box::new(LovenseInitializer::new("".to_string())))); - } - } - } - } - } -} -pub struct LovenseInitializer { - device_type: String, -} - -impl LovenseInitializer { - pub fn new(device_type: String) -> Self { - Self { device_type } - } -} - -#[async_trait] -impl ProtocolInitializer for LovenseInitializer { - async fn initialize( - &mut self, - hardware: Arc, - device_definition: &UserDeviceDefinition, - ) -> Result, ButtplugDeviceError> { - let device_type = self.device_type.clone(); - - let vibrator_count = device_definition - .features() - .iter() - .filter(|x| [FeatureType::Vibrate, FeatureType::Oscillate].contains(x.feature_type())) - .count(); - - let actuator_count = device_definition - .features() - .iter() - .filter(|x| x.actuator().is_some()) - .count(); - - // This might need better tuning if other complex Lovenses are released - // Currently this only applies to the Flexer/Lapis/Solace - let use_mply = - (vibrator_count == 2 && actuator_count > 2) || vibrator_count > 2 || device_type == "H"; - - // New Lovense devices seem to be moving to the simplified LVS:; command format. - // I'm not sure if there's a good way to detect this. - let use_lvs = device_type == "OC"; - - debug!( - "Device type {} initialized with {} vibrators {} using Mply", - device_type, - vibrator_count, - if use_mply { "" } else { "not " } - ); - - Ok(Arc::new(Lovense::new( - hardware, - &device_type, - vibrator_count, - use_mply, - use_lvs, - ))) - } -} - -pub struct Lovense { - rotation_direction: Arc, - vibrator_count: usize, - use_mply: bool, - use_lvs: bool, - device_type: String, - // Pairing of position: u8, duration: u32 - linear_info: Arc<(AtomicU8, AtomicU32)>, -} - -impl Lovense { - pub fn new( - hardware: Arc, - device_type: &str, - vibrator_count: usize, - use_mply: bool, - use_lvs: bool, - ) -> Self { - let linear_info = Arc::new((AtomicU8::new(0), AtomicU32::new(0))); - if device_type == "BA" { - async_manager::spawn(update_linear_movement( - hardware.clone(), - linear_info.clone(), - )); - } - - Self { - rotation_direction: Arc::new(AtomicBool::new(false)), - vibrator_count, - use_mply, - use_lvs, - device_type: device_type.to_owned(), - linear_info, - } - } -} - -impl ProtocolHandler for Lovense { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - // For Lovense, we'll just repeat the device type packet and drop the result. - super::ProtocolKeepaliveStrategy::RepeatPacketStrategy(HardwareWriteCmd::new( - Endpoint::Tx, - b"DeviceType;".to_vec(), - false, - )) - } - - fn handle_scalar_cmd( - &self, - cmds: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - if self.use_lvs { - let mut speeds = vec![0x4cu8, 0x56, 0x53, 0x3a]; - speeds.append( - &mut cmds - .iter() - .map(|x| if let Some(val) = x { val.1 as u8 } else { 0xff }) - .collect::>(), - ); - speeds.push(0x3b); - - return Ok(vec![ - HardwareWriteCmd::new(Endpoint::Tx, speeds, false).into() - ]); - } - - if self.use_mply { - let mut speeds = cmds - .iter() - .map(|x| { - if let Some(val) = x { - val.1.to_string() - } else { - "-1".to_string() - } - }) - .collect::>(); - - if speeds.len() == 1 && self.device_type == "H" { - // Max range unless stopped - speeds.push(if speeds[0] == "0" { - "0".to_string() - } else { - "20".to_string() - }); - } - - let lovense_cmd = format!("Mply:{};", speeds.join(":")).as_bytes().to_vec(); - - return Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - lovense_cmd, - false, - ) - .into()]); - } - - let mut hardware_cmds = vec![]; - - // Handle vibration commands, these will be by far the most common. Fucking machine oscillation - // uses lovense vibrate commands internally too, so we can include them here. - let vibrate_cmds: Vec<&(ActuatorType, u32)> = cmds - .iter() - .filter(|x| { - if let Some(val) = x { - [ActuatorType::Vibrate, ActuatorType::Oscillate].contains(&val.0) - } else { - false - } - }) - .map(|x| x.as_ref().expect("Already verified is some")) - .collect(); - - if !vibrate_cmds.is_empty() { - // Lovense is the same situation as the Lovehoney Desire, where commands - // are different if we're addressing all motors or seperate motors. - // Difference here being that there's Lovense variants with different - // numbers of motors. - // - // Neat way of checking if everything is the same via - // https://sts10.github.io/2019/06/06/is-all-equal-function.html. - // - // Just make sure we're not matching on None, 'cause if that's the case - // we ain't got shit to do. - // - // Note that the windowed comparison causes mixed types as well as mixed - // speeds to fall back to separate commands. This is because the Gravity's - // thruster on Vibrate2 is independent of Vibrate - if self.vibrator_count == vibrate_cmds.len() - && (self.vibrator_count == 1 - || vibrate_cmds - .windows(2) - .all(|w| w[0].0 == w[1].0 && w[0].1 == w[1].1)) - { - let lovense_cmd = format!("Vibrate:{};", vibrate_cmds[0].1) - .as_bytes() - .to_vec(); - hardware_cmds.push(HardwareWriteCmd::new(Endpoint::Tx, lovense_cmd, false).into()); - } else { - for (i, cmd) in cmds.iter().enumerate() { - if let Some((actuator, speed)) = cmd { - if ![ActuatorType::Vibrate, ActuatorType::Oscillate].contains(actuator) { - continue; - } - let lovense_cmd = format!("Vibrate{}:{};", i + 1, speed).as_bytes().to_vec(); - hardware_cmds.push(HardwareWriteCmd::new(Endpoint::Tx, lovense_cmd, false).into()); - } - } - } - } - - // Handle constriction commands. - let constrict_cmds: Vec<&(ActuatorType, u32)> = cmds - .iter() - .filter(|x| { - if let Some(val) = x { - val.0 == ActuatorType::Constrict - } else { - false - } - }) - .map(|x| x.as_ref().expect("Already verified is some")) - .collect(); - if !constrict_cmds.is_empty() { - // Only the max has a constriction system, and there's only one, so just parse the first command. - let lovense_cmd = format!("Air:Level:{};", constrict_cmds[0].1) - .as_bytes() - .to_vec(); - - hardware_cmds.push(HardwareWriteCmd::new(Endpoint::Tx, lovense_cmd, false).into()); - } - - Ok(hardware_cmds) - } - - fn handle_rotate_cmd( - &self, - cmds: &[Option<(u32, bool)>], - ) -> Result, ButtplugDeviceError> { - let direction = self.rotation_direction.clone(); - let mut hardware_cmds = vec![]; - if let Some(Some((speed, clockwise))) = cmds.get(0) { - let lovense_cmd = format!("Rotate:{};", speed).as_bytes().to_vec(); - hardware_cmds.push(HardwareWriteCmd::new(Endpoint::Tx, lovense_cmd, false).into()); - let dir = direction.load(Ordering::SeqCst); - // TODO Should we store speed and direction as an option for rotation caching? This is weird. - if dir != *clockwise { - direction.store(*clockwise, Ordering::SeqCst); - hardware_cmds - .push(HardwareWriteCmd::new(Endpoint::Tx, b"RotateChange;".to_vec(), false).into()); - } - } - Ok(hardware_cmds) - } - - fn handle_battery_level_cmd( - &self, - device: Arc, - message: message::SensorReadCmdV4, - ) -> BoxFuture> { - let mut device_notification_receiver = device.event_stream(); - async move { - let write_fut = device.write_value(&HardwareWriteCmd::new( - Endpoint::Tx, - b"Battery;".to_vec(), - false, - )); - write_fut.await?; - while let Ok(event) = device_notification_receiver.recv().await { - match event { - HardwareEvent::Notification(_, _, data) => { - if let Ok(data_str) = std::str::from_utf8(&data) { - debug!("Lovense event received: {}", data_str); - let len = data_str.len(); - // Depending on the state of the toy, we may get an initial - // character of some kind, i.e. if the toy is currently vibrating - // then battery level comes up as "s89;" versus just "89;". We'll - // need to chop the semicolon and make sure we only read the - // numbers in the string. - // - // Contains() is casting a wider net than we need here, but it'll - // do for now. - let start_pos = usize::from(data_str.contains('s')); - if let Ok(level) = data_str[start_pos..(len - 1)].parse::() { - return Ok( - message::SensorReadingV4::new( - message.device_index(), - *message.feature_index(), - message::SensorType::Battery, - vec![level as i32], - ) - .into(), - ); - } - } - } - HardwareEvent::Disconnected(_) => { - return Err(ButtplugDeviceError::ProtocolSpecificError( - "Lovense".to_owned(), - "Lovense Device disconnected while getting Battery info.".to_owned(), - )) - } - } - } - Err(ButtplugDeviceError::ProtocolSpecificError( - "Lovense".to_owned(), - "Lovense Device disconnected while getting Battery info.".to_owned(), - )) - } - .boxed() - } - - fn handle_linear_cmd( - &self, - message: message::LinearCmdV4, - ) -> Result, ButtplugDeviceError> { - let vector = message - .vectors() - .first() - .expect("Already checked for vector subcommand"); - self - .linear_info - .0 - .store((vector.position() * 100f64) as u8, Ordering::SeqCst); - self - .linear_info - .1 - .store(vector.duration(), Ordering::SeqCst); - Ok(vec![]) - } -} - -async fn update_linear_movement(device: Arc, linear_info: Arc<(AtomicU8, AtomicU32)>) { - let mut last_goal_position = 0i32; - let mut current_move_amount = 0i32; - let mut current_position = 0i32; - loop { - // See if we've updated our goal position - let goal_position = linear_info.0.load(Ordering::Relaxed) as i32; - // If we have and it's not the same, recalculate based on current status. - if last_goal_position != goal_position { - last_goal_position = goal_position; - // We move every 100ms, so divide the movement into that many chunks. - // If we're moving so fast it'd be under our 100ms boundary, just move in 1 step. - let move_steps = (linear_info.1.load(Ordering::Relaxed) / 100).max(1); - current_move_amount = (goal_position as i32 - current_position) as i32 / move_steps as i32; - } - - // If we aren't going anywhere, just pause then restart - if current_position == last_goal_position { - sleep(Duration::from_millis(100)).await; - continue; - } - - // Update our position, make sure we don't overshoot - current_position += current_move_amount; - if current_move_amount < 0 { - if current_position < last_goal_position { - current_position = last_goal_position; - } - } else { - if current_position > last_goal_position { - current_position = last_goal_position; - } - } - - let lovense_cmd = format!("FSetSite:{};", current_position); - - let hardware_cmd = HardwareWriteCmd::new(Endpoint::Tx, lovense_cmd.into_bytes(), false); - if device.write_value(&hardware_cmd).await.is_err() { - return; - } - sleep(Duration::from_millis(100)).await; - } -} diff --git a/buttplug/src/server/device/protocol/luvmazer.rs b/buttplug/src/server/device/protocol/luvmazer.rs deleted file mode 100644 index 7f09abc4e..000000000 --- a/buttplug/src/server/device/protocol/luvmazer.rs +++ /dev/null @@ -1,139 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2025 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 crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, - }, - server::device::{ - configuration::UserDeviceDefinition, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolCommunicationSpecifier, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - UserDeviceIdentifier, - }, - }, - util::async_manager, -}; -use async_trait::async_trait; -use std::{sync::Arc, time::Duration}; -use tokio::time::sleep; - -generic_protocol_initializer_setup!(Luvmazer, "luvmazer"); - -async fn delayed_rotate_handler(device: Arc, scalar: u8) { - sleep(Duration::from_millis(25)).await; - let res = device - .write_value(&HardwareWriteCmd::new( - Endpoint::Tx, - vec![0xa0, 0x0f, 0x00, 0x00, 0x64, scalar as u8], - false, - )) - .await; - if res.is_err() { - error!("Delayed Luvmazer Rotate command error: {:?}", res.err()); - } -} -#[derive(Default)] -pub struct LuvmazerInitializer {} - -#[async_trait] -impl ProtocolInitializer for LuvmazerInitializer { - async fn initialize( - &mut self, - hardware: Arc, - _: &UserDeviceDefinition, - ) -> Result, ButtplugDeviceError> { - Ok(Arc::new(Luvmazer::new(hardware))) - } -} - -pub struct Luvmazer { - device: Arc, -} - -impl Luvmazer { - fn new(device: Arc) -> Self { - Self { device } - } -} - -impl ProtocolHandler for Luvmazer { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( - &self, - _index: u32, - scalar: u32, - ) -> Result, ButtplugDeviceError> { - Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - vec![0xa0, 0x01, 0x00, 0x00, 0x64, scalar as u8], - false, - ) - .into()]) - } - - fn handle_scalar_rotate_cmd( - &self, - _index: u32, - scalar: u32, - ) -> Result, ButtplugDeviceError> { - Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - vec![0xa0, 0x0f, 0x00, 0x00, 0x64, scalar as u8], - false, - ) - .into()]) - } - - fn handle_scalar_cmd( - &self, - commands: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - let cmd1 = commands[0]; - let cmd2 = if commands.len() > 1 { - commands[1] - } else { - None - }; - - if let Some(cmd) = cmd2 { - if cmd.0 == ActuatorType::Rotate { - if cmd1.is_some() { - let dev = self.device.clone(); - async_manager::spawn(async move { delayed_rotate_handler(dev, cmd.1 as u8).await }); - } else { - return Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - vec![0xa0, 0x0f, 0x00, 0x00, 0x64, cmd.1 as u8], - false, - ) - .into()]); - } - } - } - - if let Some(cmd) = cmd1 { - return Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - vec![0xa0, 0x01, 0x00, 0x00, 0x64, cmd.1 as u8], - false, - ) - .into()]); - } - - Ok(vec![]) - } -} diff --git a/buttplug/src/server/device/protocol/magic_motion_v2.rs b/buttplug/src/server/device/protocol/magic_motion_v2.rs deleted file mode 100644 index ed69d7c0b..000000000 --- a/buttplug/src/server/device/protocol/magic_motion_v2.rs +++ /dev/null @@ -1,80 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, - }, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, -}; - -generic_protocol_setup!(MagicMotionV2, "magic-motion-2"); - -#[derive(Default)] -pub struct MagicMotionV2 {} - -impl ProtocolHandler for MagicMotionV2 { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn needs_full_command_set(&self) -> bool { - true - } - - fn handle_scalar_cmd( - &self, - cmds: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - let data = if cmds.len() == 1 { - vec![ - 0x10, - 0xff, - 0x04, - 0x0a, - 0x32, - 0x0a, - 0x00, - 0x04, - 0x08, - cmds[0].unwrap_or((ActuatorType::Vibrate, 0)).1 as u8, - 0x64, - 0x00, - 0x04, - 0x08, - 0, - 0x64, - 0x01, - ] - } else { - vec![ - 0x10, - 0xff, - 0x04, - 0x0a, - 0x32, - 0x0a, - 0x00, - 0x04, - 0x08, - cmds[0].unwrap_or((ActuatorType::Vibrate, 0)).1 as u8, - 0x64, - 0x00, - 0x04, - 0x08, - cmds[1].unwrap_or((ActuatorType::Vibrate, 0)).1 as u8, - 0x64, - 0x01, - ] - }; - Ok(vec![HardwareWriteCmd::new(Endpoint::Tx, data, false).into()]) - } -} diff --git a/buttplug/src/server/device/protocol/magic_motion_v4.rs b/buttplug/src/server/device/protocol/magic_motion_v4.rs deleted file mode 100644 index 39ded345d..000000000 --- a/buttplug/src/server/device/protocol/magic_motion_v4.rs +++ /dev/null @@ -1,80 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, - }, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, -}; - -generic_protocol_setup!(MagicMotionV4, "magic-motion-4"); - -#[derive(Default)] -pub struct MagicMotionV4 {} - -impl ProtocolHandler for MagicMotionV4 { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn needs_full_command_set(&self) -> bool { - true - } - - fn handle_scalar_cmd( - &self, - cmds: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - let data = if cmds.len() == 1 { - vec![ - 0x10, - 0xff, - 0x04, - 0x0a, - 0x32, - 0x32, - 0x00, - 0x04, - 0x08, - cmds[0].unwrap_or((ActuatorType::Vibrate, 0)).1 as u8, - 0x64, - 0x00, - 0x04, - 0x08, - cmds[0].unwrap_or((ActuatorType::Vibrate, 0)).1 as u8, - 0x64, - 0x01, - ] - } else { - vec![ - 0x10, - 0xff, - 0x04, - 0x0a, - 0x32, - 0x32, - 0x00, - 0x04, - 0x08, - cmds[0].unwrap_or((ActuatorType::Vibrate, 0)).1 as u8, - 0x64, - 0x00, - 0x04, - 0x08, - cmds[1].unwrap_or((ActuatorType::Vibrate, 0)).1 as u8, - 0x64, - 0x01, - ] - }; - Ok(vec![HardwareWriteCmd::new(Endpoint::Tx, data, true).into()]) - } -} diff --git a/buttplug/src/server/device/protocol/metaxsire.rs b/buttplug/src/server/device/protocol/metaxsire.rs deleted file mode 100644 index 008a4eedc..000000000 --- a/buttplug/src/server/device/protocol/metaxsire.rs +++ /dev/null @@ -1,62 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::core::message::ActuatorType; -use crate::core::message::ActuatorType::{Constrict, Oscillate, Rotate, Vibrate}; -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, -}; - -generic_protocol_setup!(MetaXSire, "metaxsire"); - -#[derive(Default)] -pub struct MetaXSire {} - -impl ProtocolHandler for MetaXSire { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn needs_full_command_set(&self) -> bool { - true - } - - fn handle_scalar_cmd( - &self, - commands: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - let mut data: Vec = vec![0x23, 0x07]; - data.push((commands.len() * 3) as u8); - - for (i, item) in commands.iter().enumerate() { - let cmd = item.unwrap_or((Vibrate, 0)); - // motor number - data.push(0x80 | ((i + 1) as u8)); - // motor type: 03=vibe 04=pump 06=rotate - data.push(if cmd.0 == Rotate { - 0x06 - } else if cmd.0 == Constrict || cmd.0 == Oscillate { - 0x04 - } else { - 0x03 - }); - data.push(cmd.1 as u8); - } - - let mut crc: u8 = 0; - for b in data.clone() { - crc ^= b; - } - data.push(crc); - - Ok(vec![HardwareWriteCmd::new(Endpoint::Tx, data, false).into()]) - } -} diff --git a/buttplug/src/server/device/protocol/metaxsire_repeat.rs b/buttplug/src/server/device/protocol/metaxsire_repeat.rs deleted file mode 100644 index c38fec8e1..000000000 --- a/buttplug/src/server/device/protocol/metaxsire_repeat.rs +++ /dev/null @@ -1,128 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ - ActuatorType::{self, Constrict, Rotate, Vibrate}, - Endpoint, - }, - }, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, - }, - - util::{async_manager, sleep}, -}; -use async_trait::async_trait; -use std::sync::Arc; -use std::time::Duration; -use tokio::sync::RwLock; - -generic_protocol_initializer_setup!(MetaXSireRepeat, "metaxsire-repeat"); -#[derive(Default)] -pub struct MetaXSireRepeatInitializer {} - -#[async_trait] -impl ProtocolInitializer for MetaXSireRepeatInitializer { - async fn initialize( - &mut self, - hardware: Arc, - _: &UserDeviceDefinition, - ) -> Result, ButtplugDeviceError> { - Ok(Arc::new(MetaXSireRepeat::new(hardware))) - } -} - -const METAXSIRE_COMMAND_DELAY_MS: u64 = 100; - -async fn command_update_handler(device: Arc, command_holder: Arc>>) { - info!("Entering metaXsire Control Loop"); - let mut current_command = command_holder.read().await.clone(); - while current_command[0] == 0 - || device - .write_value(&HardwareWriteCmd::new(Endpoint::Tx, current_command, false)) - .await - .is_ok() - { - sleep(Duration::from_millis(METAXSIRE_COMMAND_DELAY_MS)).await; - current_command = command_holder.read().await.clone(); - trace!("metaXsire Command: {:?}", current_command); - } - info!("metaXsire control loop exiting, most likely due to device disconnection."); -} - -pub struct MetaXSireRepeat { - current_command: Arc>>, -} - -impl MetaXSireRepeat { - fn new(device: Arc) -> Self { - let current_command = Arc::new(RwLock::new(vec![0u8])); - let current_command_clone = current_command.clone(); - async_manager::spawn( - async move { command_update_handler(device, current_command_clone).await }, - ); - Self { current_command } - } -} - -impl ProtocolHandler for MetaXSireRepeat { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn needs_full_command_set(&self) -> bool { - true - } - - fn handle_scalar_cmd( - &self, - commands: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - let current_command = self.current_command.clone(); - let commands = commands.to_vec(); - async_manager::spawn(async move { - let write_mutex = current_command.clone(); - let mut command_writer = write_mutex.write().await; - let mut data: Vec = vec![0x23, 0x07]; - data.push((commands.len() * 3) as u8); - - for (i, item) in commands.iter().enumerate() { - let cmd = item.unwrap_or((Vibrate, 0)); - // motor number - data.push(0x80 | ((i + 1) as u8)); - // motor type: 03=vibe 04=pump 06=rotate - data.push(if cmd.0 == Rotate { - 0x06 - } else if cmd.0 == Constrict { - 0x04 - } else { - 0x03 - }); - data.push(cmd.1 as u8); - } - - let mut crc: u8 = 0; - for b in data.clone() { - crc ^= b; - } - data.push(crc); - - *command_writer = data; - }); - Ok(vec![]) - } -} diff --git a/buttplug/src/server/device/protocol/metaxsire_v2.rs b/buttplug/src/server/device/protocol/metaxsire_v2.rs deleted file mode 100644 index 3e0e3fbd2..000000000 --- a/buttplug/src/server/device/protocol/metaxsire_v2.rs +++ /dev/null @@ -1,69 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2023 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 crate::core::message::ActuatorType; -use crate::server::device::hardware::Hardware; -use crate::server::device::protocol::ProtocolInitializer; -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_initializer_setup, ProtocolHandler, ProtocolIdentifier}, - }, -}; -use async_trait::async_trait; -use std::sync::Arc; - -generic_protocol_initializer_setup!(MetaXSireV2, "metaxsire-v2"); - -#[derive(Default)] -pub struct MetaXSireV2Initializer {} - -#[async_trait] -impl ProtocolInitializer for MetaXSireV2Initializer { - async fn initialize( - &mut self, - hardware: Arc, - _: &UserDeviceDefinition, - ) -> Result, ButtplugDeviceError> { - hardware - .write_value(&HardwareWriteCmd::new(Endpoint::Tx, vec![0xaa, 0x04], true)) - .await?; - Ok(Arc::new(MetaXSireV2::default())) - } -} - -#[derive(Default)] -pub struct MetaXSireV2 {} - -impl ProtocolHandler for MetaXSireV2 { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_cmd( - &self, - commands: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - let mut hcmds = vec![]; - for i in 0..commands.len() { - if let Some(cmd) = commands[i] { - hcmds.push( - HardwareWriteCmd::new( - Endpoint::Tx, - vec![0xaa, 0x03, 0x01, (i + 1) as u8, 0x64, cmd.1 as u8], - true, - ) - .into(), - ); - } - } - - Ok(hcmds) - } -} diff --git a/buttplug/src/server/device/protocol/metaxsire_v3.rs b/buttplug/src/server/device/protocol/metaxsire_v3.rs deleted file mode 100644 index c27ac03c9..000000000 --- a/buttplug/src/server/device/protocol/metaxsire_v3.rs +++ /dev/null @@ -1,123 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::core::message::ActuatorType; -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, - }, - util::{async_manager, sleep}, -}; -use async_trait::async_trait; -use std::sync::Arc; -use std::time::Duration; -use tokio::sync::RwLock; - -generic_protocol_initializer_setup!(MetaXSireV3, "metaxsire-v3"); -#[derive(Default)] -pub struct MetaXSireV3Initializer {} - -#[async_trait] -impl ProtocolInitializer for MetaXSireV3Initializer { - async fn initialize( - &mut self, - hardware: Arc, - device_definition: &UserDeviceDefinition, - ) -> Result, ButtplugDeviceError> { - let feature_count = device_definition - .features() - .iter() - .filter(|x| x.actuator().is_some()) - .count(); - Ok(Arc::new(MetaXSireV3::new(hardware, feature_count))) - } -} - -const METAXSIRE_COMMAND_DELAY_MS: u64 = 100; - -async fn command_update_handler(device: Arc, command_holder: Arc>>) { - trace!("Entering metaXsire v3 Control Loop"); - let mut current_commands = command_holder.read().await.clone(); - let mut errored = false; - while !errored { - for i in 0..current_commands.len() { - if current_commands[i] == 0 { - continue; - } - errored = !device - .write_value(&HardwareWriteCmd::new( - Endpoint::Tx, - vec![0xa1, 0x04, current_commands[i], i as u8 + 1], - false, - )) - .await - .is_ok(); - if errored { - break; - } - } - sleep(Duration::from_millis(METAXSIRE_COMMAND_DELAY_MS)).await; - current_commands = command_holder.read().await.clone(); - trace!("metaXsire v3 Command: {:?}", current_commands); - } - trace!("metaXsire v3 control loop exiting, most likely due to device disconnection."); -} - -pub struct MetaXSireV3 { - current_commands: Arc>>, -} - -impl MetaXSireV3 { - fn new(device: Arc, feature_count: usize) -> Self { - let current_commands = Arc::new(RwLock::new(vec![0u8; feature_count])); - let current_commands_clone = current_commands.clone(); - async_manager::spawn( - async move { command_update_handler(device, current_commands_clone).await }, - ); - Self { current_commands } - } -} - -impl ProtocolHandler for MetaXSireV3 { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_cmd( - &self, - commands: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - let mut cmds = vec![]; - for i in 0..commands.len() { - if let Some(cmd) = commands[i] { - let current_commands = self.current_commands.clone(); - async_manager::spawn(async move { - let write_mutex = current_commands.clone(); - let mut command_writer = write_mutex.write().await; - command_writer[i] = cmd.1 as u8; - }); - cmds.push( - HardwareWriteCmd::new( - Endpoint::Tx, - vec![0xa1, 0x04, cmd.1 as u8, i as u8 + 1], - true, - ) - .into(), - ); - } - } - Ok(cmds) - } -} diff --git a/buttplug/src/server/device/protocol/mizzzee_v3.rs b/buttplug/src/server/device/protocol/mizzzee_v3.rs deleted file mode 100644 index 862079b30..000000000 --- a/buttplug/src/server/device/protocol/mizzzee_v3.rs +++ /dev/null @@ -1,133 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, - }, - util::{async_manager, sleep}, -}; -use async_trait::async_trait; -use std::sync::atomic::{AtomicU32, Ordering}; -use std::{sync::Arc, time::Duration}; - -generic_protocol_initializer_setup!(MizzZeeV3, "mizzzee-v3"); - -#[derive(Default)] -pub struct MizzZeeV3Initializer {} - -#[async_trait] -impl ProtocolInitializer for MizzZeeV3Initializer { - async fn initialize( - &mut self, - hardware: Arc, - _: &UserDeviceDefinition, - ) -> Result, ButtplugDeviceError> { - Ok(Arc::new(MizzZeeV3::new(hardware))) - } -} - -// Time between MizzZee v3 update commands, in milliseconds. -const MIZZZEE3_COMMAND_DELAY_MS: u64 = 200; - -fn handle_scale(scale: f32) -> f32 { - if scale == 0.0 { - return 0.0; - } - scale * 0.7 + 0.3 -} - -fn scalar_to_vector(scalar: u32) -> Vec { - if scalar == 0 { - return vec![ - 0x03, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, - ]; - } - - const HEADER: [u8; 3] = [0x03, 0x12, 0xf3]; - const FILL_VEC: [u8; 6] = [0x00, 0xfc, 0x00, 0xfe, 0x40, 0x01]; - - let scale: f32 = handle_scale(scalar as f32 / 1000.0) * 1023.0; - let modded_scale: u16 = ((scale as u16) << 6) | 60; - - let bytes = modded_scale.to_le_bytes(); - - let mut data: Vec = Vec::new(); - data.extend_from_slice(&HEADER); - data.extend_from_slice(&FILL_VEC); - data.extend_from_slice(&bytes); - data.extend_from_slice(&FILL_VEC); - data.extend_from_slice(&bytes); - data.push(0x00); - - data -} - -async fn vibration_update_handler(device: Arc, current_scalar_holder: Arc) { - info!("Entering Mizz Zee v3 Control Loop"); - let mut current_scalar = current_scalar_holder.load(Ordering::Relaxed); - while device - .write_value(&HardwareWriteCmd::new( - Endpoint::Tx, - scalar_to_vector(current_scalar), - true, - )) - .await - .is_ok() - { - sleep(Duration::from_millis(MIZZZEE3_COMMAND_DELAY_MS)).await; - current_scalar = current_scalar_holder.load(Ordering::Relaxed); - trace!("Mizz Zee v3 scalar: {}", current_scalar); - } - info!("Mizz Zee v3 control loop exiting, most likely due to device disconnection."); -} - -#[derive(Default)] -pub struct MizzZeeV3 { - current_scalar: Arc, -} - -impl MizzZeeV3 { - fn new(device: Arc) -> Self { - let current_scalar = Arc::new(AtomicU32::new(0)); - let current_scalar_clone = current_scalar.clone(); - async_manager::spawn( - async move { vibration_update_handler(device, current_scalar_clone).await }, - ); - Self { current_scalar } - } -} - -impl ProtocolHandler for MizzZeeV3 { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::NoStrategy - } - - fn handle_scalar_vibrate_cmd( - &self, - _index: u32, - scalar: u32, - ) -> Result, ButtplugDeviceError> { - let current_scalar = self.current_scalar.clone(); - current_scalar.store(scalar, Ordering::Relaxed); - Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - scalar_to_vector(scalar), - true, - ) - .into()]) - } -} diff --git a/buttplug/src/server/device/protocol/mod.rs b/buttplug/src/server/device/protocol/mod.rs deleted file mode 100644 index 9249ce907..000000000 --- a/buttplug/src/server/device/protocol/mod.rs +++ /dev/null @@ -1,1085 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 Nonpolynomial Labs LLC. All rights reserved. -// -// Licensed under the BSD 3-Clause license. See LICENSE file in the project root -// for full license information. - -//! Implementations of communication protocols for hardware supported by Buttplug - -pub mod actuator_command_manager; - -// Utility mods -pub mod fleshlight_launch_helper; - -// Since users can pick and choose protocols, we need all of these to be public. -pub mod activejoy; -pub mod adrienlastic; -pub mod amorelie_joy; -pub mod aneros; -pub mod ankni; -pub mod bananasome; -pub mod buttplug_passthru; -pub mod cachito; -pub mod cowgirl; -pub mod cowgirl_cone; -pub mod cupido; -pub mod deepsire; -pub mod feelingso; -pub mod fleshy_thrust; -pub mod foreo; -pub mod fox; -pub mod fredorch; -pub mod fredorch_rotary; -pub mod galaku; -pub mod galaku_pump; -pub mod hgod; -pub mod hismith; -pub mod hismith_mini; -pub mod htk_bm; -pub mod itoys; -pub mod jejoue; -pub mod joyhub; -pub mod joyhub_v2; -pub mod joyhub_v3; -pub mod joyhub_v4; -pub mod joyhub_v5; -pub mod joyhub_v6; -pub mod kgoal_boost; -pub mod kiiroo_prowand; -pub mod kiiroo_spot; -pub mod kiiroo_v2; -pub mod kiiroo_v21; -pub mod kiiroo_v21_initialized; -pub mod kiiroo_v2_vibrator; -pub mod kizuna; -pub mod lelo_harmony; -pub mod lelof1s; -pub mod lelof1sv2; -pub mod leten; -pub mod libo_elle; -pub mod libo_shark; -pub mod libo_vibes; -pub mod lioness; -pub mod longlosttouch; -pub mod loob; -pub mod lovedistance; -pub mod lovehoney_desire; -pub mod lovense; -pub mod lovense_connect_service; -pub mod lovenuts; -pub mod luvmazer; -pub mod magic_motion_v1; -pub mod magic_motion_v2; -pub mod magic_motion_v3; -pub mod magic_motion_v4; -pub mod mannuo; -pub mod maxpro; -pub mod meese; -pub mod metaxsire; -pub mod metaxsire_repeat; -pub mod metaxsire_v2; -pub mod metaxsire_v3; -mod metaxsire_v4; -pub mod mizzzee; -pub mod mizzzee_v2; -pub mod mizzzee_v3; -pub mod monsterpub; -pub mod motorbunny; -pub mod mysteryvibe; -pub mod mysteryvibe_v2; -pub mod nextlevelracing; -pub mod nexus_revo; -pub mod nintendo_joycon; -pub mod nobra; -pub mod omobo; -pub mod patoo; -pub mod picobong; -pub mod pink_punch; -pub mod prettylove; -pub mod raw_protocol; -pub mod realov; -pub mod sakuraneko; -pub mod satisfyer; -pub mod sensee; -pub mod sensee_capsule; -pub mod sensee_v2; -pub mod serveu; -pub mod sexverse_lg389; -pub mod svakom; -pub mod svakom_alex; -pub mod svakom_alex_v2; -pub mod svakom_avaneo; -pub mod svakom_barnard; -pub mod svakom_barney; -pub mod svakom_dice; -pub mod svakom_dt250a; -pub mod svakom_iker; -pub mod svakom_jordan; -pub mod svakom_pulse; -pub mod svakom_sam; -pub mod svakom_sam2; -pub mod svakom_suitcase; -pub mod svakom_tarax; -pub mod svakom_v2; -pub mod svakom_v3; -pub mod svakom_v4; -pub mod svakom_v5; -pub mod svakom_v6; -pub mod synchro; -pub mod tcode_v03; -pub mod thehandy; -pub mod tryfun; -pub mod tryfun_blackhole; -pub mod tryfun_meta2; -pub mod vibcrafter; -pub mod vibratissimo; -pub mod vorze_sa; -pub mod wetoy; -pub mod wevibe; -pub mod wevibe8bit; -pub mod wevibe_chorus; -pub mod xibao; -pub mod xinput; -pub mod xiuxiuda; -pub mod xuanhuan; -pub mod youcups; -pub mod youou; -pub mod zalo; - -use crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ - self, - ActuatorType, - ButtplugDeviceCommandMessageUnion, - ButtplugDeviceMessage, - ButtplugServerDeviceMessage, - //ButtplugServerMessage, - Endpoint, - SensorReadingV4, - SensorType, - }, - }, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareReadCmd}, - }, -}; -use async_trait::async_trait; -use futures::{ - future::{self, BoxFuture, FutureExt}, - StreamExt, -}; -use std::pin::Pin; -use std::{collections::HashMap, sync::Arc}; - -/// Strategy for situations where hardware needs to get updates every so often in order to keep -/// things alive. Currently this only applies to iOS backgrounding with bluetooth devices, but since -/// we never know which of our hundreds of supported devices someone might connect, we need context -/// as to which keepalive strategy to use. -/// -/// When choosing a keepalive strategy for a protocol: -/// -/// - All protocols use NoStrategy by default. For many devices, sending trash will break them in -/// very weird ways and we can't risk that, so we need to know the protocol context. -/// - If the protocol already needs its own keepalive (Satisfyer, Mysteryvibe, etc...), use -/// NoStrategy for now. RepeatLastPacketStrategy could be used, but we'd need per-protocol -/// timeouts at that point. -/// - If the protocol has a command that essentially does nothing to the actuators, set up -/// RepeatPacketStrategy to use that. This is useful for devices that have info commands (like -/// Lovense), ping commands (like The Handy), sensor commands that aren't yet subscribed to output -/// notifications, etc... -/// - For many devices with only scalar actuators, RepeatLastPacketStrategy should work. You just -/// need to make sure the protocol doesn't have a packet counter or something else that will trip -/// if the same packet is replayed multiple times. -/// - For all other devices, use Custom Strategy. This assumes the protocol will have implemented a -/// method to generate a valid packet. -#[derive(Debug)] -pub enum ProtocolKeepaliveStrategy { - /// Do nothing. This is for protocols that already require internal keepalives, like satisfyer, - /// mysteryvibe, etc. - NoStrategy, - /// Repeat a specific packet, such as a ping or a no-op - RepeatPacketStrategy(HardwareWriteCmd), - /// Repeat whatever the last packet sent was, and send Stop commands until first packet sent. This - /// will be useful for most devices that purely use scalar commands. - RepeatLastPacketStrategy, - /// Call a specific method on the protocol implementation to generate keepalive packets. - CustomStrategy, -} - -pub trait ProtocolIdentifierFactory: Send + Sync { - fn identifier(&self) -> &str; - fn create(&self) -> Box; -} - -pub fn get_default_protocol_map() -> HashMap> { - let mut map = HashMap::new(); - fn add_to_protocol_map( - map: &mut HashMap>, - factory: T, - ) where - T: ProtocolIdentifierFactory + 'static, - { - let factory = Arc::new(factory); - map.insert(factory.identifier().to_owned(), factory); - } - - add_to_protocol_map( - &mut map, - activejoy::setup::ActiveJoyIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - adrienlastic::setup::AdrienLasticIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - amorelie_joy::setup::AmorelieJoyIdentifierFactory::default(), - ); - add_to_protocol_map(&mut map, aneros::setup::AnerosIdentifierFactory::default()); - add_to_protocol_map( - &mut map, - bananasome::setup::BananasomeIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - buttplug_passthru::setup::ButtplugPassthruIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - cachito::setup::CachitoIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - cowgirl::setup::CowgirlIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - cowgirl_cone::setup::CowgirlConeIdentifierFactory::default(), - ); - add_to_protocol_map(&mut map, cupido::setup::CupidoIdentifierFactory::default()); - add_to_protocol_map( - &mut map, - deepsire::setup::DeepSireIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - lovense::setup::LovenseIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - hismith::setup::HismithIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - hismith_mini::setup::HismithMiniIdentifierFactory::default(), - ); - add_to_protocol_map(&mut map, htk_bm::setup::HtkBmIdentifierFactory::default()); - add_to_protocol_map( - &mut map, - thehandy::setup::TheHandyIdentifierFactory::default(), - ); - - add_to_protocol_map(&mut map, ankni::setup::AnkniIdentifierFactory::default()); - add_to_protocol_map( - &mut map, - feelingso::setup::FeelingSoIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - fleshy_thrust::setup::FleshyThrustIdentifierFactory::default(), - ); - add_to_protocol_map(&mut map, foreo::setup::ForeoIdentifierFactory::default()); - add_to_protocol_map(&mut map, fox::setup::FoxIdentifierFactory::default()); - add_to_protocol_map( - &mut map, - fredorch::setup::FredorchIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - fredorch_rotary::setup::FredorchRotaryIdentifierFactory::default(), - ); - - add_to_protocol_map(&mut map, hgod::setup::HgodIdentifierFactory::default()); - - add_to_protocol_map( - &mut map, - galaku_pump::setup::GalakuPumpIdentifierFactory::default(), - ); - - add_to_protocol_map(&mut map, galaku::setup::GalakuIdentifierFactory::default()); - - add_to_protocol_map(&mut map, itoys::setup::IToysIdentifierFactory::default()); - add_to_protocol_map(&mut map, jejoue::setup::JeJoueIdentifierFactory::default()); - add_to_protocol_map(&mut map, joyhub::setup::JoyHubIdentifierFactory::default()); - add_to_protocol_map( - &mut map, - joyhub_v2::setup::JoyHubV2IdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - joyhub_v3::setup::JoyHubV3IdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - joyhub_v4::setup::JoyHubV4IdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - joyhub_v5::setup::JoyHubV5IdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - joyhub_v6::setup::JoyHubV6IdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - kiiroo_prowand::setup::KiirooProWandIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - kiiroo_spot::setup::KiirooSpotIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - kiiroo_v2::setup::KiirooV2IdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - kiiroo_v2_vibrator::setup::KiirooV2VibratorIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - kiiroo_v21::setup::KiirooV21IdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - kiiroo_v21_initialized::setup::KiirooV21InitializedIdentifierFactory::default(), - ); - add_to_protocol_map(&mut map, kizuna::setup::KizunaIdentifierFactory::default()); - add_to_protocol_map( - &mut map, - lelof1s::setup::LeloF1sIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - lelof1sv2::setup::LeloF1sV2IdentifierFactory::default(), - ); - add_to_protocol_map(&mut map, leten::setup::LetenIdentifierFactory::default()); - add_to_protocol_map( - &mut map, - lelo_harmony::setup::LeloHarmonyIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - libo_elle::setup::LiboElleIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - libo_shark::setup::LiboSharkIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - libo_vibes::setup::LiboVibesIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - lioness::setup::LionessIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - longlosttouch::setup::LongLostTouchIdentifierFactory::default(), - ); - add_to_protocol_map(&mut map, loob::setup::LoobIdentifierFactory::default()); - add_to_protocol_map( - &mut map, - lovehoney_desire::setup::LovehoneyDesireIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - lovedistance::setup::LoveDistanceIdentifierFactory::default(), - ); - - add_to_protocol_map( - &mut map, - lovense_connect_service::setup::LovenseConnectServiceIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - lovenuts::setup::LoveNutsIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - luvmazer::setup::LuvmazerIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - magic_motion_v1::setup::MagicMotionV1IdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - magic_motion_v2::setup::MagicMotionV2IdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - magic_motion_v3::setup::MagicMotionV3IdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - magic_motion_v4::setup::MagicMotionV4IdentifierFactory::default(), - ); - add_to_protocol_map(&mut map, mannuo::setup::ManNuoIdentifierFactory::default()); - add_to_protocol_map(&mut map, maxpro::setup::MaxproIdentifierFactory::default()); - add_to_protocol_map(&mut map, meese::setup::MeeseIdentifierFactory::default()); - add_to_protocol_map( - &mut map, - metaxsire::setup::MetaXSireIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - metaxsire_repeat::setup::MetaXSireRepeatIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - metaxsire_v2::setup::MetaXSireV2IdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - metaxsire_v3::setup::MetaXSireV3IdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - metaxsire_v4::setup::MetaXSireV4IdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - mizzzee::setup::MizzZeeIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - mizzzee_v2::setup::MizzZeeV2IdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - mizzzee_v3::setup::MizzZeeV3IdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - monsterpub::setup::MonsterPubIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - motorbunny::setup::MotorbunnyIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - mysteryvibe::setup::MysteryVibeIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - mysteryvibe_v2::setup::MysteryVibeV2IdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - nexus_revo::setup::NexusRevoIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - nextlevelracing::setup::NextLevelRacingIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - nintendo_joycon::setup::NintendoJoyconIdentifierFactory::default(), - ); - add_to_protocol_map(&mut map, nobra::setup::NobraIdentifierFactory::default()); - add_to_protocol_map(&mut map, omobo::setup::OmoboIdentifierFactory::default()); - add_to_protocol_map(&mut map, patoo::setup::PatooIdentifierFactory::default()); - add_to_protocol_map( - &mut map, - picobong::setup::PicobongIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - pink_punch::setup::PinkPunchIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - prettylove::setup::PrettyLoveIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - raw_protocol::setup::RawProtocolIdentifierFactory::default(), - ); - add_to_protocol_map(&mut map, realov::setup::RealovIdentifierFactory::default()); - add_to_protocol_map( - &mut map, - sakuraneko::setup::SakuranekoIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - satisfyer::setup::SatisfyerIdentifierFactory::default(), - ); - add_to_protocol_map(&mut map, sensee::setup::SenseeIdentifierFactory::default()); - add_to_protocol_map( - &mut map, - sensee_capsule::setup::SenseeCapsuleIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - sensee_v2::setup::SenseeV2IdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - sexverse_lg389::setup::SexverseLG389IdentifierFactory::default(), - ); - add_to_protocol_map(&mut map, serveu::setup::ServeUIdentifierFactory::default()); - add_to_protocol_map(&mut map, svakom::setup::SvakomIdentifierFactory::default()); - add_to_protocol_map( - &mut map, - svakom_avaneo::setup::SvakomAvaNeoIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - svakom_alex::setup::SvakomAlexIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - svakom_alex_v2::setup::SvakomAlexV2IdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - svakom_barnard::setup::SvakomBarnardIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - svakom_barney::setup::SvakomBarneyIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - svakom_dice::setup::SvakomDiceIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - svakom_dt250a::setup::SvakomDT250AIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - svakom_iker::setup::SvakomIkerIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - svakom_jordan::setup::SvakomJordanIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - svakom_pulse::setup::SvakomPulseIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - svakom_sam::setup::SvakomSamIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - svakom_sam2::setup::SvakomSam2IdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - svakom_suitcase::setup::SvakomSuitcaseIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - svakom_tarax::setup::SvakomTaraXIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - svakom_v2::setup::SvakomV2IdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - svakom_v3::setup::SvakomV3IdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - svakom_v4::setup::SvakomV4IdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - svakom_v5::setup::SvakomV5IdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - svakom_v6::setup::SvakomV6IdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - synchro::setup::SynchroIdentifierFactory::default(), - ); - add_to_protocol_map(&mut map, tryfun::setup::TryFunIdentifierFactory::default()); - add_to_protocol_map( - &mut map, - tryfun_blackhole::setup::TryFunBlackHoleIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - tryfun_meta2::setup::TryFunMeta2IdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - tcode_v03::setup::TCodeV03IdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - vibcrafter::setup::VibCrafterIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - vibratissimo::setup::VibratissimoIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - vorze_sa::setup::VorzeSAIdentifierFactory::default(), - ); - add_to_protocol_map(&mut map, wetoy::setup::WeToyIdentifierFactory::default()); - add_to_protocol_map(&mut map, wevibe::setup::WeVibeIdentifierFactory::default()); - add_to_protocol_map( - &mut map, - wevibe8bit::setup::WeVibe8BitIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - wevibe_chorus::setup::WeVibeChorusIdentifierFactory::default(), - ); - add_to_protocol_map(&mut map, xibao::setup::XibaoIdentifierFactory::default()); - add_to_protocol_map(&mut map, xinput::setup::XInputIdentifierFactory::default()); - add_to_protocol_map( - &mut map, - xiuxiuda::setup::XiuxiudaIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - xuanhuan::setup::XuanhuanIdentifierFactory::default(), - ); - add_to_protocol_map( - &mut map, - youcups::setup::YoucupsIdentifierFactory::default(), - ); - add_to_protocol_map(&mut map, youou::setup::YououIdentifierFactory::default()); - add_to_protocol_map(&mut map, zalo::setup::ZaloIdentifierFactory::default()); - add_to_protocol_map( - &mut map, - kgoal_boost::setup::KGoalBoostIdentifierFactory::default(), - ); - map -} - -fn print_type_of(_: &T) -> &'static str { - std::any::type_name::() -} - -pub struct ProtocolSpecializer { - specifiers: Vec, - identifier: Box, -} - -impl ProtocolSpecializer { - pub fn new( - specifiers: Vec, - identifier: Box, - ) -> Self { - Self { - specifiers, - identifier, - } - } - - pub fn specifiers(&self) -> &Vec { - &self.specifiers - } - - pub fn identify(self) -> Box { - self.identifier - } -} - -#[async_trait] -pub trait ProtocolIdentifier: Sync + Send { - async fn identify( - &mut self, - hardware: Arc, - specifier: ProtocolCommunicationSpecifier, - ) -> Result<(UserDeviceIdentifier, Box), ButtplugDeviceError>; -} - -#[async_trait] -pub trait ProtocolInitializer: Sync + Send { - async fn initialize( - &mut self, - hardware: Arc, - device_definition: &UserDeviceDefinition, - ) -> Result, ButtplugDeviceError>; -} - -pub struct GenericProtocolIdentifier { - handler: Option>, - protocol_identifier: String, -} - -impl GenericProtocolIdentifier { - pub fn new(handler: Arc, protocol_identifier: &str) -> Self { - Self { - handler: Some(handler), - protocol_identifier: protocol_identifier.to_owned(), - } - } -} - -#[async_trait] -impl ProtocolIdentifier for GenericProtocolIdentifier { - async fn identify( - &mut self, - hardware: Arc, - _: ProtocolCommunicationSpecifier, - ) -> Result<(UserDeviceIdentifier, Box), ButtplugDeviceError> { - let device_identifier = UserDeviceIdentifier::new( - hardware.address(), - &self.protocol_identifier, - &Some(hardware.name().to_owned()), - ); - Ok(( - device_identifier, - Box::new(GenericProtocolInitializer::new( - self.handler.take().unwrap(), - )), - )) - } -} - -pub struct GenericProtocolInitializer { - handler: Option>, -} - -impl GenericProtocolInitializer { - pub fn new(handler: Arc) -> Self { - Self { - handler: Some(handler), - } - } -} - -#[async_trait] -impl ProtocolInitializer for GenericProtocolInitializer { - async fn initialize( - &mut self, - _: Arc, - _: &UserDeviceDefinition, - ) -> Result, ButtplugDeviceError> { - Ok(self.handler.take().unwrap()) - } -} - -pub trait ProtocolHandler: Sync + Send { - fn needs_full_command_set(&self) -> bool { - false - } - - fn has_handle_message(&self) -> bool { - false - } - - fn keepalive_strategy(&self) -> ProtocolKeepaliveStrategy { - ProtocolKeepaliveStrategy::NoStrategy - } - - fn handle_message( - &self, - message: &ButtplugDeviceCommandMessageUnion, - ) -> Result, ButtplugDeviceError> { - self.command_unimplemented(print_type_of(&message)) - } - - // Allow here since this changes between debug/release - #[allow(unused_variables)] - fn command_unimplemented( - &self, - command: &str, - ) -> Result, ButtplugDeviceError> { - #[cfg(debug_assertions)] - unimplemented!("Command not implemented for this protocol"); - #[cfg(not(debug_assertions))] - Err(ButtplugDeviceError::UnhandledCommand(format!( - "Command not implemented for this protocol: {}", - command - ))) - } - - // The default scalar handler assumes that most devices require discrete commands per feature. If - // a protocol has commands that combine multiple features, either with matched or unmatched - // actuators, they should just implement their own version of this method. - fn handle_scalar_cmd( - &self, - commands: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - let mut command_vec = vec![]; - for (index, command) in commands.iter().enumerate().filter(|(_, x)| x.is_some()) { - let (actuator, scalar) = command.as_ref().expect("Already verified existence"); - command_vec.append( - &mut (match *actuator { - ActuatorType::Constrict => self.handle_scalar_constrict_cmd(index as u32, *scalar)?, - ActuatorType::Inflate => self.handle_scalar_inflate_cmd(index as u32, *scalar)?, - ActuatorType::Oscillate => self.handle_scalar_oscillate_cmd(index as u32, *scalar)?, - ActuatorType::Rotate => self.handle_scalar_rotate_cmd(index as u32, *scalar)?, - ActuatorType::Vibrate => self.handle_scalar_vibrate_cmd(index as u32, *scalar)?, - ActuatorType::Position => self.handle_scalar_position_cmd(index as u32, *scalar)?, - ActuatorType::Unknown => Err(ButtplugDeviceError::UnhandledCommand( - "Unknown actuator types are not controllable.".to_owned(), - ))?, - }), - ); - } - Ok(command_vec) - } - - fn handle_scalar_vibrate_cmd( - &self, - _index: u32, - _scalar: u32, - ) -> Result, ButtplugDeviceError> { - self.command_unimplemented("ScalarCmd (Vibrate Actuator)") - } - - fn handle_scalar_rotate_cmd( - &self, - _index: u32, - _scalar: u32, - ) -> Result, ButtplugDeviceError> { - self.command_unimplemented("ScalarCmd (Rotate Actuator)") - } - - fn handle_scalar_oscillate_cmd( - &self, - _index: u32, - _scalar: u32, - ) -> Result, ButtplugDeviceError> { - self.command_unimplemented("ScalarCmd (Osccilate Actuator)") - } - - fn handle_scalar_inflate_cmd( - &self, - _index: u32, - _scalar: u32, - ) -> Result, ButtplugDeviceError> { - self.command_unimplemented("ScalarCmd (Inflate Actuator)") - } - - fn handle_scalar_constrict_cmd( - &self, - _index: u32, - _scalar: u32, - ) -> Result, ButtplugDeviceError> { - self.command_unimplemented("ScalarCmd (Constrict Actuator)") - } - - fn handle_scalar_position_cmd( - &self, - _index: u32, - _scalar: u32, - ) -> Result, ButtplugDeviceError> { - self.command_unimplemented("ScalarCmd (Constrict Actuator)") - } - - fn handle_vorze_a10_cyclone_cmd( - &self, - message: message::VorzeA10CycloneCmdV0, - ) -> Result, ButtplugDeviceError> { - self.command_unimplemented(print_type_of(&message)) - } - - fn handle_kiiroo_cmd( - &self, - message: message::KiirooCmdV0, - ) -> Result, ButtplugDeviceError> { - self.command_unimplemented(print_type_of(&message)) - } - - fn handle_fleshlight_launch_fw12_cmd( - &self, - message: message::FleshlightLaunchFW12CmdV0, - ) -> Result, ButtplugDeviceError> { - self.command_unimplemented(print_type_of(&message)) - } - - fn handle_rotate_cmd( - &self, - _commands: &[Option<(u32, bool)>], - ) -> Result, ButtplugDeviceError> { - self.command_unimplemented("RotateCmd") - } - - fn handle_linear_cmd( - &self, - message: message::LinearCmdV4, - ) -> Result, ButtplugDeviceError> { - self.command_unimplemented(print_type_of(&message)) - } - - fn handle_sensor_subscribe_cmd( - &self, - _device: Arc, - _message: &message::SensorSubscribeCmdV4, - ) -> BoxFuture> { - future::ready(Err(ButtplugDeviceError::UnhandledCommand( - "Command not implemented for this protocol: BatteryCmd".to_string(), - ))) - .boxed() - } - - fn handle_sensor_unsubscribe_cmd( - &self, - _device: Arc, - _message: &message::SensorUnsubscribeCmdV4, - ) -> BoxFuture> { - future::ready(Err(ButtplugDeviceError::UnhandledCommand( - "Command not implemented for this protocol: BatteryCmd".to_string(), - ))) - .boxed() - } - - fn handle_sensor_read_cmd( - &self, - device: Arc, - message: &message::SensorReadCmdV4, - ) -> BoxFuture> { - match message.sensor_type() { - SensorType::Battery => self.handle_battery_level_cmd(device, message.clone()), - _ => future::ready(Err(ButtplugDeviceError::UnhandledCommand( - "Command not implemented for this protocol: SensorReadCmd".to_string(), - ))) - .boxed(), - } - } - - // Handle Battery Level returns a SensorReading, as we'll always need to do a sensor index - // conversion on it. - fn handle_battery_level_cmd( - &self, - device: Arc, - message: message::SensorReadCmdV4, - ) -> BoxFuture> { - // If we have a standardized BLE Battery endpoint, handle that above the - // protocol, as it'll always be the same. - if device.endpoints().contains(&Endpoint::RxBLEBattery) { - debug!("Trying to get battery reading."); - let msg = HardwareReadCmd::new(Endpoint::RxBLEBattery, 1, 0); - let fut = device.read_value(&msg); - async move { - let hw_msg = fut.await?; - let battery_level = hw_msg.data()[0] as i32; - let battery_reading = message::SensorReadingV4::new( - message.device_index(), - *message.feature_index(), - *message.sensor_type(), - vec![battery_level], - ); - debug!("Got battery reading: {}", battery_level); - Ok(battery_reading) - } - .boxed() - } else { - future::ready(Err(ButtplugDeviceError::UnhandledCommand( - "Command not implemented for this protocol: SensorReadCmd".to_string(), - ))) - .boxed() - } - } - - fn handle_rssi_level_cmd( - &self, - _device: Arc, - _message: message::RSSILevelCmdV2, - ) -> BoxFuture> { - future::ready(Err(ButtplugDeviceError::UnhandledCommand( - "Command not implemented for this protocol: SensorReadCmd".to_string(), - ))) - .boxed() - } - - fn event_stream( - &self, - ) -> Pin + Send>> { - tokio_stream::empty().boxed() - } -} - -#[macro_export] -macro_rules! generic_protocol_setup { - ( $protocol_name:ident, $protocol_identifier:tt) => { - paste::paste! { - pub mod setup { - use std::sync::Arc; - use $crate::server::device::protocol::{ - GenericProtocolIdentifier, ProtocolIdentifier, ProtocolIdentifierFactory, - }; - #[derive(Default)] - pub struct [< $protocol_name IdentifierFactory >] {} - - impl ProtocolIdentifierFactory for [< $protocol_name IdentifierFactory >] { - fn identifier(&self) -> &str { - $protocol_identifier - } - - fn create(&self) -> Box { - Box::new(GenericProtocolIdentifier::new( - Arc::new(super::$protocol_name::default()), - self.identifier(), - )) - } - } - } - } - }; -} - -#[macro_export] -macro_rules! generic_protocol_initializer_setup { - ( $protocol_name:ident, $protocol_identifier:tt) => { - paste::paste! { - pub mod setup { - use $crate::server::device::protocol::{ProtocolIdentifier, ProtocolIdentifierFactory}; - #[derive(Default)] - pub struct [< $protocol_name IdentifierFactory >] {} - - impl ProtocolIdentifierFactory for [< $protocol_name IdentifierFactory >] { - fn identifier(&self) -> &str { - $protocol_identifier - } - - fn create(&self) -> Box { - Box::new(super::[< $protocol_name Identifier >]::default()) - } - } - } - - #[derive(Default)] - pub struct [< $protocol_name Identifier >] {} - - #[async_trait] - impl ProtocolIdentifier for [< $protocol_name Identifier >] { - async fn identify( - &mut self, - hardware: Arc, - _: ProtocolCommunicationSpecifier, - ) -> Result<(UserDeviceIdentifier, Box), ButtplugDeviceError> { - Ok((UserDeviceIdentifier::new(hardware.address(), $protocol_identifier, &Some(hardware.name().to_owned())), Box::new([< $protocol_name Initializer >]::default()))) - } - } - } - }; -} - -pub use generic_protocol_initializer_setup; -pub use generic_protocol_setup; - -use super::hardware::HardwareWriteCmd; diff --git a/buttplug/src/server/device/protocol/mysteryvibe.rs b/buttplug/src/server/device/protocol/mysteryvibe.rs deleted file mode 100644 index 36b7ecc31..000000000 --- a/buttplug/src/server/device/protocol/mysteryvibe.rs +++ /dev/null @@ -1,115 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, - }, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, - }, - util::{async_manager, sleep}, -}; -use async_trait::async_trait; -use std::{sync::Arc, time::Duration}; -use tokio::sync::RwLock; - -generic_protocol_initializer_setup!(MysteryVibe, "mysteryvibe"); - -#[derive(Default)] -pub struct MysteryVibeInitializer {} - -#[async_trait] -impl ProtocolInitializer for MysteryVibeInitializer { - async fn initialize( - &mut self, - hardware: Arc, - _: &UserDeviceDefinition, - ) -> Result, ButtplugDeviceError> { - let msg = HardwareWriteCmd::new(Endpoint::TxMode, vec![0x43u8, 0x02u8, 0x00u8], true); - hardware.write_value(&msg).await?; - Ok(Arc::new(MysteryVibe::new(hardware))) - } -} - -// Time between Mysteryvibe update commands, in milliseconds. This is basically -// a best guess derived from watching packet timing a few years ago. -// -// Thelemic vibrator. Neat. -// -const MYSTERYVIBE_COMMAND_DELAY_MS: u64 = 93; - -async fn vibration_update_handler(device: Arc, command_holder: Arc>>) { - info!("Entering Mysteryvibe Control Loop"); - let mut current_command = command_holder.read().await.clone(); - while device - .write_value(&HardwareWriteCmd::new( - Endpoint::TxVibrate, - current_command, - false, - )) - .await - .is_ok() - { - sleep(Duration::from_millis(MYSTERYVIBE_COMMAND_DELAY_MS)).await; - current_command = command_holder.read().await.clone(); - info!("MV Command: {:?}", current_command); - } - info!("Mysteryvibe control loop exiting, most likely due to device disconnection."); -} - -pub struct MysteryVibe { - current_command: Arc>>, -} - -impl MysteryVibe { - fn new(device: Arc) -> Self { - let current_command = Arc::new(RwLock::new(vec![0u8, 0, 0, 0, 0, 0])); - let current_command_clone = current_command.clone(); - async_manager::spawn( - async move { vibration_update_handler(device, current_command_clone).await }, - ); - Self { current_command } - } -} - -impl ProtocolHandler for MysteryVibe { - fn needs_full_command_set(&self) -> bool { - true - } - - fn handle_scalar_cmd( - &self, - cmds: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - let current_command = self.current_command.clone(); - let cmds = cmds.to_vec(); - async_manager::spawn(async move { - let write_mutex = current_command.clone(); - let mut command_writer = write_mutex.write().await; - let command: Vec = cmds - .into_iter() - .map(|x| x.expect("Validity ensured via GCM match_all").1 as u8) - .collect(); - *command_writer = command; - }); - Ok(vec![]) - } -} - -// TODO Write some tests! -// -// At least, once I figure out how to do that with the weird timing on this -// thing. diff --git a/buttplug/src/server/device/protocol/mysteryvibe_v2.rs b/buttplug/src/server/device/protocol/mysteryvibe_v2.rs deleted file mode 100644 index 7dba0470a..000000000 --- a/buttplug/src/server/device/protocol/mysteryvibe_v2.rs +++ /dev/null @@ -1,115 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, - }, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, - }, - util::{async_manager, sleep}, -}; -use async_trait::async_trait; -use std::{sync::Arc, time::Duration}; -use tokio::sync::RwLock; - -generic_protocol_initializer_setup!(MysteryVibeV2, "mysteryvibe-v2"); - -#[derive(Default)] -pub struct MysteryVibeV2Initializer {} - -#[async_trait] -impl ProtocolInitializer for MysteryVibeV2Initializer { - async fn initialize( - &mut self, - hardware: Arc, - _: &UserDeviceDefinition, - ) -> Result, ButtplugDeviceError> { - let msg = HardwareWriteCmd::new(Endpoint::TxMode, vec![0x03u8, 0x02u8, 0x40u8], true); - hardware.write_value(&msg).await?; - Ok(Arc::new(MysteryVibe::new(hardware))) - } -} - -// Time between Mysteryvibe update commands, in milliseconds. This is basically -// a best guess derived from watching packet timing a few years ago. -// -// Thelemic vibrator. Neat. -// -const MYSTERYVIBE_COMMAND_DELAY_MS: u64 = 93; - -async fn vibration_update_handler(device: Arc, command_holder: Arc>>) { - info!("Entering Mysteryvibe Control Loop"); - let mut current_command = command_holder.read().await.clone(); - while device - .write_value(&HardwareWriteCmd::new( - Endpoint::TxVibrate, - current_command, - false, - )) - .await - .is_ok() - { - sleep(Duration::from_millis(MYSTERYVIBE_COMMAND_DELAY_MS)).await; - current_command = command_holder.read().await.clone(); - info!("MV Command: {:?}", current_command); - } - info!("Mysteryvibe control loop exiting, most likely due to device disconnection."); -} - -pub struct MysteryVibe { - current_command: Arc>>, -} - -impl MysteryVibe { - fn new(device: Arc) -> Self { - let current_command = Arc::new(RwLock::new(vec![0u8, 0, 0, 0, 0, 0])); - let current_command_clone = current_command.clone(); - async_manager::spawn( - async move { vibration_update_handler(device, current_command_clone).await }, - ); - Self { current_command } - } -} - -impl ProtocolHandler for MysteryVibe { - fn needs_full_command_set(&self) -> bool { - true - } - - fn handle_scalar_cmd( - &self, - cmds: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - let current_command = self.current_command.clone(); - let cmds = cmds.to_vec(); - async_manager::spawn(async move { - let write_mutex = current_command.clone(); - let mut command_writer = write_mutex.write().await; - let command: Vec = cmds - .into_iter() - .map(|x| x.expect("Validity ensured via GCM match_all").1 as u8) - .collect(); - *command_writer = command; - }); - Ok(vec![]) - } -} - -// TODO Write some tests! -// -// At least, once I figure out how to do that with the weird timing on this -// thing. diff --git a/buttplug/src/server/device/protocol/nexus_revo.rs b/buttplug/src/server/device/protocol/nexus_revo.rs deleted file mode 100644 index 87912442d..000000000 --- a/buttplug/src/server/device/protocol/nexus_revo.rs +++ /dev/null @@ -1,60 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2025 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 crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, -}; - -generic_protocol_setup!(NexusRevo, "nexus-revo"); - -#[derive(Default)] -pub struct NexusRevo {} - -impl ProtocolHandler for NexusRevo { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( - &self, - _index: u32, - scalar: u32, - ) -> Result, ButtplugDeviceError> { - Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - vec![0xaa, 0x01, 0x01, 0x00, 0x01, scalar as u8], - true, - ) - .into()]) - } - - fn handle_rotate_cmd( - &self, - commands: &[Option<(u32, bool)>], - ) -> Result, ButtplugDeviceError> { - if let Some(Some(cmd)) = commands.first() { - return Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - vec![ - 0xaa, - 0x01, - 0x02, - 0x00, - cmd.0 as u8 + if cmd.0 != 0 && cmd.1 { 2 } else { 0 }, - 0x00, - ], - true, - ) - .into()]); - } - Ok(vec![]) - } -} diff --git a/buttplug/src/server/device/protocol/sensee_v2.rs b/buttplug/src/server/device/protocol/sensee_v2.rs deleted file mode 100644 index b291ed2c6..000000000 --- a/buttplug/src/server/device/protocol/sensee_v2.rs +++ /dev/null @@ -1,160 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint, FeatureType}, - }, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareReadCmd, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, - }, -}; -use async_trait::async_trait; -use std::sync::Arc; - -generic_protocol_initializer_setup!(SenseeV2, "sensee-v2"); - -#[derive(Default)] -pub struct SenseeV2Initializer {} - -#[async_trait] -impl ProtocolInitializer for SenseeV2Initializer { - async fn initialize( - &mut self, - hardware: Arc, - device_definition: &UserDeviceDefinition, - ) -> Result, ButtplugDeviceError> { - let res = hardware - .read_value(&HardwareReadCmd::new(Endpoint::Tx, 128, 500)) - .await?; - info!("Sensee model data: {:X?}", res.data()); - let mut protocol = SenseeV2::default(); - protocol.device_type = if res.data().len() >= 6 { - res.data()[6] - } else { - 0x66 - }; - - protocol.vibe_count = device_definition - .features() - .iter() - .filter(|x| [FeatureType::Vibrate].contains(x.feature_type())) - .count(); - protocol.thrust_count = device_definition - .features() - .iter() - .filter(|x| [FeatureType::Oscillate].contains(x.feature_type())) - .count(); - protocol.suck_count = device_definition - .features() - .iter() - .filter(|x| [FeatureType::Constrict].contains(x.feature_type())) - .count(); - - Ok(Arc::new(protocol)) - } -} - -#[derive(Default)] -pub struct SenseeV2 { - device_type: u8, - vibe_count: usize, - thrust_count: usize, - suck_count: usize, -} - -fn make_cmd(dtype: u8, func: u8, cmd: Vec) -> Vec { - let mut out = vec![0x55, 0xAA, 0xF0]; // fixed start code - out.push(0x02); // version - out.push(0x00); // package numer? - out.push(0x04 + cmd.len() as u8); // Data length - out.push(dtype); // Device type - always 0x66? - out.push(func); // Function code - out.extend(cmd); - - let cdc = vec![0, 0]; - // ToDo: CDC not yet used - out.extend(cdc); - - out -} - -impl ProtocolHandler for SenseeV2 { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - fn needs_full_command_set(&self) -> bool { - true - } - - fn handle_scalar_cmd( - &self, - commands: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - let vibes: Vec<(ActuatorType, u32)> = commands - .iter() - .map(|x| x.expect("Expecting all commands")) - .filter(|x| x.0 == ActuatorType::Vibrate) - .collect(); - let thrusts: Vec<(ActuatorType, u32)> = commands - .iter() - .map(|x| x.expect("Expecting all commands")) - .filter(|x| x.0 == ActuatorType::Oscillate) - .collect(); - let sucks: Vec<(ActuatorType, u32)> = commands - .iter() - .map(|x| x.expect("Expecting all commands")) - .filter(|x| x.0 == ActuatorType::Constrict) - .collect(); - - let mut data = vec![]; - data.push( - if self.vibe_count != 0 { 1 } else { 0 } - + if self.thrust_count != 0 { 1 } else { 0 } - + if self.suck_count != 0 { 1 } else { 0 } as u8, - ); - if self.vibe_count != 0 { - data.push(0); - data.push(self.vibe_count as u8); - for i in 0..self.vibe_count { - data.push((i + 1) as u8); - data.push(vibes[i].1 as u8); - } - } - if self.thrust_count != 0 { - data.push(1); - data.push(self.thrust_count as u8); - for i in 0..self.thrust_count { - data.push((i + 1) as u8); - data.push(thrusts[i].1 as u8); - } - } - if self.suck_count != 0 { - data.push(2); - data.push(self.suck_count as u8); - for i in 0..self.suck_count { - data.push((i + 1) as u8); - data.push(sucks[i].1 as u8); - } - } - - Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - make_cmd(self.device_type, 0xf1, data), - false, - ) - .into()]) - } -} diff --git a/buttplug/src/server/device/protocol/sexverse_lg389.rs b/buttplug/src/server/device/protocol/sexverse_lg389.rs deleted file mode 100644 index 3a94003be..000000000 --- a/buttplug/src/server/device/protocol/sexverse_lg389.rs +++ /dev/null @@ -1,52 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2025 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 crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, - }, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, -}; - -generic_protocol_setup!(SexverseLG389, "sexverse-lg389"); - -#[derive(Default)] -pub struct SexverseLG389 {} - -impl ProtocolHandler for SexverseLG389 { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn needs_full_command_set(&self) -> bool { - true - } - - fn handle_scalar_cmd( - &self, - commands: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - let vibe = commands[0].unwrap_or((ActuatorType::Vibrate, 0)).1 as u8; - let osc = if commands.len() > 1 { - commands[1].unwrap_or((ActuatorType::Oscillate, 0)).1 as u8 - } else { - 0 - }; - let range = if osc == 0 { 0 } else { 4u8 }; // Full range - let anchor = if osc == 0 { 0 } else { 1u8 }; // Anchor to base - Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - vec![0xaa, 0x05, vibe, 0x14, anchor, 0x00, range, 0x00, osc, 0x00], - true, - ) - .into()]) - } -} diff --git a/buttplug/src/server/device/protocol/svakom.rs b/buttplug/src/server/device/protocol/svakom.rs deleted file mode 100644 index be5b90cae..000000000 --- a/buttplug/src/server/device/protocol/svakom.rs +++ /dev/null @@ -1,39 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, -}; - -generic_protocol_setup!(Svakom, "svakom"); - -#[derive(Default)] -pub struct Svakom {} - -impl ProtocolHandler for Svakom { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( - &self, - _index: u32, - scalar: u32, - ) -> Result, ButtplugDeviceError> { - let multiplier: u8 = if scalar == 0 { 0x00 } else { 0x01 }; - Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - [0x55, 0x04, 0x03, 0x00, multiplier, scalar as u8].to_vec(), - false, - ) - .into()]) - } -} diff --git a/buttplug/src/server/device/protocol/svakom_alex.rs b/buttplug/src/server/device/protocol/svakom_alex.rs deleted file mode 100644 index 7ed26da49..000000000 --- a/buttplug/src/server/device/protocol/svakom_alex.rs +++ /dev/null @@ -1,46 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, -}; - -generic_protocol_setup!(SvakomAlex, "svakom-alex"); - -#[derive(Default)] -pub struct SvakomAlex {} - -impl ProtocolHandler for SvakomAlex { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( - &self, - _index: u32, - scalar: u32, - ) -> Result, ButtplugDeviceError> { - Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - [ - 18, - 1, - 3, - 0, - if scalar == 0 { 0xFF } else { scalar as u8 }, - 0, - ] - .to_vec(), - false, - ) - .into()]) - } -} diff --git a/buttplug/src/server/device/protocol/svakom_avaneo.rs b/buttplug/src/server/device/protocol/svakom_avaneo.rs deleted file mode 100644 index 3e4488aa7..000000000 --- a/buttplug/src/server/device/protocol/svakom_avaneo.rs +++ /dev/null @@ -1,129 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2023 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 crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, - }, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, - }, - util::{async_manager, sleep}, -}; -use async_trait::async_trait; -use std::{sync::Arc, time::Duration}; - -generic_protocol_initializer_setup!(SvakomAvaNeo, "svakom-avaneo"); - -#[derive(Default)] -pub struct SvakomAvaNeoInitializer {} - -#[async_trait] -impl ProtocolInitializer for SvakomAvaNeoInitializer { - async fn initialize( - &mut self, - hardware: Arc, - _: &UserDeviceDefinition, - ) -> Result, ButtplugDeviceError> { - Ok(Arc::new(SvakomAvaNeo::new(hardware))) - } -} - -async fn delayed_update_handler(device: Arc, mode: u8, scalar: u8) { - sleep(Duration::from_millis(35)).await; - let res = device - .write_value(&HardwareWriteCmd::new( - Endpoint::Tx, - [0x55, mode, 0x00, 0x00, scalar as u8, 0xff].to_vec(), - false, - )) - .await; - if res.is_err() { - error!("Delayed Svakom Tara X command error: {:?}", res.err()); - } -} - -pub struct SvakomAvaNeo { - device: Arc, -} -impl SvakomAvaNeo { - fn new(device: Arc) -> Self { - Self { device } - } -} - -impl ProtocolHandler for SvakomAvaNeo { - fn handle_scalar_cmd( - &self, - cmds: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - if cmds.len() == 0 { - return Ok(vec![]); - } - - let mut hcmd = None; - if let Some(cmd) = cmds[0] { - let scalar = cmd.1; - hcmd = Some(HardwareWriteCmd::new( - Endpoint::Tx, - [ - 0x55, - 0x03, - 0x00, - 0x00, - if scalar == 0 { 0x00 } else { 0x01 }, - scalar as u8, - ] - .to_vec(), - false, - )); - } - - if cmds.len() < 2 { - return if hcmd.is_some() { - Ok(vec![hcmd.unwrap().into()]) - } else { - Ok(vec![]) - }; - } - - if let Some(cmd) = cmds[1] { - let scalar = cmd.1; - let mode = if cmd.0 == ActuatorType::Vibrate { - 0x09 - } else { - 0x08 - }; - if hcmd.is_none() { - return Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - [0x55, mode, 0x00, 0x00, scalar as u8, 0xff].to_vec(), - false, - ) - .into()]); - } else { - // Sending both commands in quick succession blots the earlier command - let dev = self.device.clone(); - async_manager::spawn(async move { delayed_update_handler(dev, mode, scalar as u8).await }); - } - } - - return if hcmd.is_some() { - Ok(vec![hcmd.unwrap().into()]) - } else { - Ok(vec![]) - }; - } -} diff --git a/buttplug/src/server/device/protocol/svakom_barney.rs b/buttplug/src/server/device/protocol/svakom_barney.rs deleted file mode 100644 index 88dafe431..000000000 --- a/buttplug/src/server/device/protocol/svakom_barney.rs +++ /dev/null @@ -1,69 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::core::message::ActuatorType; -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, -}; - -generic_protocol_setup!(SvakomBarney, "svakom-barney"); - -#[derive(Default)] -pub struct SvakomBarney {} - -impl ProtocolHandler for SvakomBarney { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn needs_full_command_set(&self) -> bool { - true - } - - fn handle_scalar_cmd( - &self, - commands: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - let mut actuator: u8 = 0; - let mut scalar: u8 = 0; - for i in 0..commands.len() { - if let Some(cmd) = commands[i] { - if cmd.1 != 0 && scalar == 0 && commands.len() > 1 { - actuator = i as u8 + 1; // just this actuators - } else if cmd.1 != 0 { - actuator = 0; // all the actuators - } - scalar = u8::max(scalar, cmd.1 as u8); // max of all actuators - } - } - - /*let mut mode = scalar % 3; - if scalar != 0 && mode == 0 { - mode = 3; - } - let speed = (scalar as f64 / 3.0).ceil() as u8;*/ - - Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - [ - 0x55, - 0x03, - actuator, - 0x00, - if scalar == 0 { 0x00 } else { 0x03 }, - scalar, - ] - .to_vec(), - false, - ) - .into()]) - } -} diff --git a/buttplug/src/server/device/protocol/svakom_dt250a.rs b/buttplug/src/server/device/protocol/svakom_dt250a.rs deleted file mode 100644 index 10f31416c..000000000 --- a/buttplug/src/server/device/protocol/svakom_dt250a.rs +++ /dev/null @@ -1,151 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, - }, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, - }, - util::{async_manager, sleep}, -}; -use async_trait::async_trait; -use std::{sync::Arc, time::Duration}; - -generic_protocol_initializer_setup!(SvakomDT250A, "svakom-dt250a"); - -#[derive(Default)] -pub struct SvakomDT250AInitializer {} - -#[async_trait] -impl ProtocolInitializer for SvakomDT250AInitializer { - async fn initialize( - &mut self, - hardware: Arc, - _: &UserDeviceDefinition, - ) -> Result, ButtplugDeviceError> { - Ok(Arc::new(SvakomDT250A::new(hardware))) - } -} - -async fn delayed_update_handler(device: Arc, cmd: Vec, delay: u64) { - sleep(Duration::from_millis(delay)).await; - let res = device - .write_value(&HardwareWriteCmd::new(Endpoint::Tx, cmd, false)) - .await; - if res.is_err() { - error!("Delayed Svakom DT250A command error: {:?}", res.err()); - } -} - -pub struct SvakomDT250A { - device: Arc, -} -impl SvakomDT250A { - fn new(device: Arc) -> Self { - Self { device } - } -} - -impl ProtocolHandler for SvakomDT250A { - fn handle_scalar_cmd( - &self, - cmds: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - if cmds.len() == 0 { - return Ok(vec![]); - } - - let mut delay = 30; - let mut hcmd = None; - if let Some(cmd) = cmds[0] { - let scalar = cmd.1; - - hcmd = Some(HardwareWriteCmd::new( - Endpoint::Tx, - [ - 0x55, - 0x03, - 0x00, - 0x00, - scalar as u8, - if scalar == 0 { 0x00 } else { 0x01 }, - ] - .to_vec(), - false, - )); - } - - if cmds.len() < 2 { - return if hcmd.is_some() { - Ok(vec![hcmd.unwrap().into()]) - } else { - Ok(vec![]) - }; - } - - if let Some(cmd) = cmds[1] { - let scalar = cmd.1; - let data = [ - 0x55, - 0x08, - 0x00, - 0x00, - scalar as u8, - if scalar == 0 { 0x00 } else { 0x01 }, - ] - .to_vec(); - - if hcmd.is_none() { - return Ok(vec![HardwareWriteCmd::new(Endpoint::Tx, data, false).into()]); - } else { - // Sending both commands in quick succession blots the earlier command - let dev = self.device.clone(); - async_manager::spawn(async move { delayed_update_handler(dev, data, delay).await }); - - // This is the minimum time between the 2nd and 3rd command that doesn't seem to just get dropped... - delay += 250; - } - } - - if cmds.len() < 3 { - return if hcmd.is_some() { - Ok(vec![hcmd.unwrap().into()]) - } else { - Ok(vec![]) - }; - } - - if let Some(cmd) = cmds[2] { - let scalar = cmd.1; - let data = [0x55, 0x09, 0x00, 0x00, scalar as u8, 0x00].to_vec(); - - if hcmd.is_none() { - return Ok(vec![HardwareWriteCmd::new(Endpoint::Tx, data, false).into()]); - } else { - // Sending both commands in quick succession blots the earlier command - let dev = self.device.clone(); - async_manager::spawn(async move { delayed_update_handler(dev, data, delay).await }); - } - } - - return if hcmd.is_some() { - Ok(vec![hcmd.unwrap().into()]) - } else { - Ok(vec![]) - }; - } -} diff --git a/buttplug/src/server/device/protocol/svakom_iker.rs b/buttplug/src/server/device/protocol/svakom_iker.rs deleted file mode 100644 index 374b11569..000000000 --- a/buttplug/src/server/device/protocol/svakom_iker.rs +++ /dev/null @@ -1,114 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, - }, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, - }, -}; -use async_trait::async_trait; -use std::sync::atomic::{AtomicU8, Ordering}; -use std::sync::Arc; - -generic_protocol_initializer_setup!(SvakomIker, "svakom-iker"); - -#[derive(Default)] -pub struct SvakomIkerInitializer {} - -#[async_trait] -impl ProtocolInitializer for SvakomIkerInitializer { - async fn initialize( - &mut self, - _: Arc, - _: &UserDeviceDefinition, - ) -> Result, ButtplugDeviceError> { - Ok(Arc::new(SvakomIker::new())) - } -} - -#[derive(Default)] -pub struct SvakomIker { - last_speeds: Arc>, -} - -impl SvakomIker { - fn new() -> Self { - let last_speeds = Arc::new((0..2).map(|_| AtomicU8::new(0)).collect::>()); - - Self { last_speeds } - } -} - -impl ProtocolHandler for SvakomIker { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_cmd( - &self, - cmds: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - let mut vibe_off = false; - let mut msg_vec = vec![]; - if let Some((_, speed)) = cmds[0] { - self.last_speeds[0].store(speed as u8, Ordering::SeqCst); - if speed == 0 { - vibe_off = true; - } - msg_vec.push( - HardwareWriteCmd::new( - Endpoint::Tx, - [0x55, 0x03, 0x03, 0x00, 0x01, speed as u8].to_vec(), - false, - ) - .into(), - ); - } - if cmds.len() > 1 { - if let Some((_, speed)) = cmds[1] { - self.last_speeds[1].store(speed as u8, Ordering::SeqCst); - msg_vec.push( - HardwareWriteCmd::new( - Endpoint::Tx, - [0x55, 0x07, 0x00, 0x00, speed as u8, 0x00].to_vec(), - false, - ) - .into(), - ); - } else if vibe_off && self.last_speeds[1].load(Ordering::SeqCst) != 0 { - msg_vec.push( - HardwareWriteCmd::new( - Endpoint::Tx, - [ - 0x55, - 0x07, - 0x00, - 0x00, - self.last_speeds[1].load(Ordering::SeqCst), - 0x00, - ] - .to_vec(), - false, - ) - .into(), - ); - } - } - Ok(msg_vec) - } -} diff --git a/buttplug/src/server/device/protocol/svakom_sam.rs b/buttplug/src/server/device/protocol/svakom_sam.rs deleted file mode 100644 index bc01e1885..000000000 --- a/buttplug/src/server/device/protocol/svakom_sam.rs +++ /dev/null @@ -1,103 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2023 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 crate::server::device::configuration::ProtocolCommunicationSpecifier; -use crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, - }, - server::device::{ - configuration::{UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareSubscribeCmd, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, - }, -}; -use async_trait::async_trait; -use std::sync::Arc; - -generic_protocol_initializer_setup!(SvakomSam, "svakom-sam"); - -#[derive(Default)] -pub struct SvakomSamInitializer {} - -#[async_trait] -impl ProtocolInitializer for SvakomSamInitializer { - async fn initialize( - &mut self, - hardware: Arc, - _: &UserDeviceDefinition, - ) -> Result, ButtplugDeviceError> { - hardware - .subscribe(&HardwareSubscribeCmd::new(Endpoint::Rx)) - .await?; - let mut gen2 = hardware.endpoints().contains(&Endpoint::TxMode); - if !gen2 && hardware.endpoints().contains(&Endpoint::Firmware) { - gen2 = true; - warn!("Svakom Sam model without speed control detected - This device will only vibrate at 1 speed"); - } - - Ok(Arc::new(SvakomSam::new(gen2))) - } -} - -pub struct SvakomSam { - gen2: bool, -} -impl SvakomSam { - pub fn new(gen2: bool) -> Self { - Self { gen2 } - } -} - -impl ProtocolHandler for SvakomSam { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_cmd( - &self, - cmds: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - let mut msg_vec = vec![]; - if let Some((_, speed)) = cmds[0] { - msg_vec.push( - HardwareWriteCmd::new( - Endpoint::Tx, - if self.gen2 { - [ - 18, - 1, - 3, - 0, - if speed == 0 { 0x00 } else { 0x04 }, - speed as u8, - ] - .to_vec() - } else { - [18, 1, 3, 0, 5, speed as u8].to_vec() - }, - false, - ) - .into(), - ); - } - if cmds.len() > 1 { - if let Some((_, speed)) = cmds[1] { - msg_vec.push( - HardwareWriteCmd::new(Endpoint::Tx, [18, 6, 1, speed as u8].to_vec(), false).into(), - ); - } - } - Ok(msg_vec) - } -} diff --git a/buttplug/src/server/device/protocol/svakom_suitcase.rs b/buttplug/src/server/device/protocol/svakom_suitcase.rs deleted file mode 100644 index 66b82dacb..000000000 --- a/buttplug/src/server/device/protocol/svakom_suitcase.rs +++ /dev/null @@ -1,129 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, - }, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, - }, - util::{async_manager, sleep}, -}; -use async_trait::async_trait; -use std::{sync::Arc, time::Duration}; - -generic_protocol_initializer_setup!(SvakomSuitcase, "svakom-suitcase"); - -#[derive(Default)] -pub struct SvakomSuitcaseInitializer {} - -#[async_trait] -impl ProtocolInitializer for SvakomSuitcaseInitializer { - async fn initialize( - &mut self, - hardware: Arc, - _: &UserDeviceDefinition, - ) -> Result, ButtplugDeviceError> { - Ok(Arc::new(SvakomSuitcase::new(hardware))) - } -} - -async fn delayed_update_handler(device: Arc, scalar: u8) { - sleep(Duration::from_millis(50)).await; - let res = device - .write_value(&HardwareWriteCmd::new( - Endpoint::Tx, - [0x55, 0x09, 0x00, 0x00, scalar as u8, 0x00].to_vec(), - false, - )) - .await; - if res.is_err() { - error!("Delayed Svakom Suitcase command error: {:?}", res.err()); - } -} - -pub struct SvakomSuitcase { - device: Arc, -} -impl SvakomSuitcase { - fn new(device: Arc) -> Self { - Self { device } - } -} - -impl ProtocolHandler for SvakomSuitcase { - fn handle_scalar_cmd( - &self, - cmds: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - if cmds.len() == 0 { - return Ok(vec![]); - } - - let mut hcmd = None; - if let Some(cmd) = cmds[0] { - let scalar = cmd.1; - let mut speed = (scalar % 10) as u8; - let mut intensity = if scalar == 0 { - 0u8 - } else { - (scalar as f32 / 10.0).floor() as u8 + 1 - }; - if speed == 0 && intensity != 0 { - // 10 -> 2,0 -> 1,A - speed = 10; - intensity -= 1; - } - - hcmd = Some(HardwareWriteCmd::new( - Endpoint::Tx, - [0x55, 0x03, 0x00, 0x00, intensity, speed].to_vec(), - false, - )); - } - - if cmds.len() < 2 { - return if hcmd.is_some() { - Ok(vec![hcmd.unwrap().into()]) - } else { - Ok(vec![]) - }; - } - - if let Some(cmd) = cmds[1] { - let scalar = cmd.1; - - if hcmd.is_none() { - return Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - [0x55, 0x09, 0x00, 0x00, scalar as u8, 0x00].to_vec(), - false, - ) - .into()]); - } else { - // Sending both commands in quick succession blots the earlier command - let dev = self.device.clone(); - async_manager::spawn(async move { delayed_update_handler(dev, scalar as u8).await }); - } - } - - return if hcmd.is_some() { - Ok(vec![hcmd.unwrap().into()]) - } else { - Ok(vec![]) - }; - } -} diff --git a/buttplug/src/server/device/protocol/svakom_tarax.rs b/buttplug/src/server/device/protocol/svakom_tarax.rs deleted file mode 100644 index 556bc1c71..000000000 --- a/buttplug/src/server/device/protocol/svakom_tarax.rs +++ /dev/null @@ -1,126 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2023 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 crate::server::device::configuration::ProtocolCommunicationSpecifier; -use crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, - }, - server::device::{ - configuration::{UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, - }, - util::{async_manager, sleep}, -}; -use async_trait::async_trait; -use std::{sync::Arc, time::Duration}; - -generic_protocol_initializer_setup!(SvakomTaraX, "svakom-tarax"); - -#[derive(Default)] -pub struct SvakomTaraXInitializer {} - -#[async_trait] -impl ProtocolInitializer for SvakomTaraXInitializer { - async fn initialize( - &mut self, - hardware: Arc, - _: &UserDeviceDefinition, - ) -> Result, ButtplugDeviceError> { - Ok(Arc::new(SvakomTaraX::new(hardware))) - } -} - -async fn delayed_update_handler(device: Arc, scalar: u8) { - sleep(Duration::from_millis(25)).await; - let res = device - .write_value(&HardwareWriteCmd::new( - Endpoint::Tx, - [0x55, 0x09, 0x00, 0x00, scalar as u8, 0x00].to_vec(), - false, - )) - .await; - if res.is_err() { - error!("Delayed Svakom Tara X command error: {:?}", res.err()); - } -} - -pub struct SvakomTaraX { - device: Arc, -} -impl SvakomTaraX { - fn new(device: Arc) -> Self { - Self { device } - } -} - -impl ProtocolHandler for SvakomTaraX { - fn handle_scalar_cmd( - &self, - cmds: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - if cmds.len() == 0 { - return Ok(vec![]); - } - - let mut hcmd = None; - if let Some(cmd) = cmds[0] { - let scalar = cmd.1; - hcmd = Some(HardwareWriteCmd::new( - Endpoint::Tx, - [ - 0x55, - 0x03, - 0x00, - 0x00, - if scalar == 0 { 0x01 } else { scalar as u8 }, - if scalar == 0 { 0x01 } else { 0x02 }, - ] - .to_vec(), - false, - )); - } - - if cmds.len() < 2 { - return if hcmd.is_some() { - Ok(vec![hcmd.unwrap().into()]) - } else { - Ok(vec![]) - }; - } - - if let Some(cmd) = cmds[1] { - let scalar = cmd.1; - - if hcmd.is_none() { - return Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - [0x55, 0x09, 0x00, 0x00, scalar as u8, 0x00].to_vec(), - false, - ) - .into()]); - } else { - // Sending both commands in quick succession blots the earlier command - let dev = self.device.clone(); - async_manager::spawn(async move { delayed_update_handler(dev, scalar as u8).await }); - } - } - - return if hcmd.is_some() { - Ok(vec![hcmd.unwrap().into()]) - } else { - Ok(vec![]) - }; - } -} diff --git a/buttplug/src/server/device/protocol/svakom_v3.rs b/buttplug/src/server/device/protocol/svakom_v3.rs deleted file mode 100644 index 044f3a92d..000000000 --- a/buttplug/src/server/device/protocol/svakom_v3.rs +++ /dev/null @@ -1,59 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2023 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 crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, -}; - -generic_protocol_setup!(SvakomV3, "svakom-v3"); - -#[derive(Default)] -pub struct SvakomV3 {} - -impl ProtocolHandler for SvakomV3 { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( - &self, - index: u32, - scalar: u32, - ) -> Result, ButtplugDeviceError> { - Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - [ - 0x55, - if index == 0 { 0x03 } else { 0x09 }, - if index == 0 { 0x03 } else { 0x00 }, - 0x00, - if scalar == 0 { 0x00 } else { 0x01 }, - scalar as u8, - ] - .to_vec(), - false, - ) - .into()]) - } - - fn handle_scalar_rotate_cmd( - &self, - _index: u32, - scalar: u32, - ) -> Result, ButtplugDeviceError> { - Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - [0x55, 0x08, 0x00, 0x00, scalar as u8, 0xff].to_vec(), - false, - ) - .into()]) - } -} diff --git a/buttplug/src/server/device/protocol/svakom_v4.rs b/buttplug/src/server/device/protocol/svakom_v4.rs deleted file mode 100644 index b96153927..000000000 --- a/buttplug/src/server/device/protocol/svakom_v4.rs +++ /dev/null @@ -1,63 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2023 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 crate::core::message::ActuatorType; -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, -}; - -generic_protocol_setup!(SvakomV4, "svakom-v4"); - -#[derive(Default)] -pub struct SvakomV4 {} - -impl ProtocolHandler for SvakomV4 { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn needs_full_command_set(&self) -> bool { - true - } - - fn handle_scalar_cmd( - &self, - commands: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - let mut actuator: u8 = 0; - let mut scalar: u8 = 0; - for i in 0..commands.len() { - if let Some(cmd) = commands[i] { - if cmd.1 != 0 && scalar == 0 && commands.len() > 1 { - actuator = i as u8 + 1; // just this actuators - } else if cmd.1 != 0 { - actuator = 0; // all the actuators - } - scalar = u8::max(scalar, cmd.1 as u8); // max of all actuators - } - } - - Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - [ - 0x55, - 0x03, - actuator, - 0x00, - if scalar == 0 { 0x00 } else { 0x01 }, - scalar as u8, - ] - .to_vec(), - false, - ) - .into()]) - } -} diff --git a/buttplug/src/server/device/protocol/svakom_v5.rs b/buttplug/src/server/device/protocol/svakom_v5.rs deleted file mode 100644 index 7e812b4ff..000000000 --- a/buttplug/src/server/device/protocol/svakom_v5.rs +++ /dev/null @@ -1,158 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::core::message::ActuatorType; -use crate::core::message::ActuatorType::{Oscillate, Vibrate}; -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, - }, -}; -use async_trait::async_trait; -use std::sync::{Arc, RwLock}; -generic_protocol_initializer_setup!(SvakomV5, "svakom-v5"); - -#[derive(Default)] -pub struct SvakomV5Initializer {} - -#[async_trait] -impl ProtocolInitializer for SvakomV5Initializer { - async fn initialize( - &mut self, - _: Arc, - _: &UserDeviceDefinition, - ) -> Result, ButtplugDeviceError> { - Ok(Arc::new(SvakomV5::new())) - } -} - -pub struct SvakomV5 { - last_cmds: RwLock>, -} - -impl SvakomV5 { - fn new() -> Self { - let last_cmds = RwLock::new(vec![]); - Self { last_cmds } - } -} - -impl ProtocolHandler for SvakomV5 { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn needs_full_command_set(&self) -> bool { - true - } - - fn handle_scalar_cmd( - &self, - commands: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - let last_commands = self.last_cmds.read().expect("Locks should work").clone(); - let mut hcmds = Vec::new(); - - let vibes = commands - .iter() - .filter(|c| c.is_some_and(|c| c.0 == Vibrate)) - .map(|c| c.unwrap_or((Vibrate, 0))) - .collect::>(); - let last_vibes = last_commands - .iter() - .filter(|c| c.0 == Vibrate) - .map(|c| (c.0, c.1)) - .collect::>(); - - if vibes.len() > 0 { - let mut changed = last_vibes.len() != vibes.len(); - let vibe1 = vibes[0].1; - if !changed && vibes[0].1 != last_vibes[0].1 { - changed = true; - } - let mut vibe2 = vibes[0].1; - if vibes.len() > 1 { - vibe2 = vibes[1].1; - if !changed && vibes[1].1 != last_vibes[1].1 { - changed = true; - } - } - if changed { - hcmds.push( - HardwareWriteCmd::new( - Endpoint::Tx, - [ - 0x55, - 0x03, - if (vibe1 > 0 && vibe2 > 0) || vibe1 == vibe2 { - 0x00 - } else if vibe1 > 0 { - 0x01 - } else { - 0x02 - }, - 0x00, - if vibe1 == vibe2 && vibe1 == 0 { - 0x00 - } else { - 0x01 - }, - vibe1.max(vibe2) as u8, - ] - .to_vec(), - false, - ) - .into(), - ); - } - } - - let oscs = commands - .iter() - .filter(|c| c.is_some_and(|c| c.0 == Oscillate)) - .map(|c| c.unwrap_or((Oscillate, 0))) - .collect::>(); - let last_oscs = last_commands - .iter() - .filter(|c| c.0 == Oscillate) - .map(|c| (c.0, c.1)) - .collect::>(); - if oscs.len() > 0 { - let mut changed = oscs.len() != last_oscs.len(); - if !changed && oscs[0].1 != last_oscs[0].1 { - changed = true; - } - - if changed { - hcmds.push( - HardwareWriteCmd::new( - Endpoint::Tx, - [0x55, 0x09, 0x00, 0x00, oscs[0].1 as u8, 0x00].to_vec(), - false, - ) - .into(), - ); - } - } - - let mut command_writer = self.last_cmds.write().expect("Locks should work"); - *command_writer = commands - .iter() - .filter(|c| c.is_some()) - .map(|c| c.unwrap_or((Vibrate, 0))) - .collect::>(); - Ok(hcmds) - } -} diff --git a/buttplug/src/server/device/protocol/svakom_v6.rs b/buttplug/src/server/device/protocol/svakom_v6.rs deleted file mode 100644 index f9a876156..000000000 --- a/buttplug/src/server/device/protocol/svakom_v6.rs +++ /dev/null @@ -1,163 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2025 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 crate::{ - core::{ - errors::ButtplugDeviceError, - message::Endpoint, - message::{ActuatorType, ActuatorType::Vibrate}, - }, - generic_protocol_initializer_setup, - server::device::{ - configuration::UserDeviceDefinition, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ - ProtocolCommunicationSpecifier, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - UserDeviceIdentifier, - }, - }, -}; -use async_trait::async_trait; -use std::sync::{Arc, RwLock}; - -generic_protocol_initializer_setup!(SvakomV6, "svakom-v6"); - -#[derive(Default)] -pub struct SvakomV6Initializer {} - -#[async_trait] -impl ProtocolInitializer for SvakomV6Initializer { - async fn initialize( - &mut self, - _: Arc, - _: &UserDeviceDefinition, - ) -> Result, ButtplugDeviceError> { - Ok(Arc::new(SvakomV6::new())) - } -} - -pub struct SvakomV6 { - last_cmds: RwLock>, -} - -impl SvakomV6 { - fn new() -> Self { - let last_cmds = RwLock::new(vec![]); - Self { last_cmds } - } -} - -impl ProtocolHandler for SvakomV6 { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn needs_full_command_set(&self) -> bool { - true - } - - fn handle_scalar_cmd( - &self, - commands: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - let last_commands = self.last_cmds.read().expect("Locks should work").clone(); - let mut hcmds = Vec::new(); - - let vibes = commands - .iter() - .filter(|c| c.is_some_and(|c| c.0 == Vibrate)) - .map(|c| c.unwrap_or((Vibrate, 0))) - .collect::>(); - let last_vibes = last_commands - .iter() - .filter(|c| c.0 == Vibrate) - .map(|c| (c.0, c.1)) - .collect::>(); - - if vibes.len() > 0 { - let mut changed = last_vibes.len() != vibes.len(); - let vibe1 = vibes[0].1; - if !changed && vibes[0].1 != last_vibes[0].1 { - changed = true; - } - let mut vibe2 = vibes[0].1; - if vibes.len() > 1 { - vibe2 = vibes[1].1; - if !changed && vibes[1].1 != last_vibes[1].1 { - changed = true; - } - } - if changed { - hcmds.push( - HardwareWriteCmd::new( - Endpoint::Tx, - [ - 0x55, - 0x03, - if (vibe1 > 0 && vibe2 > 0) || vibe1 == vibe2 { - 0x00 - } else if vibe1 > 0 { - 0x01 - } else { - 0x02 - }, - 0x00, - if vibe1 == vibe2 && vibe1 == 0 { - 0x00 - } else { - 0x01 - }, - vibe1.max(vibe2) as u8, - 0x00, - ] - .to_vec(), - false, - ) - .into(), - ); - } - } - - if vibes.len() > 2 { - let mut changed = last_vibes.len() != vibes.len(); - let vibe3 = vibes[2].1; - if !changed && vibes[2].1 != last_vibes[2].1 { - changed = true; - } - if changed { - hcmds.push( - HardwareWriteCmd::new( - Endpoint::Tx, - [ - 0x55, - 0x07, - 0x00, - 0x00, - if vibe3 == 0 { 0x00 } else { 0x01 }, - vibe3 as u8, - 0x00, - ] - .to_vec(), - false, - ) - .into(), - ); - } - } - - let mut command_writer = self.last_cmds.write().expect("Locks should work"); - *command_writer = commands - .iter() - .filter(|c| c.is_some()) - .map(|c| c.unwrap_or((Vibrate, 0))) - .collect::>(); - Ok(hcmds) - } -} diff --git a/buttplug/src/server/device/protocol/synchro.rs b/buttplug/src/server/device/protocol/synchro.rs deleted file mode 100644 index 8d2712c58..000000000 --- a/buttplug/src/server/device/protocol/synchro.rs +++ /dev/null @@ -1,52 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, -}; - -generic_protocol_setup!(Synchro, "synchro"); - -#[derive(Default)] -pub struct Synchro {} - -impl ProtocolHandler for Synchro { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_rotate_cmd( - &self, - cmds: &[Option<(u32, bool)>], - ) -> Result, ButtplugDeviceError> { - if let Some(Some((speed, clockwise))) = cmds.get(0) { - Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - vec![ - 0xa1, - 0x01, - *speed as u8 - | if *clockwise || *speed == 0 { - 0x00 - } else { - 0x80 - }, - 0x77, - 0x55, - ], - false, - ) - .into()]) - } else { - Ok(vec![]) - } - } -} diff --git a/buttplug/src/server/device/protocol/tcode_v03.rs b/buttplug/src/server/device/protocol/tcode_v03.rs deleted file mode 100644 index e10f4edd6..000000000 --- a/buttplug/src/server/device/protocol/tcode_v03.rs +++ /dev/null @@ -1,51 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::{ - core::{ - errors::ButtplugDeviceError, - message::{self, Endpoint}, - }, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, -}; - -generic_protocol_setup!(TCodeV03, "tcode-v03"); - -#[derive(Default)] -pub struct TCodeV03 {} - -impl ProtocolHandler for TCodeV03 { - fn handle_linear_cmd( - &self, - msg: message::LinearCmdV4, - ) -> Result, ButtplugDeviceError> { - let mut msg_vec = vec![]; - for v in msg.vectors() { - let position = (v.position() * 99f64) as u32; - - let command = format!("L{}{:02}I{}\n", v.feature_index(), position, v.duration()); - msg_vec.push(HardwareWriteCmd::new(Endpoint::Tx, command.as_bytes().to_vec(), false).into()); - } - Ok(msg_vec) - } - - fn handle_scalar_vibrate_cmd( - &self, - index: u32, - scalar: u32, - ) -> Result, ButtplugDeviceError> { - Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - format!("V{}{:02}\n", index, scalar).as_bytes().to_vec(), - false, - ) - .into()]) - } -} diff --git a/buttplug/src/server/device/protocol/tryfun_meta2.rs b/buttplug/src/server/device/protocol/tryfun_meta2.rs deleted file mode 100644 index dc22ef2b3..000000000 --- a/buttplug/src/server/device/protocol/tryfun_meta2.rs +++ /dev/null @@ -1,119 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2025 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 crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - generic_protocol_setup, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::ProtocolHandler, - }, -}; -use std::sync::atomic::{AtomicU8, Ordering}; - -generic_protocol_setup!(TryFunMeta2, "tryfun-meta2"); - -#[derive(Default)] -pub struct TryFunMeta2 { - packet_id: AtomicU8, -} - -impl ProtocolHandler for TryFunMeta2 { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_oscillate_cmd( - &self, - _index: u32, - scalar: u32, - ) -> Result, ButtplugDeviceError> { - let mut sum: u8 = 0xff; - let mut data = vec![ - self.packet_id.fetch_add(1, Ordering::Relaxed), - 0x02, - 0x00, - 0x05, - 0x21, - 0x05, - 0x0b, - scalar as u8, - ]; - let mut count = 1; - for item in data.iter().skip(1) { - sum -= item; - count += 1; - } - sum += count; - data.push(sum); - - Ok(vec![HardwareWriteCmd::new(Endpoint::Tx, data, false).into()]) - } - - fn handle_rotate_cmd( - &self, - commands: &[Option<(u32, bool)>], - ) -> Result, ButtplugDeviceError> { - if commands.len() >= 1 { - if let Some(cmd) = commands[0] { - let mut speed = cmd.0 as i8; - if cmd.1 { - speed += 1; - speed *= -1; - } - let mut sum: u8 = 0xff; - let mut data = vec![ - self.packet_id.fetch_add(1, Ordering::Relaxed), - 0x02, - 0x00, - 0x05, - 0x21, - 0x05, - 0x0e, - speed as u8, - ]; - let mut count = 1; - for item in data.iter().skip(1) { - sum -= item; - count += 1; - } - sum += count; - data.push(sum); - - return Ok(vec![HardwareWriteCmd::new(Endpoint::Tx, data, false).into()]); - } - } - Ok(vec![]) - } - - fn handle_scalar_vibrate_cmd( - &self, - _index: u32, - scalar: u32, - ) -> Result, ButtplugDeviceError> { - let mut sum: u8 = 0xff; - let mut data = vec![ - self.packet_id.fetch_add(1, Ordering::Relaxed), - 0x02, - 0x00, - 0x05, - 0x21, - 0x05, - 0x08, - scalar as u8, - ]; - let mut count = 1; - for item in data.iter().skip(1) { - sum -= item; - count += 1; - } - sum += count; - data.push(sum); - - Ok(vec![HardwareWriteCmd::new(Endpoint::Tx, data, false).into()]) - } -} diff --git a/buttplug/src/server/device/protocol/vibratissimo.rs b/buttplug/src/server/device/protocol/vibratissimo.rs deleted file mode 100644 index 6c89e06b2..000000000 --- a/buttplug/src/server/device/protocol/vibratissimo.rs +++ /dev/null @@ -1,97 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::server::device::configuration::ProtocolCommunicationSpecifier; -use crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, - }, - server::device::{ - configuration::{UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareReadCmd, HardwareWriteCmd}, - protocol::{ProtocolHandler, ProtocolIdentifier, ProtocolInitializer}, - }, -}; -use async_trait::async_trait; -use std::sync::Arc; - -pub mod setup { - use crate::server::device::protocol::{ProtocolIdentifier, ProtocolIdentifierFactory}; - #[derive(Default)] - pub struct VibratissimoIdentifierFactory {} - - impl ProtocolIdentifierFactory for VibratissimoIdentifierFactory { - fn identifier(&self) -> &str { - "vibratissimo" - } - - fn create(&self) -> Box { - Box::new(super::VibratissimoIdentifier::default()) - } - } -} - -#[derive(Default)] -pub struct VibratissimoIdentifier {} - -#[async_trait] -impl ProtocolIdentifier for VibratissimoIdentifier { - async fn identify( - &mut self, - hardware: Arc, - _: ProtocolCommunicationSpecifier, - ) -> Result<(UserDeviceIdentifier, Box), ButtplugDeviceError> { - let result = hardware - .read_value(&HardwareReadCmd::new(Endpoint::RxBLEModel, 128, 500)) - .await?; - let ident = - String::from_utf8(result.data().to_vec()).unwrap_or_else(|_| hardware.name().to_owned()); - Ok(( - UserDeviceIdentifier::new(hardware.address(), "vibratissimo", &Some(ident)), - Box::new(VibratissimoInitializer::default()), - )) - } -} - -#[derive(Default)] -pub struct VibratissimoInitializer {} - -#[async_trait] -impl ProtocolInitializer for VibratissimoInitializer { - async fn initialize( - &mut self, - _: Arc, - _: &UserDeviceDefinition, - ) -> Result, ButtplugDeviceError> { - Ok(Arc::new(Vibratissimo::default())) - } -} - -#[derive(Default)] -pub struct Vibratissimo {} - -impl ProtocolHandler for Vibratissimo { - fn handle_scalar_cmd( - &self, - cmds: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - let mut data: Vec = Vec::new(); - for cmd in cmds { - data.push(cmd.unwrap_or((ActuatorType::Vibrate, 0)).1 as u8); - } - if data.len() == 1 { - data.push(0x00); - } - - // Put the device in write mode - Ok(vec![ - HardwareWriteCmd::new(Endpoint::TxMode, vec![0x03, 0xff], false).into(), - HardwareWriteCmd::new(Endpoint::TxVibrate, data, false).into(), - ]) - } -} diff --git a/buttplug/src/server/device/protocol/vorze_sa.rs b/buttplug/src/server/device/protocol/vorze_sa.rs deleted file mode 100644 index f1fa82fb7..000000000 --- a/buttplug/src/server/device/protocol/vorze_sa.rs +++ /dev/null @@ -1,217 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::server::device::configuration::ProtocolCommunicationSpecifier; -use crate::{ - core::{ - errors::ButtplugDeviceError, - message::{self, Endpoint}, - }, - server::device::{ - configuration::{UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, - }, -}; -use async_trait::async_trait; -use std::sync::{ - atomic::{AtomicU8, Ordering}, - Arc, -}; - -generic_protocol_initializer_setup!(VorzeSA, "vorze-sa"); - -#[derive(Default)] -pub struct VorzeSAInitializer {} - -#[async_trait] -impl ProtocolInitializer for VorzeSAInitializer { - async fn initialize( - &mut self, - hardware: Arc, - _: &UserDeviceDefinition, - ) -> Result, ButtplugDeviceError> { - let hwname = hardware.name().to_ascii_lowercase(); - let device_type = if hwname.contains("cycsa") { - VorzeDevice::Cyclone - } else if hwname.contains("ufo-tw") { - VorzeDevice::UfoTw - } else if hwname.contains("ufo") { - VorzeDevice::Ufo - } else if hwname.contains("bach") { - VorzeDevice::Bach - } else if hwname.contains("rocket") { - VorzeDevice::Rocket - } else if hwname.contains("piston") { - VorzeDevice::Piston - } else { - return Err(ButtplugDeviceError::ProtocolNotImplemented(format!( - "No protocol implementation for Vorze Device {}", - hardware.name() - ))); - }; - Ok(Arc::new(VorzeSA::new(device_type))) - } -} - -pub struct VorzeSA { - previous_position: Arc, - device_type: VorzeDevice, -} - -impl VorzeSA { - pub fn new(device_type: VorzeDevice) -> Self { - Self { - previous_position: Arc::new(AtomicU8::new(0)), - device_type, - } - } -} - -#[repr(u8)] -#[derive(PartialEq, Eq, Clone, Copy)] -pub enum VorzeDevice { - Bach = 6, - Piston = 3, - Cyclone = 1, - Rocket = 7, - Ufo = 2, - UfoTw = 5, -} - -#[repr(u8)] -enum VorzeActions { - Rotate = 1, - Vibrate = 3, -} - -pub fn get_piston_speed(mut distance: f64, mut duration: f64) -> u8 { - if distance <= 0f64 { - return 100; - } - - if distance > 200f64 { - distance = 200f64; - } - - // Convert duration to max length - duration = 200f64 * duration / distance; - - let mut speed = (duration / 6658f64).powf(-1.21); - - if speed > 100f64 { - speed = 100f64; - } - - if speed < 0f64 { - speed = 0f64; - } - - speed as u8 -} - -impl ProtocolHandler for VorzeSA { - fn needs_full_command_set(&self) -> bool { - true - } - - fn handle_scalar_vibrate_cmd( - &self, - _index: u32, - scalar: u32, - ) -> Result, ButtplugDeviceError> { - Ok(vec![{ - HardwareWriteCmd::new( - Endpoint::Tx, - vec![ - self.device_type as u8, - VorzeActions::Vibrate as u8, - scalar as u8, - ], - true, - ) - .into() - }]) - } - - fn handle_rotate_cmd( - &self, - cmds: &[Option<(u32, bool)>], - ) -> Result, ButtplugDeviceError> { - if cmds.len() == 1 { - if let Some((speed, clockwise)) = cmds[0] { - let data: u8 = (clockwise as u8) << 7 | (speed as u8); - Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - vec![self.device_type as u8, VorzeActions::Rotate as u8, data], - true, - ) - .into()]) - } else { - Ok(vec![]) - } - } else { - let mut data_left = 0u8; - let mut data_right = 0u8; - let mut changed = false; - if let Some((speed, clockwise)) = cmds[0] { - data_left = (clockwise as u8) << 7 | (speed as u8); - changed = true; - } - if let Some((speed, clockwise)) = cmds[1] { - data_right = (clockwise as u8) << 7 | (speed as u8); - changed = true; - } - if changed { - Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - vec![self.device_type as u8, data_left, data_right], - true, - ) - .into()]) - } else { - Ok(vec![]) - } - } - } - - fn handle_linear_cmd( - &self, - msg: message::LinearCmdV4, - ) -> Result, ButtplugDeviceError> { - let v = msg.vectors()[0].clone(); - - let previous_position = self.previous_position.load(Ordering::SeqCst); - let position = v.position() * 200f64; - let distance = (previous_position as f64 - position).abs(); - - let speed = get_piston_speed(distance, v.duration() as f64); - - self - .previous_position - .store(position as u8, Ordering::SeqCst); - - Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - vec![self.device_type as u8, position as u8, speed as u8], - true, - ) - .into()]) - } - - fn handle_vorze_a10_cyclone_cmd( - &self, - msg: message::VorzeA10CycloneCmdV0, - ) -> Result, ButtplugDeviceError> { - self.handle_rotate_cmd(&[Some((msg.speed(), msg.clockwise()))]) - } -} diff --git a/buttplug/src/server/device/protocol/wevibe.rs b/buttplug/src/server/device/protocol/wevibe.rs deleted file mode 100644 index 6922511a1..000000000 --- a/buttplug/src/server/device/protocol/wevibe.rs +++ /dev/null @@ -1,97 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::server::device::configuration::ProtocolCommunicationSpecifier; -use crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, - }, - server::device::{ - configuration::{UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, - }, -}; -use async_trait::async_trait; -use std::sync::Arc; - -generic_protocol_initializer_setup!(WeVibe, "wevibe"); - -#[derive(Default)] -pub struct WeVibeInitializer {} - -#[async_trait] -impl ProtocolInitializer for WeVibeInitializer { - async fn initialize( - &mut self, - hardware: Arc, - _: &UserDeviceDefinition, - ) -> Result, ButtplugDeviceError> { - debug!("calling WeVibe init"); - hardware - .write_value(&HardwareWriteCmd::new( - Endpoint::Tx, - vec![0x0f, 0x03, 0x00, 0x99, 0x00, 0x03, 0x00, 0x00], - true, - )) - .await?; - hardware - .write_value(&HardwareWriteCmd::new( - Endpoint::Tx, - vec![0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], - true, - )) - .await?; - Ok(Arc::new(WeVibe::default())) - } -} - -#[derive(Default)] -pub struct WeVibe {} - -impl ProtocolHandler for WeVibe { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn needs_full_command_set(&self) -> bool { - true - } - - fn handle_scalar_cmd( - &self, - cmds: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - let r_speed_int = cmds[0].unwrap_or((ActuatorType::Vibrate, 0)).1 as u8; - let r_speed_ext = cmds - .last() - .unwrap_or(&None) - .unwrap_or((ActuatorType::Vibrate, 0u32)) - .1 as u8; - let data = if r_speed_int == 0 && r_speed_ext == 0 { - vec![0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] - } else { - vec![ - 0x0f, - 0x03, - 0x00, - r_speed_ext | (r_speed_int << 4), - 0x00, - 0x03, - 0x00, - 0x00, - ] - }; - Ok(vec![HardwareWriteCmd::new(Endpoint::Tx, data, true).into()]) - } -} diff --git a/buttplug/src/server/device/protocol/wevibe8bit.rs b/buttplug/src/server/device/protocol/wevibe8bit.rs deleted file mode 100644 index 19429f741..000000000 --- a/buttplug/src/server/device/protocol/wevibe8bit.rs +++ /dev/null @@ -1,61 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, - }, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, -}; - -generic_protocol_setup!(WeVibe8Bit, "wevibe-8bit"); - -#[derive(Default)] -pub struct WeVibe8Bit {} - -impl ProtocolHandler for WeVibe8Bit { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn needs_full_command_set(&self) -> bool { - true - } - - fn handle_scalar_cmd( - &self, - cmds: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - let r_speed_int = cmds[0].unwrap_or((ActuatorType::Vibrate, 0u32)).1 as u8; - let r_speed_ext = cmds - .last() - .unwrap_or(&None) - .unwrap_or((ActuatorType::Vibrate, 0u32)) - .1 as u8; - let data = if r_speed_int == 0 && r_speed_ext == 0 { - vec![0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] - } else { - let status_byte: u8 = - (if r_speed_ext == 0 { 0 } else { 2 }) | (if r_speed_int == 0 { 0 } else { 1 }); - vec![ - 0x0f, - 0x03, - 0x00, - r_speed_ext + 3, - r_speed_int + 3, - status_byte, - 0x00, - 0x00, - ] - }; - Ok(vec![HardwareWriteCmd::new(Endpoint::Tx, data, true).into()]) - } -} diff --git a/buttplug/src/server/device/protocol/wevibe_chorus.rs b/buttplug/src/server/device/protocol/wevibe_chorus.rs deleted file mode 100644 index 76216c5fa..000000000 --- a/buttplug/src/server/device/protocol/wevibe_chorus.rs +++ /dev/null @@ -1,62 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, - }, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, -}; - -generic_protocol_setup!(WeVibeChorus, "wevibe-chorus"); - -#[derive(Default)] -pub struct WeVibeChorus {} - -impl ProtocolHandler for WeVibeChorus { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn needs_full_command_set(&self) -> bool { - true - } - - fn handle_scalar_cmd( - &self, - cmds: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - let r_speed_int = cmds[0].unwrap_or((ActuatorType::Vibrate, 0u32)).1 as u8; - let r_speed_ext = cmds - .last() - .unwrap_or(&None) - .unwrap_or((ActuatorType::Vibrate, 0u32)) - .1 as u8; - let data = if r_speed_int == 0 && r_speed_ext == 0 { - vec![0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] - } else { - // Note the motor order is flipped for the Chorus - let status_byte: u8 = - (if r_speed_ext == 0 { 0 } else { 2 }) | (if r_speed_int == 0 { 0 } else { 1 }); - vec![ - 0x0f, - 0x03, - 0x00, - r_speed_int, - r_speed_ext, - status_byte, - 0x00, - 0x00, - ] - }; - Ok(vec![HardwareWriteCmd::new(Endpoint::Tx, data, true).into()]) - } -} diff --git a/buttplug/src/server/device/server_device.rs b/buttplug/src/server/device/server_device.rs deleted file mode 100644 index 199aa8d6a..000000000 --- a/buttplug/src/server/device/server_device.rs +++ /dev/null @@ -1,716 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 Nonpolynomial Labs LLC. All rights reserved. -// -// Licensed under the BSD 3-Clause license. See LICENSE file in the project root -// for full license information. - -//! Server Device Implementation -//! -//! This struct manages the trip from buttplug protocol actuator/sensor/raw message to hardware -//! communication. This involves: -//! -//! - Taking buttplug device command messages from the exposed server -//! - Converting older spec version messages to the newest spec version, which usually requires -//! device information for actuation/sensor messages. -//! - Validity checking the messages to make sure they match the capabilities of the hardware -//! - Turning the buttplug messages into hardware commands via the associated protocol -//! - Sending them to the hardware -//! - Possibly receiving back information (in the case of sensors), possibly firing and forgetting -//! (in terms of almost everything else) -//! -//! We make a lot of assumptions in here based on the devices we support right now, including: -//! -//! - Devices will only ever have one directional rotation actuator (we have no device that supports -//! two rotational components currently) -//! - Devices will only ever have one linear actuator (we have no device that supports multiple -//! linear actuators currently) -//! - Devices scalar command ordering is explicitly set by the device config file -//! - This means that we rely on the config file to know which vibrator is which on a device with -//! multiple vibrators. In protocols, especially for toy brands that release a large line of -//! different toys all using the same protocols (lovense, wevibe, etc), the order of features in -//! the config file MATTERS and needs to be tested against an actual device to make sure we're -//! controlling the actuator we think we are. -//! - This situation sucks and we should have better definitions, a problem outlined at -//! https://github.com/buttplugio/buttplug/issues/646 -//! -//! In order to handle multiple message spec versions - -use std::{ - fmt::{self, Debug}, - sync::Arc, - time::Duration, -}; - -use crate::{ - core::{ - errors::{ButtplugDeviceError, ButtplugError}, - message::{ - self, - ButtplugDeviceCommandMessageUnion, - ButtplugDeviceMessageType, - ButtplugMessage, - ButtplugServerDeviceMessage, - ButtplugServerMessageV4, - Endpoint, - FeatureType, - RawReadingV2, - RawSubscribeCmdV2, - ScalarCmdV4, - SensorType, - }, - ButtplugResultFuture, - }, - server::{ - device::{ - configuration::DeviceConfigurationManager, - hardware::{Hardware, HardwareCommand, HardwareConnector, HardwareEvent}, - protocol::ProtocolHandler, - }, - ButtplugServerResultFuture, - }, - util::{self, async_manager, stream::convert_broadcast_receiver_to_stream}, -}; -use core::hash::{Hash, Hasher}; -use dashmap::DashSet; -use futures::future::{self, BoxFuture, FutureExt}; -use getset::Getters; -use tokio::sync::RwLock; -use tokio_stream::StreamExt; - -use super::{ - configuration::{UserDeviceDefinition, UserDeviceIdentifier}, - hardware::HardwareWriteCmd, - protocol::{ - actuator_command_manager::ActuatorCommandManager, - ProtocolKeepaliveStrategy, - ProtocolSpecializer, - }, -}; - -#[derive(Debug)] -pub enum ServerDeviceEvent { - Connected(Arc), - Notification(UserDeviceIdentifier, ButtplugServerDeviceMessage), - Disconnected(UserDeviceIdentifier), -} - -#[derive(Getters)] -pub struct ServerDevice { - hardware: Arc, - handler: Arc, - #[getset(get = "pub")] - definition: UserDeviceDefinition, - actuator_command_manager: ActuatorCommandManager, - /// Unique identifier for the device - #[getset(get = "pub")] - identifier: UserDeviceIdentifier, - raw_subscribed_endpoints: Arc>, - keepalive_packet: Arc>>, -} -impl Debug for ServerDevice { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("ButtplugDevice") - .field("name", &self.name()) - .field("identifier", &self.identifier) - .finish() - } -} - -impl Hash for ServerDevice { - fn hash(&self, state: &mut H) { - self.identifier.hash(state); - } -} - -impl Eq for ServerDevice { -} - -impl PartialEq for ServerDevice { - fn eq(&self, other: &Self) -> bool { - self.identifier == *other.identifier() - } -} - -impl ServerDevice { - pub(super) async fn build( - device_config_manager: Arc, - mut hardware_connector: Box, - protocol_specializers: Vec, - ) -> Result { - // We've already checked to make sure we have specializers in the server device manager event - // loop. That check used to be here for sake of continuity in building devices in this method, but - // having that done before we get here fixes issues with some device advertisement timing (See - // #462 for more info.) - - // At this point, we know we've got hardware that is waiting to connect, and enough protocol - // info to actually do something after we connect. So go ahead and connect. - trace!("Connecting to {:?}", hardware_connector); - let mut hardware_specializer = hardware_connector.connect().await?; - - // We can't run these in parallel because we need to only accept one specializer. - let mut protocol_identifier = None; - let mut hardware_out = None; - for protocol_specializer in protocol_specializers { - if let Ok(specialized_hardware) = hardware_specializer - .specialize(protocol_specializer.specifiers()) - .await - { - protocol_identifier = Some(protocol_specializer.identify()); - hardware_out = Some(specialized_hardware); - break; - } - } - - if protocol_identifier.is_none() { - return Err(ButtplugDeviceError::DeviceConfigurationError( - "No protocols with viable communication matches for hardware.".to_owned(), - )); - } - - let mut protocol_identifier_stage = protocol_identifier.unwrap(); - let hardware = Arc::new(hardware_out.unwrap()); - - let (identifier, mut protocol_initializer) = protocol_identifier_stage - .identify(hardware.clone(), hardware_connector.specifier()) - .await?; - - // Now we have an identifier. After this point, if anything fails, consider it a complete - // connection failure, as identify may have already run commands on the device, and therefore - // put it in an unknown state if anything fails. - - // Check in the DeviceConfigurationManager to make sure we have attributes for this device. - let attrs = if let Some(attrs) = - device_config_manager.device_definition(&identifier, &hardware.endpoints()) - { - attrs - } else { - return Err(ButtplugDeviceError::DeviceConfigurationError(format!( - "No protocols with viable protocol attributes for hardware {:?}.", - identifier - ))); - }; - - // If we have attributes, go ahead and initialize, handing us back our hardware instance that - // is now ready to use with the protocol handler. - - // Build the server device and return. - let handler = protocol_initializer - .initialize(hardware.clone(), &attrs.clone().into()) - .await?; - - let requires_keepalive = hardware.requires_keepalive(); - let strategy = handler.keepalive_strategy(); - - // We now have fully initialized hardware, return a server device. - let device = Self::new(identifier, handler, hardware, &attrs); - - // If we need a keepalive with a packet replay, set this up via stopping the device on connect. - if requires_keepalive - && matches!( - strategy, - ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - ) - { - if let Err(e) = device.handle_stop_device_cmd().await { - return Err(ButtplugDeviceError::DeviceConnectionError(format!( - "Error setting up keepalive: {}", - e - ))); - } - } - - Ok(device) - } - - /// Given a protocol and a device impl, create a new ButtplugDevice instance - fn new( - identifier: UserDeviceIdentifier, - handler: Arc, - hardware: Arc, - definition: &UserDeviceDefinition, - ) -> Self { - let keepalive_packet = Arc::new(RwLock::new(None)); - let acm = ActuatorCommandManager::new(definition.features()); - // If we've gotten here, we know our hardware is connected. This means we can start the keepalive if it's required. - if hardware.requires_keepalive() - && !matches!( - handler.keepalive_strategy(), - ProtocolKeepaliveStrategy::NoStrategy - ) - { - let hardware = hardware.clone(); - let strategy = handler.keepalive_strategy(); - let keepalive_packet = keepalive_packet.clone(); - async_manager::spawn(async move { - // Arbitrary wait time for now. - let wait_duration = Duration::from_secs(5); - loop { - if hardware.time_since_last_write().await > wait_duration { - match &strategy { - ProtocolKeepaliveStrategy::RepeatPacketStrategy(packet) => { - if let Err(e) = hardware.write_value(&packet).await { - warn!("Error writing keepalive packet: {:?}", e); - break; - } - } - ProtocolKeepaliveStrategy::RepeatLastPacketStrategy => { - if let Some(packet) = &*keepalive_packet.read().await { - if let Err(e) = hardware.write_value(&packet).await { - warn!("Error writing keepalive packet: {:?}", e); - break; - } - } - } - _ => { - info!( - "Protocol keepalive strategy {:?} not implemented, replacing with NoStrategy", - strategy - ); - } - } - } - // Arbitrary wait time for now. - util::sleep(wait_duration).await; - } - info!("Leaving keepalive task for {}", hardware.name()); - }); - } - - Self { - identifier, - actuator_command_manager: acm, - handler, - hardware, - keepalive_packet, - definition: definition.clone(), - raw_subscribed_endpoints: Arc::new(DashSet::new()), - } - } - - /// Get the name of the device as set in the Device Configuration File. - /// - /// This will also append "(Raw Messaged Allowed)" to the device name if raw mode is on, to warn - /// users that the device is capable of direct communication. - pub fn name(&self) -> String { - // Instead of checking for raw messages at the protocol level, add the raw - // call here, since this is the only way to access devices in the library - // anyways. - // - // Having raw turned on means it'll work for read/write/sub/unsub on any - // endpoint so just use an arbitrary message here to check. - if self - .supports_message(&ButtplugDeviceCommandMessageUnion::RawSubscribeCmd( - RawSubscribeCmdV2::new(1, Endpoint::Tx), - )) - .is_ok() - { - format!("{} (Raw Messages Allowed)", self.definition.name()) - } else { - self.definition.name().to_owned() - } - } - - /// Disconnect from the device, if it's connected. - pub fn disconnect(&self) -> ButtplugResultFuture { - let fut = self.hardware.disconnect(); - async move { fut.await.map_err(|err| err.into()) }.boxed() - } - - /// Retreive the event stream for the device. - /// - /// This will include connections, disconnections, and notification events from subscribed - /// endpoints. - pub fn event_stream(&self) -> impl futures::Stream + Send { - let identifier = self.identifier.clone(); - let raw_endpoints = self.raw_subscribed_endpoints.clone(); - let hardware_stream = convert_broadcast_receiver_to_stream(self.hardware.event_stream()) - .filter_map(move |hardware_event| { - let id = identifier.clone(); - match hardware_event { - HardwareEvent::Disconnected(_) => Some(ServerDeviceEvent::Disconnected(id)), - HardwareEvent::Notification(_address, endpoint, data) => { - // TODO Figure out how we're going to parse raw data into something sendable to the client. - if raw_endpoints.contains(&endpoint) { - Some(ServerDeviceEvent::Notification( - id, - ButtplugServerDeviceMessage::RawReading(RawReadingV2::new(0, endpoint, data)), - )) - } else { - None - } - } - } - }); - - let identifier = self.identifier.clone(); - let handler_mapped_stream = self.handler.event_stream().map(move |incoming_message| { - let id = identifier.clone(); - ServerDeviceEvent::Notification(id, incoming_message) - }); - hardware_stream.merge(handler_mapped_stream) - } - - pub fn supports_message( - &self, - message: &ButtplugDeviceCommandMessageUnion, - ) -> Result<(), ButtplugError> { - // TODO This should be generated by a macro, as should the types enum. - let check_msg = |msg_type| { - self - .definition - .allows_message(&msg_type) - .then_some(()) - .ok_or(ButtplugDeviceError::MessageNotSupported(msg_type)) - }; - - match message { - ButtplugDeviceCommandMessageUnion::LinearCmd(_) => { - check_msg(ButtplugDeviceMessageType::LinearCmd) - } - ButtplugDeviceCommandMessageUnion::RawReadCmd(_) => { - check_msg(ButtplugDeviceMessageType::RawReadCmd) - } - ButtplugDeviceCommandMessageUnion::RawSubscribeCmd(_) => { - check_msg(ButtplugDeviceMessageType::RawSubscribeCmd) - } - ButtplugDeviceCommandMessageUnion::RawUnsubscribeCmd(_) => { - check_msg(ButtplugDeviceMessageType::RawUnsubscribeCmd) - } - ButtplugDeviceCommandMessageUnion::RawWriteCmd(_) => { - check_msg(ButtplugDeviceMessageType::RawWriteCmd) - } - ButtplugDeviceCommandMessageUnion::RotateCmd(_) => { - check_msg(ButtplugDeviceMessageType::RotateCmd) - } - ButtplugDeviceCommandMessageUnion::ScalarCmd(_) => { - check_msg(ButtplugDeviceMessageType::ScalarCmd) - } - ButtplugDeviceCommandMessageUnion::StopDeviceCmd(_) => { - //check_msg(ButtplugDeviceMessageType::StopDeviceCmd) - Ok(()) - } - ButtplugDeviceCommandMessageUnion::SensorReadCmd(_) => { - check_msg(ButtplugDeviceMessageType::SensorReadCmd) - } - ButtplugDeviceCommandMessageUnion::SensorSubscribeCmd(_) => { - check_msg(ButtplugDeviceMessageType::SensorSubscribeCmd) - } - ButtplugDeviceCommandMessageUnion::SensorUnsubscribeCmd(_) => { - check_msg(ButtplugDeviceMessageType::SensorUnsubscribeCmd) - } - } - .map_err(|err| err.into()) - } - - // In order to not have to worry about id setting at the protocol level (this - // should be taken care of in the server's device manager), we return server - // messages but Buttplug errors. - pub fn parse_message( - &self, - command_message: ButtplugDeviceCommandMessageUnion, - ) -> ButtplugServerResultFuture { - if let Err(err) = self.supports_message(&command_message) { - return future::ready(Err(err)).boxed(); - } - - // If a handler implements handle message, bypass all of our parsing and let it do its own - // thing. This should be a very rare thing. - if self.handler.has_handle_message() { - let fut = self.handle_generic_command_result(self.handler.handle_message(&command_message)); - return async move { fut.await }.boxed(); - } - - match command_message { - // Raw messages - ButtplugDeviceCommandMessageUnion::RawReadCmd(msg) => self.handle_raw_read_cmd(msg), - ButtplugDeviceCommandMessageUnion::RawWriteCmd(msg) => self.handle_raw_write_cmd(msg), - ButtplugDeviceCommandMessageUnion::RawSubscribeCmd(msg) => self.handle_raw_subscribe_cmd(msg), - ButtplugDeviceCommandMessageUnion::RawUnsubscribeCmd(msg) => { - self.handle_raw_unsubscribe_cmd(msg) - } - // Sensor messages - ButtplugDeviceCommandMessageUnion::SensorReadCmd(msg) => self.handle_sensor_read_cmd_v4(msg), - ButtplugDeviceCommandMessageUnion::SensorSubscribeCmd(msg) => { - self.handle_sensor_subscribe_cmd_v4(msg) - } - ButtplugDeviceCommandMessageUnion::SensorUnsubscribeCmd(msg) => { - self.handle_sensor_unsubscribe_cmd_v4(msg) - } - // Actuator messages - ButtplugDeviceCommandMessageUnion::ScalarCmd(msg) => self.handle_scalarcmd_v4(&msg), - ButtplugDeviceCommandMessageUnion::RotateCmd(msg) => { - let commands = match self - .actuator_command_manager - .update_rotation(&msg, self.handler.needs_full_command_set()) - { - Ok(values) => values, - Err(err) => return future::ready(Err(err)).boxed(), - }; - self.handle_generic_command_result(self.handler.handle_rotate_cmd(&commands)) - } - ButtplugDeviceCommandMessageUnion::LinearCmd(msg) => { - self.handle_generic_command_result(self.handler.handle_linear_cmd(msg)) - } - // Other generic messages - ButtplugDeviceCommandMessageUnion::StopDeviceCmd(_) => self.handle_stop_device_cmd(), - } - } - - fn handle_scalarcmd_v4(&self, msg: &ScalarCmdV4) -> ButtplugServerResultFuture { - if msg.scalars().is_empty() { - return future::ready(Err( - ButtplugDeviceError::ProtocolRequirementError( - "ScalarCmd with no subcommands is not valid.".to_owned(), - ) - .into(), - )) - .boxed(); - } - - for command in msg.scalars() { - if command.feature_index() > self.definition.features().len() as u32 { - return future::ready(Err( - ButtplugDeviceError::DeviceFeatureIndexError( - self.definition.features().len() as u32, - command.feature_index(), - ) - .into(), - )) - .boxed(); - } - let feature_type = - self.definition.features()[command.feature_index() as usize].feature_type(); - if *feature_type != command.actuator_type().into() { - return future::ready(Err( - ButtplugDeviceError::DeviceActuatorTypeMismatch( - self.name(), - command.actuator_type(), - *feature_type, - ) - .into(), - )) - .boxed(); - } - } - - let commands = match self - .actuator_command_manager - .update_scalar(&msg, self.handler.needs_full_command_set()) - { - Ok(values) => values, - Err(err) => return future::ready(Err(err)).boxed(), - }; - - if commands.is_empty() || commands.iter().filter(|x| x.is_some()).count() == 0 { - trace!("No commands generated for incoming device packet, skipping and returning success."); - return future::ready(Ok(message::OkV0::default().into())).boxed(); - } - self.handle_generic_command_result(self.handler.handle_scalar_cmd(&commands)) - } - - fn handle_hardware_commands(&self, commands: Vec) -> ButtplugServerResultFuture { - let hardware = self.hardware.clone(); - let keepalive_type = self.handler.keepalive_strategy(); - let keepalive_packet = self.keepalive_packet.clone(); - async move { - // Run commands in order, otherwise we may end up sending out of order. This may take a while, - // but it's what 99% of protocols expect. If they want something else, they can implement it - // themselves. - // - // If anything errors out, just bail on the command series. This most likely means the device - // disconnected. - for command in commands { - hardware.parse_message(&command).await?; - if hardware.requires_keepalive() - && matches!( - keepalive_type, - ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - ) - { - if let HardwareCommand::Write(command) = command { - *keepalive_packet.write().await = Some(command); - } - } - } - Ok(message::OkV0::default().into()) - } - .boxed() - } - - fn handle_generic_command_result( - &self, - command_result: Result, ButtplugDeviceError>, - ) -> ButtplugServerResultFuture { - let hardware_commands = match command_result { - Ok(commands) => commands, - Err(err) => return future::ready(Err(err.into())).boxed(), - }; - - self.handle_hardware_commands(hardware_commands) - } - - fn handle_stop_device_cmd(&self) -> ButtplugServerResultFuture { - let commands = self.actuator_command_manager.stop_commands(); - let mut fut_vec = vec![]; - commands - .iter() - .for_each(|msg| fut_vec.push(self.parse_message(msg.clone()))); - async move { - for fut in fut_vec { - fut.await?; - } - Ok(message::OkV0::default().into()) - } - .boxed() - } - - fn check_sensor_command( - &self, - feature_index: &u32, - sensor_type: &SensorType, - ) -> Result<(), ButtplugDeviceError> { - if *feature_index > self.definition.features().len() as u32 { - return Err(ButtplugDeviceError::DeviceSensorIndexError( - self.definition.features().len() as u32, - *feature_index, - )); - } - let feature_type = self.definition.features()[*feature_index as usize].feature_type(); - if *feature_type != FeatureType::from(*sensor_type) { - Err(ButtplugDeviceError::DeviceSensorTypeMismatch( - *feature_index, - *sensor_type, - *feature_type, - )) - } else { - Ok(()) - } - } - - fn handle_sensor_read_cmd_v4( - &self, - message: message::SensorReadCmdV4, - ) -> BoxFuture<'static, Result> { - let result = self.check_sensor_command(message.feature_index(), message.sensor_type()); - let device = self.hardware.clone(); - let handler = self.handler.clone(); - async move { - result?; - handler - .handle_sensor_read_cmd(device, &message) - .await - .map_err(|e| e.into()) - .map(|e| e.into()) - } - .boxed() - } - - fn handle_sensor_subscribe_cmd_v4( - &self, - message: message::SensorSubscribeCmdV4, - ) -> ButtplugServerResultFuture { - let result = self.check_sensor_command(message.feature_index(), message.sensor_type()); - let device = self.hardware.clone(); - let handler = self.handler.clone(); - async move { - result?; - handler - .handle_sensor_subscribe_cmd(device, &message) - .await - .map(|_| message::OkV0::new(message.id()).into()) - .map_err(|e| e.into()) - } - .boxed() - } - - fn handle_sensor_unsubscribe_cmd_v4( - &self, - message: message::SensorUnsubscribeCmdV4, - ) -> ButtplugServerResultFuture { - let result = self.check_sensor_command(message.feature_index(), message.sensor_type()); - let device = self.hardware.clone(); - let handler = self.handler.clone(); - async move { - result?; - handler - .handle_sensor_unsubscribe_cmd(device, &message) - .await - .map(|_| message::OkV0::new(message.id()).into()) - .map_err(|e| e.into()) - } - .boxed() - } - - fn handle_raw_write_cmd(&self, message: message::RawWriteCmdV2) -> ButtplugServerResultFuture { - let id = message.id(); - let fut = self.hardware.write_value(&message.into()); - async move { - fut - .await - .map(|_| message::OkV0::new(id).into()) - .map_err(|err| err.into()) - } - .boxed() - } - - fn handle_raw_read_cmd(&self, message: message::RawReadCmdV2) -> ButtplugServerResultFuture { - let id = message.id(); - let fut = self.hardware.read_value(&message.into()); - async move { - fut - .await - .map(|msg| { - let mut raw_msg: RawReadingV2 = msg.into(); - raw_msg.set_id(id); - raw_msg.into() - }) - .map_err(|err| err.into()) - } - .boxed() - } - - fn handle_raw_unsubscribe_cmd( - &self, - message: message::RawUnsubscribeCmdV2, - ) -> ButtplugServerResultFuture { - let id = message.id(); - let endpoint = message.endpoint(); - let fut = self.hardware.unsubscribe(&message.into()); - let raw_endpoints = self.raw_subscribed_endpoints.clone(); - async move { - if !raw_endpoints.contains(&endpoint) { - return Ok(message::OkV0::new(id).into()); - } - let result = fut - .await - .map(|_| message::OkV0::new(id).into()) - .map_err(|err| err.into()); - raw_endpoints.remove(&endpoint); - result - } - .boxed() - } - - fn handle_raw_subscribe_cmd( - &self, - message: message::RawSubscribeCmdV2, - ) -> ButtplugServerResultFuture { - let id = message.id(); - let endpoint = message.endpoint(); - let fut = self.hardware.subscribe(&message.into()); - let raw_endpoints = self.raw_subscribed_endpoints.clone(); - async move { - if raw_endpoints.contains(&endpoint) { - return Ok(message::OkV0::new(id).into()); - } - let result = fut - .await - .map(|_| message::OkV0::new(id).into()) - .map_err(|err| err.into()); - raw_endpoints.insert(endpoint); - result - } - .boxed() - } -} diff --git a/buttplug/src/server/server_downgrade_wrapper.rs b/buttplug/src/server/server_downgrade_wrapper.rs deleted file mode 100644 index b9be42a0b..000000000 --- a/buttplug/src/server/server_downgrade_wrapper.rs +++ /dev/null @@ -1,231 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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, sync::Arc}; - -use crate::core::{ - errors::{ButtplugError, ButtplugMessageError}, - message::{ - self, - ButtplugClientMessageVariant, - ButtplugMessageSpecVersion, - ButtplugServerMessageV4, - ButtplugServerMessageVariant, - ErrorV0, - }, -}; - -use super::{ - device::ServerDeviceManager, - server_message_conversion::ButtplugServerMessageConverter, - ButtplugServer, - ButtplugServerResultFuture, -}; -use futures::{ - future::{self, BoxFuture, FutureExt}, - Stream, -}; -use once_cell::sync::OnceCell; -use tokio_stream::StreamExt; - -pub struct ButtplugServerDowngradeWrapper { - /// Spec version of the currently connected client. Held as an atomic so we don't have to worry - /// about locks when doing lookups. - spec_version: Arc>, - server: ButtplugServer, -} - -impl std::fmt::Debug for ButtplugServerDowngradeWrapper { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.server.fmt(f) - } -} - -impl ButtplugServerDowngradeWrapper { - pub fn new(server: ButtplugServer) -> Self { - Self { - spec_version: Arc::new(OnceCell::new()), - server, - } - } - - pub fn client_name(&self) -> Option { - self.server.client_name() - } - - /// Returns a references to the internal device manager, for handling configuration. - pub fn device_manager(&self) -> Arc { - self.server.device_manager() - } - - /// If true, client is currently connected to the server. - pub fn connected(&self) -> bool { - self.server.connected() - } - - /// Disconnects the server from a client, if it is connected. - pub fn disconnect(&self) -> BoxFuture> { - self.server.disconnect() - } - - pub fn spec_version(&self) -> Option { - self.spec_version.get().copied() - } - - pub fn client_version_event_stream(&self) -> impl Stream { - let spec_version = self.spec_version.clone(); - self.server.event_stream().map(move |m| { - let converter = ButtplugServerMessageConverter::new(None); - // If we get an event and don't have a spec version yet, just throw out the latest. - converter - .convert_outgoing( - &m, - spec_version - .get() - .unwrap_or(&ButtplugMessageSpecVersion::Version4), - ) - .unwrap() - }) - } - - pub fn server_version_event_stream(&self) -> impl Stream { - // Unlike the client API, we can expect anyone using the server to pin this - // themselves. - self.server.event_stream() - } - - /// Sends a [ButtplugClientMessage] to be parsed by the server (for handshake or ping), or passed - /// into the server's [DeviceManager] for communication with devices. - pub fn parse_message( - &self, - msg: ButtplugClientMessageVariant, - ) -> BoxFuture<'static, Result> { - match msg { - ButtplugClientMessageVariant::V4(msg) => { - if cfg!(feature = "allow-unstable-v4-connections") { - let fut = self.server.parse_message(msg); - async move { - Ok( - fut - .await - .map_err(|e| ButtplugServerMessageVariant::from(ButtplugServerMessageV4::from(e)))? - .into(), - ) - } - .boxed() - } else { - future::ready(Err( - ButtplugServerMessageV4::from(ErrorV0::from(ButtplugError::from( - ButtplugMessageError::UnhandledMessage( - "Buttplug not compiled to handle v4 messages.".to_owned(), - ), - ))) - .into(), - )) - .boxed() - } - } - msg => { - let v = msg.version(); - let converter = ButtplugServerMessageConverter::new(Some(msg)); - let spec_version = *self.spec_version.get_or_init(|| { - info!( - "Setting Buttplug Server Message Spec Downgrade version to {}", - v - ); - v - }); - match converter.convert_incoming(&self.server.device_manager()) { - Ok(converted_msg) => { - let fut = self.server.parse_message(converted_msg); - async move { - let result = fut.await.map_err(|e| { - converter - .convert_outgoing(&e.into(), &spec_version) - .unwrap() - })?; - converter - .convert_outgoing(&result, &spec_version) - .map_err(|e| { - converter - .convert_outgoing( - &&ButtplugServerMessageV4::from(ErrorV0::from(e)), - &spec_version, - ) - .unwrap() - }) - } - .boxed() - } - Err(e) => future::ready(Err( - converter - .convert_outgoing( - &ButtplugServerMessageV4::from(ErrorV0::from(e)), - &spec_version, - ) - .unwrap(), - )) - .boxed(), - } - } - } - } - - pub fn shutdown(&self) -> ButtplugServerResultFuture { - self.server.shutdown() - } - - pub fn destroy(self) -> ButtplugServer { - self.server - } -} - -#[cfg(test)] -mod test { - use crate::{ - core::message::{ - ButtplugClientMessageV4, - ButtplugClientMessageVariant, - RequestServerInfoV1, - BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION, - }, - server::{ButtplugServerBuilder, ButtplugServerDowngradeWrapper}, - }; - - #[cfg_attr(feature = "allow-unstable-v4-connections", ignore)] - #[tokio::test] - async fn test_downgrader_v4_block() { - let wrapper = - ButtplugServerDowngradeWrapper::new(ButtplugServerBuilder::default().finish().unwrap()); - assert!(wrapper - .parse_message(ButtplugClientMessageVariant::V4( - ButtplugClientMessageV4::RequestServerInfo(RequestServerInfoV1::new( - "TestClient", - BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION - )) - )) - .await - .is_err()); - } - - #[cfg_attr(not(feature = "allow-unstable-v4-connections"), ignore)] - #[tokio::test] - async fn test_downgrader_v4_allow() { - let wrapper = - ButtplugServerDowngradeWrapper::new(ButtplugServerBuilder::default().finish().unwrap()); - let result = wrapper - .parse_message(ButtplugClientMessageVariant::V4( - ButtplugClientMessageV4::RequestServerInfo(RequestServerInfoV1::new( - "TestClient", - BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION, - )), - )) - .await; - println!("{:?}", result); - assert!(result.is_ok()); - } -} diff --git a/buttplug/src/server/server_message_conversion.rs b/buttplug/src/server/server_message_conversion.rs deleted file mode 100644 index 061f61199..000000000 --- a/buttplug/src/server/server_message_conversion.rs +++ /dev/null @@ -1,899 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 Nonpolynomial Labs LLC. All rights reserved. -// -// Licensed under the BSD 3-Clause license. See LICENSE file in the project root -// for full license information. - -//! Buttplug Message Spec Conversion -//! -//! This module contains code to convert any message from an older spec version up to the current -//! message spec, and then convert any response from the current message spec back down the sending -//! spec. This is handled within the server, as the server is the only portion of Buttplug that -//! needs to handle up/downgrading (the client should never have to care and should only ever talk -//! one version of the spec, preferably the latest). Having this done within the server also allows -//! us to access required state for converting between messages that requires knowledge of ephemeral -//! device structures (i.e. converting from v4 device features to <= v3 message attributes for -//! messages like DeviceAdded). - -use std::fmt::Debug; - -use super::device::ServerDeviceManager; -use crate::core::{ - errors::{ButtplugDeviceError, ButtplugError, ButtplugMessageError}, - message::{ - self, - ActuatorType, - BatteryLevelCmdV2, - BatteryLevelReadingV2, - ButtplugClientMessageV0, - ButtplugClientMessageV1, - ButtplugClientMessageV2, - ButtplugClientMessageV3, - ButtplugClientMessageV4, - ButtplugClientMessageVariant, - ButtplugDeviceMessage, - ButtplugMessage, - ButtplugMessageSpecVersion, - ButtplugServerMessageV0, - ButtplugServerMessageV1, - ButtplugServerMessageV2, - ButtplugServerMessageV3, - ButtplugServerMessageV4, - ButtplugServerMessageVariant, - DeviceFeature, - ErrorV0, - FeatureType, - LinearCmdV1, - LinearCmdV4, - RSSILevelCmdV2, - RSSILevelReadingV2, - RotateCmdV1, - RotateCmdV4, - RotationSubcommandV4, - ScalarCmdV3, - ScalarCmdV4, - ScalarSubcommandV4, - SensorReadCmdV3, - SensorReadCmdV4, - SensorReadingV3, - SensorSubscribeCmdV3, - SensorSubscribeCmdV4, - SensorType, - SensorUnsubscribeCmdV3, - SensorUnsubscribeCmdV4, - VectorSubcommandV4, - VibrateCmdV1, - VorzeA10CycloneCmdV0, - }, -}; - -// -// TryFrom Conversion Traits -// -// Trait implementation is universal for structs, and message structs are defined in the -// core::message module. Even so, we include the TryFrom traits for upgrading client/downgrading -// server messages here in order to keep all of our conversion code in the same module. - -// For v3 to v4, all deprecations should be treated as conversions, but will require current -// connected device state, meaning they'll need to be implemented where they can also access the -// device manager. -impl TryFrom for ButtplugClientMessageV4 { - type Error = ButtplugMessageError; - - fn try_from(value: ButtplugClientMessageV3) -> Result { - match value { - ButtplugClientMessageV3::Ping(m) => Ok(ButtplugClientMessageV4::Ping(m.clone())), - ButtplugClientMessageV3::RequestServerInfo(m) => { - Ok(ButtplugClientMessageV4::RequestServerInfo(m.clone())) - } - ButtplugClientMessageV3::StartScanning(m) => { - Ok(ButtplugClientMessageV4::StartScanning(m.clone())) - } - ButtplugClientMessageV3::StopScanning(m) => { - Ok(ButtplugClientMessageV4::StopScanning(m.clone())) - } - ButtplugClientMessageV3::RequestDeviceList(m) => { - Ok(ButtplugClientMessageV4::RequestDeviceList(m.clone())) - } - ButtplugClientMessageV3::StopAllDevices(m) => { - Ok(ButtplugClientMessageV4::StopAllDevices(m.clone())) - } - ButtplugClientMessageV3::StopDeviceCmd(m) => { - Ok(ButtplugClientMessageV4::StopDeviceCmd(m.clone())) - } - ButtplugClientMessageV3::RawReadCmd(m) => Ok(ButtplugClientMessageV4::RawReadCmd(m)), - ButtplugClientMessageV3::RawWriteCmd(m) => Ok(ButtplugClientMessageV4::RawWriteCmd(m)), - ButtplugClientMessageV3::RawSubscribeCmd(m) => { - Ok(ButtplugClientMessageV4::RawSubscribeCmd(m)) - } - ButtplugClientMessageV3::RawUnsubscribeCmd(m) => { - Ok(ButtplugClientMessageV4::RawUnsubscribeCmd(m)) - } - _ => Err(ButtplugMessageError::MessageConversionError(format!( - "Cannot convert message {:?} to V4 message spec while lacking state.", - value - ))), - } - } -} - -// For v2 to v3, all deprecations should be treated as conversions, but will require current -// connected device state, meaning they'll need to be implemented where they can also access the -// device manager. -impl TryFrom for ButtplugClientMessageV3 { - type Error = ButtplugMessageError; - - fn try_from(value: ButtplugClientMessageV2) -> Result { - match value { - ButtplugClientMessageV2::Ping(m) => Ok(ButtplugClientMessageV3::Ping(m.clone())), - ButtplugClientMessageV2::RequestServerInfo(m) => { - Ok(ButtplugClientMessageV3::RequestServerInfo(m.clone())) - } - ButtplugClientMessageV2::StartScanning(m) => { - Ok(ButtplugClientMessageV3::StartScanning(m.clone())) - } - ButtplugClientMessageV2::StopScanning(m) => { - Ok(ButtplugClientMessageV3::StopScanning(m.clone())) - } - ButtplugClientMessageV2::RequestDeviceList(m) => { - Ok(ButtplugClientMessageV3::RequestDeviceList(m.clone())) - } - ButtplugClientMessageV2::StopAllDevices(m) => { - Ok(ButtplugClientMessageV3::StopAllDevices(m.clone())) - } - ButtplugClientMessageV2::StopDeviceCmd(m) => { - Ok(ButtplugClientMessageV3::StopDeviceCmd(m.clone())) - } - // Vibrate was supposed to be phased out in v3 but was left in the allowable message set. - // Oops. - ButtplugClientMessageV2::VibrateCmd(m) => Ok(ButtplugClientMessageV3::VibrateCmd(m)), - ButtplugClientMessageV2::LinearCmd(m) => Ok(ButtplugClientMessageV3::LinearCmd(m)), - ButtplugClientMessageV2::RotateCmd(m) => Ok(ButtplugClientMessageV3::RotateCmd(m)), - ButtplugClientMessageV2::RawReadCmd(m) => Ok(ButtplugClientMessageV3::RawReadCmd(m)), - ButtplugClientMessageV2::RawWriteCmd(m) => Ok(ButtplugClientMessageV3::RawWriteCmd(m)), - ButtplugClientMessageV2::RawSubscribeCmd(m) => { - Ok(ButtplugClientMessageV3::RawSubscribeCmd(m)) - } - ButtplugClientMessageV2::RawUnsubscribeCmd(m) => { - Ok(ButtplugClientMessageV3::RawUnsubscribeCmd(m)) - } - _ => Err(ButtplugMessageError::MessageConversionError(format!( - "Cannot convert message {:?} to V3 message spec while lacking state.", - value - ))), - } - } -} - -// For v1 to v2, several messages were deprecated. Throw errors when trying to convert those. -impl TryFrom for ButtplugClientMessageV2 { - type Error = ButtplugMessageError; - - fn try_from(value: ButtplugClientMessageV1) -> Result { - match value { - ButtplugClientMessageV1::Ping(m) => Ok(ButtplugClientMessageV2::Ping(m.clone())), - ButtplugClientMessageV1::RequestServerInfo(m) => { - Ok(ButtplugClientMessageV2::RequestServerInfo(m.clone())) - } - ButtplugClientMessageV1::StartScanning(m) => { - Ok(ButtplugClientMessageV2::StartScanning(m.clone())) - } - ButtplugClientMessageV1::StopScanning(m) => { - Ok(ButtplugClientMessageV2::StopScanning(m.clone())) - } - ButtplugClientMessageV1::RequestDeviceList(m) => { - Ok(ButtplugClientMessageV2::RequestDeviceList(m.clone())) - } - ButtplugClientMessageV1::StopAllDevices(m) => { - Ok(ButtplugClientMessageV2::StopAllDevices(m.clone())) - } - ButtplugClientMessageV1::StopDeviceCmd(m) => { - Ok(ButtplugClientMessageV2::StopDeviceCmd(m.clone())) - } - ButtplugClientMessageV1::VibrateCmd(m) => Ok(ButtplugClientMessageV2::VibrateCmd(m.clone())), - ButtplugClientMessageV1::LinearCmd(m) => Ok(ButtplugClientMessageV2::LinearCmd(m.clone())), - ButtplugClientMessageV1::RotateCmd(m) => Ok(ButtplugClientMessageV2::RotateCmd(m.clone())), - ButtplugClientMessageV1::FleshlightLaunchFW12Cmd(_) => { - // Direct access to FleshlightLaunchFW12Cmd could cause some devices to break via rapid - // changes of position/speed. Yes, some Kiiroo devices really *are* that fragile. - Err(ButtplugMessageError::MessageConversionError("FleshlightLaunchFW12Cmd is not implemented. Please update the client software to use a newer command".to_owned()).into()) - } - ButtplugClientMessageV1::RequestLog(_) => { - // Log was a huge security hole, as we'd just send our server logs to whomever asked, which - // contain all sorts of identifying information. Always return an error here. - Err( - ButtplugMessageError::MessageConversionError( - "RequestLog is no longer allowed by any version of Buttplug.".to_owned(), - ) - .into(), - ) - } - ButtplugClientMessageV1::KiirooCmd(_) => { - // No device protocol implementation ever worked with KiirooCmd, so no one ever should've - // used it. We'll just return an error if we ever see it. - Err(ButtplugMessageError::MessageConversionError("KiirooCmd is not implemented. Please update the client software to use a newer command".to_owned()).into()) - } - ButtplugClientMessageV1::LovenseCmd(_) => { - // LovenseCmd allowed users to directly send strings to a Lovense device, which was a Bad - // Idea. Will always return an error. - Err(ButtplugMessageError::MessageConversionError("LovenseCmd is not implemented. Please update the client software to use a newer command".to_owned()).into()) - } - _ => Err(ButtplugMessageError::MessageConversionError(format!( - "Cannot convert message {:?} to current message spec while lacking state.", - value - ))), - } - } -} - -// No messages were changed or deprecated before v2, so we can convert all v0 messages to v1. -impl From for ButtplugClientMessageV1 { - fn from(value: ButtplugClientMessageV0) -> Self { - match value { - ButtplugClientMessageV0::Ping(m) => ButtplugClientMessageV1::Ping(m), - ButtplugClientMessageV0::RequestServerInfo(m) => { - ButtplugClientMessageV1::RequestServerInfo(m) - } - ButtplugClientMessageV0::StartScanning(m) => ButtplugClientMessageV1::StartScanning(m), - ButtplugClientMessageV0::StopScanning(m) => ButtplugClientMessageV1::StopScanning(m), - ButtplugClientMessageV0::RequestDeviceList(m) => { - ButtplugClientMessageV1::RequestDeviceList(m) - } - ButtplugClientMessageV0::StopAllDevices(m) => ButtplugClientMessageV1::StopAllDevices(m), - ButtplugClientMessageV0::StopDeviceCmd(m) => ButtplugClientMessageV1::StopDeviceCmd(m), - ButtplugClientMessageV0::FleshlightLaunchFW12Cmd(m) => { - ButtplugClientMessageV1::FleshlightLaunchFW12Cmd(m) - } - ButtplugClientMessageV0::KiirooCmd(m) => ButtplugClientMessageV1::KiirooCmd(m), - ButtplugClientMessageV0::LovenseCmd(m) => ButtplugClientMessageV1::LovenseCmd(m), - ButtplugClientMessageV0::RequestLog(m) => ButtplugClientMessageV1::RequestLog(m), - ButtplugClientMessageV0::SingleMotorVibrateCmd(m) => { - ButtplugClientMessageV1::SingleMotorVibrateCmd(m) - } - ButtplugClientMessageV0::VorzeA10CycloneCmd(m) => { - ButtplugClientMessageV1::VorzeA10CycloneCmd(m) - } - } - } -} - -impl TryFrom for ButtplugServerMessageV3 { - type Error = ButtplugMessageError; - - fn try_from( - value: ButtplugServerMessageV4, - ) -> Result>::Error> { - match value { - // Direct conversions - ButtplugServerMessageV4::Ok(m) => Ok(ButtplugServerMessageV3::Ok(m)), - ButtplugServerMessageV4::Error(m) => Ok(ButtplugServerMessageV3::Error(m)), - ButtplugServerMessageV4::ServerInfo(m) => Ok(ButtplugServerMessageV3::ServerInfo(m)), - ButtplugServerMessageV4::DeviceRemoved(m) => Ok(ButtplugServerMessageV3::DeviceRemoved(m)), - ButtplugServerMessageV4::ScanningFinished(m) => { - Ok(ButtplugServerMessageV3::ScanningFinished(m)) - } - ButtplugServerMessageV4::RawReading(m) => Ok(ButtplugServerMessageV3::RawReading(m)), - ButtplugServerMessageV4::DeviceList(m) => Ok(ButtplugServerMessageV3::DeviceList(m.into())), - ButtplugServerMessageV4::DeviceAdded(m) => Ok(ButtplugServerMessageV3::DeviceAdded(m.into())), - // All other messages (SensorReading) requires device manager context. - _ => Err(ButtplugMessageError::MessageConversionError(format!( - "Cannot convert message {:?} to current message spec while lacking state.", - value - ))), - } - } -} - -impl From for ButtplugServerMessageV2 { - fn from(value: ButtplugServerMessageV3) -> Self { - match value { - ButtplugServerMessageV3::Ok(m) => ButtplugServerMessageV2::Ok(m), - ButtplugServerMessageV3::Error(m) => ButtplugServerMessageV2::Error(m), - ButtplugServerMessageV3::ServerInfo(m) => ButtplugServerMessageV2::ServerInfo(m), - ButtplugServerMessageV3::DeviceRemoved(m) => ButtplugServerMessageV2::DeviceRemoved(m), - ButtplugServerMessageV3::ScanningFinished(m) => ButtplugServerMessageV2::ScanningFinished(m), - ButtplugServerMessageV3::RawReading(m) => ButtplugServerMessageV2::RawReading(m), - ButtplugServerMessageV3::DeviceAdded(m) => ButtplugServerMessageV2::DeviceAdded(m.into()), - ButtplugServerMessageV3::DeviceList(m) => ButtplugServerMessageV2::DeviceList(m.into()), - ButtplugServerMessageV3::SensorReading(_) => ButtplugServerMessageV2::Error(ErrorV0::from( - ButtplugError::from(ButtplugMessageError::MessageConversionError( - "SensorReading cannot be converted to Buttplug Message Spec V2".to_owned(), - )), - )), - } - } -} - -impl From for ButtplugServerMessageV1 { - fn from(value: ButtplugServerMessageV2) -> Self { - match value { - ButtplugServerMessageV2::Ok(m) => ButtplugServerMessageV1::Ok(m), - ButtplugServerMessageV2::Error(m) => ButtplugServerMessageV1::Error(m), - ButtplugServerMessageV2::ServerInfo(m) => ButtplugServerMessageV1::ServerInfo(m.into()), - ButtplugServerMessageV2::DeviceRemoved(m) => ButtplugServerMessageV1::DeviceRemoved(m), - ButtplugServerMessageV2::ScanningFinished(m) => ButtplugServerMessageV1::ScanningFinished(m), - ButtplugServerMessageV2::DeviceAdded(m) => ButtplugServerMessageV1::DeviceAdded(m.into()), - ButtplugServerMessageV2::DeviceList(m) => ButtplugServerMessageV1::DeviceList(m.into()), - ButtplugServerMessageV2::BatteryLevelReading(_) => { - ButtplugServerMessageV1::Error(ErrorV0::from(ButtplugError::from( - ButtplugMessageError::MessageConversionError( - "BatteryLevelReading cannot be converted to Buttplug Message Spec V1".to_owned(), - ), - ))) - } - ButtplugServerMessageV2::RSSILevelReading(_) => { - ButtplugServerMessageV1::Error(ErrorV0::from(ButtplugError::from( - ButtplugMessageError::MessageConversionError( - "RSSILevelReading cannot be converted to Buttplug Message Spec V1".to_owned(), - ), - ))) - } - ButtplugServerMessageV2::RawReading(_) => ButtplugServerMessageV1::Error(ErrorV0::from( - ButtplugError::from(ButtplugMessageError::MessageConversionError( - "RawReading cannot be converted to Buttplug Message Spec V1".to_owned(), - )), - )), - } - } -} - -impl From for ButtplugServerMessageV0 { - fn from(value: ButtplugServerMessageV1) -> Self { - match value { - ButtplugServerMessageV1::Ok(m) => ButtplugServerMessageV0::Ok(m), - ButtplugServerMessageV1::Error(m) => ButtplugServerMessageV0::Error(m), - ButtplugServerMessageV1::ServerInfo(m) => ButtplugServerMessageV0::ServerInfo(m.into()), - ButtplugServerMessageV1::DeviceRemoved(m) => ButtplugServerMessageV0::DeviceRemoved(m), - ButtplugServerMessageV1::ScanningFinished(m) => ButtplugServerMessageV0::ScanningFinished(m), - ButtplugServerMessageV1::DeviceAdded(m) => ButtplugServerMessageV0::DeviceAdded(m.into()), - ButtplugServerMessageV1::DeviceList(m) => ButtplugServerMessageV0::DeviceList(m.into()), - ButtplugServerMessageV1::Log(_) => ButtplugServerMessageV0::Error(ErrorV0::from( - ButtplugError::from(ButtplugMessageError::MessageConversionError( - "For security reasons, Log should never be sent from a Buttplug Server".to_owned(), - )), - )), - } - } -} - -pub struct ButtplugServerMessageConverter { - original_message: Option, -} - -impl ButtplugServerMessageConverter { - pub fn new(msg: Option) -> Self { - Self { - original_message: msg, - } - } - - pub fn convert_incoming( - &self, - device_manager: &ServerDeviceManager, - ) -> Result { - if let Some(msg) = &self.original_message { - let mut outgoing_msg = match msg { - ButtplugClientMessageVariant::V0(m) => self.convert_incoming_v0(m, device_manager)?, - ButtplugClientMessageVariant::V1(m) => self.convert_incoming_v1(m, device_manager)?, - ButtplugClientMessageVariant::V2(m) => self.convert_incoming_v2(m, device_manager)?, - ButtplugClientMessageVariant::V3(m) => self.convert_incoming_v3(m, device_manager)?, - ButtplugClientMessageVariant::V4(m) => m.clone(), - }; - // Always make sure the ID is set after conversion - outgoing_msg.set_id(msg.id()); - Ok(outgoing_msg) - } else { - Err( - ButtplugMessageError::MessageConversionError( - "No incoming message provided for conversion".to_owned(), - ) - .into(), - ) - } - } - - fn convert_incoming_v0( - &self, - msg_v0: &ButtplugClientMessageV0, - device_manager: &ServerDeviceManager, - ) -> Result { - // All v0 messages can be converted to v1 messages. - self.convert_incoming_v1(&msg_v0.clone().into(), device_manager) - } - - fn convert_incoming_v1( - &self, - msg_v1: &ButtplugClientMessageV1, - device_manager: &ServerDeviceManager, - ) -> Result { - // Instead of converting to v2 message attributes then to v4 device features, we move directly - // from v0 command messages to v4 device features here. There's no reason to do the middle step. - match msg_v1 { - ButtplugClientMessageV1::VorzeA10CycloneCmd(m) => { - // Vorze and RotateCmd are equivalent, so this is an ok conversion. - self.convert_vorzea10cyclonecmdv0_to_rotatecmdv4(m, device_manager) - } - ButtplugClientMessageV1::SingleMotorVibrateCmd(m) => { - // SingleMotorVibrate is a ScalarCmd w/ Vibrate type for all vibrate functionality. - self.convert_singlemotorvibratecmdv0_to_scalarcmdv4(m, device_manager) - } - _ => self.convert_incoming_v2(&msg_v1.clone().try_into()?, device_manager), - } - } - - fn convert_incoming_v2( - &self, - msg_v2: &ButtplugClientMessageV2, - device_manager: &ServerDeviceManager, - ) -> Result { - match msg_v2 { - // Convert v2 specific queries to v3 generic sensor queries - ButtplugClientMessageV2::BatteryLevelCmd(m) => { - self.convert_batterylevelcmd_v2_to_sensorreadcmd_v4(m, device_manager) - } - ButtplugClientMessageV2::RSSILevelCmd(m) => { - self.convert_rssilevelcmd_v2_to_sensorreadv4(m, device_manager) - } - // Convert VibrateCmd to a ScalarCmd command - ButtplugClientMessageV2::VibrateCmd(m) => { - self.convert_vibratecmdv1_to_scalarcmdv4(m, device_manager) - } - _ => self.convert_incoming_v3(&msg_v2.clone().try_into()?, device_manager), - } - } - - fn convert_incoming_v3( - &self, - msg_v3: &ButtplugClientMessageV3, - device_manager: &ServerDeviceManager, - ) -> Result { - match msg_v3 { - // Convert v1/v2 message attribute commands into device feature commands - ButtplugClientMessageV3::VibrateCmd(m) => { - self.convert_vibratecmdv1_to_scalarcmdv4(m, device_manager) - } - ButtplugClientMessageV3::ScalarCmd(m) => { - self.convert_scalarcmdv3_to_scalarcmdv4(m, device_manager) - } - ButtplugClientMessageV3::RotateCmd(m) => { - self.convert_rotatecmdv1_to_scalarcmdv4(m, device_manager) - } - ButtplugClientMessageV3::LinearCmd(m) => { - self.convert_linearcmdv1_to_linearcmdv4(m, device_manager) - } - ButtplugClientMessageV3::SensorReadCmd(m) => { - self.convert_sensorreadv3_to_sensorreadv4(m, device_manager) - } - ButtplugClientMessageV3::SensorSubscribeCmd(m) => { - self.convert_sensorsubscribev3_to_sensorsubcribe4(m, device_manager) - } - ButtplugClientMessageV3::SensorUnsubscribeCmd(m) => { - self.convert_sensorunsubscribev3_to_sensorunsubcribe4(m, device_manager) - } - _ => msg_v3 - .clone() - .try_into() - .map_err(|e: ButtplugMessageError| e.into()), - } - } - - // - // Incoming Conversion Utility Methods - // - - fn find_device_features( - &self, - message: &M, - device_manager: &ServerDeviceManager, - criteria: P, - ) -> Result, ButtplugError> - where - M: ButtplugDeviceMessage + Debug, - P: FnMut(&(usize, &DeviceFeature)) -> bool, - { - let device_index = message.device_index(); - - let device = device_manager - .devices() - .get(&device_index) - .ok_or(ButtplugDeviceError::DeviceNotAvailable(device_index))?; - - let features: Vec = device - .definition() - .features() - .iter() - .enumerate() - .filter(criteria) - .map(|(index, _)| index) - .collect(); - - if features.is_empty() { - Err( - ButtplugDeviceError::ProtocolRequirementError(format!( - "Device does not have any features that accommodate the following message: {:?}", - message - )) - .into(), - ) - } else { - Ok(features) - } - } - - fn convert_singlemotorvibratecmdv0_to_scalarcmdv4( - &self, - message: &message::SingleMotorVibrateCmdV0, - device_manager: &ServerDeviceManager, - ) -> Result { - let vibrate_features: Vec = - self.find_device_features(message, device_manager, |(_, x)| { - *x.feature_type() == FeatureType::Vibrate - && x.actuator().as_ref().is_some_and(|y| { - y.messages() - .contains(&message::ButtplugActuatorFeatureMessageType::ScalarCmd) - }) - })?; - - let cmds: Vec = vibrate_features - .iter() - .map(|x| ScalarSubcommandV4::new(*x as u32, message.speed(), ActuatorType::Vibrate)) - .collect(); - - Ok(ScalarCmdV4::new(message.device_index(), cmds).into()) - } - - fn convert_vorzea10cyclonecmdv0_to_rotatecmdv4( - &self, - message: &VorzeA10CycloneCmdV0, - device_manager: &ServerDeviceManager, - ) -> Result { - let rotate_features: Vec = - self.find_device_features(message, device_manager, |(_, x)| { - *x.feature_type() == FeatureType::Rotate - && x.actuator().as_ref().is_some_and(|y| { - y.messages() - .contains(&message::ButtplugActuatorFeatureMessageType::RotateCmd) - }) - })?; - - let cmds: Vec = rotate_features - .iter() - .map(|x| { - RotationSubcommandV4::new( - *x as u32, - message.speed() as f64 / 99f64, - message.clockwise(), - ) - }) - .collect(); - - Ok(RotateCmdV4::new(message.device_index(), cmds).into()) - } - - fn convert_batterylevelcmd_v2_to_sensorreadcmd_v4( - &self, - message: &BatteryLevelCmdV2, - device_manager: &ServerDeviceManager, - ) -> Result { - let battery_features = self.find_device_features(message, device_manager, |(_, x)| { - *x.feature_type() == FeatureType::Battery - && x.sensor().as_ref().is_some_and(|y| { - y.messages() - .contains(&message::ButtplugSensorFeatureMessageType::SensorReadCmd) - }) - })?; - - Ok( - SensorReadCmdV4::new( - message.device_index(), - battery_features[0] as u32, - SensorType::Battery, - ) - .into(), - ) - } - - fn convert_rssilevelcmd_v2_to_sensorreadv4( - &self, - message: &RSSILevelCmdV2, - device_manager: &ServerDeviceManager, - ) -> Result { - let rssi_features = self.find_device_features(message, device_manager, |(_, x)| { - *x.feature_type() == FeatureType::RSSI - && x.sensor().as_ref().is_some_and(|y| { - y.messages() - .contains(&message::ButtplugSensorFeatureMessageType::SensorReadCmd) - }) - })?; - Ok( - SensorReadCmdV4::new( - message.device_index(), - rssi_features[0] as u32, - SensorType::RSSI, - ) - .into(), - ) - } - - fn convert_vibratecmdv1_to_scalarcmdv4( - &self, - message: &VibrateCmdV1, - device_manager: &ServerDeviceManager, - ) -> Result { - let vibrate_features: Vec = - self.find_device_features(message, device_manager, |(_, x)| { - *x.feature_type() == FeatureType::Vibrate - && x.actuator().as_ref().is_some_and(|y| { - y.messages() - .contains(&message::ButtplugActuatorFeatureMessageType::ScalarCmd) - }) - })?; - - let cmds: Vec = message - .speeds() - .iter() - .map(|x| { - ScalarSubcommandV4::new( - vibrate_features[x.index() as usize] as u32, - x.speed(), - ActuatorType::Vibrate, - ) - }) - .collect(); - - Ok(ScalarCmdV4::new(message.device_index(), cmds).into()) - } - - fn convert_scalarcmdv3_to_scalarcmdv4( - &self, - message: &ScalarCmdV3, - device_manager: &ServerDeviceManager, - ) -> Result { - let scalar_features: Vec = - self.find_device_features(message, device_manager, |(_, x)| { - x.actuator().as_ref().is_some_and(|y| { - y.messages() - .contains(&message::ButtplugActuatorFeatureMessageType::ScalarCmd) - }) - })?; - - let scalars_v4: Vec = message - .scalars() - .iter() - .map(|x| { - ScalarSubcommandV4::new( - scalar_features[x.index() as usize] as u32, - x.scalar().clone(), - x.actuator_type().clone(), - ) - }) - .collect(); - - Ok(ScalarCmdV4::new(message.device_index(), scalars_v4).into()) - } - - fn convert_rotatecmdv1_to_scalarcmdv4( - &self, - message: &RotateCmdV1, - device_manager: &ServerDeviceManager, - ) -> Result { - let rotate_features: Vec = - self.find_device_features(message, device_manager, |(_, x)| { - x.actuator().as_ref().is_some_and(|y| { - y.messages() - .contains(&message::ButtplugActuatorFeatureMessageType::RotateCmd) - }) - })?; - - let cmds: Vec = message - .rotations() - .iter() - .map(|x| { - RotationSubcommandV4::new( - rotate_features[x.index() as usize] as u32, - x.speed(), - x.clockwise(), - ) - }) - .collect(); - - Ok(RotateCmdV4::new(message.device_index(), cmds).into()) - } - - fn convert_linearcmdv1_to_linearcmdv4( - &self, - message: &LinearCmdV1, - device_manager: &ServerDeviceManager, - ) -> Result { - let linear_features: Vec = - self.find_device_features(message, device_manager, |(_, x)| { - x.actuator().as_ref().is_some_and(|y| { - y.messages() - .contains(&message::ButtplugActuatorFeatureMessageType::LinearCmd) - }) - })?; - - let cmds: Vec = message - .vectors() - .iter() - .map(|x| { - VectorSubcommandV4::new( - linear_features[x.index() as usize] as u32, - x.duration(), - x.position(), - ) - }) - .collect(); - - Ok(LinearCmdV4::new(message.device_index(), cmds).into()) - } - - fn convert_sensorreadv3_to_sensorreadv4( - &self, - message: &SensorReadCmdV3, - device_manager: &ServerDeviceManager, - ) -> Result { - let features = self.find_device_features(message, device_manager, |(_, x)| { - x.sensor().as_ref().is_some_and(|y| { - y.messages() - .contains(&message::ButtplugSensorFeatureMessageType::SensorReadCmd) - }) - })?; - - let sensor_feature_index = features[*message.sensor_index() as usize] as u32; - - Ok( - SensorReadCmdV4::new( - message.device_index(), - sensor_feature_index, - *message.sensor_type(), - ) - .into(), - ) - } - - fn convert_sensorsubscribev3_to_sensorsubcribe4( - &self, - message: &SensorSubscribeCmdV3, - device_manager: &ServerDeviceManager, - ) -> Result { - let features = self.find_device_features(message, device_manager, |(_, x)| { - x.sensor().as_ref().is_some_and(|y| { - y.messages() - .contains(&message::ButtplugSensorFeatureMessageType::SensorSubscribeCmd) - }) - })?; - - let sensor_feature_index = features[*message.sensor_index() as usize] as u32; - - Ok( - SensorSubscribeCmdV4::new( - message.device_index(), - sensor_feature_index, - *message.sensor_type(), - ) - .into(), - ) - } - - fn convert_sensorunsubscribev3_to_sensorunsubcribe4( - &self, - message: &SensorUnsubscribeCmdV3, - device_manager: &ServerDeviceManager, - ) -> Result { - let features = self.find_device_features(message, device_manager, |(_, x)| { - x.sensor().as_ref().is_some_and(|y| { - y.messages() - .contains(&message::ButtplugSensorFeatureMessageType::SensorSubscribeCmd) - }) - })?; - - let sensor_feature_index = features[*message.sensor_index() as usize] as u32; - - Ok( - SensorUnsubscribeCmdV4::new( - message.device_index(), - sensor_feature_index, - *message.sensor_type(), - ) - .into(), - ) - } - - // - // Outgoing Conversion - // - - pub fn convert_outgoing( - &self, - msg: &ButtplugServerMessageV4, - version: &ButtplugMessageSpecVersion, - ) -> Result { - let mut outgoing_msg = match version { - ButtplugMessageSpecVersion::Version0 => { - ButtplugServerMessageVariant::V0(self.convert_servermessagev4_to_servermessagev0(msg)?) - } - ButtplugMessageSpecVersion::Version1 => { - ButtplugServerMessageVariant::V1(self.convert_servermessagev4_to_servermessagev1(msg)?) - } - ButtplugMessageSpecVersion::Version2 => { - ButtplugServerMessageVariant::V2(self.convert_servermessagev4_to_servermessagev2(msg)?) - } - ButtplugMessageSpecVersion::Version3 => { - ButtplugServerMessageVariant::V3(self.convert_servermessagev4_to_servermessagev3(msg)?) - } - ButtplugMessageSpecVersion::Version4 => ButtplugServerMessageVariant::V4(msg.clone()), - }; - // Always make sure the ID is set after conversion - outgoing_msg.set_id(msg.id()); - Ok(outgoing_msg) - } - - fn convert_servermessagev4_to_servermessagev3( - &self, - msg: &ButtplugServerMessageV4, - ) -> Result { - match msg { - ButtplugServerMessageV4::SensorReading(m) => { - let original_msg = self.original_message.as_ref().unwrap(); - if let ButtplugClientMessageVariant::V3(ButtplugClientMessageV3::SensorReadCmd(msg)) = - &original_msg - { - let msg_out = SensorReadingV3::new( - msg.device_index(), - *msg.sensor_index(), - *msg.sensor_type(), - m.data().clone(), - ); - Ok(msg_out.into()) - } else { - Err(ButtplugMessageError::UnexpectedMessageType("SensorReading".to_owned()).into()) - } - } - _ => Ok(msg.clone().try_into()?), - } - } - - fn convert_servermessagev4_to_servermessagev2( - &self, - msg: &ButtplugServerMessageV4, - ) -> Result { - let msg_v3 = self.convert_servermessagev4_to_servermessagev3(msg)?; - match msg_v3 { - ButtplugServerMessageV3::SensorReading(m) => { - let original_msg = self.original_message.as_ref().unwrap(); - // Sensor Reading didn't exist in v2, we only had Battery or RSSI. Therefore we need to - // context of the original message to make sure this conversion happens correctly. - if let ButtplugClientMessageVariant::V2(ButtplugClientMessageV2::BatteryLevelCmd(msg)) = - &original_msg - { - Ok(BatteryLevelReadingV2::new(msg.device_index(), m.data()[0] as f64 / 100f64).into()) - } else if let ButtplugClientMessageVariant::V2(ButtplugClientMessageV2::RSSILevelCmd(msg)) = - &original_msg - { - Ok(RSSILevelReadingV2::new(msg.device_index(), m.data()[0]).into()) - } else { - Err(ButtplugMessageError::UnexpectedMessageType("SensorReading".to_owned()).into()) - } - } - _ => Ok(msg_v3.into()), - } - } - - fn convert_servermessagev4_to_servermessagev1( - &self, - msg: &ButtplugServerMessageV4, - ) -> Result { - Ok(self.convert_servermessagev4_to_servermessagev2(msg)?.into()) - } - - fn convert_servermessagev4_to_servermessagev0( - &self, - msg: &ButtplugServerMessageV4, - ) -> Result { - Ok(self.convert_servermessagev4_to_servermessagev1(msg)?.into()) - } - - // Outgoing Conversion Utility Methods -} diff --git a/buttplug/src/util/logging.rs b/buttplug/src/util/logging.rs deleted file mode 100644 index 8ffd3805c..000000000 --- a/buttplug/src/util/logging.rs +++ /dev/null @@ -1,53 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 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 crate::util::async_manager; -use tokio::sync::mpsc::Sender; - -use tracing_subscriber::fmt::MakeWriter; - -/// Convenience struct for handling tracing output from Buttplug. -/// -/// Since Buttplug uses tracing for logging internally, we expect executables to -/// handle setting up the outputs. However, there are a few situations we deal -/// with where we want to shove out to a channel instead of stdout or other -/// writers. We just shove out a Vec and expect the other end to do whatever -/// string parsing it might need. -pub struct ChannelWriter { - log_sender: Sender>, -} - -impl ChannelWriter { - pub fn new(sender: Sender>) -> Self { - Self { log_sender: sender } - } -} - -impl std::io::Write for ChannelWriter { - fn write(&mut self, buf: &[u8]) -> Result { - let sender = self.log_sender.clone(); - let len = buf.len(); - let send_buf = buf.to_vec(); - async_manager::spawn(async move { - // Ignore errors on dropped channels here. We can't really do a ton about - // them. - let _ = sender.send(send_buf.to_vec()).await; - }); - Ok(len) - } - - fn flush(&mut self) -> Result<(), std::io::Error> { - Ok(()) - } -} - -impl MakeWriter<'_> for ChannelWriter { - type Writer = ChannelWriter; - fn make_writer(&self) -> Self::Writer { - ChannelWriter::new(self.log_sender.clone()) - } -} diff --git a/buttplug/tests/test_client_device.rs b/buttplug/tests/test_client_device.rs deleted file mode 100644 index b9ec9fa1c..000000000 --- a/buttplug/tests/test_client_device.rs +++ /dev/null @@ -1,418 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 Nonpolynomial Labs LLC. All rights reserved. -// -// Licensed under the BSD 3-Clause license. See LICENSE file in the project root -// for full license information. - -mod util; -use buttplug::{ - client::{ - ButtplugClientDeviceEvent, - ButtplugClientError, - ButtplugClientEvent, - ScalarValueCommand, - }, - core::{ - errors::{ButtplugDeviceError, ButtplugError, ButtplugMessageError}, - message::{ - self, - ButtplugActuatorFeatureMessageType, - ClientDeviceMessageAttributesV3, - DeviceFeature, - DeviceFeatureActuator, - Endpoint, - FeatureType, - }, - }, - server::device::{ - configuration::{UserDeviceCustomization, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{HardwareCommand, HardwareWriteCmd}, - }, - util::{async_manager, device_configuration::load_protocol_configs}, -}; -use futures::StreamExt; -use std::{sync::Arc, time::Duration}; -use tokio::time::sleep; -use util::test_device_manager::{check_test_recv_value, TestDeviceIdentifier}; -use util::{ - test_client_with_device, - test_client_with_device_and_custom_dcm, - test_device_manager::TestHardwareEvent, -}; - -#[cfg(feature = "server")] -#[tokio::test] -async fn test_client_device_connected_status() { - let (client, device) = test_client_with_device().await; - - let mut event_stream = client.event_stream(); - client - .start_scanning() - .await - .expect("Test, assuming infallible."); - let mut client_device = None; - while let Some(msg) = event_stream.next().await { - if let ButtplugClientEvent::DeviceAdded(da) = msg { - client_device = Some(da); - break; - } - } - let test_device = client_device.expect("Test, assuming infallible."); - let mut device_event_stream = test_device.event_stream(); - assert!(test_device.connected()); - device - .sender - .send(TestHardwareEvent::Disconnect) - .await - .expect("Test, assuming infallible."); - while let Some(msg) = device_event_stream.next().await { - if let ButtplugClientDeviceEvent::DeviceRemoved = msg { - assert!(!test_device.connected()); - break; - } - } - client - .disconnect() - .await - .expect("Test, assuming infallible."); - assert!(!client.connected()); -} - -#[cfg(feature = "server")] -#[tokio::test] -async fn test_client_device_client_disconnected_status() { - let (client, _) = test_client_with_device().await; - - let mut event_stream = client.event_stream(); - client - .start_scanning() - .await - .expect("Test, assuming infallible."); - let mut client_device = None; - while let Some(msg) = event_stream.next().await { - if let ButtplugClientEvent::DeviceAdded(da) = msg { - client_device = Some(da); - break; - } - } - let test_device = client_device.expect("Test, assuming infallible."); - let mut device_event_stream = test_device.event_stream(); - assert!(test_device.connected()); - client - .disconnect() - .await - .expect("Test, assuming infallible."); - while let Some(msg) = event_stream.next().await { - if let ButtplugClientEvent::ServerDisconnect = msg { - assert!(!client.connected()); - assert!(!test_device.connected()); - break; - } - } - while let Some(msg) = device_event_stream.next().await { - if let ButtplugClientDeviceEvent::DeviceRemoved = msg { - break; - } - } -} - -#[cfg(feature = "server")] -#[tokio::test] -async fn test_client_device_connected_no_event_listener() { - let (client, device) = test_client_with_device().await; - - client - .start_scanning() - .await - .expect("Test, assuming infallible."); - sleep(Duration::from_millis(100)).await; - device - .sender - .send(TestHardwareEvent::Disconnect) - .await - .expect("Test, assuming infallible."); - sleep(Duration::from_millis(100)).await; - client - .disconnect() - .await - .expect("Test, assuming infallible."); - assert!(!client.connected()); - sleep(Duration::from_millis(100)).await; -} - -#[cfg(feature = "server")] -#[tokio::test] -async fn test_client_device_invalid_command() { - tracing_subscriber::fmt::init(); - let (client, _) = test_client_with_device().await; - - let mut event_stream = client.event_stream(); - client - .start_scanning() - .await - .expect("Test, assuming infallible."); - let mut client_device = None; - while let Some(msg) = event_stream.next().await { - if let ButtplugClientEvent::DeviceAdded(da) = msg { - client_device = Some(da); - break; - } - } - let test_device = client_device.expect("Test, assuming infallible."); - assert!(matches!( - test_device - .vibrate(&ScalarValueCommand::ScalarValue(2.0)) - .await - .unwrap_err(), - ButtplugClientError::ButtplugError(ButtplugError::ButtplugMessageError( - ButtplugMessageError::InvalidMessageContents(..) - )) - )); - assert!(matches!( - test_device - .vibrate(&ScalarValueCommand::ScalarValueVec(vec!(0.5, 0.5, 0.5))) - .await - .unwrap_err(), - ButtplugClientError::ButtplugError(ButtplugError::ButtplugDeviceError( - ButtplugDeviceError::DeviceFeatureCountMismatch(..) - )) - )); - assert!(matches!( - test_device - .vibrate(&ScalarValueCommand::ScalarValueVec(vec!())) - .await - .unwrap_err(), - ButtplugClientError::ButtplugError(ButtplugError::ButtplugDeviceError( - ButtplugDeviceError::ProtocolRequirementError(..) - )) - )); -} - -#[cfg(feature = "server")] -#[tokio::test] -async fn test_client_repeated_deviceadded_message() { - use buttplug::core::message::{ - ButtplugClientMessageV3, - ButtplugClientMessageVariant, - ButtplugServerMessageVariant, - }; - - let helper = Arc::new(util::channel_transport::ChannelClientTestHelper::new()); - helper.simulate_successful_connect().await; - let helper_clone = helper.clone(); - let mut event_stream = helper.client().event_stream(); - async_manager::spawn(async move { - assert!(matches!( - helper_clone.next_client_message().await, - ButtplugClientMessageVariant::V3(ButtplugClientMessageV3::StartScanning(..)) - )); - helper_clone - .send_client_incoming(ButtplugServerMessageVariant::V3( - message::OkV0::new(3).into(), - )) - .await; - let device_added = message::DeviceAddedV3::new( - 1, - "Test Device", - &None, - &None, - &ClientDeviceMessageAttributesV3::default(), - ); - helper_clone - .send_client_incoming(ButtplugServerMessageVariant::V3( - device_added.clone().into(), - )) - .await; - helper_clone - .send_client_incoming(ButtplugServerMessageVariant::V3(device_added.into())) - .await; - }); - helper - .client() - .start_scanning() - .await - .expect("Test, assuming infallible."); - assert!(matches!( - event_stream - .next() - .await - .expect("Test, assuming infallible."), - ButtplugClientEvent::DeviceAdded(..) - )); - assert!(matches!( - event_stream - .next() - .await - .expect("Test, assuming infallible."), - ButtplugClientEvent::Error(..) - )); -} - -#[cfg(feature = "server")] -#[tokio::test] -async fn test_client_repeated_deviceremoved_message() { - use buttplug::core::message::{ - ButtplugClientMessageV3, - ButtplugClientMessageVariant, - ButtplugServerMessageVariant, - }; - - let helper = Arc::new(util::channel_transport::ChannelClientTestHelper::new()); - helper.simulate_successful_connect().await; - let helper_clone = helper.clone(); - let mut event_stream = helper.client().event_stream(); - async_manager::spawn(async move { - assert!(matches!( - helper_clone.next_client_message().await, - ButtplugClientMessageVariant::V3(ButtplugClientMessageV3::StartScanning(..)) - )); - helper_clone - .send_client_incoming(ButtplugServerMessageVariant::V3( - message::OkV0::new(3).into(), - )) - .await; - let device_added = message::DeviceAddedV3::new( - 1, - "Test Device", - &None, - &None, - &ClientDeviceMessageAttributesV3::default(), - ); - let device_removed = message::DeviceRemovedV0::new(1); - helper_clone - .send_client_incoming(ButtplugServerMessageVariant::V3(device_added.into())) - .await; - helper_clone - .send_client_incoming(ButtplugServerMessageVariant::V3( - device_removed.clone().into(), - )) - .await; - helper_clone - .send_client_incoming(ButtplugServerMessageVariant::V3(device_removed.into())) - .await; - }); - helper - .client() - .start_scanning() - .await - .expect("Test, assuming infallible."); - assert!(matches!( - event_stream - .next() - .await - .expect("Test, assuming infallible."), - ButtplugClientEvent::DeviceAdded(..) - )); - assert!(matches!( - event_stream - .next() - .await - .expect("Test, assuming infallible."), - ButtplugClientEvent::DeviceRemoved(..) - )); - assert!(matches!( - event_stream - .next() - .await - .expect("Test, assuming infallible."), - ButtplugClientEvent::Error(..) - )); -} - -#[tokio::test] -async fn test_client_range_limits() { - let dcm = load_protocol_configs(&None, &None, false) - .expect("Test, assuming infallible.") - .finish() - .expect("Test, assuming infallible."); - - // Add a user config that configures the test device to only user the lower and upper half for the two vibrators - let identifier = UserDeviceIdentifier::new("range-test", "aneros", &Some("Massage Demo".into())); - let test_identifier = TestDeviceIdentifier::new("Massage Demo", Some("range-test".into())); - dcm - .add_user_device_definition( - &identifier, - &UserDeviceDefinition::new( - "Massage Demo", - &[ - DeviceFeature::new( - "Lower half", - FeatureType::Vibrate, - &Some(DeviceFeatureActuator::new( - &(0..=127), - &(0..=64), - &[ButtplugActuatorFeatureMessageType::ScalarCmd].into(), - )), - &None, - ), - DeviceFeature::new( - "Upper half", - FeatureType::Vibrate, - &Some(DeviceFeatureActuator::new( - &(0..=127), - &(64..=127), - &[ButtplugActuatorFeatureMessageType::ScalarCmd].into(), - )), - &None, - ), - ], - &UserDeviceCustomization::default(), - ), - ) - .unwrap(); - - // Start the server & client - let (client, mut device) = test_client_with_device_and_custom_dcm(&test_identifier, dcm).await; - let mut event_stream = client.event_stream(); - assert!(client.start_scanning().await.is_ok()); - - while let Some(event) = event_stream.next().await { - if let ButtplugClientEvent::DeviceAdded(dev) = event { - // Vibrate at half strength - assert!(dev - .vibrate(&ScalarValueCommand::ScalarValue(0.5)) - .await - .is_ok()); - - // Lower half - check_test_recv_value( - &mut device, - HardwareCommand::Write(HardwareWriteCmd::new(Endpoint::Tx, vec![0xF1, 32], false)), - ); - - // Upper half - check_test_recv_value( - &mut device, - HardwareCommand::Write(HardwareWriteCmd::new(Endpoint::Tx, vec![0xF2, 96], false)), - ); - - // Disable device - assert!(dev - .vibrate(&ScalarValueCommand::ScalarValue(0.0)) - .await - .is_ok()); - - // Lower half - check_test_recv_value( - &mut device, - HardwareCommand::Write(HardwareWriteCmd::new(Endpoint::Tx, vec![0xF1, 0], false)), - ); - - // Upper half - check_test_recv_value( - &mut device, - HardwareCommand::Write(HardwareWriteCmd::new(Endpoint::Tx, vec![0xF2, 0], false)), - ); - break; - } - } - assert!(client.stop_all_devices().await.is_ok()); -} - -// TODO Test invalid messages to device -// TODO Test invalid parameters in message -// TODO Test device invalidation across client connections (i.e. a device shouldn't be allowed to reconnect even if index is the same) -// TODO Test DeviceList being sent followed by repeat DeviceAdded -// TODO Test DeviceList being sent multiple times -// TODO Test sending device return for device that doesn't exist (in client) diff --git a/buttplug/tests/test_server_device.rs b/buttplug/tests/test_server_device.rs deleted file mode 100644 index c7986fba2..000000000 --- a/buttplug/tests/test_server_device.rs +++ /dev/null @@ -1,276 +0,0 @@ -// Buttplug Rust Source Code File - See https://buttplug.io for more info. -// -// Copyright 2016-2024 Nonpolynomial Labs LLC. All rights reserved. -// -// Licensed under the BSD 3-Clause license. See LICENSE file in the project root -// for full license information. - -mod util; -use buttplug::core::{ - errors::{ButtplugDeviceError, ButtplugError}, - message::{ - self, - ButtplugClientMessageV4, - ButtplugClientMessageVariant, - ButtplugServerMessageV3, - ButtplugServerMessageV4, - ButtplugServerMessageVariant, - Endpoint, - BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION, - }, -}; -use futures::{pin_mut, StreamExt}; -use std::matches; -pub use util::test_device_manager::TestDeviceCommunicationManagerBuilder; -use util::{test_server_v4_with_device, test_server_with_device}; - -// Test devices that have protocols that support movements not all devices do. -// For instance, the Onyx+ is part of a protocol that supports vibration, but -// the device itself does not. -#[tokio::test] -async fn test_capabilities_exposure() { - // Hold the channel but don't do anything with it. - let (server, _channel) = test_server_with_device("Onyx+", false); - let recv = server.client_version_event_stream(); - pin_mut!(recv); - - server - .parse_message(ButtplugClientMessageVariant::V3( - message::RequestServerInfoV1::new("Test Client", BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION) - .into(), - )) - .await - .expect("Test, assuming infallible."); - server - .parse_message(ButtplugClientMessageVariant::V3( - message::StartScanningV0::default().into(), - )) - .await - .expect("Test, assuming infallible."); - while let Some(msg) = recv.next().await { - if let ButtplugServerMessageVariant::V3(ButtplugServerMessageV3::DeviceAdded(device)) = msg { - assert!(device.device_messages().scalar_cmd().is_none()); - assert!(device.device_messages().linear_cmd().is_some()); - return; - } - } -} - -#[tokio::test] -async fn test_server_raw_message() { - let (server, _) = test_server_with_device("Massage Demo", true); - let recv = server.client_version_event_stream(); - pin_mut!(recv); - assert!(server - .parse_message(ButtplugClientMessageVariant::V3( - message::RequestServerInfoV1::new("Test Client", BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION) - .into() - )) - .await - .is_ok()); - assert!(server - .parse_message(ButtplugClientMessageVariant::V3( - message::StartScanningV0::default().into() - )) - .await - .is_ok()); - while let Some(msg) = recv.next().await { - if let ButtplugServerMessageVariant::V3(ButtplugServerMessageV3::ScanningFinished(_)) = msg { - continue; - } else if let ButtplugServerMessageVariant::V3(ButtplugServerMessageV3::DeviceAdded(da)) = msg { - assert!(da.device_messages().raw_read_cmd().is_some()); - assert!(da.device_messages().raw_write_cmd().is_some()); - assert!(da.device_messages().raw_subscribe_cmd().is_some()); - assert_eq!(da.device_name(), "Aneros Vivi (Raw Messages Allowed)"); - return; - } else { - panic!( - "Returned message was not a DeviceAdded message or timed out: {:?}", - msg - ); - } - } -} - -#[tokio::test] -async fn test_server_no_raw_message() { - let (server, _) = test_server_with_device("Massage Demo", false); - let recv = server.client_version_event_stream(); - pin_mut!(recv); - assert!(server - .parse_message(ButtplugClientMessageVariant::V3( - message::RequestServerInfoV1::new("Test Client", BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION) - .into() - )) - .await - .is_ok()); - assert!(server - .parse_message(ButtplugClientMessageVariant::V3( - message::StartScanningV0::default().into() - )) - .await - .is_ok()); - while let Some(msg) = recv.next().await { - if let ButtplugServerMessageVariant::V3(ButtplugServerMessageV3::ScanningFinished(_)) = msg { - continue; - } else if let ButtplugServerMessageVariant::V3(ButtplugServerMessageV3::DeviceAdded(da)) = msg { - assert_eq!(da.device_name(), "Aneros Vivi"); - assert!(da.device_messages().raw_read_cmd().is_none()); - assert!(da.device_messages().raw_write_cmd().is_none()); - assert!(da.device_messages().raw_subscribe_cmd().is_none()); - break; - } else { - panic!( - "Returned message was not a DeviceAdded message or timed out: {:?}", - msg - ); - } - } -} - -#[tokio::test] -async fn test_reject_on_no_raw_message() { - let (server, _) = test_server_v4_with_device("Massage Demo", false); - let recv = server.event_stream(); - pin_mut!(recv); - assert!(server - .parse_message(ButtplugClientMessageV4::from( - message::RequestServerInfoV1::new("Test Client", BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION) - )) - .await - .is_ok()); - assert!(server - .parse_message(ButtplugClientMessageV4::from( - message::StartScanningV0::default() - )) - .await - .is_ok()); - while let Some(msg) = recv.next().await { - if let ButtplugServerMessageV4::ScanningFinished(_) = msg { - continue; - } else if let ButtplugServerMessageV4::DeviceAdded(da) = msg { - assert_eq!(da.device_name(), "Aneros Vivi"); - let mut should_be_err; - should_be_err = server - .parse_message(ButtplugClientMessageV4::from(message::RawWriteCmdV2::new( - da.device_index(), - Endpoint::Tx, - &[0x0], - false, - ))) - .await; - assert!(should_be_err.is_err()); - assert!(matches!( - should_be_err.unwrap_err().original_error(), - ButtplugError::ButtplugDeviceError(ButtplugDeviceError::MessageNotSupported(_)) - )); - - should_be_err = server - .parse_message(ButtplugClientMessageV4::from(message::RawReadCmdV2::new( - da.device_index(), - Endpoint::Tx, - 0, - 0, - ))) - .await; - assert!(should_be_err.is_err()); - assert!(matches!( - should_be_err.unwrap_err().original_error(), - ButtplugError::ButtplugDeviceError(ButtplugDeviceError::MessageNotSupported(_)) - )); - - should_be_err = server - .parse_message(ButtplugClientMessageV4::from( - message::RawSubscribeCmdV2::new(da.device_index(), Endpoint::Tx), - )) - .await; - assert!(should_be_err.is_err()); - assert!(matches!( - should_be_err.unwrap_err().original_error(), - ButtplugError::ButtplugDeviceError(ButtplugDeviceError::MessageNotSupported(_)) - )); - - should_be_err = server - .parse_message(ButtplugClientMessageV4::from( - message::RawUnsubscribeCmdV2::new(da.device_index(), Endpoint::Tx), - )) - .await; - assert!(should_be_err.is_err()); - assert!(matches!( - should_be_err.unwrap_err().original_error(), - ButtplugError::ButtplugDeviceError(ButtplugDeviceError::MessageNotSupported(_)) - )); - return; - } else { - panic!( - "Returned message was not a DeviceAdded message or timed out: {:?}", - msg - ); - } - } -} - -/* -#[cfg(target_os = "windows")] -#[ignore = "Has weird timeout issues"] -#[tokio::test] -async fn test_repeated_address_additions() { - let mut server_builder = ButtplugServerBuilder::default(); - let builder = TestDeviceCommunicationManagerBuilder::default(); - let helper = builder.helper(); - server_builder.comm_manager(builder); - let server = server_builder.finish().unwrap(); - let recv = server.event_stream(); - pin_mut!(recv); - helper - .add_ble_device_with_address("Massage Demo", "SameAddress") - .await; - helper - .add_ble_device_with_address("Massage Demo", "SameAddress") - .await; - assert!(server - .parse_message( - messages::RequestServerInfo::new("Test Client", BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION) - .into() - ) - .await - .is_ok()); - assert!(server - .parse_message(messages::StartScanning::default().into()) - .await - .is_ok()); - let mut device_index = None; - let mut device_removed_called = true; - while let Some(msg) = recv.next().await { - match msg { - ButtplugServerMessage::ScanningFinished(_) => continue, - ButtplugServerMessage::DeviceAdded(da) => { - assert_eq!(da.device_name(), "Aneros Vivi"); - if device_index.is_none() { - device_index = Some(da.device_index()); - } else { - assert!(device_removed_called); - assert_eq!( - da.device_index(), - device_index.expect("Test, assuming infallible.") - ); - return; - } - } - ButtplugServerMessage::DeviceRemoved(dr) => { - assert_eq!( - dr.device_index(), - device_index.expect("Test, assuming infallible.") - ); - device_removed_called = true; - } - _ => { - panic!( - "Returned message was not a DeviceAdded message or timed out: {:?}", - msg - ); - } - } - } -} -*/ diff --git a/buttplug/tests/util/device_test/device_test_case/config/lovense_ridge_user_config.json b/buttplug/tests/util/device_test/device_test_case/config/lovense_ridge_user_config.json deleted file mode 100644 index 1355ecc43..000000000 --- a/buttplug/tests/util/device_test/device_test_case/config/lovense_ridge_user_config.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "version": { - "major": 3, - "minor": 999 - }, - "user-configs": { - "devices": [ - { - "identifier": { - "address": "UserConfigTest", - "protocol": "lovense", - "identifier": "F" - }, - "config": { - "name": "Lovense Sex Machine", - "features": [ - { - "feature-type": "Oscillate", - "description": "Fucking Machine Oscillation Speed", - "actuator": { - "step-range": [ - 0, - 10 - ], - "step-limit": [ - 0, - 10 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ], - "user-config": { - "allow": false, - "deny": false, - "index": 0, - "display-name": "Lovense Name Test" - } - } - } - ] - } -} \ No newline at end of file diff --git a/buttplug/tests/util/device_test/device_test_case/config/lovense_ridge_user_config_invalid_range.json b/buttplug/tests/util/device_test/device_test_case/config/lovense_ridge_user_config_invalid_range.json deleted file mode 100644 index a1668513f..000000000 --- a/buttplug/tests/util/device_test/device_test_case/config/lovense_ridge_user_config_invalid_range.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "version": { - "major": 3, - "minor": 999 - }, - "user-configs": { - "devices": [ - { - "identifier": { - "address": "UserConfigTest", - "protocol": "lovense", - "identifier": "F" - }, - "config": { - "name": "Lovense Ridge", - "features": [ - { - "feature-type": "Oscillate", - "description": "Fucking Machine Oscillation Speed", - "actuator": { - "step-range": [ - 0, - 30 - ], - "step-limit": [ - 0, - 30 - ], - "messages": [ - "ScalarCmd" - ] - } - }, - { - "feature-type": "Battery", - "description": "Battery Level", - "sensor": { - "value-range": [ - [ - 0, - 100 - ] - ], - "messages": [ - "SensorReadCmd" - ] - } - } - ], - "user-config": { - "allow": false, - "deny": false, - "index": 0 - } - } - } - ] - } -} \ No newline at end of file diff --git a/buttplug/tests/util/device_test/device_test_case/config/tcode_linear_and_vibrate_user_config.json b/buttplug/tests/util/device_test/device_test_case/config/tcode_linear_and_vibrate_user_config.json deleted file mode 100644 index 46e29ccbc..000000000 --- a/buttplug/tests/util/device_test/device_test_case/config/tcode_linear_and_vibrate_user_config.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "version": { - "major": 3, - "minor": 999 - }, - "user-configs": { - "protocols": { - "tcode-v03": { - "communication": [{ - "btle": { - "names": [ - "tcode-v03" - ], - "services": { - "0000eea0-0000-1000-8000-00805f9b34fb": { - "tx": "0000ee01-0000-1000-8000-00805f9b34fb" - } - } - } - } - ], - "configurations": [] - } - }, - "devices": [ - { - "identifier": { - "protocol": "tcode-v03", - "identifier": "tcode-v03", - "address": "COM7" - }, - "config": { - "name": "TCode v0.3 (Single Linear Axis + Single Vibe)", - "features": [{ - "description": "", - "feature-type": "Position", - "actuator": { - "step-range": [0, 100], - "step-limit": [0, 100], - "messages": ["LinearCmd"] - } - }, { - "description": "", - "feature-type": "Vibrate", - "actuator": { - "step-range": [0, 99], - "step-limit": [0, 99], - "messages": ["ScalarCmd"] - } - } - ], - "user-config": { - "allow": false, - "deny": false, - "index": 0 - } - } - } - ] - } -} \ No newline at end of file diff --git a/crates/buttplug_client/Cargo.toml b/crates/buttplug_client/Cargo.toml new file mode 100644 index 000000000..c6abf7ba5 --- /dev/null +++ b/crates/buttplug_client/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "buttplug_client" +version = "10.0.0" +authors = ["Nonpolynomial Labs, LLC "] +description = "Buttplug Intimate Hardware Control Library - Core Library" +license = "BSD-3-Clause" +homepage = "http://buttplug.io" +repository = "https://github.com/buttplugio/buttplug.git" +readme = "./README.md" +keywords = ["usb", "serial", "hardware", "bluetooth", "teledildonics"] +edition = "2024" +exclude = ["examples/**"] + +[lib] +name = "buttplug_client" +path = "src/lib.rs" +test = true +doctest = true +doc = true + + +[dependencies] +buttplug_derive = "0.8.1" +buttplug_core = { path = "../buttplug_core" } +# buttplug_derive = { path = "../buttplug_derive" } +futures = "0.3.31" +thiserror = "2.0.12" +log = "0.4.27" +getset = "0.1.6" +tokio = { version = "1.46.1", features = ["macros"] } +dashmap = { version = "6.1.0" } +tracing-futures = "0.2.5" +tracing = "0.1.41" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" +jsonschema = { version = "0.30.0", default-features = false } +strum = "0.27.1" +strum_macros = "0.27.1" diff --git a/crates/buttplug_client/src/client_event_loop.rs b/crates/buttplug_client/src/client_event_loop.rs new file mode 100644 index 000000000..a69136b15 --- /dev/null +++ b/crates/buttplug_client/src/client_event_loop.rs @@ -0,0 +1,348 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 Nonpolynomial Labs LLC. All rights reserved. +// +// Licensed under the BSD 3-Clause license. See LICENSE file in the project root +// for full license information. + +//! Implementation of internal Buttplug Client event loop. + +use super::{ + client_message_sorter::ClientMessageSorter, + device::{ButtplugClientDevice, ButtplugClientDeviceEvent}, + ButtplugClientEvent, + ButtplugClientMessageFuturePair, + ButtplugClientMessageSender, +}; +use buttplug_core::{ + connector::{ButtplugConnector, ButtplugConnectorStateShared}, + errors::ButtplugError, + message::{ + ButtplugClientMessageV4, + ButtplugDeviceMessage, + ButtplugMessageValidator, + ButtplugServerMessageV4, + DeviceListV4, + DeviceMessageInfoV4, + }, +}; +use dashmap::DashMap; +use log::*; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; +use tokio::{ + select, + sync::{broadcast, mpsc}, +}; + +/// Enum used for communication from the client to the event loop. +#[derive(Clone)] +pub enum ButtplugClientRequest { + /// Client request to disconnect, via already sent connector instance. + Disconnect(ButtplugConnectorStateShared), + /// Given a DeviceList message, update the inner loop values and create + /// events for additions. + HandleDeviceList(DeviceListV4), + /// Client request to send a message via the connector. + /// + /// Bundled future should have reply set and waker called when this is + /// finished. + Message(ButtplugClientMessageFuturePair), +} + +/// Event loop for running [ButtplugClient] connections. +/// +/// Acts as a hub for communication between the connector and [ButtplugClient] +/// instances. +/// +/// Created whenever a new [super::ButtplugClient] is created, the internal loop +/// handles connection and communication with the server through the connector, +/// and creation of events received from the server. +/// +/// The event_loop does a few different things during its lifetime: +/// +/// - It will listen for events from the connector, or messages from the client, +/// routing them to their proper receivers until either server/client +/// disconnects. +/// +/// - On disconnect, it will tear down, and cannot be used again. All clients +/// and devices associated with the loop will be invalidated, and connect must +/// be called on the client again (or a new client should be created). +/// +/// # Why an event loop? +/// +/// Due to the async nature of Buttplug, we many channels routed to many +/// different tasks. However, all of those tasks will refer to the same event +/// loop. This allows us to coordinate and centralize our information while +/// keeping the API async. +/// +/// Note that no async call here should block. Any .await should only be on +/// async channels, and those channels should never have backpressure. We hope. +pub(super) struct ButtplugClientEventLoop +where + ConnectorType: ButtplugConnector + 'static, +{ + /// Connected status from client, managed by the event loop in case of disconnect. + connected_status: Arc, + /// Connector the event loop will use to communicate with the [ButtplugServer] + connector: ConnectorType, + /// Receiver for messages send from the [ButtplugServer] via the connector. + from_connector_receiver: mpsc::Receiver, + /// Map of devices shared between the client and the event loop + device_map: Arc>>, + /// Sends events to the [ButtplugClient] instance. + to_client_sender: broadcast::Sender, + /// Sends events to the client receiver. Stored here so it can be handed to + /// new ButtplugClientDevice instances. + from_client_sender: Arc, + /// Receives incoming messages from client instances. + from_client_receiver: broadcast::Receiver, + sorter: ClientMessageSorter, +} + +impl ButtplugClientEventLoop +where + ConnectorType: ButtplugConnector + 'static, +{ + /// Creates a new [ButtplugClientEventLoop]. + /// + /// Given the [ButtplugClientConnector] object, as well as the channels used + /// for communicating with the client, creates an event loop structure and + /// returns it. + pub fn new( + connected_status: Arc, + connector: ConnectorType, + from_connector_receiver: mpsc::Receiver, + to_client_sender: broadcast::Sender, + from_client_sender: Arc, + device_map: Arc>>, + ) -> Self { + trace!("Creating ButtplugClientEventLoop instance."); + Self { + connected_status, + device_map, + from_client_receiver: from_client_sender.subscribe(), + from_client_sender, + to_client_sender, + from_connector_receiver, + connector, + sorter: ClientMessageSorter::default(), + } + } + + /// Creates a [ButtplugClientDevice] from [DeviceMessageInfo]. + /// + /// Given a [DeviceMessageInfo] from a [DeviceAdded] or [DeviceList] message, + /// creates a ButtplugClientDevice and adds it the internal device map, then + /// returns the instance. + fn create_client_device(&mut self, info: &DeviceMessageInfoV4) -> Arc { + debug!( + "Trying to create a client device from DeviceMessageInfo: {:?}", + info + ); + match self.device_map.get(&info.device_index()) { + // If the device already exists in our map, clone it. + Some(dev) => { + debug!("Device already exists, creating clone."); + dev.clone() + } + // If it doesn't, insert it. + None => { + debug!("Device does not exist, creating new entry."); + let device = Arc::new(ButtplugClientDevice::new_from_device_info( + info, + &self.from_client_sender, + )); + self.device_map.insert(info.device_index(), device.clone()); + device + } + } + } + + fn send_client_event(&mut self, event: ButtplugClientEvent) { + trace!("Forwarding event {:?} to client", event); + + if self.to_client_sender.receiver_count() == 0 { + error!( + "Client event {:?} dropped, no client event listener available.", + event + ); + return; + } + + self + .to_client_sender + .send(event) + .expect("Already checked for receivers."); + } + + fn disconnect_device(&mut self, device_index: u32) { + if !self.device_map.contains_key(&device_index) { + return; + } + + let device = (*self + .device_map + .get(&device_index) + .expect("Checked for device index already.")) + .clone(); + device.set_device_connected(false); + device.queue_event(ButtplugClientDeviceEvent::DeviceRemoved); + // Then remove it from our storage map + self.device_map.remove(&device_index); + self.send_client_event(ButtplugClientEvent::DeviceRemoved(device)); + } + + /// Parse device messages from the connector. + /// + /// Since the event loop maintains the state of all devices reported from the + /// server, it will catch [DeviceAdded]/[DeviceList]/[DeviceRemoved] messages + /// and update its map accordingly. After that, it will pass the information + /// on as a [ButtplugClientEvent] to the [ButtplugClient]. + async fn parse_connector_message(&mut self, msg: ButtplugServerMessageV4) { + if self.sorter.maybe_resolve_result(&msg) { + trace!("Message future found, returning"); + return; + } + if let Err(e) = msg.is_valid() { + error!("Message not valid: {:?} - Error: {}", msg, e); + self.send_client_event(ButtplugClientEvent::Error(ButtplugError::from(e))); + return; + } + trace!("Message future not found, assuming server event."); + info!("{:?}", msg); + match msg { + ButtplugServerMessageV4::DeviceList(list) => { + trace!("Got device list, devices either added or removed"); + for dev in list.devices() { + if self.device_map.contains_key(&dev.1.device_index()) { + continue; + } + trace!("Device added, updating map and sending to client"); + let info = DeviceMessageInfoV4::from(dev.1.clone()); + let device = self.create_client_device(&info); + self.send_client_event(ButtplugClientEvent::DeviceAdded(device)); + } + let new_indexes: Vec = list.devices().iter().map(|x| x.1.device_index()).collect(); + let disconnected_indexes: Vec = self.device_map.iter().filter(|x| !new_indexes.contains(x.key())).map(|x| *x.key()).collect(); + for index in disconnected_indexes { + trace!("Device removed, updating map and sending to client"); + self.disconnect_device(index); + } + } + ButtplugServerMessageV4::ScanningFinished(_) => { + trace!("Scanning finished event received, forwarding to client."); + self.send_client_event(ButtplugClientEvent::ScanningFinished); + } + ButtplugServerMessageV4::InputReading(msg) => { + let device_idx = msg.device_index(); + if let Some(device) = self.device_map.get(&device_idx) { + device + .value() + .queue_event(ButtplugClientDeviceEvent::Message( + ButtplugServerMessageV4::from(msg), + )); + } + } + ButtplugServerMessageV4::Error(e) => { + self.send_client_event(ButtplugClientEvent::Error(e.into())); + } + _ => error!("Cannot process message, dropping: {:?}", msg), + } + } + + /// Send a message from the [ButtplugClient] to the [ButtplugClientConnector]. + async fn send_message(&mut self, mut msg_fut: ButtplugClientMessageFuturePair) { + if let Err(e) = &msg_fut.msg.is_valid() { + error!("Message not valid: {:?} - Error: {}", msg_fut.msg, e); + msg_fut + .waker + .set_reply(Err(ButtplugError::from(e.clone()).into())); + return; + } + + trace!("Sending message to connector: {:?}", msg_fut.msg); + self.sorter.register_future(&mut msg_fut); + if self.connector.send(msg_fut.msg).await.is_err() { + error!("Sending message failed, connector most likely no longer connected."); + } + } + + /// Parses message types from the client, returning false when disconnect + /// happens. + /// + /// Takes different messages from the client and handles them: + /// + /// - For outbound messages to the server, sends them to the connector/server. + /// - For disconnections, requests connector disconnect + /// - For RequestDeviceList, builds a reply out of its own + async fn parse_client_request(&mut self, msg: ButtplugClientRequest) -> bool { + match msg { + ButtplugClientRequest::Message(msg_fut) => { + trace!("Sending message through connector: {:?}", msg_fut.msg); + self.send_message(msg_fut).await; + true + } + ButtplugClientRequest::Disconnect(state) => { + trace!("Client requested disconnect"); + state.set_reply(self.connector.disconnect().await); + false + } + ButtplugClientRequest::HandleDeviceList(device_list) => { + trace!("Device list received, updating map."); + for (i, device) in device_list.devices() { + if self.device_map.contains_key(i) { + continue; + } + let device = self.create_client_device(device); + self.send_client_event(ButtplugClientEvent::DeviceAdded(device)); + } + true + } + } + } + + /// Runs the event loop, returning once either the client or connector drops. + pub async fn run(&mut self) { + debug!("Running client event loop."); + loop { + select! { + event = self.from_connector_receiver.recv() => match event { + None => { + info!("Connector disconnected, exiting loop."); + break; + } + Some(msg) => { + self.parse_connector_message(msg).await; + } + }, + client = self.from_client_receiver.recv() => match client { + Err(_) => { + info!("Client disconnected, exiting loop."); + break; + } + Ok(msg) => { + if !self.parse_client_request(msg).await { + break; + } + } + }, + }; + } + self + .device_map + .iter() + .for_each(|val| val.value().set_client_connected(false)); + + let device_indexes: Vec = self.device_map.iter().map(|k| *k.key()).collect(); + device_indexes + .iter() + .for_each(|k| self.disconnect_device(*k)); + self.connected_status.store(false, Ordering::Relaxed); + self.send_client_event(ButtplugClientEvent::ServerDisconnect); + + debug!("Exiting client event loop."); + } +} diff --git a/crates/buttplug_client/src/client_message_sorter.rs b/crates/buttplug_client/src/client_message_sorter.rs new file mode 100644 index 000000000..7752a83b9 --- /dev/null +++ b/crates/buttplug_client/src/client_message_sorter.rs @@ -0,0 +1,123 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 Nonpolynomial Labs LLC. All rights reserved. +// +// Licensed under the BSD 3-Clause license. See LICENSE file in the project root +// for full license information. + +//! Handling of remote message pairing and future resolution. + +use super::{ + ButtplugClientError, + ButtplugClientMessageFuturePair, + ButtplugServerMessageStateShared, +}; +use buttplug_core::message::{ButtplugMessage, ButtplugMessageValidator, ButtplugServerMessageV4}; +use dashmap::DashMap; +use log::*; +use std::sync::{ + atomic::{AtomicU32, Ordering}, + Arc, +}; + +/// Message sorting and pairing for remote client connectors. +/// +/// In order to create reliable connections to remote systems, we need a way to maintain message +/// coherence. We expect that whenever a client sends the server a request message, the server will +/// always send back a response message. +/// +/// For the [in-process][crate::connector::ButtplugInProcessClientConnector] case, where the client and +/// server are in the same process, we can simply use execution flow to match the client message and +/// server response. However, when going over IPC or network, we have to wait to hear back from the +/// server. To match the outgoing client request message with the incoming server response message +/// in the remote case, we use the `id` field of [ButtplugMessage]. The client's request message +/// will have a server response with a matching index. Any message that comes from the server +/// without an originating client message ([DeviceAdded][crate::core::messages::DeviceAdded], +/// [Log][crate::core::messages::Log], etc...) will have an `id` of 0 and is considered an *event*, +/// meaning something happened on the server that was not directly tied to a client request. +/// +/// The ClientConnectionMessageSorter does two things to facilitate this matching: +/// +/// - Creates and keeps track of the current message `id`, as a [u32] +/// - Manages a HashMap of indexes to resolvable futures. +/// +/// Whenever a remote connector sends a [ButtplugMessage], it first puts it through its +/// ClientMessageSorter to fill in the message `id`. Similarly, when a [ButtplugMessage] is +/// received, it comes through the sorter, with one of 3 outcomes: +/// +/// - If there is a future with matching `id` waiting on a response, it resolves that future using +/// the incoming message +/// - If the message `id` is 0, the message is emitted as an *event*. +/// - If the message `id` is not zero but there is no future waiting, the message is dropped and an +/// error is emitted. +/// +pub struct ClientMessageSorter { + /// Map of message `id`s to their related future. + /// + /// This is where we store message `id`s that are waiting for a return from the server. Once we + /// get back a response with a matching `id`, we remove the entry from this map, and use the waker + /// to complete the future with the received response message. + future_map: DashMap, + + /// Message `id` counter + /// + /// Every time we add a message to the future_map, we need it to have a unique `id`. We assume + /// that unsigned 2^32 will be enough (Buttplug isn't THAT chatty), and use it as a monotonically + /// increasing counter for setting `id`s. + current_id: Arc, +} + +impl ClientMessageSorter { + /// Registers a future to be resolved when we receive a response. + /// + /// Given a message and its related future, set the message's `id`, and match that id with the + /// future to be resolved when we get a response back. + pub fn register_future(&self, msg_fut: &mut ButtplugClientMessageFuturePair) { + let id = self.current_id.load(Ordering::Relaxed); + trace!("Setting message id to {}", id); + msg_fut.msg.set_id(id); + self.future_map.insert(id, msg_fut.waker.clone()); + self.current_id.store(id + 1, Ordering::Relaxed); + } + + /// Given a response message from the server, resolve related future if we have one. + /// + /// Returns true if the response message was resolved to a future via matching `id`, otherwise + /// returns false. False returns mean the message should be considered as an *event*. + pub fn maybe_resolve_result(&self, msg: &ButtplugServerMessageV4) -> bool { + trace!("{:?}", msg); + let id = msg.id(); + trace!("Trying to resolve message future for id {}.", id); + match self.future_map.remove(&id) { + Some((_, state)) => { + trace!("Resolved id {} to a future.", id); + if let Err(e) = msg.is_valid() { + error!("Message not valid: {:?} - Error: {}", msg, e); + state.set_reply(Err(ButtplugClientError::ButtplugError(e.into()))); + } else if let ButtplugServerMessageV4::Error(e) = msg { + state.set_reply(Err(e.original_error().into())) + } else { + state.set_reply(Ok(msg.clone())) + } + true + } + None => { + trace!("Message id {} not found, considering it an event.", id); + false + } + } + } +} + +impl Default for ClientMessageSorter { + /// Create a default implementation of the ClientConnectorMessageSorter + /// + /// Sets the current_id to 1, since as a client we can't send message `id` of 0 (0 is reserved for + /// system incoming messages). + fn default() -> Self { + Self { + future_map: DashMap::new(), + current_id: Arc::new(AtomicU32::new(1)), + } + } +} diff --git a/crates/buttplug_client/src/connector/mod.rs b/crates/buttplug_client/src/connector/mod.rs new file mode 100644 index 000000000..60a867e17 --- /dev/null +++ b/crates/buttplug_client/src/connector/mod.rs @@ -0,0 +1,15 @@ +use super::serializer::ButtplugClientJSONSerializer; +use buttplug_core::{ + connector::ButtplugRemoteConnector, + message::{ButtplugClientMessageV4, ButtplugServerMessageV4}, +}; + +pub type ButtplugRemoteClientConnector< + TransportType, + SerializerType = ButtplugClientJSONSerializer, +> = ButtplugRemoteConnector< + TransportType, + SerializerType, + ButtplugClientMessageV4, + ButtplugServerMessageV4, +>; diff --git a/crates/buttplug_client/src/device/command.rs b/crates/buttplug_client/src/device/command.rs new file mode 100644 index 000000000..443cad62d --- /dev/null +++ b/crates/buttplug_client/src/device/command.rs @@ -0,0 +1,60 @@ +use buttplug_core::message::OutputType; + +pub enum ClientDeviceCommandValue { + Int(u32), + Float(f64), +} + +impl Into for u32 { + fn into(self) -> ClientDeviceCommandValue { + ClientDeviceCommandValue::Int(self) + } +} + +impl Into for f64 { + fn into(self) -> ClientDeviceCommandValue { + ClientDeviceCommandValue::Float(self) + } +} + +pub enum ClientDeviceOutputCommand { + // u32 types use steps, need to compare before sending + Vibrate(u32), + Rotate(u32), + Oscillate(u32), + Constrict(u32), + Heater(u32), + Led(u32), + Spray(u32), + Position(u32), + RotateWithDirection(u32, bool), + PositionWithDuration(u32, u32), + // f64 types are old style float, will need to convert before sending + VibrateFloat(f64), + RotateFloat(f64), + OscillateFloat(f64), + ConstrictFloat(f64), + HeaterFloat(f64), + LedFloat(f64), + SprayFloat(f64), + PositionFloat(f64), + RotateWithDirectionFloat(f64, bool), + PositionWithDurationFloat(f64, u32), +} + +impl Into for &ClientDeviceOutputCommand { + fn into(self) -> OutputType { + match self { + ClientDeviceOutputCommand::Vibrate(_) | ClientDeviceOutputCommand::VibrateFloat(_) => OutputType::Vibrate, + ClientDeviceOutputCommand::Oscillate(_) | ClientDeviceOutputCommand::OscillateFloat(_) => OutputType::Oscillate, + ClientDeviceOutputCommand::Rotate(_) | ClientDeviceOutputCommand::RotateFloat(_) => OutputType::Rotate, + ClientDeviceOutputCommand::Constrict(_) | ClientDeviceOutputCommand::ConstrictFloat(_) => OutputType::Constrict, + ClientDeviceOutputCommand::Heater(_) | ClientDeviceOutputCommand::HeaterFloat(_) => OutputType::Heater, + ClientDeviceOutputCommand::Led(_) | ClientDeviceOutputCommand::LedFloat(_) => OutputType::Led, + ClientDeviceOutputCommand::Spray(_) | ClientDeviceOutputCommand::SprayFloat(_) => OutputType::Spray, + ClientDeviceOutputCommand::Position(_) | ClientDeviceOutputCommand::PositionFloat(_) => OutputType::Position, + ClientDeviceOutputCommand::PositionWithDuration(_, _) | ClientDeviceOutputCommand::PositionWithDurationFloat(_, _) => OutputType::PositionWithDuration, + ClientDeviceOutputCommand::RotateWithDirection(_, _) | ClientDeviceOutputCommand::RotateWithDirectionFloat(_, _) => OutputType::RotateWithDirection, + } + } +} \ No newline at end of file diff --git a/crates/buttplug_client/src/device/device.rs b/crates/buttplug_client/src/device/device.rs new file mode 100644 index 000000000..894d80412 --- /dev/null +++ b/crates/buttplug_client/src/device/device.rs @@ -0,0 +1,304 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 Nonpolynomial Labs LLC. All rights reserved. +// +// Licensed under the BSD 3-Clause license. See LICENSE file in the project root +// for full license information. + +//! Representation and management of devices connected to the server. + +use crate::device::{ClientDeviceCommandValue, ClientDeviceOutputCommand}; + +use crate::{ + device::ClientDeviceFeature, + create_boxed_future_client_error, + ButtplugClientMessageSender, + ButtplugClientResultFuture, +}; +use buttplug_core::{ + errors::ButtplugDeviceError, + message::{ + ButtplugServerMessageV4, DeviceFeature, DeviceMessageInfoV4, FeatureType, OutputType, StopDeviceCmdV0 + }, + util::stream::convert_broadcast_receiver_to_stream, +}; +use futures::{future, FutureExt, Stream}; +use getset::{CopyGetters, Getters}; +use log::*; +use std::collections::BTreeMap; +use std::{ + fmt, sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + } +}; +use tokio::sync::broadcast; + +/// Enum for messages going to a [ButtplugClientDevice] instance. +#[derive(Clone, Debug)] +// The message enum is what we'll fly with this most of the time. DeviceRemoved/ClientDisconnect +// will happen at most once, so we don't care that those contentless traits still take up > 200 +// bytes. +#[allow(clippy::large_enum_variant)] +pub enum ButtplugClientDeviceEvent { + /// Device has disconnected from server. + DeviceRemoved, + /// Client has disconnected from server. + ClientDisconnect, + /// Message was received from server for that specific device. + Message(ButtplugServerMessageV4), +} + +#[derive(Getters, CopyGetters)] +/// Client-usable representation of device connected to the corresponding +/// [ButtplugServer][crate::server::ButtplugServer] +/// +/// [ButtplugClientDevice] instances are obtained from the +/// [ButtplugClient][super::ButtplugClient], and allow the user to send commands +/// to a device connected to the server. +pub struct ButtplugClientDevice { + /// Name of the device + #[getset(get = "pub")] + name: String, + /// Display name of the device + #[getset(get = "pub")] + display_name: Option, + /// Index of the device, matching the index in the + /// [ButtplugServer][crate::server::ButtplugServer]'s + /// [DeviceManager][crate::server::device_manager::DeviceManager]. + #[getset(get_copy = "pub")] + index: u32, + /// Actuators and sensors available on the device. + #[getset(get = "pub")] + device_features: BTreeMap, + /// Sends commands from the [ButtplugClientDevice] instance to the + /// [ButtplugClient][super::ButtplugClient]'s event loop, which will then send + /// the message on to the [ButtplugServer][crate::server::ButtplugServer] + /// through the connector. + event_loop_sender: Arc, + internal_event_sender: broadcast::Sender, + /// True if this [ButtplugClientDevice] is currently connected to the + /// [ButtplugServer][crate::server::ButtplugServer]. + device_connected: Arc, + /// True if the [ButtplugClient][super::ButtplugClient] that generated this + /// [ButtplugClientDevice] instance is still connected to the + /// [ButtplugServer][crate::server::ButtplugServer]. + client_connected: Arc, +} + +impl ButtplugClientDevice { + /// Creates a new [ButtplugClientDevice] instance + /// + /// Fills out the struct members for [ButtplugClientDevice]. + /// `device_connected` and `client_connected` are automatically set to true + /// because we assume we're only created connected devices. + /// + /// # Why is this pub(super)? + /// + /// There's really no reason for anyone but a + /// [ButtplugClient][super::ButtplugClient] to create a + /// [ButtplugClientDevice]. A [ButtplugClientDevice] is mostly a shim around + /// the [ButtplugClient] that generated it, with some added convenience + /// functions for forming device control messages. + pub(super) fn new( + name: &str, + display_name: &Option, + index: u32, + device_features: &BTreeMap, + message_sender: &Arc, + ) -> Self { + info!( + "Creating client device {} with index {} and messages {:?}.", + name, index, device_features + ); + let (event_sender, _) = broadcast::channel(256); + let device_connected = Arc::new(AtomicBool::new(true)); + let client_connected = Arc::new(AtomicBool::new(true)); + + Self { + name: name.to_owned(), + display_name: display_name.clone(), + index, + device_features: device_features.iter().map(|(i, x)| (*i, ClientDeviceFeature::new(index, *i, &x, &message_sender))).collect(), + event_loop_sender: message_sender.clone(), + internal_event_sender: event_sender, + device_connected, + client_connected, + } + } + + pub(crate) fn new_from_device_info( + info: &DeviceMessageInfoV4, + sender: &Arc, + ) -> Self { + ButtplugClientDevice::new( + info.device_name(), + info.device_display_name(), + info.device_index(), + info.device_features(), + sender, + ) + } + + pub fn connected(&self) -> bool { + self.device_connected.load(Ordering::Relaxed) + } + + pub fn event_stream(&self) -> Box + Send + Unpin> { + Box::new(Box::pin(convert_broadcast_receiver_to_stream( + self.internal_event_sender.subscribe(), + ))) + } + + fn filter_device_actuators(&self, actuator_type: OutputType) -> Vec { + self + .device_features + .iter() + .filter(|x| { + x.1 + .feature() + .output() + .as_ref() + .ok_or(false) + .unwrap() + .contains_key(&actuator_type) + }) + .map(|(_, x)| x) + .cloned() + .collect() + } + + fn set_client_value(&self, client_device_command: &ClientDeviceOutputCommand) -> ButtplugClientResultFuture { + let features = self.filter_device_actuators(client_device_command.into()); + if features.is_empty() { + // TODO err + } + let mut fut_vec: Vec = vec!(); + for x in features { + let val = x.convert_client_cmd_to_output_cmd(client_device_command); + match val { + Ok(v) => fut_vec.push(self.event_loop_sender.send_message_expect_ok( + v.into(), + )), + Err(e) => return future::ready(Err(e)).boxed() + } + } + async move { + futures::future::try_join_all(fut_vec).await?; + Ok(()) + } + .boxed() + } + + pub fn send_command(&self, client_device_command: &ClientDeviceOutputCommand) -> ButtplugClientResultFuture { + self.set_client_value(client_device_command) + } + + pub fn vibrate_features(&self) -> Vec { + self.filter_device_actuators(OutputType::Vibrate) + } + + /// Commands device to vibrate, assuming it has the features to do so. + pub fn vibrate(&self, level: impl Into) -> ButtplugClientResultFuture { + let val = level.into(); + self.set_client_value(&match val { + ClientDeviceCommandValue::Int(v) => ClientDeviceOutputCommand::Vibrate(v), + ClientDeviceCommandValue::Float(f) => ClientDeviceOutputCommand::VibrateFloat(f) + }) + } + + pub fn has_battery_level(&self) -> bool { + self + .device_features + .iter() + .any(|x| x.1.feature().feature_type() == FeatureType::Battery) + } + + pub fn battery_level(&self) -> ButtplugClientResultFuture { + if let Some(battery) = self + .device_features + .iter() + .find(|x| x.1.feature().feature_type() == FeatureType::Battery) + { + battery.1.battery_level() + } else { + create_boxed_future_client_error( + ButtplugDeviceError::DeviceFeatureMismatch( + "Device does not have battery feature available".to_owned(), + ) + .into(), + ) + } + } + + pub fn has_rssi_level(&self) -> bool { + self + .device_features + .iter() + .any(|x| x.1.feature().feature_type() == FeatureType::Rssi) + } + + pub fn rssi_level(&self) -> ButtplugClientResultFuture { + if let Some(rssi) = self + .device_features + .iter() + .find(|x| x.1.feature().feature_type() == FeatureType::Rssi) + { + rssi.1.rssi_level() + } else { + create_boxed_future_client_error( + ButtplugDeviceError::DeviceFeatureMismatch( + "Device does not have RSSI feature available".to_owned(), + ) + .into(), + ) + } + } + + /// Commands device to stop all movement. + pub fn stop(&self) -> ButtplugClientResultFuture { + // All devices accept StopDeviceCmd + self + .event_loop_sender + .send_message_expect_ok(StopDeviceCmdV0::new(self.index).into()) + } + + pub(crate) fn set_device_connected(&self, connected: bool) { + self.device_connected.store(connected, Ordering::Relaxed); + } + + pub(crate) fn set_client_connected(&self, connected: bool) { + self.client_connected.store(connected, Ordering::Relaxed); + } + + pub(crate) fn queue_event(&self, event: ButtplugClientDeviceEvent) { + if self.internal_event_sender.receiver_count() == 0 { + // We can drop devices before we've hooked up listeners or after the device manager drops, + // which is common, so only show this when in debug. + debug!("No handlers for device event, dropping event: {:?}", event); + return; + } + self + .internal_event_sender + .send(event) + .expect("Checked for receivers already."); + } +} + +impl Eq for ButtplugClientDevice { +} + +impl PartialEq for ButtplugClientDevice { + fn eq(&self, other: &Self) -> bool { + self.index == other.index + } +} + +impl fmt::Debug for ButtplugClientDevice { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ButtplugClientDevice") + .field("name", &self.name) + .field("index", &self.index) + .finish() + } +} diff --git a/crates/buttplug_client/src/device/feature.rs b/crates/buttplug_client/src/device/feature.rs new file mode 100644 index 000000000..480fd3396 --- /dev/null +++ b/crates/buttplug_client/src/device/feature.rs @@ -0,0 +1,317 @@ +use std::sync::Arc; + +use futures::{future, FutureExt}; +use getset::{CopyGetters, Getters}; + +use buttplug_core::{ + errors::{ButtplugDeviceError, ButtplugError, ButtplugMessageError}, + message::{ + ButtplugDeviceMessageNameV4, ButtplugServerMessageV4, DeviceFeature, DeviceFeatureOutput, InputCmdV4, InputCommandType, InputType, InputTypeData, OutputCmdV4, OutputCommand, OutputPositionWithDuration, OutputRotateWithDirection, OutputType, OutputValue + }, +}; + +use super::ClientDeviceOutputCommand; + +use crate::{ + create_boxed_future_client_error, device::ClientDeviceCommandValue, ButtplugClientError, ButtplugClientMessageSender, ButtplugClientResultFuture +}; + +#[derive(Getters, CopyGetters, Clone)] +pub struct ClientDeviceFeature { + #[getset(get_copy = "pub")] + device_index: u32, + #[getset(get_copy = "pub")] + feature_index: u32, + #[getset(get = "pub")] + feature: DeviceFeature, + /// Sends commands from the [ButtplugClientDevice] instance to the + /// [ButtplugClient][super::ButtplugClient]'s event loop, which will then send + /// the message on to the [ButtplugServer][crate::server::ButtplugServer] + /// through the connector. + event_loop_sender: Arc, +} + +impl ClientDeviceFeature { + pub(super) fn new( + device_index: u32, + feature_index: u32, + feature: &DeviceFeature, + event_loop_sender: &Arc, + ) -> Self { + Self { + device_index, + feature_index, + feature: feature.clone(), + event_loop_sender: event_loop_sender.clone(), + } + } + + fn check_step_value(&self, feature_output: &DeviceFeatureOutput, steps: u32) -> Result { + if steps <= feature_output.step_count() { + Ok(steps) + } else { + Err(ButtplugClientError::ButtplugOutputCommandConversionError(format!("{} is larger than the maximum number of steps ({}).", steps, feature_output.step_count()))) + } + } + + fn convert_float_value(&self, feature_output: &DeviceFeatureOutput, float_amt: f64) -> Result { + if float_amt < 0.0f64 || float_amt > 1.0f64 { + Err(ButtplugClientError::ButtplugOutputCommandConversionError("Float values must be between 0.0 and 1.0".to_owned())) + } else { + Ok((float_amt * feature_output.step_count() as f64).ceil() as u32) + } + } + + pub(super) fn convert_client_cmd_to_output_cmd(&self, client_cmd: &ClientDeviceOutputCommand) -> 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_cmd = match client_cmd { + ClientDeviceOutputCommand::VibrateFloat(v) => OutputCommand::Vibrate(OutputValue::new(self.convert_float_value(output, *v)?)), + ClientDeviceOutputCommand::OscillateFloat(v) => OutputCommand::Oscillate(OutputValue::new(self.convert_float_value(output, *v)?)), + ClientDeviceOutputCommand::RotateFloat(v) => OutputCommand::Rotate(OutputValue::new(self.convert_float_value(output, *v)?)), + ClientDeviceOutputCommand::ConstrictFloat(v) => OutputCommand::Constrict(OutputValue::new(self.convert_float_value(output, *v)?)), + ClientDeviceOutputCommand::HeaterFloat(v) => OutputCommand::Heater(OutputValue::new(self.convert_float_value(output, *v)?)), + ClientDeviceOutputCommand::LedFloat(v )=> OutputCommand::Led(OutputValue::new(self.convert_float_value(output, *v)?)), + ClientDeviceOutputCommand::SprayFloat(v) => OutputCommand::Spray(OutputValue::new(self.convert_float_value(output, *v)?)), + ClientDeviceOutputCommand::PositionFloat(v) => OutputCommand::Position(OutputValue::new(self.convert_float_value(output, *v)?)), + ClientDeviceOutputCommand::PositionWithDurationFloat(v, d) => OutputCommand::PositionWithDuration(OutputPositionWithDuration::new(self.convert_float_value(output, *v)?, *d)), + ClientDeviceOutputCommand::RotateWithDirectionFloat(v, d) => OutputCommand::RotateWithDirection(OutputRotateWithDirection::new(self.convert_float_value(output, *v)?, *d)), + ClientDeviceOutputCommand::Vibrate(v) => OutputCommand::Vibrate(OutputValue::new(self.check_step_value(output, *v)?)), + ClientDeviceOutputCommand::Oscillate(v) => OutputCommand::Oscillate(OutputValue::new(self.check_step_value(output, *v)?)), + ClientDeviceOutputCommand::Rotate(v) => OutputCommand::Rotate(OutputValue::new(self.check_step_value(output, *v)?)), + ClientDeviceOutputCommand::Constrict(v) => OutputCommand::Constrict(OutputValue::new(self.check_step_value(output, *v)?)), + ClientDeviceOutputCommand::Heater(v) => OutputCommand::Heater(OutputValue::new(self.check_step_value(output, *v)?)), + ClientDeviceOutputCommand::Led(v )=> OutputCommand::Led(OutputValue::new(self.check_step_value(output, *v)?)), + ClientDeviceOutputCommand::Spray(v) => OutputCommand::Spray(OutputValue::new(self.check_step_value(output, *v)?)), + ClientDeviceOutputCommand::Position(v) => OutputCommand::Position(OutputValue::new(self.check_step_value(output, *v)?)), + ClientDeviceOutputCommand::PositionWithDuration(v, d) => OutputCommand::PositionWithDuration(OutputPositionWithDuration::new(self.check_step_value(output, *v)?, *d)), + ClientDeviceOutputCommand::RotateWithDirection(v, d) => OutputCommand::RotateWithDirection(OutputRotateWithDirection::new(self.check_step_value(output, *v)?, *d)), + }; + Ok(OutputCmdV4::new(self.device_index, self.feature_index, output_cmd)) + } + + pub fn send_command(&self, client_device_command: &ClientDeviceOutputCommand) -> ButtplugClientResultFuture { + match self.convert_client_cmd_to_output_cmd(&client_device_command) { + Ok(cmd) => self.event_loop_sender.send_message_expect_ok(cmd.into()), + Err(e) => future::ready(Err(e)).boxed() + } + } + + pub fn vibrate(&self, level: impl Into) -> ButtplugClientResultFuture { + let val = level.into(); + self.send_command(&match val { + ClientDeviceCommandValue::Int(v) => ClientDeviceOutputCommand::Vibrate(v), + ClientDeviceCommandValue::Float(f) => ClientDeviceOutputCommand::VibrateFloat(f) + }) + } + + pub fn oscillate(&self, level: impl Into) -> ButtplugClientResultFuture { + let val = level.into(); + self.send_command(&match val { + ClientDeviceCommandValue::Int(v) => ClientDeviceOutputCommand::Oscillate(v), + ClientDeviceCommandValue::Float(f) => ClientDeviceOutputCommand::OscillateFloat(f) + }) + } + + pub fn rotate(&self, level: impl Into) -> ButtplugClientResultFuture { + let val = level.into(); + self.send_command(&match val { + ClientDeviceCommandValue::Int(v) => ClientDeviceOutputCommand::Rotate(v), + ClientDeviceCommandValue::Float(f) => ClientDeviceOutputCommand::RotateFloat(f) + }) + } + + pub fn spray(&self, level: impl Into) -> ButtplugClientResultFuture { + let val = level.into(); + self.send_command(&match val { + ClientDeviceCommandValue::Int(v) => ClientDeviceOutputCommand::Spray(v), + ClientDeviceCommandValue::Float(f) => ClientDeviceOutputCommand::SprayFloat(f) + }) + } + + pub fn constrict(&self, level: impl Into) -> ButtplugClientResultFuture { + let val = level.into(); + self.send_command(&match val { + ClientDeviceCommandValue::Int(v) => ClientDeviceOutputCommand::Constrict(v), + ClientDeviceCommandValue::Float(f) => ClientDeviceOutputCommand::ConstrictFloat(f) + }) + } + + pub fn position(&self, level: impl Into) -> ButtplugClientResultFuture { + let val = level.into(); + self.send_command(&match val { + ClientDeviceCommandValue::Int(v) => ClientDeviceOutputCommand::Position(v), + ClientDeviceCommandValue::Float(f) => ClientDeviceOutputCommand::PositionFloat(f) + }) + } + + pub fn position_with_duration( + &self, + position: impl Into, + duration_in_ms: u32, + ) -> ButtplugClientResultFuture { + let val = position.into(); + self.send_command(&match val { + ClientDeviceCommandValue::Int(v) => ClientDeviceOutputCommand::PositionWithDuration(v, duration_in_ms), + ClientDeviceCommandValue::Float(f) => ClientDeviceOutputCommand::PositionWithDurationFloat(f, duration_in_ms) + }) + } + + pub fn rotate_with_direction(&self, level: impl Into, clockwise: bool) -> ButtplugClientResultFuture { + let val = level.into(); + self.send_command(&match val { + ClientDeviceCommandValue::Int(v) => ClientDeviceOutputCommand::RotateWithDirection(v, clockwise), + ClientDeviceCommandValue::Float(f) => ClientDeviceOutputCommand::RotateWithDirectionFloat(f, clockwise) + }) + } + + pub fn subscribe_sensor(&self, sensor_type: InputType) -> ButtplugClientResultFuture { + if let Some(sensor_map) = self.feature.input() { + if let Some(sensor) = sensor_map.get(&sensor_type) { + if sensor + .input_commands() + .contains(&InputCommandType::Subscribe) + { + let msg = InputCmdV4::new( + self.device_index, + self.feature_index, + sensor_type, + InputCommandType::Subscribe, + ) + .into(); + return self.event_loop_sender.send_message_expect_ok(msg); + } + } + } + create_boxed_future_client_error( + ButtplugDeviceError::MessageNotSupported(ButtplugDeviceMessageNameV4::InputCmd.to_string()) + .into(), + ) + } + + pub fn unsubscribe_sensor(&self, sensor_type: InputType) -> ButtplugClientResultFuture { + if let Some(sensor_map) = self.feature.input() { + if let Some(sensor) = sensor_map.get(&sensor_type) { + if sensor + .input_commands() + .contains(&InputCommandType::Subscribe) + { + let msg = InputCmdV4::new( + self.device_index, + self.feature_index, + sensor_type, + InputCommandType::Unsubscribe, + ) + .into(); + return self.event_loop_sender.send_message_expect_ok(msg); + } + } + } + create_boxed_future_client_error( + ButtplugDeviceError::MessageNotSupported(ButtplugDeviceMessageNameV4::InputCmd.to_string()) + .into(), + ) + } + + fn read_sensor(&self, sensor_type: InputType) -> ButtplugClientResultFuture { + if let Some(sensor_map) = self.feature.input() { + if let Some(sensor) = sensor_map.get(&sensor_type) { + if sensor.input_commands().contains(&InputCommandType::Read) { + let msg = InputCmdV4::new( + self.device_index, + self.feature_index, + sensor_type, + InputCommandType::Read, + ) + .into(); + let reply = self.event_loop_sender.send_message(msg); + return async move { + if let ButtplugServerMessageV4::InputReading(data) = reply.await? { + if sensor_type == data.data().as_input_type() { + Ok(data.data()) + } else { + Err(ButtplugError::ButtplugMessageError(ButtplugMessageError::UnexpectedMessageType( + "InputReading".to_owned(), + )) + .into()) + } + } else { + Err( + ButtplugError::ButtplugMessageError(ButtplugMessageError::UnexpectedMessageType( + "InputReading".to_owned(), + )) + .into() + ) + } + } + .boxed(); + } + } + } + create_boxed_future_client_error( + ButtplugDeviceError::MessageNotSupported(ButtplugDeviceMessageNameV4::InputCmd.to_string()) + .into(), + ) + } + + pub fn battery_level(&self) -> ButtplugClientResultFuture { + if self + .feature() + .input() + .as_ref() + .ok_or(false) + .unwrap() + .contains_key(&InputType::Battery) + { + let send_fut = self.read_sensor(InputType::Battery); + Box::pin(async move { + let data = send_fut.await?; + let battery_level = if let InputTypeData::Battery(level) = data { + level.data() + } else { + 0 + }; + Ok(battery_level as u32) + }) + } else { + create_boxed_future_client_error( + ButtplugDeviceError::DeviceFeatureMismatch("Device feature is not battery".to_owned()) + .into(), + ) + } + } + + pub fn rssi_level(&self) -> ButtplugClientResultFuture { + if self + .feature() + .input() + .as_ref() + .ok_or(false) + .unwrap() + .contains_key(&InputType::Rssi) + { + let send_fut = self.read_sensor(InputType::Rssi); + Box::pin(async move { + let data = send_fut.await?; + let rssi_level = if let InputTypeData::Rssi(level) = data { + level.data() + } else { + 0 + }; + Ok(rssi_level) + }) + } else { + create_boxed_future_client_error( + ButtplugDeviceError::DeviceFeatureMismatch("Device feature is not RSSI".to_owned()).into(), + ) + } + } +} diff --git a/crates/buttplug_client/src/device/mod.rs b/crates/buttplug_client/src/device/mod.rs new file mode 100644 index 000000000..438b4daef --- /dev/null +++ b/crates/buttplug_client/src/device/mod.rs @@ -0,0 +1,7 @@ +mod device; +mod feature; +mod command; + +pub use device::*; +pub use feature::*; +pub use command::*; \ No newline at end of file diff --git a/crates/buttplug_client/src/lib.rs b/crates/buttplug_client/src/lib.rs new file mode 100644 index 000000000..c3e77ebda --- /dev/null +++ b/crates/buttplug_client/src/lib.rs @@ -0,0 +1,472 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 Nonpolynomial Labs LLC. All rights reserved. +// +// Licensed under the BSD 3-Clause license. See LICENSE file in the project root +// for full license information. + +//! Communications API for accessing Buttplug Servers +pub mod client_event_loop; +pub mod client_message_sorter; +pub mod connector; +pub mod device; +pub mod serializer; + +use buttplug_core::{ + connector::{ButtplugConnector, ButtplugConnectorError, ButtplugConnectorFuture}, + errors::{ButtplugError, ButtplugHandshakeError}, + message::{ + ButtplugClientMessageV4, + ButtplugServerMessageV4, + PingV0, + RequestDeviceListV0, + RequestServerInfoV4, + StartScanningV0, + StopAllDevicesV0, + StopScanningV0, + BUTTPLUG_CURRENT_API_MAJOR_VERSION, + BUTTPLUG_CURRENT_API_MINOR_VERSION, + }, + util::{ + async_manager, + future::{ButtplugFuture, ButtplugFutureStateShared}, + stream::convert_broadcast_receiver_to_stream, + }, +}; +use client_event_loop::{ButtplugClientEventLoop, ButtplugClientRequest}; +use dashmap::DashMap; +pub use device::{ButtplugClientDevice, ButtplugClientDeviceEvent}; +use futures::{ + future::{self, BoxFuture, FutureExt}, + Stream, +}; +use log::*; +use strum_macros::Display; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; +use thiserror::Error; +use tokio::sync::{broadcast, mpsc, Mutex}; +use tracing_futures::Instrument; + +/// Result type used for public APIs. +/// +/// Allows us to differentiate between an issue with the connector (as a +/// [ButtplugConnectorError]) and an issue within Buttplug (as a +/// [ButtplugError]). +type ButtplugClientResult = Result; +type ButtplugClientResultFuture = BoxFuture<'static, ButtplugClientResult>; + +/// Result type used for passing server responses. +pub type ButtplugServerMessageResult = ButtplugClientResult; +pub type ButtplugServerMessageResultFuture = ButtplugClientResultFuture; +/// Future state type for returning server responses across futures. +pub(crate) type ButtplugServerMessageStateShared = + ButtplugFutureStateShared; +/// Future type that expects server responses. +pub(crate) type ButtplugServerMessageFuture = ButtplugFuture; + +/// Future state for messages sent from the client that expect a server response. +/// +/// When a message is sent from the client and expects a response from the server, we'd like to know +/// when that response arrives, and usually we'll want to wait for it. We can do so by creating a +/// future that will be resolved when a response is received from the server. +/// +/// To do this, we build a [ButtplugFuture], then take its waker and pass it along with the message +/// we send to the connector, using the [ButtplugClientMessageFuturePair] type. We can then expect +/// the connector to get the response from the server, match it with our message (using something +/// like the ClientMessageSorter, an internal structure in the Buttplug library), and set the reply +/// in the waker we've sent along. This will resolve the future we're waiting on and allow us to +/// continue execution. +#[derive(Clone)] +pub struct ButtplugClientMessageFuturePair { + msg: ButtplugClientMessageV4, + waker: ButtplugServerMessageStateShared, +} + +impl ButtplugClientMessageFuturePair { + pub fn new(msg: ButtplugClientMessageV4, waker: ButtplugServerMessageStateShared) -> Self { + Self { msg, waker } + } +} + +/// Represents all of the different types of errors a ButtplugClient can return. +/// +/// Clients can return two types of errors: +/// +/// - [ButtplugConnectorError], which means there was a problem with the connection between the +/// client and the server, like a network connection issue. +/// - [ButtplugError], which is an error specific to the Buttplug Protocol. +#[derive(Debug, Error, Display)] +pub enum ButtplugClientError { + /// Connector error + #[error(transparent)] + ButtplugConnectorError(#[from] ButtplugConnectorError), + /// Protocol error + #[error(transparent)] + ButtplugError(#[from] ButtplugError), + /// Error converting output command: {} + ButtplugOutputCommandConversionError(String) +} + +/// Enum representing different events that can be emitted by a client. +/// +/// These events are created by the server and sent to the client, and represent +/// unrequested actions that the client will need to respond to, or that +/// applications using the client may be interested in. +#[derive(Clone, Debug)] +pub enum ButtplugClientEvent { + /// Emitted when a scanning session (started via a StartScanning call on + /// [ButtplugClient]) has finished. + ScanningFinished, + /// Emitted when a device has been added to the server. Includes a + /// [ButtplugClientDevice] object representing the device. + DeviceAdded(Arc), + /// Emitted when a device has been removed from the server. Includes a + /// [ButtplugClientDevice] object representing the device. + DeviceRemoved(Arc), + /// Emitted when a client has not pinged the server in a sufficient amount of + /// time. + PingTimeout, + /// Emitted when the client successfully connects to a server. + ServerConnect, + /// Emitted when a client connector detects that the server has disconnected. + ServerDisconnect, + /// Emitted when an error that cannot be matched to a request is received from + /// the server. + Error(ButtplugError), +} + +impl Unpin for ButtplugClientEvent { +} + +pub(crate) fn create_boxed_future_client_error( + err: ButtplugError, +) -> ButtplugClientResultFuture +where + T: 'static + Send + Sync, +{ + future::ready(Err(ButtplugClientError::ButtplugError(err))).boxed() +} + +pub(crate) struct ButtplugClientMessageSender { + message_sender: broadcast::Sender, + connected: Arc, +} + +impl ButtplugClientMessageSender { + fn new( + message_sender: &broadcast::Sender, + connected: &Arc, + ) -> Self { + Self { + message_sender: message_sender.clone(), + connected: connected.clone(), + } + } + + /// Send message to the internal event loop. + /// + /// Mostly for handling boilerplate around possible send errors. + pub fn send_message_to_event_loop( + &self, + msg: ButtplugClientRequest, + ) -> BoxFuture<'static, Result<(), ButtplugClientError>> { + // If we're running the event loop, we should have a message_sender. + // Being connected to the server doesn't matter here yet because we use + // this function in order to connect also. + // + // The message sender doesn't require an async send now, but we still want + // to delay execution as part of our future in order to keep task coherency. + let message_sender = self.message_sender.clone(); + async move { + message_sender + .send(msg) + .map_err(|_| ButtplugConnectorError::ConnectorChannelClosed)?; + Ok(()) + } + .boxed() + } + + pub fn subscribe(&self) -> broadcast::Receiver { + self.message_sender.subscribe() + } + + pub fn send_message(&self, msg: ButtplugClientMessageV4) -> ButtplugServerMessageResultFuture { + if !self.connected.load(Ordering::Relaxed) { + future::ready(Err(ButtplugConnectorError::ConnectorNotConnected.into())).boxed() + } else { + self.send_message_ignore_connect_status(msg) + } + } + + /// Sends a ButtplugMessage from client to server. Expects to receive a ButtplugMessage back from + /// the server. + pub fn send_message_ignore_connect_status( + &self, + msg: ButtplugClientMessageV4, + ) -> ButtplugServerMessageResultFuture { + // Create a future to pair with the message being resolved. + let fut = ButtplugServerMessageFuture::default(); + let internal_msg = ButtplugClientRequest::Message(ButtplugClientMessageFuturePair::new( + msg, + fut.get_state_clone(), + )); + + // Send message to internal loop and wait for return. + let send_fut = self.send_message_to_event_loop(internal_msg); + async move { + send_fut.await?; + fut.await + } + .boxed() + } + + /// Sends a ButtplugMessage from client to server. Expects to receive an [Ok] + /// type ButtplugMessage back from the server. + pub fn send_message_expect_ok(&self, msg: ButtplugClientMessageV4) -> ButtplugClientResultFuture { + let send_fut = self.send_message(msg); + async move { send_fut.await.map(|_| ()) }.boxed() + } +} + +/// Struct used by applications to communicate with a Buttplug Server. +/// +/// Buttplug Clients provide an API layer on top of the Buttplug Protocol that +/// handles boring things like message creation and pairing, protocol ordering, +/// etc... This allows developers to concentrate on controlling hardware with +/// the API. +/// +/// Clients serve a few different purposes: +/// - Managing connections to servers, thru [ButtplugConnector]s +/// - Emitting events received from the Server +/// - Holding state related to the server (i.e. what devices are currently +/// connected, etc...) +/// +/// Clients are created by the [ButtplugClient::new()] method, which also +/// handles spinning up the event loop and connecting the client to the server. +/// Closures passed to the run() method can access and use the Client object. +pub struct ButtplugClient { + /// The client name. Depending on the connection type and server being used, + /// this name is sometimes shown on the server logs or GUI. + client_name: String, + /// The server name that we're current connected to. + server_name: Arc>>, + event_stream: broadcast::Sender, + // Sender to relay messages to the internal client loop + message_sender: Arc, + connected: Arc, + device_map: Arc>>, +} + +impl ButtplugClient { + pub fn new(name: &str) -> Self { + let (message_sender, _) = broadcast::channel(256); + let (event_stream, _) = broadcast::channel(256); + let connected = Arc::new(AtomicBool::new(false)); + Self { + client_name: name.to_owned(), + server_name: Arc::new(Mutex::new(None)), + event_stream, + message_sender: Arc::new(ButtplugClientMessageSender::new( + &message_sender, + &connected, + )), + connected, + device_map: Arc::new(DashMap::new()), + } + } + + pub async fn connect( + &self, + mut connector: ConnectorType, + ) -> Result<(), ButtplugClientError> + where + ConnectorType: ButtplugConnector + 'static, + { + if self.connected() { + return Err(ButtplugClientError::ButtplugConnectorError( + ButtplugConnectorError::ConnectorAlreadyConnected, + )); + } + + // If connect is being called again, clear out the device map and start over. + self.device_map.clear(); + + info!("Connecting to server."); + let (connector_sender, connector_receiver) = mpsc::channel(256); + connector.connect(connector_sender).await.map_err(|e| { + error!("Connection to server failed: {:?}", e); + ButtplugClientError::from(e) + })?; + info!("Connection to server succeeded."); + let mut client_event_loop = ButtplugClientEventLoop::new( + self.connected.clone(), + connector, + connector_receiver, + self.event_stream.clone(), + self.message_sender.clone(), + self.device_map.clone(), + ); + + // Start the event loop before we run the handshake. + async_manager::spawn( + async move { + client_event_loop.run().await; + } + .instrument(tracing::info_span!("Client Loop Span")), + ); + self.run_handshake().await + } + + /// Creates the ButtplugClient instance and tries to establish a connection. + /// + /// Takes all of the components needed to build a [ButtplugClient], creates + /// the struct, then tries to run connect and execute the Buttplug protocol + /// handshake. Will return a connected and ready to use ButtplugClient is all + /// goes well. + async fn run_handshake(&self) -> ButtplugClientResult { + // Run our handshake + info!("Running handshake with server."); + let msg = self + .message_sender + .send_message_ignore_connect_status( + RequestServerInfoV4::new( + &self.client_name, + BUTTPLUG_CURRENT_API_MAJOR_VERSION, + BUTTPLUG_CURRENT_API_MINOR_VERSION, + ) + .into(), + ) + .await?; + + debug!("Got ServerInfo return."); + if let ButtplugServerMessageV4::ServerInfo(server_info) = msg { + info!("Connected to {}", server_info.server_name()); + *self.server_name.lock().await = Some(server_info.server_name().clone()); + // Don't set ourselves as connected until after ServerInfo has been + // received. This means we avoid possible races with the RequestServerInfo + // handshake. + self.connected.store(true, Ordering::Relaxed); + + // Get currently connected devices. The event loop will + // handle sending the message and getting the return, and + // will send the client updates as events. + let msg = self + .message_sender + .send_message(RequestDeviceListV0::default().into()) + .await?; + if let ButtplugServerMessageV4::DeviceList(m) = msg { + self + .message_sender + .send_message_to_event_loop(ButtplugClientRequest::HandleDeviceList(m)) + .await?; + } + Ok(()) + } else { + self.disconnect().await?; + Err(ButtplugClientError::ButtplugError( + ButtplugHandshakeError::UnexpectedHandshakeMessageReceived(format!("{msg:?}")).into(), + )) + } + } + + /// Returns true if client is currently connected. + pub fn connected(&self) -> bool { + self.connected.load(Ordering::Relaxed) + } + + /// Disconnects from server, if connected. + /// + /// Returns Err(ButtplugClientError) if disconnection fails. It can be assumed + /// that even on failure, the client will be disconnected. + pub fn disconnect(&self) -> ButtplugClientResultFuture { + if !self.connected() { + return future::ready(Err(ButtplugConnectorError::ConnectorNotConnected.into())).boxed(); + } + // Send the connector to the internal loop for management. Once we throw + // the connector over, the internal loop will handle connecting and any + // further communications with the server, if connection is successful. + let fut = ButtplugConnectorFuture::default(); + let msg = ButtplugClientRequest::Disconnect(fut.get_state_clone()); + let send_fut = self.message_sender.send_message_to_event_loop(msg); + let connected = self.connected.clone(); + async move { + connected.store(false, Ordering::Relaxed); + send_fut.await?; + Ok(()) + } + .boxed() + } + + /// Tells server to start scanning for devices. + /// + /// Returns Err([ButtplugClientError]) if request fails due to issues with + /// DeviceManagers on the server, disconnection, etc. + pub fn start_scanning(&self) -> ButtplugClientResultFuture { + self + .message_sender + .send_message_expect_ok(StartScanningV0::default().into()) + } + + /// Tells server to stop scanning for devices. + /// + /// Returns Err([ButtplugClientError]) if request fails due to issues with + /// DeviceManagers on the server, disconnection, etc. + pub fn stop_scanning(&self) -> ButtplugClientResultFuture { + self + .message_sender + .send_message_expect_ok(StopScanningV0::default().into()) + } + + /// Tells server to stop all devices. + /// + /// Returns Err([ButtplugClientError]) if request fails due to issues with + /// DeviceManagers on the server, disconnection, etc. + pub fn stop_all_devices(&self) -> ButtplugClientResultFuture { + self + .message_sender + .send_message_expect_ok(StopAllDevicesV0::default().into()) + } + + pub fn event_stream(&self) -> impl Stream + use<>{ + let stream = convert_broadcast_receiver_to_stream(self.event_stream.subscribe()); + // We can either Box::pin here or force the user to pin_mut!() on their + // end. While this does end up with a dynamic dispatch on our end, it + // still makes the API nicer for the user, so we'll just eat the perf hit. + // Not to mention, this is not a high throughput system really, so it + // shouldn't matter. + Box::pin(stream) + } + + /// Retreives a list of currently connected devices. + pub fn devices(&self) -> Vec> { + self + .device_map + .iter() + .map(|map_pair| map_pair.value().clone()) + .collect() + } + + pub fn ping(&self) -> ButtplugClientResultFuture { + let ping_fut = self + .message_sender + .send_message_expect_ok(PingV0::default().into()); + ping_fut.boxed() + } + + pub fn server_name(&self) -> Option { + // We'd have to be calling server_name in an extremely tight, asynchronous + // loop for this to return None, so we'll treat this as lockless. + // + // Dear users actually reading this code: This is not an invitation for you + // to get the server name in a tight, asynchronous loop. This will never + // change throughout the life to the connection. + if let Ok(name) = self.server_name.try_lock() { + name.clone() + } else { + None + } + } +} diff --git a/crates/buttplug_client/src/serializer/mod.rs b/crates/buttplug_client/src/serializer/mod.rs new file mode 100644 index 000000000..1d988dac5 --- /dev/null +++ b/crates/buttplug_client/src/serializer/mod.rs @@ -0,0 +1,119 @@ +use buttplug_core::message::{ + serializer::{ + json_serializer::{create_message_validator, deserialize_to_message, vec_to_protocol_json}, + ButtplugMessageSerializer, + ButtplugSerializedMessage, + ButtplugSerializerError, + }, + ButtplugClientMessageV4, + ButtplugMessage, + ButtplugMessageFinalizer, + ButtplugServerMessageV4, +}; +use jsonschema::Validator; +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; + +pub struct ButtplugClientJSONSerializerImpl { + validator: Validator, +} + +impl Default for ButtplugClientJSONSerializerImpl { + fn default() -> Self { + Self { + validator: create_message_validator(), + } + } +} + +impl ButtplugClientJSONSerializerImpl { + pub fn deserialize( + &self, + msg: &ButtplugSerializedMessage, + ) -> Result, ButtplugSerializerError> + where + T: serde::de::DeserializeOwned + ButtplugMessageFinalizer + Clone + Debug, + { + if let ButtplugSerializedMessage::Text(text_msg) = msg { + deserialize_to_message::(Some(&self.validator), text_msg) + } else { + Err(ButtplugSerializerError::BinaryDeserializationError) + } + } + + pub fn serialize(&self, msg: &[T]) -> ButtplugSerializedMessage + where + T: ButtplugMessage + Serialize + Deserialize<'static>, + { + ButtplugSerializedMessage::Text(vec_to_protocol_json(msg)) + } +} + +#[derive(Default)] +pub struct ButtplugClientJSONSerializer { + serializer_impl: ButtplugClientJSONSerializerImpl, +} + +impl ButtplugMessageSerializer for ButtplugClientJSONSerializer { + type Inbound = ButtplugServerMessageV4; + type Outbound = ButtplugClientMessageV4; + + fn deserialize( + &self, + msg: &ButtplugSerializedMessage, + ) -> Result, ButtplugSerializerError> { + self.serializer_impl.deserialize(msg) + } + + fn serialize(&self, msg: &[Self::Outbound]) -> ButtplugSerializedMessage { + self.serializer_impl.serialize(msg) + } +} + +#[cfg(test)] +mod test { + use super::*; + use buttplug_core::message::{ + RequestServerInfoV4, + BUTTPLUG_CURRENT_API_MAJOR_VERSION, + BUTTPLUG_CURRENT_API_MINOR_VERSION, + }; + + #[test] + fn test_client_incorrect_messages() { + let incorrect_incoming_messages = vec![ + // Not valid JSON + "not a json message", + // Valid json object but no contents + "{}", + // Valid json but not an object + "[]", + // Not a message type + "[{\"NotAMessage\":{}}]", + // Valid json and message type but not in correct format + "[{\"Ok\":[]}]", + // Valid json and message type but not in correct format + "[{\"Ok\":{}}]", + // Valid json and message type but not an array. + "{\"Ok\":{\"Id\":0}}", + // Valid json and message type but not an array. + "[{\"Ok\":{\"Id\":0}}]", + // Valid json and message type but with extra content + "[{\"Ok\":{\"NotAField\":\"NotAValue\",\"Id\":1}}]", + ]; + let serializer = ButtplugClientJSONSerializer::default(); + let _ = serializer.serialize(&vec![RequestServerInfoV4::new( + "test client", + BUTTPLUG_CURRENT_API_MAJOR_VERSION, + BUTTPLUG_CURRENT_API_MINOR_VERSION, + ) + .into()]); + for msg in incorrect_incoming_messages { + let res = serializer.deserialize(&ButtplugSerializedMessage::Text(msg.to_owned())); + assert!(res.is_err(), "{} should be an error", msg); + if let Err(ButtplugSerializerError::MessageSpecVersionNotReceived) = res { + assert!(false, "Wrong error!"); + } + } + } +} diff --git a/crates/buttplug_client_in_process/Cargo.toml b/crates/buttplug_client_in_process/Cargo.toml new file mode 100644 index 000000000..9edd96c08 --- /dev/null +++ b/crates/buttplug_client_in_process/Cargo.toml @@ -0,0 +1,55 @@ +[package] +name = "buttplug_client_in_process" +version = "10.0.0" +authors = ["Nonpolynomial Labs, LLC "] +description = "Buttplug Intimate Hardware Control Library - Core Library" +license = "BSD-3-Clause" +homepage = "http://buttplug.io" +repository = "https://github.com/buttplugio/buttplug.git" +readme = "./README.md" +keywords = ["usb", "serial", "hardware", "bluetooth", "teledildonics"] +edition = "2024" +exclude = ["examples/**"] + +[lib] +name = "buttplug_client_in_process" +path = "src/lib.rs" +test = true +doctest = true +doc = true + + +[features] +default = ["btleplug-manager", "hid-manager", "lovense-dongle-manager", "lovense-connect-service-manager", "serial-manager", "websocket-manager", "xinput-manager", "udp-manager"] +btleplug-manager=["buttplug_server_hwmgr_btleplug"] +hid-manager=["buttplug_server_hwmgr_hid"] +lovense-dongle-manager=["buttplug_server_hwmgr_lovense_dongle"] +lovense-connect-service-manager=["buttplug_server_hwmgr_lovense_connect"] +serial-manager=["buttplug_server_hwmgr_serial"] +websocket-manager=["buttplug_server_hwmgr_websocket"] +xinput-manager=["buttplug_server_hwmgr_xinput"] +udp-manager=["buttplug_server_hwmgr_udp"] + +[dependencies] +buttplug_derive = "0.8.1" +buttplug_core = { path = "../buttplug_core" } +buttplug_client = { path = "../buttplug_client" } +buttplug_server = { path = "../buttplug_server" } +buttplug_server_device_config = { path = "../buttplug_server_device_config" } +buttplug_server_hwmgr_btleplug = { path = "../buttplug_server_hwmgr_btleplug", optional = true} +buttplug_server_hwmgr_hid = { path = "../buttplug_server_hwmgr_hid", optional = true} +buttplug_server_hwmgr_lovense_connect = { path = "../buttplug_server_hwmgr_lovense_connect", optional = true} +buttplug_server_hwmgr_lovense_dongle = { path = "../buttplug_server_hwmgr_lovense_dongle", optional = true} +buttplug_server_hwmgr_serial = { path = "../buttplug_server_hwmgr_serial", optional = true} +buttplug_server_hwmgr_websocket = { path = "../buttplug_server_hwmgr_websocket", optional = true} +buttplug_server_hwmgr_xinput = { path = "../buttplug_server_hwmgr_xinput", optional = true} +buttplug_server_hwmgr_udp = { path = "../buttplug_server_hwmgr_udp", optional = true} +futures = "0.3.31" +futures-util = "0.3.31" +thiserror = "2.0.12" +log = "0.4.27" +getset = "0.1.6" +tokio = { version = "1.46.1", features = ["macros"] } +dashmap = { version = "6.1.0" } +tracing-futures = "0.2.5" +tracing = "0.1.41" diff --git a/buttplug/src/util/mod.rs b/crates/buttplug_client_in_process/src/in_process_client.rs similarity index 64% rename from buttplug/src/util/mod.rs rename to crates/buttplug_client_in_process/src/in_process_client.rs index f78c2ee2a..3754bb52d 100644 --- a/buttplug/src/util/mod.rs +++ b/crates/buttplug_client_in_process/src/in_process_client.rs @@ -5,29 +5,10 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -//! Utility module, for storing types and functions used across other modules in -//! the library. - -pub mod async_manager; -#[cfg(feature = "server")] -pub mod device_configuration; -pub mod future; -pub mod json; -pub mod logging; -pub mod stream; - -#[cfg(not(feature = "wasm"))] -pub use tokio::time::sleep; -#[cfg(feature = "wasm")] -pub use wasmtimer::tokio::sleep; - -#[cfg(all(feature = "server", feature = "client"))] -use crate::{ - client::ButtplugClient, - core::connector::ButtplugInProcessClientConnectorBuilder, - server::device::{configuration::DeviceConfigurationManagerBuilder, ServerDeviceManagerBuilder}, - server::ButtplugServerBuilder, -}; +use super::ButtplugInProcessClientConnectorBuilder; +use buttplug_client::ButtplugClient; +use buttplug_server::{device::ServerDeviceManagerBuilder, ButtplugServerBuilder}; +use buttplug_server_device_config::DeviceConfigurationManagerBuilder; /// Convenience function for creating in-process connectors. /// @@ -63,31 +44,20 @@ use crate::{ /// If the library is using outside device managers, it is recommended to /// build your own connector, add your device manager to those, and use the /// `run()` method to pass it in. -#[cfg(all(feature = "server", feature = "client"))] -pub async fn in_process_client(client_name: &str, allow_raw_messages: bool) -> ButtplugClient { +pub async fn in_process_client(client_name: &str) -> ButtplugClient { let dcm = DeviceConfigurationManagerBuilder::default() - .allow_raw_messages(allow_raw_messages) .finish() .unwrap(); let mut device_manager_builder = ServerDeviceManagerBuilder::new(dcm); - #[cfg(all( - feature = "btleplug-manager", - any( - target_os = "windows", - target_os = "macos", - target_os = "linux", - target_os = "ios", - target_os = "android" - ) - ))] + #[cfg(feature = "btleplug-manager")] { - use crate::server::device::hardware::communication::btleplug::BtlePlugCommunicationManagerBuilder; + use buttplug_server_hwmgr_btleplug::BtlePlugCommunicationManagerBuilder; device_manager_builder.comm_manager(BtlePlugCommunicationManagerBuilder::default()); } - #[cfg(feature = "websocket-server-manager")] + #[cfg(feature = "websocket-manager")] { - use crate::server::device::hardware::communication::websocket_server::websocket_server_comm_manager::WebsocketServerDeviceCommunicationManagerBuilder; + use buttplug_server_hwmgr_websocket::WebsocketServerDeviceCommunicationManagerBuilder; device_manager_builder.comm_manager( WebsocketServerDeviceCommunicationManagerBuilder::default().listen_on_all_interfaces(true), ); @@ -97,12 +67,12 @@ pub async fn in_process_client(client_name: &str, allow_raw_messages: bool) -> B any(target_os = "windows", target_os = "macos", target_os = "linux") ))] { - use crate::server::device::hardware::communication::serialport::SerialPortCommunicationManagerBuilder; + use buttplug_server_hwmgr_serial::SerialPortCommunicationManagerBuilder; device_manager_builder.comm_manager(SerialPortCommunicationManagerBuilder::default()); } #[cfg(feature = "lovense-connect-service-manager")] { - use crate::server::device::hardware::communication::lovense_connect_service::LovenseConnectServiceCommunicationManagerBuilder; + use buttplug_server_hwmgr_lovense_connect::LovenseConnectServiceCommunicationManagerBuilder; device_manager_builder .comm_manager(LovenseConnectServiceCommunicationManagerBuilder::default()); } @@ -111,18 +81,19 @@ pub async fn in_process_client(client_name: &str, allow_raw_messages: bool) -> B any(target_os = "windows", target_os = "macos", target_os = "linux") ))] { - use crate::server::device::hardware::communication::lovense_dongle::{ - LovenseHIDDongleCommunicationManagerBuilder, - LovenseSerialDongleCommunicationManagerBuilder, - }; + use buttplug_server_hwmgr_lovense_dongle::LovenseHIDDongleCommunicationManagerBuilder; device_manager_builder.comm_manager(LovenseHIDDongleCommunicationManagerBuilder::default()); - device_manager_builder.comm_manager(LovenseSerialDongleCommunicationManagerBuilder::default()); } #[cfg(all(feature = "xinput-manager", target_os = "windows"))] { - use crate::server::device::hardware::communication::xinput::XInputDeviceCommunicationManagerBuilder; + use buttplug_server_hwmgr_xinput::XInputDeviceCommunicationManagerBuilder; device_manager_builder.comm_manager(XInputDeviceCommunicationManagerBuilder::default()); } + #[cfg(all(feature = "udp-manager", target_os = "windows"))] + { + use buttplug_server_hwmgr_udp::UdpCommunicationManagerBuilder; + device_manager_builder.comm_manager(UdpCommunicationManagerBuilder::default()); + } let server_builder = ButtplugServerBuilder::new(device_manager_builder.finish().unwrap()); let server = server_builder.finish().unwrap(); let connector = ButtplugInProcessClientConnectorBuilder::default() diff --git a/crates/buttplug_client_in_process/src/in_process_connector.rs b/crates/buttplug_client_in_process/src/in_process_connector.rs new file mode 100644 index 000000000..5eeb20997 --- /dev/null +++ b/crates/buttplug_client_in_process/src/in_process_connector.rs @@ -0,0 +1,192 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 Nonpolynomial Labs LLC. All rights reserved. +// +// Licensed under the BSD 3-Clause license. See LICENSE file in the project root +// for full license information. + +//! In-process communication between clients and servers + +use buttplug_core::{ + connector::{ButtplugConnector, ButtplugConnectorError, ButtplugConnectorResultFuture}, + errors::{ButtplugError, ButtplugMessageError}, + message::{ButtplugClientMessageV4, ButtplugServerMessageV4}, + util::async_manager, +}; +use buttplug_server::{ + message::ButtplugServerMessageVariant, + ButtplugServer, + ButtplugServerBuilder, +}; + +use futures::{ + future::{self, BoxFuture, FutureExt}, + StreamExt, +}; +use futures_util::pin_mut; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; +use tokio::sync::mpsc::{channel, Sender}; +use tracing_futures::Instrument; + +#[derive(Default)] +pub struct ButtplugInProcessClientConnectorBuilder { + server: Option, +} + +impl ButtplugInProcessClientConnectorBuilder { + pub fn server(&mut self, server: ButtplugServer) -> &mut Self { + self.server = Some(server); + self + } + + pub fn finish(&mut self) -> ButtplugInProcessClientConnector { + ButtplugInProcessClientConnector::new(self.server.take()) + } +} + +/// In-process Buttplug Server Connector +/// +/// The In-Process Connector contains a [ButtplugServer], meaning that both the +/// [ButtplugClient][crate::client::ButtplugClient] and [ButtplugServer] will exist in the same +/// process. This is useful for developing applications, or for distributing an applications without +/// requiring access to an outside [ButtplugServer]. +/// +/// # Notes +/// +/// Buttplug is built in a way that tries to make sure all programs will work with new versions of +/// the library. This is why we have [ButtplugClient][crate::client::ButtplugClient] for +/// applications, and Connectors to access out-of-process [ButtplugServer]s over IPC, network, etc. +/// It means that the out-of-process server can be upgraded by the user at any time, even if the +/// [ButtplugClient][crate::client::ButtplugClient] using application hasn't been upgraded. This +/// allows the program to support hardware that may not have even been released when it was +/// published. +/// +/// While including an EmbeddedConnector in your application is the quickest and easiest way to +/// develop (and we highly recommend developing that way), and also an easy way to get users up and +/// running as quickly as possible, we recommend also including some sort of IPC Connector in order +/// for your application to connect to newer servers when they come out. +#[derive(Clone)] +pub struct ButtplugInProcessClientConnector { + /// Internal server object for the embedded connector. + server: Arc, + server_outbound_sender: Sender, + connected: Arc, +} + +impl Default for ButtplugInProcessClientConnector { + fn default() -> Self { + ButtplugInProcessClientConnectorBuilder::default().finish() + } +} + +impl ButtplugInProcessClientConnector { + /// Creates a new in-process connector, with a server instance. + /// + /// Sets up a server, using the basic [ButtplugServer] construction arguments. + /// Takes the server's name and the ping time it should use, with a ping time + /// of 0 meaning infinite ping. + fn new(server: Option) -> Self { + // Create a dummy channel, will just be overwritten on connect. + let (server_outbound_sender, _) = channel(256); + Self { + server_outbound_sender, + server: Arc::new(server.unwrap_or_else(|| { + ButtplugServerBuilder::default() + .finish() + .expect("Default server builder should always work.") + })), + connected: Arc::new(AtomicBool::new(false)), + } + } +} + +impl ButtplugConnector + for ButtplugInProcessClientConnector +{ + fn connect( + &mut self, + message_sender: Sender, + ) -> BoxFuture<'static, Result<(), ButtplugConnectorError>> { + if !self.connected.load(Ordering::Relaxed) { + let connected = self.connected.clone(); + let send = message_sender.clone(); + self.server_outbound_sender = message_sender; + let server_recv = self.server.server_version_event_stream(); + async move { + async_manager::spawn(async move { + info!("Starting In Process Client Connector Event Sender Loop"); + pin_mut!(server_recv); + while let Some(event) = server_recv.next().await { + // If we get an error back, it means the client dropped our event + // handler, so just stop trying. Otherwise, since this is an + // in-process conversion, we can unwrap because we know our + // try_into() will always succeed (which may not be the case with + // remote connections that have different spec versions). + if send.send(event).await.is_err() { + break; + } + } + info!("Stopping In Process Client Connector Event Sender Loop, due to channel receiver being dropped."); + }.instrument(tracing::info_span!("InProcessClientConnectorEventSenderLoop"))); + connected.store(true, Ordering::Relaxed); + Ok(()) + }.boxed() + } else { + ButtplugConnectorError::ConnectorAlreadyConnected.into() + } + } + + fn disconnect(&self) -> ButtplugConnectorResultFuture { + if self.connected.load(Ordering::Relaxed) { + self.connected.store(false, Ordering::Relaxed); + future::ready(Ok(())).boxed() + } else { + ButtplugConnectorError::ConnectorNotConnected.into() + } + } + + fn send(&self, msg: ButtplugClientMessageV4) -> ButtplugConnectorResultFuture { + if !self.connected.load(Ordering::Relaxed) { + return ButtplugConnectorError::ConnectorNotConnected.into(); + } + let input = msg.into(); + let output_fut = self.server.parse_message(input); + let sender = self.server_outbound_sender.clone(); + async move { + let output = match output_fut.await { + Ok(m) => { + if let ButtplugServerMessageVariant::V4(msg) = m { + msg + } else { + ButtplugServerMessageV4::Error( + ButtplugError::from(ButtplugMessageError::MessageConversionError( + "In-process connector messages should never have differing versions.".to_owned(), + )) + .into(), + ) + } + } + Err(e) => { + if let ButtplugServerMessageVariant::V4(msg) = e { + msg + } else { + ButtplugServerMessageV4::Error( + ButtplugError::from(ButtplugMessageError::MessageConversionError( + "In-process connector messages should never have differing versions.".to_owned(), + )) + .into(), + ) + } + } + }; + sender + .send(output) + .await + .map_err(|_| ButtplugConnectorError::ConnectorNotConnected) + } + .boxed() + } +} diff --git a/crates/buttplug_client_in_process/src/lib.rs b/crates/buttplug_client_in_process/src/lib.rs new file mode 100644 index 000000000..64c0d6bd0 --- /dev/null +++ b/crates/buttplug_client_in_process/src/lib.rs @@ -0,0 +1,11 @@ +#[macro_use] +extern crate log; + +mod in_process_client; +mod in_process_connector; + +pub use in_process_client::in_process_client; +pub use in_process_connector::{ + ButtplugInProcessClientConnector, + ButtplugInProcessClientConnectorBuilder, +}; diff --git a/crates/buttplug_core/Cargo.toml b/crates/buttplug_core/Cargo.toml new file mode 100644 index 000000000..a5ed22646 --- /dev/null +++ b/crates/buttplug_core/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "buttplug_core" +version = "10.0.0" +authors = ["Nonpolynomial Labs, LLC "] +description = "Buttplug Intimate Hardware Control Library - Core Library" +license = "BSD-3-Clause" +homepage = "http://buttplug.io" +repository = "https://github.com/buttplugio/buttplug.git" +readme = "./README.md" +keywords = ["usb", "serial", "hardware", "bluetooth", "teledildonics"] +edition = "2024" +exclude = ["examples/**"] + +[lib] +name = "buttplug_core" +path = "src/lib.rs" +test = true +doctest = true +doc = true + +[features] +default=["tokio-runtime"] +tokio-runtime=["tokio/rt"] +wasm=[] + +# Only build docs on one platform (linux) +[package.metadata.docs.rs] +targets = [] +# Features to pass to Cargo (default: []) +features = ["default", "unstable"] + +[build-dependencies] +serde = "1.0.219" +serde_json = "1.0.140" +jsonschema = { version = "0.30.0", default-features = false } + +[dependencies] +buttplug_derive = "0.8.1" +# buttplug_derive = { path = "../buttplug_derive" } +futures = "0.3.31" +futures-util = "0.3.31" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" +serde_repr = "0.1.20" +thiserror = "2.0.12" +displaydoc = "0.2.5" +log = "0.4.27" +getset = "0.1.6" +jsonschema = { version = "0.30.0", default-features = false } +cfg-if = "1.0.1" +tokio = { version = "1.46.1", features = ["sync", "time", "macros"] } +async-stream = "0.3.6" +strum_macros = "0.27.1" +strum = "0.27.1" diff --git a/crates/buttplug_core/build.rs b/crates/buttplug_core/build.rs new file mode 100644 index 000000000..cbaef01cd --- /dev/null +++ b/crates/buttplug_core/build.rs @@ -0,0 +1,9 @@ +const SCHEMA_DIR: &str = "./schema/"; +use jsonschema::Validator; + +fn main() { + println!("cargo:rerun-if-changed={}", SCHEMA_DIR); + let schema: serde_json::Value = + serde_json::from_str(&std::fs::read_to_string(std::path::Path::new(SCHEMA_DIR).join("buttplug-schema.json")).unwrap()).expect("Built in schema better be valid json"); + let _ = Validator::new(&schema).expect("Built in schema better be a valid schema"); +} \ No newline at end of file diff --git a/buttplug/buttplug-schema/schema/buttplug-schema.json b/crates/buttplug_core/schema/buttplug-schema.json similarity index 82% rename from buttplug/buttplug-schema/schema/buttplug-schema.json rename to crates/buttplug_core/schema/buttplug-schema.json index be0d0136f..1d866337f 100644 --- a/buttplug/buttplug-schema/schema/buttplug-schema.json +++ b/crates/buttplug_core/schema/buttplug-schema.json @@ -291,9 +291,322 @@ "description": "Specifies granularity of each feature on the device.", "minimum": 1, "type": "integer" + }, + "DeviceFeatureV4": { + "description": "Specifies feature for a device.", + "type": "object", + "properties": { + "FeatureDescription": { + "type": "string" + }, + "FeatureIndex": { + "type": "integer" + }, + "FeatureType": { + "type": "string" + }, + "Output": { + "type": "object", + "patternProperties": { + "^.*$": { + "type": "object", + "properties": { + "StepCount": { + "type": "integer" + } + }, + "required": [ + "StepCount" + ] + } + } + }, + "Input": { + "type": "object", + "patternProperties": { + "^.*$": { + "type": "object", + "properties": { + "ValueRange": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "required": [ + "ValueRange" + ] + } + } + }, + "Raw": { + "type": "object", + "properties": { + "Endpoints": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "Endpoints" + ] + } + }, + "required": [ + "FeatureDescription", + "FeatureIndex", + "FeatureType" + ] } }, "messages": { + "SpecV4Messages": { + "OutputCmd": { + "type": "object", + "description": "Sends a generic value set command to a device.", + "properties": { + "Id": { "$ref": "#/components/ClientId" }, + "DeviceIndex": { "$ref": "#/components/DeviceIndex" }, + "FeatureIndex": { "$ref": "#/components/DeviceIndex" }, + "Command": { + "type": "object", + "patternProperties": { + "^(Vibrate|Rotate|Oscillate|Constrict|Spray|Position|Heater|Led)$": { + "type": "object", + "properties": { + "Value": { + "type": "number", + "minimum": 0 + } + }, + "required": [ + "Value" + ] + }, + "^RotateWithDirection$": { + "type": "object", + "properties": { + "Speed": { + "type": "number", + "minimum": 0 + }, + "Clockwise": { + "type": "boolean" + } + }, + "required": [ + "Speed", + "Clockwise" + ] + }, + "^PositionWithDuration$": { + "type": "object", + "properties": { + "Position": { + "type": "number", + "minimum": 0 + }, + "Duration": { + "type": "number", + "minimum": 0 + } + }, + "required": [ + "Position", + "Duration" + ] + } + } + } + }, + "additionalProperties": false, + "required": [ + "Id", + "DeviceIndex", + "FeatureIndex", + "Command" + ] + }, + "InputCmd": { + "type": "object", + "description": "Sends a request to read a sensor value.", + "properties": { + "Id": { "$ref": "#/components/ClientId" }, + "DeviceIndex": { "$ref": "#/components/DeviceIndex" }, + "FeatureIndex": { "type": "integer" }, + "InputType": { "type": "string" }, + "InputCommand": { "type": "string" } + }, + "additionalProperties": false, + "required": [ + "Id", + "DeviceIndex", + "FeatureIndex", + "InputType", + "InputCommand" + ] + }, + "InputReading": { + "type": "object", + "description": "Returns from either a sensor read request or a subscribed sensor event.", + "properties": { + "Id": { "$ref": "#/components/ServerId" }, + "DeviceIndex": { "$ref": "#/components/DeviceIndex" }, + "FeatureIndex": { "type": "integer" }, + "Data": { + "type": "object", + "patternProperties": { + "^Battery|Rssi|Pressure|Button": { + "type": "integer" + } + }, + "maxProperties": 1 + } + }, + "additionalProperties": false, + "required": [ + "Id", + "DeviceIndex", + "FeatureIndex", + "Data" + ] + }, + "RawCmd": { + "type": "object", + "description": "Sends a request to raw endpoints.", + "properties": { + "Id": { "$ref": "#/components/ClientId" }, + "DeviceIndex": { "$ref": "#/components/DeviceIndex" }, + "Endpoint": { "type": "string" }, + "RawCommand": { + "type": "object", + "properties": { + "Read": { + "type": "object" + }, + "Write": { + "type": "object" + }, + "Subscribe": { + "type": "object" + }, + "Unsubscribe": { + "type": "object" + } + } + } + }, + "additionalProperties": false, + "required": [ + "Id", + "DeviceIndex", + "Endpoint", + "RawCommand" + ] + }, + "DeviceList": { + "type": "object", + "description": "List of all available devices known to the system.", + "properties": { + "Id": { "$ref": "#/components/ServerId" }, + "Devices": { + "description": "Array of device ids and names.", + "type": "object", + "patternProperties": { + "^[0-9]*": { + "type": "object", + "properties": { + "DeviceName": { "$ref": "#/components/DeviceName" }, + "DeviceIndex": { "$ref": "#/components/DeviceIndex" }, + "DeviceDisplayName": { "type": "string" }, + "DeviceMessageTimingGap": { "type": "integer" }, + "DeviceFeatures": { + "type": "object", + "patternProperties": { + "^[0-9]*": { + "$ref": "#/components/DeviceFeatureV4" + } + } + } + }, + "additionalProperties": false, + "required": [ + "DeviceName", + "DeviceIndex", + "DeviceMessageTimingGap", + "DeviceFeatures" + ] + } + } + } + }, + "additionalProperties": false, + "required": [ + "Id", + "Devices" + ] + }, + "RequestServerInfo": { + "type": "object", + "description": "Request server version, and relay client name.", + "properties": { + "Id": { "$ref": "#/components/ClientId" }, + "ClientName": { + "description": "Name of the client software.", + "type": "string" + }, + "ProtocolVersionMajor": { + "description": "Message template version of the server software.", + "type": "integer", + "minimum": 0 + }, + "ProtocolVersionMinor": { + "description": "Message template version of the server software.", + "type": "integer", + "minimum": 0 + } + }, + "additionalProperties": false, + "required": [ + "Id", + "ClientName", + "ProtocolVersionMajor", + "ProtocolVersionMinor" + ] + }, + "ServerInfo": { + "type": "object", + "description": "Server version information, with API version in Major.Minor format.", + "properties": { + "Id": { "$ref": "#/components/ClientId" }, + "ServerName": { + "description": "Name of the server. Can be 0-length.", + "type": "string" + }, + "ProtocolVersionMajor": { + "description": "Message template version of the server software.", + "type": "integer", + "minimum": 0 + }, + "ProtocolVersionMinor": { + "description": "Message template version of the server software.", + "type": "integer", + "minimum": 0 + }, + "MaxPingTime": { + "description": "Maximum time (in milliseconds) the server will wait between ping messages from client before shutting down.", + "type": "integer", + "minimum": 0 + } + } + } + }, "SpecV3Messages": { "DeviceList": { "type": "object", @@ -465,7 +778,7 @@ "SensorIndex", "SensorType" ] - } + } }, "SpecV2Messages": { "DeviceList": { @@ -1333,6 +1646,36 @@ } }, "specs": { + "MessageSpecV4": { + "type": "array", + "items": { + "type": "object", + "description": "All messages valid in Buttplug Spec v4", + "properties": { + "DeviceList": { "$ref": "#/messages/SpecV4Messages/DeviceList" }, + "Error": { "$ref": "#/messages/SpecV0Messages/Error" }, + "Ok": { "$ref": "#/messages/SpecV0Messages/Ok" }, + "Ping": { "$ref": "#/messages/SpecV0Messages/Ping" }, + "RawCmd": { "$ref": "#/messages/SpecV4Messages/RawCmd" }, + "RawReading": { "$ref": "#/messages/SpecV2Messages/RawReading" }, + "RequestDeviceList": { "$ref": "#/messages/SpecV0Messages/RequestDeviceList" }, + "RequestServerInfo": { "$ref": "#/messages/SpecV4Messages/RequestServerInfo" }, + "ScanningFinished": { "$ref": "#/messages/SpecV0Messages/ScanningFinished" }, + "InputCmd": { "$ref": "#/messages/SpecV4Messages/InputCmd" }, + "InputReading": { "$ref": "#/messages/SpecV4Messages/InputReading" }, + "ServerInfo": { "$ref": "#/messages/SpecV4Messages/ServerInfo" }, + "StartScanning": { "$ref": "#/messages/SpecV0Messages/StartScanning" }, + "StopAllDevices": { "$ref": "#/messages/SpecV0Messages/StopAllDevices" }, + "StopDeviceCmd": { "$ref": "#/messages/SpecV0Messages/StopDeviceCmd" }, + "StopScanning": { "$ref": "#/messages/SpecV0Messages/StopScanning" }, + "OutputCmd": { "$ref": "#/messages/SpecV4Messages/OutputCmd" } + }, + "additionalProperties": false, + "minProperties": 1, + "maxProperties": 1 + }, + "minItems": 1 + }, "MessageSpecV3": { "type": "array", "items": { @@ -1482,6 +1825,7 @@ } }, "anyOf": [ + { "$ref": "#/specs/MessageSpecV4" }, { "$ref": "#/specs/MessageSpecV3" }, { "$ref": "#/specs/MessageSpecV2" }, { "$ref": "#/specs/MessageSpecV1" }, diff --git a/buttplug/src/core/connector/mod.rs b/crates/buttplug_core/src/connector/mod.rs similarity index 80% rename from buttplug/src/core/connector/mod.rs rename to crates/buttplug_core/src/connector/mod.rs index c5389d59d..13c52f25b 100644 --- a/buttplug/src/core/connector/mod.rs +++ b/crates/buttplug_core/src/connector/mod.rs @@ -16,7 +16,7 @@ //! //! A Buttplug Client uses a connector to communicate with a server, be it in the same process or on //! another machine. The client's connector handles establishing the connection to the server, as -//! well as sending ([possibly serialized][crate::core::messages::serializer]) messages to the +//! well as sending ([possibly serialized][crate::messages::serializer]) messages to the //! server and matching replies from the server to waiting futures. //! //! Buttplug servers use connectors to receive info from clients. They usually have less to do than @@ -63,34 +63,18 @@ //! There are slightly more useful situations like device forwarders where this work comes in also, //! but that Windows 7/Android example is where the idea originally came from. -#[cfg(all(feature = "server", feature = "client", not(feature = "wasm")))] -mod in_process_connector; pub mod remote_connector; pub mod transport; use crate::{ - core::message::{serializer::ButtplugSerializedMessage, ButtplugMessage}, + message::{serializer::ButtplugSerializedMessage, ButtplugMessage}, util::future::{ButtplugFuture, ButtplugFutureStateShared}, }; use displaydoc::Display; use futures::future::{self, BoxFuture, FutureExt}; -#[cfg(all(feature = "server", feature = "client", not(feature = "wasm")))] -pub use in_process_connector::{ - ButtplugInProcessClientConnector, - ButtplugInProcessClientConnectorBuilder, -}; -pub use remote_connector::{ - ButtplugRemoteClientConnector, - ButtplugRemoteConnector, - ButtplugRemoteServerConnector, -}; +pub use remote_connector::ButtplugRemoteConnector; use thiserror::Error; use tokio::sync::mpsc::Sender; -#[cfg(feature = "websockets")] -pub use transport::ButtplugWebsocketClientTransport; - -#[cfg(feature = "websockets")] -pub use transport::{ButtplugWebsocketServerTransport, ButtplugWebsocketServerTransportBuilder}; pub type ButtplugConnectorResult = Result<(), ButtplugConnectorError>; pub type ButtplugConnectorStateShared = @@ -134,11 +118,11 @@ where /// /// The `O` type specifies the outbound message type. This will usually be a /// message enum. For instance, in a client connector, this would usually be -/// [ButtplugClientMessage][crate::core::messages::ButtplugClientMessage]. +/// [ButtplugClientMessage][crate::messages::ButtplugClientMessage]. /// /// The `I` type specifies the inbound message type. This will usually be a /// message enum. For instance, in a client connector, this would usually be -/// [ButtplugServerMessage][crate::core::messages::ButtplugServerMessage]. +/// [ButtplugServerMessage][crate::messages::ButtplugServerMessage]. pub trait ButtplugConnector: Send + Sync where OutboundMessageType: ButtplugMessage + 'static, @@ -182,23 +166,3 @@ where /// the send operation, this will return a [ButtplugConnectorError] fn send(&self, msg: OutboundMessageType) -> ButtplugConnectorResultFuture; } - -#[cfg(all(feature = "websockets", feature = "serialize-json"))] -use crate::core::message::{ButtplugClientMessageCurrent, ButtplugServerMessageCurrent}; - -/// Convenience method for creating a new Buttplug Client Websocket connector that uses the JSON -/// serializer. This is pretty much the only connector used for IPC right now, so this makes it easy -/// to create one without having to fill in the generic types. -#[cfg(all(feature = "websockets", feature = "serialize-json"))] -pub fn new_json_ws_client_connector( - address: &str, -) -> impl ButtplugConnector { - use crate::core::message::serializer::ButtplugClientJSONSerializer; - - ButtplugRemoteClientConnector::< - ButtplugWebsocketClientTransport, - ButtplugClientJSONSerializer, - >::new(ButtplugWebsocketClientTransport::new_insecure_connector( - address, - )) -} diff --git a/buttplug/src/core/connector/remote_connector.rs b/crates/buttplug_core/src/connector/remote_connector.rs similarity index 92% rename from buttplug/src/core/connector/remote_connector.rs rename to crates/buttplug_core/src/connector/remote_connector.rs index 3f032306c..32c42ac9d 100644 --- a/buttplug/src/core/connector/remote_connector.rs +++ b/crates/buttplug_core/src/connector/remote_connector.rs @@ -14,21 +14,14 @@ use super::{ ButtplugConnectorResultFuture, }; use crate::{ - core::message::{ - serializer::{ - ButtplugClientJSONSerializer, - ButtplugMessageSerializer, - ButtplugSerializedMessage, - }, - ButtplugClientMessageCurrent, - ButtplugClientMessageVariant, + message::{ + serializer::{ButtplugMessageSerializer, ButtplugSerializedMessage}, ButtplugMessage, - ButtplugServerMessageCurrent, - ButtplugServerMessageVariant, }, util::async_manager, }; use futures::{future::BoxFuture, select, FutureExt}; +use log::*; use std::marker::PhantomData; use tokio::sync::mpsc::{channel, Receiver, Sender}; @@ -165,23 +158,6 @@ async fn remote_connector_event_loop< } } -pub type ButtplugRemoteClientConnector< - TransportType, - SerializerType = ButtplugClientJSONSerializer, -> = ButtplugRemoteConnector< - TransportType, - SerializerType, - ButtplugClientMessageCurrent, - ButtplugServerMessageCurrent, ->; - -pub type ButtplugRemoteServerConnector = ButtplugRemoteConnector< - TransportType, - SerializerType, - ButtplugServerMessageVariant, - ButtplugClientMessageVariant, ->; - pub struct ButtplugRemoteConnector< TransportType, SerializerType, @@ -222,7 +198,7 @@ where Self { transport: Some(transport), event_loop_sender: None, - dummy_serializer: PhantomData::default(), + dummy_serializer: PhantomData, } } } @@ -263,7 +239,7 @@ where // If we connect successfully, we get back the channel from the transport // to send outgoing messages and receieve incoming events, all serialized. Ok(()) => { - async_manager::spawn(async move { + let _ = async_manager::spawn(async move { remote_connector_event_loop::< TransportType, SerializerType, diff --git a/buttplug/src/core/connector/transport/mod.rs b/crates/buttplug_core/src/connector/transport/mod.rs similarity index 72% rename from buttplug/src/core/connector/transport/mod.rs rename to crates/buttplug_core/src/connector/transport/mod.rs index 94d714c74..a63ca6f83 100644 --- a/buttplug/src/core/connector/transport/mod.rs +++ b/crates/buttplug_core/src/connector/transport/mod.rs @@ -5,28 +5,17 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -//! Transports for remote (IPC/network/etc) communication between clients and servers +pub mod stream; -mod stream; -pub use stream::ButtplugStreamTransport; - -#[cfg(feature = "websockets")] -mod websocket; -use crate::core::connector::{ +use crate::connector::{ ButtplugConnectorError, ButtplugConnectorResultFuture, ButtplugSerializedMessage, }; +use displaydoc::Display; use futures::future::BoxFuture; use thiserror::Error; use tokio::sync::mpsc::{Receiver, Sender}; -#[cfg(feature = "websockets")] -pub use websocket::{ - ButtplugWebsocketClientTransport, - ButtplugWebsocketServerTransport, - ButtplugWebsocketServerTransportBuilder, - TungsteniteError, -}; /// Messages we can receive from a connector. #[derive(Clone, Debug, Display)] @@ -53,9 +42,6 @@ pub trait ButtplugConnectorTransport: Send + Sync { #[derive(Error, Debug)] pub enum ButtplugConnectorTransportSpecificError { - #[cfg(feature = "websockets")] - #[error("Tungstenite specific error: {0}")] - TungsteniteError(#[from] TungsteniteError), #[error("Network error: {0}")] GenericNetworkError(String), } diff --git a/buttplug/src/core/connector/transport/stream/mod.rs b/crates/buttplug_core/src/connector/transport/stream.rs similarity index 90% rename from buttplug/src/core/connector/transport/stream/mod.rs rename to crates/buttplug_core/src/connector/transport/stream.rs index fb841d1a9..b59339b5b 100644 --- a/buttplug/src/core/connector/transport/stream/mod.rs +++ b/crates/buttplug_core/src/connector/transport/stream.rs @@ -9,14 +9,12 @@ //! process space. use crate::{ - core::{ - connector::{ - transport::{ButtplugConnectorTransport, ButtplugTransportIncomingMessage}, - ButtplugConnectorError, - ButtplugConnectorResultFuture, - }, - message::serializer::ButtplugSerializedMessage, + connector::{ + transport::{ButtplugConnectorTransport, ButtplugTransportIncomingMessage}, + ButtplugConnectorError, + ButtplugConnectorResultFuture, }, + message::serializer::ButtplugSerializedMessage, util::async_manager, }; use futures::{ diff --git a/buttplug/src/core/errors.rs b/crates/buttplug_core/src/errors.rs similarity index 85% rename from buttplug/src/core/errors.rs rename to crates/buttplug_core/src/errors.rs index 215949fd0..8caf3933d 100644 --- a/buttplug/src/core/errors.rs +++ b/crates/buttplug_core/src/errors.rs @@ -10,17 +10,12 @@ use super::message::{ self, serializer::ButtplugSerializerError, - ActuatorType, - ButtplugDeviceMessageType, ButtplugMessageSpecVersion, - Endpoint, ErrorCode, FeatureType, - SensorType, + InputType, + OutputType, }; -#[cfg(feature = "server")] -use crate::server::device::hardware::communication::HardwareSpecificError; -use displaydoc::Display; use futures::future::BoxFuture; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -40,8 +35,7 @@ where } } -#[derive(Debug, Error, Display, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] +#[derive(Debug, Error, Display, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum ButtplugHandshakeError { /// Expected either a ServerInfo or Error message, received {0} UnexpectedHandshakeMessageReceived(String), @@ -49,10 +43,14 @@ pub enum ButtplugHandshakeError { RequestServerInfoExpected, /// Handshake already happened, cannot run handshake again. HandshakeAlreadyHappened, + /// Server has already connected and disconnected, cannot be reused + ReconnectDenied, /// Server spec version ({0}) must be equal or greater than client version ({1}) MessageSpecVersionMismatch(ButtplugMessageSpecVersion, ButtplugMessageSpecVersion), /// Untyped Deserialized Error: {0} UntypedDeserializedError(String), + /// Unhandled spec version requested, may require extra arguments to activate: {0} + UnhandledMessageSpecVersionRequested(ButtplugMessageSpecVersion), } /// Message errors occur when a message is somehow malformed on creation, or @@ -66,8 +64,7 @@ where } } -#[derive(Debug, Error, Display, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] +#[derive(Debug, Error, Display, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum ButtplugMessageError { /// Got unexpected message type: {0} UnexpectedMessageType(String), @@ -100,8 +97,7 @@ where } } -#[derive(Debug, Error, Display, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] +#[derive(Debug, Error, Display, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum ButtplugPingError { /// Pinged timer exhausted, system has shut down. PingedOut, @@ -124,32 +120,34 @@ where ButtplugError::from(err).into() } } -#[derive(Debug, Error, Display, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] +#[derive(Debug, Error, Display, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum ButtplugDeviceError { /// Device {0} not connected DeviceNotConnected(String), /// Device does not support message type {0}. - MessageNotSupported(ButtplugDeviceMessageType), + MessageNotSupported(String), /// Device only has {0} features, but {1} commands were sent. DeviceFeatureCountMismatch(u32, u32), /// Device only has {0} features, but was given an index of {1} DeviceFeatureIndexError(u32, u32), + /// Device feature mismatch: {0} + DeviceFeatureMismatch(String), /// Device only has {0} sensors, but was given an index of {1} DeviceSensorIndexError(u32, u32), /// Device connection error: {0} DeviceConnectionError(String), /// Device communication error: {0} DeviceCommunicationError(String), + /// Device feature only has {0} steps for control, but {1} steps specified. + DeviceStepRangeError(u32, u32), + /// Device got {0} message but has no actuators + DeviceNoActuatorError(String), + /// Device got {0} message but has no sensors + DeviceNoSensorError(String), /// Device does not have endpoint {0} - InvalidEndpoint(Endpoint), + InvalidEndpoint(String), /// Device does not handle command type: {0} UnhandledCommand(String), - #[cfg(feature = "server")] - #[error(transparent)] - /// Device type specific error: {0}. - DeviceSpecificError(#[from] HardwareSpecificError), - #[cfg(not(feature = "server"))] /// Device type specific error: {0}. DeviceSpecificError(String), /// No device available at index {0} @@ -175,11 +173,13 @@ pub enum ButtplugDeviceError { /// Device Configuration Error: {0} DeviceConfigurationError(String), /// Actuator Type Mismatch: Index {0} got command for {1}, but expects {2} - DeviceActuatorTypeMismatch(String, ActuatorType, FeatureType), + DeviceActuatorTypeMismatch(u32, OutputType, FeatureType), /// Sensor Type Mismatch: Index {0} got command for {1}, but expects {2} - DeviceSensorTypeMismatch(u32, SensorType, FeatureType), + DeviceSensorTypeMismatch(u32, InputType, FeatureType), /// Protocol does not have an implementation available for Sensor Type {0} - ProtocolSensorNotSupported(SensorType), + ProtocolSensorNotSupported(InputType), + /// Device does not support {0} + ActuatorNotSupported(OutputType), } /// Unknown errors occur in exceptional circumstances where no other error type @@ -193,8 +193,7 @@ where } } -#[derive(Debug, Error, Display, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] +#[derive(Debug, Error, Display, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum ButtplugUnknownError { /// Cannot start scanning, no device communication managers available to use for scanning. NoDeviceCommManagers, @@ -207,8 +206,7 @@ pub enum ButtplugUnknownError { } /// Aggregation enum for protocol error types. -#[derive(Debug, Error, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] +#[derive(Debug, Error, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum ButtplugError { #[error(transparent)] ButtplugHandshakeError(#[from] ButtplugHandshakeError), diff --git a/buttplug/src/core/mod.rs b/crates/buttplug_core/src/lib.rs similarity index 89% rename from buttplug/src/core/mod.rs rename to crates/buttplug_core/src/lib.rs index 6143b3dac..5b7a18193 100644 --- a/buttplug/src/core/mod.rs +++ b/crates/buttplug_core/src/lib.rs @@ -10,6 +10,12 @@ pub mod connector; pub mod errors; pub mod message; +pub mod util; + +#[macro_use] +extern crate buttplug_derive; +#[macro_use] +extern crate strum_macros; use errors::ButtplugError; use futures::future::{self, BoxFuture, FutureExt}; diff --git a/crates/buttplug_core/src/message/device_feature.rs b/crates/buttplug_core/src/message/device_feature.rs new file mode 100644 index 000000000..374b0b5e4 --- /dev/null +++ b/crates/buttplug_core/src/message/device_feature.rs @@ -0,0 +1,256 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 crate::message::InputCommandType; +use getset::{CopyGetters, Getters, MutGetters, Setters}; +use serde::{ser::SerializeSeq, Deserialize, Serialize, Serializer}; +use std::{ + collections::{HashMap, HashSet}, + ops::RangeInclusive, +}; + +#[derive(Debug, Default, Display, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, EnumIter)] +pub enum FeatureType { + #[default] + // Used for when types are added that we do not know how to handle + Unknown, + // Level/ValueCmd types + Vibrate, + // Single Direction Rotation Speed + Rotate, + Oscillate, + Constrict, + Spray, + Heater, + Led, + // For instances where we specify a position to move to ASAP. Usually servos, probably for the + // OSR-2/SR-6. + Position, + // ValueWithParameterCmd types + // Two Direction Rotation Speed + RotateWithDirection, + PositionWithDuration, + // Might be useful but dunno if we need it yet, or how to convey "speed" units + // PositionWithSpeed + // Sensor Types + Battery, + Rssi, + Button, + Pressure, + // Currently unused but possible sensor features: + // Temperature, + // Accelerometer, + // Gyro, + // +} + +#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, EnumIter)] +pub enum OutputType { + Unknown, + Vibrate, + // Single Direction Rotation Speed + Rotate, + // Two Direction Rotation Speed + RotateWithDirection, + Oscillate, + Constrict, + Heater, + Led, + // For instances where we specify a position to move to ASAP. Usually servos, probably for the + // OSR-2/SR-6. + Position, + PositionWithDuration, + // Lube shooters + Spray, + // Things we might add in the future + // Inflate, +} + +impl TryFrom for OutputType { + type Error = String; + fn try_from(value: FeatureType) -> Result { + match value { + FeatureType::Unknown => Ok(OutputType::Unknown), + FeatureType::Vibrate => Ok(OutputType::Vibrate), + FeatureType::Rotate => Ok(OutputType::Rotate), + FeatureType::Heater => Ok(OutputType::Heater), + FeatureType::Led => Ok(OutputType::Led), + FeatureType::RotateWithDirection => Ok(OutputType::RotateWithDirection), + FeatureType::PositionWithDuration => Ok(OutputType::PositionWithDuration), + FeatureType::Oscillate => Ok(OutputType::Oscillate), + FeatureType::Constrict => Ok(OutputType::Constrict), + FeatureType::Spray => Ok(OutputType::Spray), + FeatureType::Position => Ok(OutputType::Position), + _ => Err(format!( + "Feature type {value} not valid for OutputType conversion" + )), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, Hash, EnumIter)] +pub enum InputType { + Unknown, + Battery, + Rssi, + Button, + Pressure, + // Temperature, + // Accelerometer, + // Gyro, +} + +impl TryFrom for InputType { + type Error = String; + fn try_from(value: FeatureType) -> Result { + match value { + FeatureType::Unknown => Ok(InputType::Unknown), + FeatureType::Battery => Ok(InputType::Battery), + FeatureType::Rssi => Ok(InputType::Rssi), + FeatureType::Button => Ok(InputType::Button), + FeatureType::Pressure => Ok(InputType::Pressure), + _ => Err(format!( + "Feature type {value} not valid for SensorType conversion" + )), + } + } +} + +impl From for FeatureType { + fn from(value: OutputType) -> Self { + match value { + OutputType::Unknown => FeatureType::Unknown, + OutputType::Vibrate => FeatureType::Vibrate, + OutputType::Rotate => FeatureType::Rotate, + OutputType::Heater => FeatureType::Heater, + OutputType::Led => FeatureType::Led, + OutputType::RotateWithDirection => FeatureType::RotateWithDirection, + OutputType::PositionWithDuration => FeatureType::PositionWithDuration, + OutputType::Oscillate => FeatureType::Oscillate, + OutputType::Constrict => FeatureType::Constrict, + OutputType::Spray => FeatureType::Spray, + OutputType::Position => FeatureType::Position, + } + } +} + +impl From for FeatureType { + fn from(value: InputType) -> Self { + match value { + InputType::Unknown => FeatureType::Unknown, + InputType::Battery => FeatureType::Battery, + InputType::Rssi => FeatureType::Rssi, + InputType::Button => FeatureType::Button, + InputType::Pressure => FeatureType::Pressure, + } + } +} + +// 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 +// what will be sent to the client as part of DeviceAdded/DeviceList messages. It should not be used +// for outside configuration/serialization, rather it should be a subset of that information. +// +// 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. +#[derive( + Clone, Debug, Default, PartialEq, Getters, MutGetters, CopyGetters, Setters, Serialize, Deserialize, +)] +pub struct DeviceFeature { + // Index of the feature on the device. This was originally implicit as the position in the feature + // array. We now make it explicit even though it's still just array position, because implicit + // array positions have made life hell in so many different ways. + #[getset(get_copy = "pub")] + #[serde(rename = "FeatureIndex")] + feature_index: u32, + #[getset(get = "pub", get_mut = "pub(super)")] + #[serde(default)] + #[serde(rename = "FeatureDescription")] + description: String, + #[getset(get_copy = "pub")] + #[serde(rename = "FeatureType")] + feature_type: FeatureType, + #[getset(get = "pub")] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "Output")] + output: Option>, + #[getset(get = "pub")] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "Input")] + input: Option>, +} + +impl DeviceFeature { + pub fn new( + index: u32, + description: &str, + feature_type: FeatureType, + output: &Option>, + input: &Option>, + ) -> Self { + Self { + feature_index: index, + description: description.to_owned(), + feature_type, + output: output.clone(), + 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()])?; + } + seq.end() +} + +#[derive(Clone, Debug, PartialEq, Eq, CopyGetters, Serialize, Deserialize)] +pub struct DeviceFeatureOutput { + #[getset(get_copy = "pub")] + #[serde(rename = "StepCount")] + step_count: u32, +} + +impl DeviceFeatureOutput { + pub fn new(step_count: u32) -> Self { + Self { step_count } + } +} + +#[derive( + Clone, Debug, Default, PartialEq, Eq, Getters, MutGetters, Setters, Serialize, Deserialize, +)] +pub struct DeviceFeatureInput { + #[getset(get = "pub", get_mut = "pub(super)")] + #[serde(rename = "ValueRange")] + #[serde(serialize_with = "range_sequence_serialize")] + value_range: Vec>, + #[getset(get = "pub")] + #[serde(rename = "InputCommands")] + input_commands: HashSet, +} + +impl DeviceFeatureInput { + pub fn new( + value_range: &Vec>, + sensor_commands: &HashSet, + ) -> Self { + Self { + value_range: value_range.clone(), + input_commands: sensor_commands.clone(), + } + } +} diff --git a/crates/buttplug_core/src/message/mod.rs b/crates/buttplug_core/src/message/mod.rs new file mode 100644 index 000000000..2f3f51e18 --- /dev/null +++ b/crates/buttplug_core/src/message/mod.rs @@ -0,0 +1,148 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 Nonpolynomial Labs LLC. All rights reserved. +// +// Licensed under the BSD 3-Clause license. See LICENSE file in the project root +// for full license information. + +//! Representations of low level [Buttplug Protocol](https://buttplug-spec.docs.buttplug.io) +//! messages +//! +//! The core communication types for the Buttplug protocol. There are structs for each message type, +//! with only the current message spec being included here (older message specs are only used for +//! backward compatibilty and are in the server::message module). There are also enum types that are +//! used to classify messages into categories, for instance, messages that only should be sent by a +//! client or server. + +pub mod v0; +pub mod v4; + +mod device_feature; +pub mod serializer; + +pub use device_feature::*; +pub use v0::*; +pub use v4::*; + +use crate::errors::ButtplugMessageError; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use std::convert::TryFrom; + +use super::errors::ButtplugError; + +/// Enum of possible [Buttplug Message +/// Spec](https://buttplug-spec.docs.buttplug.io) versions. +#[derive( + Debug, Clone, Copy, Display, PartialEq, Eq, PartialOrd, Ord, Serialize_repr, Deserialize_repr, +)] +#[repr(u32)] +pub enum ButtplugMessageSpecVersion { + Version0 = 0, + Version1 = 1, + Version2 = 2, + Version3 = 3, + Version4 = 4, +} + +impl TryFrom for ButtplugMessageSpecVersion { + type Error = ButtplugError; + + // There's probably another crate to make this easier but eh. + fn try_from(value: i32) -> Result { + match value { + 0 => Ok(ButtplugMessageSpecVersion::Version0), + 1 => Ok(ButtplugMessageSpecVersion::Version1), + 2 => Ok(ButtplugMessageSpecVersion::Version2), + 3 => Ok(ButtplugMessageSpecVersion::Version3), + 4 => Ok(ButtplugMessageSpecVersion::Version4), + _ => Err( + ButtplugMessageError::InvalidMessageContents(format!( + "Message spec version {value} is not valid" + )) + .into(), + ), + } + } +} + +/// Message Id for events sent from the server, which are not in response to a +/// client request. +pub const BUTTPLUG_SERVER_EVENT_ID: u32 = 0; + +/// The current latest version of the spec implemented by the library. +pub const BUTTPLUG_CURRENT_API_MAJOR_VERSION: ButtplugMessageSpecVersion = + ButtplugMessageSpecVersion::Version4; + +pub const BUTTPLUG_CURRENT_API_MINOR_VERSION: u32 = 0; + +pub trait ButtplugMessageFinalizer { + fn finalize(&mut self) { + } +} + +/// Base trait for all Buttplug Protocol Message Structs. Handles management of +/// message ids, as well as implementing conveinence functions for converting +/// between message structs and various message enums, serialization, etc... +pub trait ButtplugMessage: + ButtplugMessageValidator + ButtplugMessageFinalizer + Send + Sync + Clone +{ + /// Returns the id number of the message + fn id(&self) -> u32; + /// Sets the id number of the message. + fn set_id(&mut self, id: u32); + /// True if the message is an event (message id of 0) from the server. + fn is_server_event(&self) -> bool { + self.id() == BUTTPLUG_SERVER_EVENT_ID + } +} + +/// Validation function for message contents. Can be run before message is +/// transmitted, as message may be formed and mutated at multiple points in the +/// library, or may need to be checked after deserialization. Message enums will +/// run this on whatever their variant is. +pub trait ButtplugMessageValidator { + /// Returns () if the message is valid, otherwise returns a message error. + fn is_valid(&self) -> Result<(), ButtplugMessageError> { + // By default, return Ok, as many messages won't have any checks. + Ok(()) + } + + fn is_system_id(&self, id: u32) -> Result<(), ButtplugMessageError> { + if id == 0 { + Ok(()) + } else { + Err(ButtplugMessageError::InvalidMessageContents( + "Message should have id of 0, as it is a system message.".to_string(), + )) + } + } + + fn is_not_system_id(&self, id: u32) -> Result<(), ButtplugMessageError> { + if id == 0 { + Err(ButtplugMessageError::InvalidMessageContents( + "Message should not have 0 for an Id. Id of 0 is reserved for system messages.".to_string(), + )) + } else { + Ok(()) + } + } + + fn is_in_command_range(&self, value: f64, error_msg: String) -> Result<(), ButtplugMessageError> { + if !(0.0..=1.0).contains(&value) { + Err(ButtplugMessageError::InvalidMessageContents(error_msg)) + } else { + Ok(()) + } + } +} + +/// Adds device index handling to the [ButtplugMessage] trait. +pub trait ButtplugDeviceMessage: ButtplugMessage { + fn device_index(&self) -> u32; + fn set_device_index(&mut self, id: u32); +} + +/// Type alias for the latest version of client-to-server messages. +pub type ButtplugClientMessageCurrent = ButtplugClientMessageV4; +/// Type alias for the latest version of server-to-client messages. +pub type ButtplugServerMessageCurrent = ButtplugServerMessageV4; diff --git a/crates/buttplug_core/src/message/serializer/json_serializer.rs b/crates/buttplug_core/src/message/serializer/json_serializer.rs new file mode 100644 index 000000000..0f5ff45ca --- /dev/null +++ b/crates/buttplug_core/src/message/serializer/json_serializer.rs @@ -0,0 +1,89 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 super::ButtplugSerializerError; +use crate::message::{ButtplugMessage, ButtplugMessageFinalizer}; +use jsonschema::Validator; +use serde::{Deserialize, Serialize}; +use serde_json::{Deserializer, Value}; +use std::fmt::Debug; + +static MESSAGE_JSON_SCHEMA: &str = + include_str!("../../../schema/buttplug-schema.json"); + +/// Creates a [jsonschema::JSONSchema] validator using the built in buttplug message schema. +pub fn create_message_validator() -> Validator { + let schema: serde_json::Value = + serde_json::from_str(MESSAGE_JSON_SCHEMA).expect("Built in schema better be valid"); + Validator::new(&schema).expect("Built in schema better be valid") +} + +/// Returns the message as a string in Buttplug JSON Protocol format. +pub fn msg_to_protocol_json(msg: T) -> String +where + T: ButtplugMessage + Serialize + Deserialize<'static>, +{ + serde_json::to_string(&[&msg]).expect("Infallible serialization") +} + +pub fn vec_to_protocol_json(msg: &[T]) -> String +where + T: ButtplugMessage + Serialize + Deserialize<'static>, +{ + serde_json::to_string(msg).expect("Infallible serialization") +} + +pub fn deserialize_to_message( + validator: Option<&Validator>, + msg_str: &str, +) -> Result, ButtplugSerializerError> +where + T: serde::de::DeserializeOwned + ButtplugMessageFinalizer + Clone + Debug, +{ + // TODO This assumes that we've gotten a full JSON string to deserialize, which may not be the + // case. + let stream = Deserializer::from_str(msg_str).into_iter::(); + + let mut result = vec![]; + + for msg in stream { + match msg { + Ok(json_msg) => { + if let Some(validator) = validator { + if !validator.is_valid(&json_msg) { + // If is_valid fails, re-run validation to get our error message. + let e = validator + .validate(&json_msg) + .expect_err("We can't get here without validity checks failing."); + return Err(ButtplugSerializerError::JsonSerializerError(format!( + "Error during JSON Schema Validation - Message: {json_msg} - Error: {e:?}" + ))); + } + } + match serde_json::from_value::>(json_msg) { + Ok(mut msg_vec) => { + for msg in msg_vec.iter_mut() { + msg.finalize(); + } + result.append(&mut msg_vec); + } + Err(e) => { + return Err(ButtplugSerializerError::JsonSerializerError(format!( + "Message: {msg_str} - Error: {e:?}" + ))) + } + } + } + Err(e) => { + return Err(ButtplugSerializerError::JsonSerializerError(format!( + "Message: {msg_str} - Error: {e:?}" + ))) + } + } + } + Ok(result) +} diff --git a/buttplug/src/core/message/serializer/mod.rs b/crates/buttplug_core/src/message/serializer/mod.rs similarity index 85% rename from buttplug/src/core/message/serializer/mod.rs rename to crates/buttplug_core/src/message/serializer/mod.rs index 35a835a76..72279c4df 100644 --- a/buttplug/src/core/message/serializer/mod.rs +++ b/crates/buttplug_core/src/message/serializer/mod.rs @@ -6,16 +6,7 @@ // for full license information. //! Message de/serialization handling - -#[cfg(feature = "serialize-json")] -mod json_serializer; -#[cfg(feature = "serialize-json")] -pub use json_serializer::{ - vec_to_protocol_json, - ButtplugClientJSONSerializer, - ButtplugClientJSONSerializerImpl, - ButtplugServerJSONSerializer, -}; +pub mod json_serializer; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -38,7 +29,7 @@ pub enum ButtplugSerializerError { MessageSpecVersionNotReceived, } -#[derive(Debug, Display, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum ButtplugSerializedMessage { Text(String), Binary(Vec), diff --git a/buttplug/src/core/message/device_removed.rs b/crates/buttplug_core/src/message/v0/device_removed.rs similarity index 70% rename from buttplug/src/core/message/device_removed.rs rename to crates/buttplug_core/src/message/v0/device_removed.rs index ed4c0c4d2..abca31f14 100644 --- a/buttplug/src/core/message/device_removed.rs +++ b/crates/buttplug_core/src/message/v0/device_removed.rs @@ -7,17 +7,22 @@ //! Notification that a device has disconnected from the server. -use super::*; +use crate::message::{ + ButtplugMessage, + ButtplugMessageError, + ButtplugMessageFinalizer, + ButtplugMessageValidator, +}; use getset::CopyGetters; -#[cfg(feature = "serialize-json")] use serde::{Deserialize, Serialize}; -#[derive(Debug, Default, ButtplugMessage, Clone, PartialEq, Eq, CopyGetters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] +#[derive( + Debug, Default, ButtplugMessage, Clone, PartialEq, Eq, CopyGetters, Serialize, Deserialize, +)] pub struct DeviceRemovedV0 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] + #[serde(rename = "Id")] id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] + #[serde(rename = "DeviceIndex")] #[getset(get_copy = "pub")] device_index: u32, } diff --git a/buttplug/src/core/message/error.rs b/crates/buttplug_core/src/message/v0/error.rs similarity index 79% rename from buttplug/src/core/message/error.rs rename to crates/buttplug_core/src/message/v0/error.rs index d24b0e3be..f4151ff13 100644 --- a/buttplug/src/core/message/error.rs +++ b/crates/buttplug_core/src/message/v0/error.rs @@ -7,18 +7,17 @@ //! Notification of an error in the system, due to a failed external command or internal failure -use super::*; -use crate::core::errors::*; +use crate::{ + errors::*, + message::{ButtplugMessage, ButtplugMessageFinalizer, ButtplugMessageValidator}, +}; use getset::{CopyGetters, Getters}; -#[cfg(feature = "serialize-json")] use serde::{Deserialize, Serialize}; -#[cfg(feature = "serialize-json")] use serde_repr::{Deserialize_repr, Serialize_repr}; /// Error codes pertaining to error classes that can be represented in the /// Buttplug [Error] message. -#[derive(Debug, Clone, PartialEq, Eq, Copy)] -#[cfg_attr(feature = "serialize-json", derive(Serialize_repr, Deserialize_repr))] +#[derive(Debug, Clone, PartialEq, Eq, Copy, Serialize_repr, Deserialize_repr)] #[repr(u8)] pub enum ErrorCode { ErrorUnknown = 0, @@ -41,21 +40,22 @@ pub enum ErrorCode { ButtplugMessageFinalizer, Getters, CopyGetters, + Serialize, + Deserialize, )] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] pub struct ErrorV0 { /// Message Id, used for matching message pairs in remote connection instances. - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] + #[serde(rename = "Id")] id: u32, /// Specifies the class of the error. - #[cfg_attr(feature = "serialize-json", serde(rename = "ErrorCode"))] + #[serde(rename = "ErrorCode")] #[getset(get_copy = "pub")] error_code: ErrorCode, /// Description of the error. - #[cfg_attr(feature = "serialize-json", serde(rename = "ErrorMessage"))] + #[serde(rename = "ErrorMessage")] #[getset(get = "pub")] error_message: String, - #[cfg_attr(feature = "serialize-json", serde(skip))] + #[serde(skip)] original_error: Option, } @@ -90,11 +90,8 @@ impl ErrorV0 { .expect("Already checked that it's valid.") } else { // Try deserializing what's in the error_message field - #[cfg(feature = "serialize-json")] - { - if let Ok(deserialized_msg) = serde_json::from_str(&self.error_message) { - return deserialized_msg; - } + if let Ok(deserialized_msg) = serde_json::from_str(&self.error_message) { + return deserialized_msg; } ButtplugError::from(self.clone()) } @@ -112,18 +109,14 @@ impl From for ErrorV0 { ButtplugError::ButtplugHandshakeError { .. } => ErrorCode::ErrorHandshake, ButtplugError::ButtplugUnknownError { .. } => ErrorCode::ErrorUnknown, }; - #[cfg(feature = "serialize-json")] let msg = serde_json::to_string(&error).expect("All buttplug errors are serializable"); - #[cfg(not(feature = "serialize-json"))] - let msg = error.to_string(); ErrorV0::new(code, &msg, Some(error)) } } -#[cfg(feature = "serialize-json")] #[cfg(test)] mod test { - use crate::core::message::{ButtplugServerMessageCurrent, ErrorCode, ErrorV0}; + use crate::message::{ButtplugServerMessageCurrent, ErrorCode, ErrorV0}; const ERROR_STR: &str = "{\"Error\":{\"Id\":0,\"ErrorCode\":1,\"ErrorMessage\":\"Test Error\"}}"; diff --git a/crates/buttplug_core/src/message/v0/mod.rs b/crates/buttplug_core/src/message/v0/mod.rs new file mode 100644 index 000000000..a7e7b9df5 --- /dev/null +++ b/crates/buttplug_core/src/message/v0/mod.rs @@ -0,0 +1,21 @@ +mod device_removed; +mod error; +mod ok; +mod ping; +mod request_device_list; +mod scanning_finished; +mod start_scanning; +mod stop_all_devices; +mod stop_device_cmd; +mod stop_scanning; + +pub use device_removed::DeviceRemovedV0; +pub use error::{ErrorCode, ErrorV0}; +pub use ok::OkV0; +pub use ping::PingV0; +pub use request_device_list::RequestDeviceListV0; +pub use scanning_finished::ScanningFinishedV0; +pub use start_scanning::StartScanningV0; +pub use stop_all_devices::StopAllDevicesV0; +pub use stop_device_cmd::StopDeviceCmdV0; +pub use stop_scanning::StopScanningV0; diff --git a/buttplug/src/core/message/ok.rs b/crates/buttplug_core/src/message/v0/ok.rs similarity index 79% rename from buttplug/src/core/message/ok.rs rename to crates/buttplug_core/src/message/v0/ok.rs index 133564e58..d91b8f0c3 100644 --- a/buttplug/src/core/message/ok.rs +++ b/crates/buttplug_core/src/message/v0/ok.rs @@ -5,16 +5,21 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use super::*; -#[cfg(feature = "serialize-json")] +use crate::message::{ + ButtplugMessage, + ButtplugMessageError, + ButtplugMessageFinalizer, + ButtplugMessageValidator, +}; use serde::{Deserialize, Serialize}; /// Ok message, signifying successful response to a command. [Spec link](https://buttplug-spec.docs.buttplug.io/status.html#ok). -#[derive(Debug, PartialEq, Eq, ButtplugMessage, ButtplugMessageFinalizer, Clone)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] +#[derive( + Debug, PartialEq, Eq, ButtplugMessage, ButtplugMessageFinalizer, Clone, Serialize, Deserialize, +)] pub struct OkV0 { /// Message Id, used for matching message pairs in remote connection instances. - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] + #[serde(rename = "Id")] id: u32, } @@ -37,10 +42,9 @@ impl ButtplugMessageValidator for OkV0 { } } -#[cfg(feature = "serialize-json")] #[cfg(test)] mod test { - use crate::core::message::{ButtplugServerMessageCurrent, OkV0}; + use crate::message::{ButtplugServerMessageCurrent, OkV0}; const OK_STR: &str = "{\"Ok\":{\"Id\":0}}"; diff --git a/buttplug/src/core/message/ping.rs b/crates/buttplug_core/src/message/v0/ping.rs similarity index 72% rename from buttplug/src/core/message/ping.rs rename to crates/buttplug_core/src/message/v0/ping.rs index cd3eb55b0..b85d8a21a 100644 --- a/buttplug/src/core/message/ping.rs +++ b/crates/buttplug_core/src/message/v0/ping.rs @@ -5,14 +5,19 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use super::*; -#[cfg(feature = "serialize-json")] +use crate::message::{ + ButtplugMessage, + ButtplugMessageError, + ButtplugMessageFinalizer, + ButtplugMessageValidator, +}; use serde::{Deserialize, Serialize}; -#[derive(Debug, ButtplugMessage, ButtplugMessageFinalizer, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] +#[derive( + Debug, ButtplugMessage, ButtplugMessageFinalizer, Clone, PartialEq, Eq, Serialize, Deserialize, +)] pub struct PingV0 { /// Message Id, used for matching message pairs in remote connection instances. - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] + #[serde(rename = "Id")] id: u32, } diff --git a/buttplug/src/core/message/request_device_list.rs b/crates/buttplug_core/src/message/v0/request_device_list.rs similarity index 69% rename from buttplug/src/core/message/request_device_list.rs rename to crates/buttplug_core/src/message/v0/request_device_list.rs index 44366eb9a..23cc68923 100644 --- a/buttplug/src/core/message/request_device_list.rs +++ b/crates/buttplug_core/src/message/v0/request_device_list.rs @@ -5,14 +5,19 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use super::*; -#[cfg(feature = "serialize-json")] +use crate::message::{ + ButtplugMessage, + ButtplugMessageError, + ButtplugMessageFinalizer, + ButtplugMessageValidator, +}; use serde::{Deserialize, Serialize}; -#[derive(Debug, ButtplugMessage, ButtplugMessageFinalizer, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] +#[derive( + Debug, ButtplugMessage, ButtplugMessageFinalizer, Clone, PartialEq, Eq, Serialize, Deserialize, +)] pub struct RequestDeviceListV0 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] + #[serde(rename = "Id")] id: u32, } diff --git a/buttplug/src/core/message/scanning_finished.rs b/crates/buttplug_core/src/message/v0/scanning_finished.rs similarity index 63% rename from buttplug/src/core/message/scanning_finished.rs rename to crates/buttplug_core/src/message/v0/scanning_finished.rs index 588981fe9..df21cc1a7 100644 --- a/buttplug/src/core/message/scanning_finished.rs +++ b/crates/buttplug_core/src/message/v0/scanning_finished.rs @@ -5,14 +5,27 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use super::*; -#[cfg(feature = "serialize-json")] +use crate::message::{ + ButtplugMessage, + ButtplugMessageError, + ButtplugMessageFinalizer, + ButtplugMessageValidator, +}; use serde::{Deserialize, Serialize}; -#[derive(Debug, Default, ButtplugMessage, ButtplugMessageFinalizer, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] +#[derive( + Debug, + Default, + ButtplugMessage, + ButtplugMessageFinalizer, + Clone, + PartialEq, + Eq, + Serialize, + Deserialize, +)] pub struct ScanningFinishedV0 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] + #[serde(rename = "Id")] id: u32, } diff --git a/buttplug/src/core/message/start_scanning.rs b/crates/buttplug_core/src/message/v0/start_scanning.rs similarity index 68% rename from buttplug/src/core/message/start_scanning.rs rename to crates/buttplug_core/src/message/v0/start_scanning.rs index 727d23785..b10c76847 100644 --- a/buttplug/src/core/message/start_scanning.rs +++ b/crates/buttplug_core/src/message/v0/start_scanning.rs @@ -5,14 +5,19 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use super::*; -#[cfg(feature = "serialize-json")] +use crate::message::{ + ButtplugMessage, + ButtplugMessageError, + ButtplugMessageFinalizer, + ButtplugMessageValidator, +}; use serde::{Deserialize, Serialize}; -#[derive(Debug, ButtplugMessage, ButtplugMessageFinalizer, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] +#[derive( + Debug, ButtplugMessage, ButtplugMessageFinalizer, Clone, PartialEq, Eq, Serialize, Deserialize, +)] pub struct StartScanningV0 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] + #[serde(rename = "Id")] id: u32, } diff --git a/buttplug/src/core/message/stop_all_devices.rs b/crates/buttplug_core/src/message/v0/stop_all_devices.rs similarity index 68% rename from buttplug/src/core/message/stop_all_devices.rs rename to crates/buttplug_core/src/message/v0/stop_all_devices.rs index 138e2f1b4..bb401b950 100644 --- a/buttplug/src/core/message/stop_all_devices.rs +++ b/crates/buttplug_core/src/message/v0/stop_all_devices.rs @@ -5,14 +5,19 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use super::*; -#[cfg(feature = "serialize-json")] +use crate::message::{ + ButtplugMessage, + ButtplugMessageError, + ButtplugMessageFinalizer, + ButtplugMessageValidator, +}; use serde::{Deserialize, Serialize}; -#[derive(Debug, ButtplugMessage, ButtplugMessageFinalizer, PartialEq, Eq, Clone)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] +#[derive( + Debug, ButtplugMessage, ButtplugMessageFinalizer, PartialEq, Eq, Clone, Serialize, Deserialize, +)] pub struct StopAllDevicesV0 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] + #[serde(rename = "Id")] id: u32, } diff --git a/buttplug/src/core/message/stop_device_cmd.rs b/crates/buttplug_core/src/message/v0/stop_device_cmd.rs similarity index 64% rename from buttplug/src/core/message/stop_device_cmd.rs rename to crates/buttplug_core/src/message/v0/stop_device_cmd.rs index 05ca25820..57c3675a0 100644 --- a/buttplug/src/core/message/stop_device_cmd.rs +++ b/crates/buttplug_core/src/message/v0/stop_device_cmd.rs @@ -5,16 +5,29 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use super::*; -#[cfg(feature = "serialize-json")] +use crate::message::{ + ButtplugDeviceMessage, + ButtplugMessage, + ButtplugMessageError, + ButtplugMessageFinalizer, + ButtplugMessageValidator, +}; use serde::{Deserialize, Serialize}; -#[derive(Debug, ButtplugDeviceMessage, ButtplugMessageFinalizer, PartialEq, Eq, Clone)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] +#[derive( + Debug, + ButtplugDeviceMessage, + ButtplugMessageFinalizer, + PartialEq, + Eq, + Clone, + Serialize, + Deserialize, +)] pub struct StopDeviceCmdV0 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] + #[serde(rename = "Id")] id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] + #[serde(rename = "DeviceIndex")] device_index: u32, } diff --git a/buttplug/src/core/message/stop_scanning.rs b/crates/buttplug_core/src/message/v0/stop_scanning.rs similarity index 68% rename from buttplug/src/core/message/stop_scanning.rs rename to crates/buttplug_core/src/message/v0/stop_scanning.rs index be540966f..45209500f 100644 --- a/buttplug/src/core/message/stop_scanning.rs +++ b/crates/buttplug_core/src/message/v0/stop_scanning.rs @@ -5,14 +5,19 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use super::*; -#[cfg(feature = "serialize-json")] +use crate::message::{ + ButtplugMessage, + ButtplugMessageError, + ButtplugMessageFinalizer, + ButtplugMessageValidator, +}; use serde::{Deserialize, Serialize}; -#[derive(Debug, ButtplugMessage, ButtplugMessageFinalizer, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] +#[derive( + Debug, ButtplugMessage, ButtplugMessageFinalizer, Clone, PartialEq, Eq, Serialize, Deserialize, +)] pub struct StopScanningV0 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] + #[serde(rename = "Id")] id: u32, } diff --git a/crates/buttplug_core/src/message/v4/device_list.rs b/crates/buttplug_core/src/message/v4/device_list.rs new file mode 100644 index 000000000..7735ab1f4 --- /dev/null +++ b/crates/buttplug_core/src/message/v4/device_list.rs @@ -0,0 +1,46 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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::collections::HashMap; + +use super::DeviceMessageInfoV4; +use crate::message::{ + ButtplugMessage, + ButtplugMessageError, + ButtplugMessageFinalizer, + ButtplugMessageValidator, +}; +use getset::Getters; +use serde::{Deserialize, Serialize}; + +/// List of all devices currently connected to the server. +#[derive(Default, Clone, Debug, PartialEq, ButtplugMessage, Getters, Serialize, Deserialize)] +pub struct DeviceListV4 { + #[serde(rename = "Id")] + id: u32, + #[serde(rename = "Devices")] + #[getset(get = "pub")] + devices: HashMap, +} + +impl DeviceListV4 { + pub fn new(devices: Vec) -> Self { + let device_map = devices.iter().map(|x| (x.device_index(), x.clone())).collect(); + Self { id: 1, devices: device_map } + } +} + +impl ButtplugMessageValidator for DeviceListV4 { + fn is_valid(&self) -> Result<(), ButtplugMessageError> { + self.is_not_system_id(self.id) + } +} + +impl ButtplugMessageFinalizer for DeviceListV4 { + fn finalize(&mut self) { + } +} diff --git a/crates/buttplug_core/src/message/v4/device_message_info.rs b/crates/buttplug_core/src/message/v4/device_message_info.rs new file mode 100644 index 000000000..8b2720e92 --- /dev/null +++ b/crates/buttplug_core/src/message/v4/device_message_info.rs @@ -0,0 +1,51 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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::collections::BTreeMap; + +use crate::message::DeviceFeature; +use getset::{CopyGetters, Getters, MutGetters}; +use serde::{Deserialize, Serialize}; + +/// Substructure of device messages, used for attribute information (name, messages supported, etc...) +#[derive(Clone, Debug, PartialEq, MutGetters, Getters, CopyGetters, Serialize, Deserialize)] +pub struct DeviceMessageInfoV4 { + #[serde(rename = "DeviceIndex")] + #[getset(get_copy = "pub")] + device_index: u32, + #[serde(rename = "DeviceName")] + #[getset(get = "pub")] + device_name: String, + #[serde(rename = "DeviceDisplayName", skip_serializing_if = "Option::is_none")] + #[getset(get = "pub")] + device_display_name: Option, + #[serde(rename = "DeviceMessageTimingGap")] + #[getset(get_copy = "pub")] + device_message_timing_gap: u32, + #[serde(rename = "DeviceFeatures")] + #[getset(get = "pub", get_mut = "pub(super)")] + device_features: BTreeMap, +} + +impl DeviceMessageInfoV4 { + pub fn new( + device_index: u32, + device_name: &str, + device_display_name: &Option, + device_message_timing_gap: u32, + device_features: &Vec, + ) -> Self { + let feature_map = device_features.iter().map(|x| (x.feature_index(), x.clone())).collect(); + Self { + device_index, + device_name: device_name.to_owned(), + device_display_name: device_display_name.clone(), + device_message_timing_gap, + device_features: feature_map, + } + } +} diff --git a/crates/buttplug_core/src/message/v4/input_cmd.rs b/crates/buttplug_core/src/message/v4/input_cmd.rs new file mode 100644 index 000000000..3a8be4d34 --- /dev/null +++ b/crates/buttplug_core/src/message/v4/input_cmd.rs @@ -0,0 +1,76 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 crate::message::{ + ButtplugDeviceMessage, + ButtplugMessage, + ButtplugMessageError, + ButtplugMessageFinalizer, + ButtplugMessageValidator, + InputType, +}; +use getset::CopyGetters; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Display, PartialEq, Eq, Clone, Serialize, Deserialize, Hash, Copy)] +pub enum InputCommandType { + Read, + Subscribe, + Unsubscribe, +} + +#[derive( + Debug, + ButtplugDeviceMessage, + ButtplugMessageFinalizer, + PartialEq, + Eq, + Clone, + Copy, + CopyGetters, + Serialize, + Deserialize, +)] +pub struct InputCmdV4 { + #[serde(rename = "Id")] + id: u32, + #[serde(rename = "DeviceIndex")] + device_index: u32, + #[getset(get_copy = "pub")] + #[serde(rename = "FeatureIndex")] + feature_index: u32, + #[getset(get_copy = "pub")] + #[serde(rename = "InputType")] + input_type: InputType, + #[getset(get_copy = "pub")] + #[serde(rename = "InputCommand")] + input_command: InputCommandType, +} + +impl InputCmdV4 { + pub fn new( + device_index: u32, + feature_index: u32, + input_type: InputType, + input_command_type: InputCommandType, + ) -> Self { + Self { + id: 1, + device_index, + feature_index, + input_type, + input_command: input_command_type, + } + } +} + +impl ButtplugMessageValidator for InputCmdV4 { + fn is_valid(&self) -> Result<(), ButtplugMessageError> { + self.is_not_system_id(self.id) + // TODO Should expected_length always be > 0? + } +} diff --git a/crates/buttplug_core/src/message/v4/input_reading.rs b/crates/buttplug_core/src/message/v4/input_reading.rs new file mode 100644 index 000000000..f2f11cd08 --- /dev/null +++ b/crates/buttplug_core/src/message/v4/input_reading.rs @@ -0,0 +1,91 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 crate::message::{ + ButtplugDeviceMessage, + ButtplugMessage, + ButtplugMessageFinalizer, + ButtplugMessageValidator, + InputType, +}; +use getset::{CopyGetters, Getters}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, CopyGetters)] +#[getset(get_copy = "pub")] +pub struct InputData where T: Copy + Clone { + #[serde(rename = "Data")] + data: T, +} + +impl InputData where T: Copy + Clone { + pub fn new(data: T) -> Self { + Self { data } + } +} + +#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum InputTypeData { + Battery(InputData), + Rssi(InputData), + Button(InputData), + Pressure(InputData) +} + +impl InputTypeData { + pub fn as_input_type(&self) -> InputType { + match self { + Self::Battery(_) => InputType::Battery, + Self::Rssi(_) => InputType::Rssi, + Self::Button(_) => InputType::Button, + Self::Pressure(_) => InputType::Pressure, + } + } +} + +// This message can have an Id of 0, as it can be emitted as part of a +// subscription and won't have a matching task Id in that case. +#[derive( + Debug, + ButtplugDeviceMessage, + ButtplugMessageValidator, + ButtplugMessageFinalizer, + Clone, + Getters, + CopyGetters, + PartialEq, + Eq, + Serialize, + Deserialize, +)] +pub struct InputReadingV4 { + #[serde(rename = "Id")] + id: u32, + #[serde(rename = "DeviceIndex")] + device_index: u32, + #[serde(rename = "FeatureIndex")] + #[getset[get_copy="pub"]] + feature_index: u32, + #[serde(rename = "Data")] + #[getset[get_copy="pub"]] + data: InputTypeData, +} + +impl InputReadingV4 { + pub fn new( + device_index: u32, + feature_index: u32, + data: InputTypeData + ) -> Self { + Self { + id: 0, + device_index, + feature_index, + data, + } + } +} diff --git a/crates/buttplug_core/src/message/v4/mod.rs b/crates/buttplug_core/src/message/v4/mod.rs new file mode 100644 index 000000000..5c3aa578f --- /dev/null +++ b/crates/buttplug_core/src/message/v4/mod.rs @@ -0,0 +1,32 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 Nonpolynomial Labs LLC. All rights reserved. +// +// Licensed under the BSD 3-Clause license. See LICENSE file in the project root +// for full license information. + +mod device_list; +mod device_message_info; +mod input_cmd; +mod input_reading; +mod output_cmd; +mod request_server_info; +mod server_info; +mod spec_enums; + +pub use { + device_list::DeviceListV4, + device_message_info::DeviceMessageInfoV4, + input_cmd::{InputCmdV4, InputCommandType}, + input_reading::{InputData, InputTypeData, InputReadingV4}, + output_cmd::{ + OutputCmdV4, + OutputCommand, + OutputPositionWithDuration, + OutputRotateWithDirection, + OutputValue, + }, + request_server_info::RequestServerInfoV4, + server_info::ServerInfoV4, + spec_enums::{ButtplugClientMessageV4, ButtplugDeviceMessageNameV4, ButtplugServerMessageV4}, +}; diff --git a/crates/buttplug_core/src/message/v4/output_cmd.rs b/crates/buttplug_core/src/message/v4/output_cmd.rs new file mode 100644 index 000000000..b3b2ed438 --- /dev/null +++ b/crates/buttplug_core/src/message/v4/output_cmd.rs @@ -0,0 +1,184 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 crate::{ + errors::{ButtplugDeviceError, ButtplugError}, + message::{ + ButtplugDeviceMessage, + ButtplugMessage, + ButtplugMessageError, + ButtplugMessageFinalizer, + ButtplugMessageValidator, + OutputType, + }, +}; +use getset::CopyGetters; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, CopyGetters)] +#[getset(get_copy = "pub")] +pub struct OutputValue { + #[serde(rename = "Value")] + value: u32, +} + +impl OutputValue { + pub fn new(value: u32) -> Self { + Self { value } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, CopyGetters)] +#[getset(get_copy = "pub")] +pub struct OutputPositionWithDuration { + #[serde(rename = "Position")] + position: u32, + #[serde(rename = "Duration")] + duration: u32, +} + +impl OutputPositionWithDuration { + pub fn new(position: u32, duration: u32) -> Self { + Self { position, duration } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, CopyGetters)] +#[getset(get_copy = "pub")] +pub struct OutputRotateWithDirection { + #[serde(rename = "Speed")] + speed: u32, + #[serde(rename = "Clockwise")] + clockwise: bool, +} + +impl OutputRotateWithDirection { + pub fn new(speed: u32, clockwise: bool) -> Self { + Self { speed, clockwise } + } +} + +#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum OutputCommand { + Vibrate(OutputValue), + // Single Direction Rotation Speed + Rotate(OutputValue), + // Two Direction Rotation Speed + RotateWithDirection(OutputRotateWithDirection), + Oscillate(OutputValue), + Constrict(OutputValue), + Spray(OutputValue), + Heater(OutputValue), + Led(OutputValue), + // For instances where we specify a position to move to ASAP. Usually servos, probably for the + // OSR-2/SR-6. + Position(OutputValue), + PositionWithDuration(OutputPositionWithDuration), +} + +impl OutputCommand { + pub fn value(&self) -> u32 { + match self { + OutputCommand::Constrict(x) + | OutputCommand::Spray(x) + | OutputCommand::Heater(x) + | OutputCommand::Led(x) + | OutputCommand::Oscillate(x) + | OutputCommand::Position(x) + | OutputCommand::Rotate(x) + | OutputCommand::Vibrate(x) => x.value(), + OutputCommand::RotateWithDirection(x) => x.speed(), + OutputCommand::PositionWithDuration(x) => x.position(), + } + } + + pub fn set_value(&mut self, value: u32) { + match self { + OutputCommand::Constrict(x) + | OutputCommand::Spray(x) + | OutputCommand::Heater(x) + | OutputCommand::Led(x) + | OutputCommand::Oscillate(x) + | OutputCommand::Position(x) + | OutputCommand::Rotate(x) + | OutputCommand::Vibrate(x) => x.value = value, + OutputCommand::RotateWithDirection(x) => x.speed = value, + OutputCommand::PositionWithDuration(x) => x.position = value, + } + } + + pub fn as_output_type(&self) -> OutputType { + match self { + Self::Vibrate(_) => OutputType::Vibrate, + Self::Rotate(_) => OutputType::Rotate, + Self::RotateWithDirection(_) => OutputType::RotateWithDirection, + Self::Oscillate(_) => OutputType::Oscillate, + Self::Constrict(_) => OutputType::Constrict, + Self::Spray(_) => OutputType::Spray, + Self::Led(_) => OutputType::Led, + Self::Position(_) => OutputType::Position, + Self::PositionWithDuration(_) => OutputType::PositionWithDuration, + Self::Heater(_) => OutputType::Heater, + } + } + + pub fn from_output_type(output_type: OutputType, value: u32) -> Result { + match output_type { + OutputType::Constrict => Ok(Self::Constrict(OutputValue::new(value))), + OutputType::Heater => Ok(Self::Heater(OutputValue::new(value))), + OutputType::Spray => Ok(Self::Spray(OutputValue::new(value))), + OutputType::Led => Ok(Self::Led(OutputValue::new(value))), + OutputType::Oscillate => Ok(Self::Oscillate(OutputValue::new(value))), + OutputType::Position => Ok(Self::Position(OutputValue::new(value))), + OutputType::Rotate => Ok(Self::Rotate(OutputValue::new(value))), + OutputType::Vibrate => Ok(Self::Vibrate(OutputValue::new(value))), + x => Err(ButtplugError::ButtplugDeviceError( + ButtplugDeviceError::ActuatorNotSupported(x), + )), + } + } +} + +#[derive( + Debug, + ButtplugDeviceMessage, + ButtplugMessageFinalizer, + PartialEq, + Clone, + CopyGetters, + Serialize, + Deserialize, +)] +#[getset(get_copy = "pub")] +pub struct OutputCmdV4 { + #[serde(rename = "Id")] + id: u32, + #[serde(rename = "DeviceIndex")] + device_index: u32, + #[serde(rename = "FeatureIndex")] + feature_index: u32, + #[serde(rename = "Command")] + command: OutputCommand, +} + +impl OutputCmdV4 { + pub fn new(device_index: u32, feature_index: u32, command: OutputCommand) -> Self { + Self { + id: 1, + device_index, + feature_index, + command, + } + } +} + +impl ButtplugMessageValidator for OutputCmdV4 { + fn is_valid(&self) -> Result<(), ButtplugMessageError> { + self.is_not_system_id(self.id)?; + Ok(()) + } +} diff --git a/crates/buttplug_core/src/message/v4/request_server_info.rs b/crates/buttplug_core/src/message/v4/request_server_info.rs new file mode 100644 index 000000000..54c5c9752 --- /dev/null +++ b/crates/buttplug_core/src/message/v4/request_server_info.rs @@ -0,0 +1,66 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 crate::message::{ + ButtplugMessage, + ButtplugMessageError, + ButtplugMessageFinalizer, + ButtplugMessageSpecVersion, + ButtplugMessageValidator, +}; +use getset::{CopyGetters, Getters}; +use serde::{Deserialize, Serialize}; + +// For RequestServerInfo, serde will take care of invalid message versions from json, and internal +// representations of versions require using the version enum as a type bound. Therefore we do not +// need explicit content checking for the message. +#[derive( + Debug, + ButtplugMessage, + ButtplugMessageFinalizer, + Clone, + PartialEq, + Eq, + Getters, + CopyGetters, + Serialize, + Deserialize, +)] +pub struct RequestServerInfoV4 { + #[serde(rename = "Id")] + id: u32, + #[serde(rename = "ClientName")] + #[getset(get = "pub")] + client_name: String, + #[serde(rename = "ProtocolVersionMajor")] + #[getset(get_copy = "pub")] + protocol_version_major: ButtplugMessageSpecVersion, + #[serde(rename = "ProtocolVersionMinor")] + #[getset(get_copy = "pub")] + protocol_version_minor: u32, +} + +impl RequestServerInfoV4 { + pub fn new( + client_name: &str, + protocol_version_major: ButtplugMessageSpecVersion, + protocol_version_minor: u32, + ) -> Self { + Self { + id: 1, + client_name: client_name.to_string(), + protocol_version_major, + protocol_version_minor, + } + } +} + +impl ButtplugMessageValidator for RequestServerInfoV4 { + fn is_valid(&self) -> Result<(), ButtplugMessageError> { + self.is_not_system_id(self.id) + } +} diff --git a/crates/buttplug_core/src/message/v4/server_info.rs b/crates/buttplug_core/src/message/v4/server_info.rs new file mode 100644 index 000000000..6717be905 --- /dev/null +++ b/crates/buttplug_core/src/message/v4/server_info.rs @@ -0,0 +1,68 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 crate::message::{ + ButtplugMessage, + ButtplugMessageError, + ButtplugMessageFinalizer, + ButtplugMessageSpecVersion, + ButtplugMessageValidator, +}; +use getset::{CopyGetters, Getters}; +use serde::{Deserialize, Serialize}; + +#[derive( + Debug, + ButtplugMessage, + ButtplugMessageFinalizer, + PartialEq, + Eq, + Clone, + Getters, + CopyGetters, + Serialize, + Deserialize, +)] +pub struct ServerInfoV4 { + #[serde(rename = "Id")] + id: u32, + #[serde(rename = "ProtocolVersionMajor")] + #[getset(get_copy = "pub")] + protocol_version_major: ButtplugMessageSpecVersion, + #[serde(rename = "ProtocolVersionMinor")] + #[getset(get_copy = "pub")] + protocol_version_minor: u32, + #[serde(rename = "MaxPingTime")] + #[getset(get_copy = "pub")] + max_ping_time: u32, + #[serde(rename = "ServerName")] + #[getset(get = "pub")] + server_name: String, +} + +impl ServerInfoV4 { + pub fn new( + server_name: &str, + protocol_version_major: ButtplugMessageSpecVersion, + protocol_version_minor: u32, + max_ping_time: u32, + ) -> Self { + Self { + id: 1, + protocol_version_major, + protocol_version_minor, + max_ping_time, + server_name: server_name.to_string(), + } + } +} + +impl ButtplugMessageValidator for ServerInfoV4 { + fn is_valid(&self) -> Result<(), ButtplugMessageError> { + self.is_not_system_id(self.id) + } +} diff --git a/crates/buttplug_core/src/message/v4/spec_enums.rs b/crates/buttplug_core/src/message/v4/spec_enums.rs new file mode 100644 index 000000000..c874afec1 --- /dev/null +++ b/crates/buttplug_core/src/message/v4/spec_enums.rs @@ -0,0 +1,96 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 crate::message::{ + v4::input_cmd::InputCmdV4, + ButtplugMessage, + ButtplugMessageError, + ButtplugMessageFinalizer, + ButtplugMessageValidator, + ErrorV0, + OkV0, + OutputCmdV4, + PingV0, + RequestDeviceListV0, + RequestServerInfoV4, + ScanningFinishedV0, + ServerInfoV4, + StartScanningV0, + StopAllDevicesV0, + StopDeviceCmdV0, + StopScanningV0, +}; +use serde::{Deserialize, Serialize}; + +use super::{DeviceListV4, InputReadingV4}; + +/// Represents all client-to-server messages in v3 of the Buttplug Spec +#[derive( + Debug, + Clone, + PartialEq, + ButtplugMessage, + ButtplugMessageValidator, + ButtplugMessageFinalizer, + FromSpecificButtplugMessage, + Serialize, + Deserialize, +)] +pub enum ButtplugClientMessageV4 { + // Handshake messages + RequestServerInfo(RequestServerInfoV4), + Ping(PingV0), + // Device enumeration messages + StartScanning(StartScanningV0), + StopScanning(StopScanningV0), + RequestDeviceList(RequestDeviceListV0), + // Generic commands + StopDeviceCmd(StopDeviceCmdV0), + StopAllDevices(StopAllDevicesV0), + OutputCmd(OutputCmdV4), + InputCmd(InputCmdV4), +} + +/// Represents all server-to-client messages in v3 of the Buttplug Spec +#[derive( + Debug, + Clone, + PartialEq, + ButtplugMessage, + ButtplugMessageValidator, + FromSpecificButtplugMessage, + Serialize, + Deserialize, +)] +pub enum ButtplugServerMessageV4 { + // Status messages + Ok(OkV0), + Error(ErrorV0), + // Handshake messages + ServerInfo(ServerInfoV4), + // Device enumeration messages + DeviceList(DeviceListV4), + ScanningFinished(ScanningFinishedV0), + // Sensor commands + InputReading(InputReadingV4), +} + +impl ButtplugMessageFinalizer for ButtplugServerMessageV4 { + fn finalize(&mut self) { + match self { + ButtplugServerMessageV4::DeviceList(dl) => dl.finalize(), + _ => (), + } + } +} + +#[derive(Copy, Debug, Clone, PartialEq, Eq, Hash, Display)] +pub enum ButtplugDeviceMessageNameV4 { + StopDeviceCmd, + InputCmd, + OutputCmd, +} diff --git a/buttplug/src/util/async_manager/dummy.rs b/crates/buttplug_core/src/util/async_manager/dummy.rs similarity index 83% rename from buttplug/src/util/async_manager/dummy.rs rename to crates/buttplug_core/src/util/async_manager/dummy.rs index 7939e6c3c..bb6923001 100644 --- a/buttplug/src/util/async_manager/dummy.rs +++ b/crates/buttplug_core/src/util/async_manager/dummy.rs @@ -19,7 +19,7 @@ impl Spawn for DummyAsyncManager { } } -pub fn spawn(_: Fut) -> Result<(), SpawnError> +pub fn spawn(_: Fut) where Fut: Future + Send + 'static, { @@ -33,10 +33,3 @@ where { unimplemented!("Dummy executor can't actually spawn!") } - -pub fn block_on(_: F) -> ::Output -where - F: Future, -{ - unimplemented!("Dummy executor can't actually spawn!") -} diff --git a/buttplug/src/util/async_manager/mod.rs b/crates/buttplug_core/src/util/async_manager/mod.rs similarity index 63% rename from buttplug/src/util/async_manager/mod.rs rename to crates/buttplug_core/src/util/async_manager/mod.rs index 2e92c0435..f81c21653 100644 --- a/buttplug/src/util/async_manager/mod.rs +++ b/crates/buttplug_core/src/util/async_manager/mod.rs @@ -6,17 +6,15 @@ // for full license information. cfg_if::cfg_if! { - if #[cfg(feature = "dummy-runtime")] { - mod dummy; - pub use dummy::{DummyAsyncManager as AsyncManager, spawn, spawn_with_handle, block_on}; - } else if #[cfg(feature = "wasm-bindgen-runtime")] { + if #[cfg(feature = "wasm")] { mod wasm_bindgen; - pub use self::wasm_bindgen::{WasmBindgenAsyncManager as AsyncManager, spawn, spawn_with_handle, block_on}; + pub use self::wasm_bindgen::{WasmBindgenAsyncManager as AsyncManager, spawn, spawn_with_handle}; } else if #[cfg(feature = "tokio-runtime")] { mod tokio; - pub use self::tokio::{TokioAsyncManager as AsyncManager, spawn, spawn_with_handle, block_on}; - } - else { - std::compile_error!("Please choose a runtime feature: tokio-runtime, wasm-bindgen-runtime, dummy-runtime"); + pub use self::tokio::{TokioAsyncManager as AsyncManager, spawn, spawn_with_handle}; + } else { + mod dummy; + pub use dummy::{DummyAsyncManager as AsyncManager, spawn, spawn_with_handle}; + //std::compile_error!("Please choose a runtime feature: tokio-runtime, wasm-bindgen-runtime, dummy-runtime"); } } diff --git a/buttplug/src/util/async_manager/tokio.rs b/crates/buttplug_core/src/util/async_manager/tokio.rs similarity index 78% rename from buttplug/src/util/async_manager/tokio.rs rename to crates/buttplug_core/src/util/async_manager/tokio.rs index c49b06a4c..a81eb5882 100644 --- a/buttplug/src/util/async_manager/tokio.rs +++ b/crates/buttplug_core/src/util/async_manager/tokio.rs @@ -37,13 +37,3 @@ where { TokioAsyncManager::default().spawn_with_handle(future) } - -pub fn block_on(f: F) -> ::Output -where - F: Future, -{ - let handle = tokio::runtime::Handle::current(); - let _ = handle.enter(); - // Execute the future, blocking the current thread until completion - futures::executor::block_on(async move { f.await }) -} diff --git a/buttplug/src/util/async_manager/wasm_bindgen.rs b/crates/buttplug_core/src/util/async_manager/wasm_bindgen.rs similarity index 89% rename from buttplug/src/util/async_manager/wasm_bindgen.rs rename to crates/buttplug_core/src/util/async_manager/wasm_bindgen.rs index 696bcd035..21496a600 100644 --- a/buttplug/src/util/async_manager/wasm_bindgen.rs +++ b/crates/buttplug_core/src/util/async_manager/wasm_bindgen.rs @@ -36,10 +36,3 @@ where { WasmBindgenAsyncManager::default().spawn_with_handle(future) } - -pub fn block_on(_: F) -> ::Output -where - F: Future, -{ - unimplemented!("Can't block in wasm!") -} diff --git a/buttplug/src/util/future.rs b/crates/buttplug_core/src/util/future.rs similarity index 100% rename from buttplug/src/util/future.rs rename to crates/buttplug_core/src/util/future.rs diff --git a/buttplug/src/util/json.rs b/crates/buttplug_core/src/util/json.rs similarity index 85% rename from buttplug/src/util/json.rs rename to crates/buttplug_core/src/util/json.rs index 9e9c18054..b27df6970 100644 --- a/buttplug/src/util/json.rs +++ b/crates/buttplug_core/src/util/json.rs @@ -10,7 +10,7 @@ //! buttplug message de/serializers in both the client and server. Uses the //! jsonschema library. -use crate::core::message::serializer::ButtplugSerializerError; +use crate::message::serializer::ButtplugSerializerError; use jsonschema::Validator; pub struct JSONValidator { @@ -38,15 +38,11 @@ impl JSONValidator { /// - `json_str`: JSON string to validate. pub fn validate(&self, json_str: &str) -> Result<(), ButtplugSerializerError> { let check_value = serde_json::from_str(json_str).map_err(|err| { - ButtplugSerializerError::JsonSerializerError(format!( - "Message: {} - Error: {:?}", - json_str, err - )) + ButtplugSerializerError::JsonSerializerError(format!("Message: {json_str} - Error: {err:?}")) })?; self.schema.validate(&check_value).map_err(|err| { ButtplugSerializerError::JsonSerializerError(format!( - "Error during JSON Schema Validation: {:?}", - err + "Error during JSON Schema Validation: {err:?}" )) }) } diff --git a/crates/buttplug_core/src/util/mod.rs b/crates/buttplug_core/src/util/mod.rs new file mode 100644 index 000000000..3ad10e05f --- /dev/null +++ b/crates/buttplug_core/src/util/mod.rs @@ -0,0 +1,19 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 Nonpolynomial Labs LLC. All rights reserved. +// +// Licensed under the BSD 3-Clause license. See LICENSE file in the project root +// for full license information. + +//! Utility module, for storing types and functions used across other modules in +//! the library. + +pub mod async_manager; +pub mod future; +pub mod json; +pub mod stream; + +#[cfg(not(feature = "wasm"))] +pub use tokio::time::sleep; +#[cfg(feature = "wasm")] +pub use wasmtimer::tokio::sleep; diff --git a/buttplug/src/util/stream.rs b/crates/buttplug_core/src/util/stream.rs similarity index 64% rename from buttplug/src/util/stream.rs rename to crates/buttplug_core/src/util/stream.rs index ee109fb0c..d33f0382a 100644 --- a/buttplug/src/util/stream.rs +++ b/crates/buttplug_core/src/util/stream.rs @@ -6,8 +6,8 @@ // for full license information. use async_stream::stream; -use futures::{pin_mut, FutureExt, Stream}; -use tokio::sync::{broadcast, mpsc}; +use futures::{pin_mut, Stream}; +use tokio::sync::broadcast; pub fn convert_broadcast_receiver_to_stream( receiver: broadcast::Receiver, @@ -22,11 +22,3 @@ where } } } - -pub fn recv_now(receiver: &mut mpsc::Receiver) -> Option> { - receiver.recv().now_or_never() -} - -pub fn iffy_is_empty_check(receiver: &mut mpsc::Receiver) -> bool { - recv_now(receiver).is_none() -} diff --git a/buttplug_derive/CHANGELOG.md b/crates/buttplug_derive/CHANGELOG.md similarity index 95% rename from buttplug_derive/CHANGELOG.md rename to crates/buttplug_derive/CHANGELOG.md index 5f74bab31..85932b7ee 100644 --- a/buttplug_derive/CHANGELOG.md +++ b/crates/buttplug_derive/CHANGELOG.md @@ -1,3 +1,9 @@ +# 0.9.0 - 2025/07/04 + +## Features + +- Update to rust edition 2024 + # 0.8.1 - 2024/08/17 ## Bugfixes diff --git a/buttplug_derive/Cargo.toml b/crates/buttplug_derive/Cargo.toml similarity index 89% rename from buttplug_derive/Cargo.toml rename to crates/buttplug_derive/Cargo.toml index e41676c8b..4d2f15b86 100644 --- a/buttplug_derive/Cargo.toml +++ b/crates/buttplug_derive/Cargo.toml @@ -1,17 +1,17 @@ [package] name = "buttplug_derive" -version = "0.8.1" +version = "0.9.0" authors = ["Nonpolynomial Labs, LLC "] description = "Trait Derive Macros for Buttplug Intimate Hardware Control Library" license = "BSD-3-Clause" homepage = "http://buttplug.io" repository = "https://github.com/buttplugio/buttplug-rs.git" keywords = ["usb", "serial", "hardware", "bluetooth", "teledildonics"] -edition = "2021" +edition = "2024" [lib] proc-macro = true [dependencies] -syn = "2.0.100" +syn = "2.0.104" quote = "1.0.40" diff --git a/buttplug_derive/README.md b/crates/buttplug_derive/README.md similarity index 100% rename from buttplug_derive/README.md rename to crates/buttplug_derive/README.md diff --git a/buttplug_derive/rustfmt.toml b/crates/buttplug_derive/rustfmt.toml similarity index 100% rename from buttplug_derive/rustfmt.toml rename to crates/buttplug_derive/rustfmt.toml diff --git a/buttplug_derive/src/lib.rs b/crates/buttplug_derive/src/lib.rs similarity index 93% rename from buttplug_derive/src/lib.rs rename to crates/buttplug_derive/src/lib.rs index bdd56daf7..351692b86 100644 --- a/buttplug_derive/src/lib.rs +++ b/crates/buttplug_derive/src/lib.rs @@ -27,7 +27,7 @@ fn impl_buttplug_message_macro(ast: &syn::DeriveInput) -> TokenStream { syn::Data::Enum(ref e) => { let idents = e.variants.iter().map(|x| x.ident.clone()); let idents2 = idents.clone(); - let gen = quote! { + let r#gen = quote! { impl ButtplugMessage for #name { fn id(&self) -> u32 { match self { @@ -42,10 +42,10 @@ fn impl_buttplug_message_macro(ast: &syn::DeriveInput) -> TokenStream { } } }; - gen.into() + r#gen.into() } syn::Data::Struct(_) => { - let gen = quote! { + let r#gen = quote! { impl ButtplugMessage for #name { fn id(&self) -> u32 { self.id @@ -56,7 +56,7 @@ fn impl_buttplug_message_macro(ast: &syn::DeriveInput) -> TokenStream { } } }; - gen.into() + r#gen.into() } _ => panic!("Derivation only works on structs and enums"), } @@ -78,7 +78,7 @@ fn impl_buttplug_device_message_macro(ast: &syn::DeriveInput) -> TokenStream { match ast.data { syn::Data::Enum(ref e) => { let idents: Vec<_> = e.variants.iter().map(|x| x.ident.clone()).collect(); - let gen = quote! { + let r#gen = quote! { impl ButtplugDeviceMessage for #name { fn device_index(&self) -> u32 { match self { @@ -93,10 +93,10 @@ fn impl_buttplug_device_message_macro(ast: &syn::DeriveInput) -> TokenStream { } } }; - gen.into() + r#gen.into() } syn::Data::Struct(_) => { - let gen = quote! { + let r#gen = quote! { impl ButtplugDeviceMessage for #name { fn device_index(&self) -> u32 { self.device_index @@ -107,7 +107,7 @@ fn impl_buttplug_device_message_macro(ast: &syn::DeriveInput) -> TokenStream { } } }; - gen.into() + r#gen.into() } _ => panic!("Derivation only works on structs and enums"), } @@ -129,7 +129,7 @@ fn impl_buttplug_message_validator_macro(ast: &syn::DeriveInput) -> TokenStream match &ast.data { syn::Data::Enum(e) => { let idents: Vec<_> = e.variants.iter().map(|x| x.ident.clone()).collect(); - let gen = quote! { + let r#gen = quote! { impl ButtplugMessageValidator for #name { fn is_valid(&self) -> Result<(), ButtplugMessageError> { match self { @@ -138,14 +138,14 @@ fn impl_buttplug_message_validator_macro(ast: &syn::DeriveInput) -> TokenStream } } }; - gen.into() + r#gen.into() } syn::Data::Struct(_) => { - let gen = quote! { + let r#gen = quote! { impl ButtplugMessageValidator for #name { } }; - gen.into() + r#gen.into() } _ => panic!("Derivation only works on structs and enums"), } @@ -166,16 +166,16 @@ fn impl_buttplug_message_finalizer_macro(ast: &syn::DeriveInput) -> TokenStream match &ast.data { syn::Data::Enum(_) => { - let gen = quote! { + let r#gen = quote! { impl ButtplugMessageFinalizer for #name {} }; - gen.into() + r#gen.into() } syn::Data::Struct(_) => { - let gen = quote! { + let r#gen = quote! { impl ButtplugMessageFinalizer for #name {} }; - gen.into() + r#gen.into() } _ => panic!("Derivation only works on structs and enums"), } @@ -199,21 +199,21 @@ fn impl_from_specific_buttplug_message_derive_macro(ast: &syn::DeriveInput) -> T // iterate our field identifiers and the identifier of the first member. This means we're locked // to an enum style of field name([unnamed type]), but we're the only ones who use this macro, // and on structs that almost never change, so hopefully leaving this comment will be enough. - let mut fields: Vec<_> = vec!(); + let mut fields: Vec<_> = vec![]; for var in e.variants.iter() { for field in var.fields.iter() { fields.push(field.ty.clone()); } } - let gen = quote! { + let r#gen = quote! { #(impl From<#fields> for #name { fn from(msg: #fields) -> #name { #name::#idents(msg) } })* }; - gen.into() + r#gen.into() } else { panic!("FromButtplugMessageUnion only works on structs"); } -} \ No newline at end of file +} diff --git a/crates/buttplug_server/Cargo.toml b/crates/buttplug_server/Cargo.toml new file mode 100644 index 000000000..7b688b13f --- /dev/null +++ b/crates/buttplug_server/Cargo.toml @@ -0,0 +1,63 @@ +[package] +name = "buttplug_server" +version = "10.0.0" +authors = ["Nonpolynomial Labs, LLC "] +description = "Buttplug Intimate Hardware Control Library - Core Library" +license = "BSD-3-Clause" +homepage = "http://buttplug.io" +repository = "https://github.com/buttplugio/buttplug.git" +readme = "./README.md" +keywords = ["usb", "serial", "hardware", "bluetooth", "teledildonics"] +edition = "2024" +exclude = ["examples/**"] + +[lib] +name = "buttplug_server" +path = "src/lib.rs" +test = true +doctest = true +doc = true +crate-type = ["cdylib", "rlib"] + +[features] +default=[] +wasm=["uuid/js"] + +[dependencies] +buttplug_derive = "0.8.1" +buttplug_core = { path = "../buttplug_core" } +buttplug_server_device_config = { path = "../buttplug_server_device_config" } +# buttplug_derive = { path = "../buttplug_derive" } +futures = "0.3.31" +futures-util = "0.3.31" +thiserror = "2.0.12" +log = "0.4.27" +getset = "0.1.6" +tokio = { version = "1.46.1", features = ["macros"] } +dashmap = { version = "6.1.0" } +tracing-futures = "0.2.5" +tracing = "0.1.41" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" +jsonschema = { version = "0.30.0", default-features = false } +once_cell = "1.21.3" +tokio-stream = "0.1.17" +strum_macros = "0.27.1" +strum = "0.27.1" +uuid = { version = "1.17.0", features = ["serde", "v4"] } +async-trait = "0.1.88" +instant = "0.1.13" +tokio-util = "0.7.15" +regex = "1.11.1" +prost = "0.14.1" +paste = "1.0.15" +aes = { version = "0.8.4" } +ecb = { version = "0.1.2", features = ["std"] } +sha2 = { version = "0.10.9", features = ["std"] } +byteorder = "1.5.0" +# Used by several packages, but we need to bring in the JS feature for wasm. Pinned at 0.2 until dependencies update +rand = { version = "0.8" } + +[target.wasm32-unknown-unknown.dependencies] +getrandom = { version = "0.3.3", features = ["wasm_js"]} +getrandom_old = { version = "0.2.15", features = ["js"], package = "getrandom"} \ No newline at end of file diff --git a/crates/buttplug_server/src/connector.rs b/crates/buttplug_server/src/connector.rs new file mode 100644 index 000000000..12dcbfb38 --- /dev/null +++ b/crates/buttplug_server/src/connector.rs @@ -0,0 +1,10 @@ +use buttplug_core::connector::ButtplugRemoteConnector; + +use super::message::{ButtplugClientMessageVariant, ButtplugServerMessageVariant}; + +pub type ButtplugRemoteServerConnector = ButtplugRemoteConnector< + TransportType, + SerializerType, + ButtplugServerMessageVariant, + ButtplugClientMessageVariant, +>; diff --git a/buttplug/src/server/device/hardware/communication/mod.rs b/crates/buttplug_server/src/device/hardware/communication.rs similarity index 67% rename from buttplug/src/server/device/hardware/communication/mod.rs rename to crates/buttplug_server/src/device/hardware/communication.rs index a224ac430..d21146453 100644 --- a/buttplug/src/server/device/hardware/communication/mod.rs +++ b/crates/buttplug_server/src/device/hardware/communication.rs @@ -5,53 +5,12 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -// Network DCMs work on all platforms -#[cfg(feature = "lovense-connect-service-manager")] -pub mod lovense_connect_service; -#[cfg(feature = "websocket-server-manager")] -pub mod websocket_server; - -// BTLEPlug works on anything not WASM -#[cfg(all( - feature = "btleplug-manager", - any( - target_os = "windows", - target_os = "macos", - target_os = "linux", - target_os = "ios", - target_os = "android" - ) -))] -pub mod btleplug; - -// Lovense Dongles and Serial Ports work on all desktop platforms -#[cfg(all( - feature = "lovense-dongle-manager", - any(target_os = "windows", target_os = "macos", target_os = "linux") -))] -pub mod lovense_dongle; -#[cfg(all( - feature = "serial-manager", - any(target_os = "windows", target_os = "macos", target_os = "linux") -))] -pub mod serialport; - -#[cfg(all( - feature = "hid-manager", - any(target_os = "windows", target_os = "macos", target_os = "linux") -))] -pub mod hid; - -// XInput is windows only -#[cfg(all(feature = "xinput-manager", target_os = "windows"))] -pub mod xinput; - -use crate::{ - core::{errors::ButtplugDeviceError, ButtplugResultFuture}, - server::device::hardware::HardwareConnector, +use crate::device::hardware::HardwareConnector; +use async_trait::async_trait; +use buttplug_core::{ util::{async_manager, sleep}, + {errors::ButtplugDeviceError, ButtplugResultFuture}, }; -use async_trait::async_trait; use futures::future::{self, FutureExt}; use serde::{Deserialize, Serialize}; use std::{sync::Arc, time::Duration}; @@ -89,31 +48,10 @@ pub trait HardwareCommunicationManager: Send + Sync { // Events happen via channel senders passed to the comm manager. } -#[derive(Error, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Error, Debug, Clone, Display, Serialize, Deserialize, PartialEq, Eq)] pub enum HardwareSpecificError { - // XInput library doesn't derive error on its error enum. :( - #[cfg(all(feature = "xinput-manager", target_os = "windows"))] - #[error("XInput usage error: {0}")] - XInputError(String), - // Btleplug library uses Failure, not Error, on its error enum. :( - #[cfg(all( - feature = "btleplug-manager", - any( - target_os = "windows", - target_os = "macos", - target_os = "linux", - target_os = "ios", - target_os = "android" - ) - ))] - #[error("Btleplug error: {0}")] - BtleplugError(String), - #[cfg(all( - feature = "serial-manager", - any(target_os = "windows", target_os = "macos", target_os = "linux") - ))] - #[error("Serial error: {0}")] - SerialError(String), + // HardwareSpecificError: {} Error: {} + HardwareSpecificError(String, String), } #[async_trait] diff --git a/buttplug/src/server/device/hardware/mod.rs b/crates/buttplug_server/src/device/hardware/mod.rs similarity index 76% rename from buttplug/src/server/device/hardware/mod.rs rename to crates/buttplug_server/src/device/hardware/mod.rs index 97b77ebaf..c52415177 100644 --- a/buttplug/src/server/device/hardware/mod.rs +++ b/crates/buttplug_server/src/device/hardware/mod.rs @@ -1,28 +1,16 @@ pub mod communication; +use std::{collections::HashSet, fmt::Debug, sync::Arc, time::Duration}; -use std::{fmt::Debug, sync::Arc, time::Duration}; - -use crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ - Endpoint, - RawReadCmdV2, - RawReadingV2, - RawSubscribeCmdV2, - RawUnsubscribeCmdV2, - RawWriteCmdV2, - }, - }, - server::device::configuration::ProtocolCommunicationSpecifier, -}; use async_trait::async_trait; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::{ProtocolCommunicationSpecifier, Endpoint}; use futures::future::BoxFuture; use futures_util::FutureExt; use getset::{CopyGetters, Getters}; use instant::Instant; use serde::{Deserialize, Serialize}; use tokio::sync::{broadcast, RwLock}; +use uuid::Uuid; /// Parameters for reading data from a [Hardware](crate::device::Hardware) endpoint /// @@ -32,6 +20,9 @@ use tokio::sync::{broadcast, RwLock}; #[derive(PartialEq, Eq, Debug, Clone, Copy, Serialize, Deserialize, CopyGetters)] #[getset(get_copy = "pub")] pub struct HardwareReadCmd { + /// Feature ID for reading + #[serde(default)] + command_id: Uuid, /// Endpoint to read from endpoint: Endpoint, /// Amount of data to read from endpoint @@ -42,8 +33,9 @@ pub struct HardwareReadCmd { impl HardwareReadCmd { /// Creates a new DeviceReadCmd instance - pub fn new(endpoint: Endpoint, length: u32, timeout_ms: u32) -> Self { + pub fn new(command_id: Uuid, endpoint: Endpoint, length: u32, timeout_ms: u32) -> Self { Self { + command_id, endpoint, length, timeout_ms, @@ -51,23 +43,18 @@ impl HardwareReadCmd { } } -impl From for HardwareReadCmd { - fn from(msg: RawReadCmdV2) -> Self { - Self { - endpoint: msg.endpoint(), - length: msg.expected_length(), - timeout_ms: msg.timeout(), - } - } -} - /// Parameters for writing data to a [Hardware](crate::device::Hardware) endpoint /// /// Low level write command structure, used by /// [ButtplugProtocol](crate::device::protocol::ButtplugProtocol) implementations when working with /// [Hardware](crate::device::Hardware) structures. -#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Getters, CopyGetters)] +#[derive(Eq, Debug, Clone, Serialize, Deserialize, Getters, CopyGetters)] pub struct HardwareWriteCmd { + /// Feature ID for this command. As a write command can possibly write to multiple features in one + /// call, this can have multiple feature IDs. + #[getset(get = "pub")] + #[serde(default)] + command_id: HashSet, /// Endpoint to write to #[getset(get_copy = "pub")] endpoint: Endpoint, @@ -79,10 +66,24 @@ pub struct HardwareWriteCmd { write_with_response: bool, } +impl PartialEq for HardwareWriteCmd { + fn eq(&self, other: &Self) -> bool { + self.endpoint() == other.endpoint() + && self.data() == other.data() + && self.write_with_response() == other.write_with_response() + } +} + impl HardwareWriteCmd { /// Create a new DeviceWriteCmd instance. - pub fn new(endpoint: Endpoint, data: Vec, write_with_response: bool) -> Self { + pub fn new( + command_id: &[Uuid], + endpoint: Endpoint, + data: Vec, + write_with_response: bool, + ) -> Self { Self { + command_id: HashSet::from_iter(command_id.iter().cloned()), endpoint, data, write_with_response, @@ -90,16 +91,6 @@ impl HardwareWriteCmd { } } -impl From for HardwareWriteCmd { - fn from(msg: RawWriteCmdV2) -> Self { - Self { - endpoint: msg.endpoint(), - data: msg.data().clone(), - write_with_response: msg.write_with_response(), - } - } -} - /// Parameters for subscribing to a [Hardware](crate::device::Hardware) endpoint /// /// Low level subscribe structure, used by @@ -109,24 +100,29 @@ impl From for HardwareWriteCmd { /// While usually related to notify/indicate characteristics on Bluetooth LE devices, can be used /// with any read endpoint to signal that any information received should be automatically passed to /// the protocol implementation. -#[derive(PartialEq, Eq, Debug, Clone, Copy, Serialize, Deserialize, CopyGetters)] +#[derive(Eq, Debug, Clone, Copy, Serialize, Deserialize, CopyGetters)] #[getset(get_copy = "pub")] pub struct HardwareSubscribeCmd { + /// Feature ID for this command + #[getset(get_copy = "pub")] + #[serde(default)] + command_id: Uuid, /// Endpoint to subscribe to notifications from. endpoint: Endpoint, } -impl HardwareSubscribeCmd { - /// Create a new DeviceSubscribeCmd instance - pub fn new(endpoint: Endpoint) -> Self { - Self { endpoint } +impl PartialEq for HardwareSubscribeCmd { + fn eq(&self, other: &Self) -> bool { + self.endpoint() == other.endpoint() } } -impl From for HardwareSubscribeCmd { - fn from(msg: RawSubscribeCmdV2) -> Self { +impl HardwareSubscribeCmd { + /// Create a new DeviceSubscribeCmd instance + pub fn new(command_id: Uuid, endpoint: Endpoint) -> Self { Self { - endpoint: msg.endpoint(), + command_id, + endpoint, } } } @@ -137,23 +133,26 @@ impl From for HardwareSubscribeCmd { /// Low level subscribe structure, used by /// [ButtplugProtocol](crate::device::protocol::ButtplugProtocol) implementations when working with /// [Hardware](crate::device::Hardware) structures. -#[derive(PartialEq, Eq, Debug, Clone, Copy, Serialize, Deserialize, CopyGetters)] +#[derive(Eq, Debug, Clone, Copy, Serialize, Deserialize, CopyGetters)] #[getset(get_copy = "pub")] pub struct HardwareUnsubscribeCmd { + #[serde(default)] + command_id: Uuid, endpoint: Endpoint, } -impl HardwareUnsubscribeCmd { - /// Create a new DeviceUnsubscribeCmd instance - pub fn new(endpoint: Endpoint) -> Self { - Self { endpoint } +impl PartialEq for HardwareUnsubscribeCmd { + fn eq(&self, other: &Self) -> bool { + self.endpoint() == other.endpoint() } } -impl From for HardwareUnsubscribeCmd { - fn from(msg: RawUnsubscribeCmdV2) -> Self { +impl HardwareUnsubscribeCmd { + /// Create a new DeviceUnsubscribeCmd instance + pub fn new(command_id: Uuid, endpoint: Endpoint) -> Self { Self { - endpoint: msg.endpoint(), + command_id, + endpoint, } } } @@ -168,21 +167,33 @@ pub enum HardwareCommand { Unsubscribe(HardwareUnsubscribeCmd), } -impl From for HardwareCommand { - fn from(msg: RawWriteCmdV2) -> Self { - HardwareCommand::Write(msg.into()) - } -} - -impl From for HardwareCommand { - fn from(msg: RawSubscribeCmdV2) -> Self { - HardwareCommand::Subscribe(msg.into()) - } -} - -impl From for HardwareCommand { - fn from(msg: RawUnsubscribeCmdV2) -> Self { - HardwareCommand::Unsubscribe(msg.into()) +impl HardwareCommand { + pub fn overlaps(&self, command: &HardwareCommand) -> bool { + // There is probably a cleaner way to write these match branches to drop the if/else and default + // out to false, but I can't figure it out right now. + match self { + HardwareCommand::Write(c) => { + if let HardwareCommand::Write(write) = command { + c.command_id().intersection(&write.command_id).count() > 0 + } else { + false + } + } + HardwareCommand::Subscribe(c) => { + if let HardwareCommand::Subscribe(sub) = command { + c.command_id() == sub.command_id + } else { + false + } + } + HardwareCommand::Unsubscribe(c) => { + if let HardwareCommand::Unsubscribe(sub) = command { + c.command_id() == sub.command_id + } else { + false + } + } + } } } @@ -220,12 +231,6 @@ impl HardwareReading { } } -impl From for RawReadingV2 { - fn from(reading: HardwareReading) -> Self { - RawReadingV2::new(0, *reading.endpoint(), reading.data().clone()) - } -} - /// Events that can be emitted from a [Hardware](crate::device::Hardware). #[derive(Debug, Clone)] pub enum HardwareEvent { @@ -240,15 +245,23 @@ pub enum HardwareEvent { /// HardwareInternal, which handles all of the actual hardware communication. However, the struct /// also needs to carry around identifying information, so we wrap it in this type instead of /// requiring that all implementors of deal with name/address/endpoint accessors. -#[derive(CopyGetters)] +#[derive(CopyGetters, Getters)] pub struct Hardware { /// Device name + #[getset(get = "pub")] name: String, /// Device address + #[getset(get = "pub")] address: String, /// Communication endpoints + #[getset(get = "pub")] endpoints: Vec, - /// Internal implementation details + /// Minimum time between two packets being sent to the device. Used to deal with congestion across + /// protocols like Bluetooth LE, which have guaranteed delivery but can be overloaded due to + /// connection intervals. + #[getset(get_copy = "pub")] + message_gap: Option, + /// Internal implementation details internal_impl: Box, /// Requires a keepalive signal to be sent by the Server Device class #[getset(get_copy = "pub")] @@ -261,14 +274,17 @@ impl Hardware { name: &str, address: &str, endpoints: &[Endpoint], + message_gap: &Option, + requires_keepalive: bool, internal_impl: Box, ) -> Self { Self { name: name.to_owned(), address: address.to_owned(), endpoints: endpoints.into(), + message_gap: message_gap.clone(), internal_impl, - requires_keepalive: false, + requires_keepalive, last_write_time: Arc::new(RwLock::new(Instant::now())), } } @@ -277,25 +293,6 @@ impl Hardware { Instant::now().duration_since(*self.last_write_time.read().await) } - pub fn set_requires_keepalive(&mut self) { - self.requires_keepalive = true; - } - - /// Returns the device name - pub fn name(&self) -> &str { - &self.name - } - - /// Returns the device address - pub fn address(&self) -> &str { - &self.address - } - - /// Returns the device endpoint list - pub fn endpoints(&self) -> Vec { - self.endpoints.clone() - } - /// Returns a receiver for any events the device may emit. /// /// This uses a broadcast channel and can be called multiple times to create multiple streams if diff --git a/buttplug/src/server/device/mod.rs b/crates/buttplug_server/src/device/mod.rs similarity index 97% rename from buttplug/src/server/device/mod.rs rename to crates/buttplug_server/src/device/mod.rs index 0b2fcdf55..e7f36e08c 100644 --- a/buttplug/src/server/device/mod.rs +++ b/crates/buttplug_server/src/device/mod.rs @@ -25,7 +25,7 @@ //! network ports, etc...) //! - Protocols (represented by [ButtplugProtocol]), which hold information about the capabilities //! of a device (can it vibrate/rotate/etc, at what speeds, so on and so forth), and translate -//! from [Buttplug Device Messages](crate::core::messages::ButtplugDeviceMessage) into strings or +//! from [Buttplug Device Messages](buttplug_core::messages::ButtplugDeviceMessage) into strings or //! binary arrays to send to devices via their implementation. //! //! # Device Lifetimes in Buttplug @@ -95,12 +95,13 @@ //! //! -pub mod configuration; pub mod hardware; pub mod protocol; +pub mod protocol_impl; pub mod server_device; mod server_device_manager; mod server_device_manager_event_loop; +pub use protocol_impl::get_default_protocol_map; pub use server_device::{ServerDevice, ServerDeviceEvent}; pub use server_device_manager::{ServerDeviceManager, ServerDeviceManagerBuilder}; diff --git a/crates/buttplug_server/src/device/protocol.rs b/crates/buttplug_server/src/device/protocol.rs new file mode 100644 index 000000000..bbdee6b9d --- /dev/null +++ b/crates/buttplug_server/src/device/protocol.rs @@ -0,0 +1,701 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 Nonpolynomial Labs LLC. All rights reserved. +// +// Licensed under the BSD 3-Clause license. See LICENSE file in the project root +// for full license information. + +//! Implementations of communication protocols for hardware supported by Buttplug + +use buttplug_core::{ + errors::ButtplugDeviceError, + message::{InputData, InputReadingV4, InputType, OutputCommand}, +}; +use buttplug_server_device_config::{ + Endpoint, + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; +use dashmap::DashMap; + +use crate::{ + device::{hardware::{Hardware, HardwareCommand, HardwareReadCmd}, protocol_impl::get_default_protocol_map}, + message::{ + checked_output_cmd::CheckedOutputCmdV4, + spec_enums::ButtplugDeviceCommandMessageUnionV4, + ButtplugServerDeviceMessage, + }, +}; +use async_trait::async_trait; +use futures::{ + future::{self, BoxFuture, FutureExt}, + StreamExt, +}; +use std::{collections::HashMap, sync::Arc}; +use std::{pin::Pin, time::Duration}; +use uuid::Uuid; +use super::hardware::HardwareWriteCmd; + +/// Strategy for situations where hardware needs to get updates every so often in order to keep +/// things alive. Currently this applies to iOS backgrounding with bluetooth devices, as well as +/// some protocols like Satisfyer and Mysteryvibe that need constant command refreshing, but since +/// we never know which of our hundreds of supported devices someone might connect, we need context +/// as to which keepalive strategy to use. +/// +/// When choosing a keepalive strategy for a protocol: +/// +/// - If the protocol has a command that essentially does nothing to the actuators, set up +/// RepeatPacketStrategy to use that. This is useful for devices that have info commands (like +/// Lovense), ping commands (like The Handy), sensor commands that aren't yet subscribed to output +/// notifications, etc... +/// - If a protocol needs specific timing or keepalives, regardless of the OS/hardware manager being +/// used, like Satisfyer or Mysteryvibe, use RepeatLastPacketStrategyWithTiming. +/// - For many devices with only scalar actuators, RepeatLastPacketStrategy should work. You just +/// need to make sure the protocol doesn't have a packet counter or something else that will trip +/// if the same packet is replayed multiple times. +#[derive(Debug)] +pub enum ProtocolKeepaliveStrategy { + /// Repeat a specific packet, such as a ping or a no-op. Only do this when the hardware manager + /// requires it (currently only iOS bluetooth during backgrounding). + HardwareRequiredRepeatPacketStrategy(HardwareWriteCmd), + /// Repeat whatever the last packet sent was, and send Stop commands until first packet sent. Uses + /// a default timing, suitable for most protocols that don't need constant device updates outside + /// of OS requirements. Only do this when the hardware manager requires it (currently only iOS + /// bluetooth during backgrounding). + HardwareRequiredRepeatLastPacketStrategy, + /// Repeat whatever the last packet sent was, and send Stop commands until first packet sent. Do + /// this regardless of whether or not the hardware manager requires it. Useful for hardware that + /// requires keepalives, like Satisfyer, Mysteryvibe, Leten, etc... + RepeatLastPacketStrategyWithTiming(Duration), +} + +pub trait ProtocolIdentifierFactory: Send + Sync { + fn identifier(&self) -> &str; + fn create(&self) -> Box; +} + +pub enum ProtocolValueCommandPrefilterStrategy { + /// Drop repeated ValueCmd/ValueWithParameterCmd messages + DropRepeats, + /// No filter, send all value messages for processing + None, +} + +fn print_type_of(_: &T) -> &'static str { + std::any::type_name::() +} + +pub struct ProtocolSpecializer { + specifiers: Vec, + identifier: Box, +} + +impl ProtocolSpecializer { + pub fn new( + specifiers: Vec, + identifier: Box, + ) -> Self { + Self { + specifiers, + identifier, + } + } + + pub fn specifiers(&self) -> &Vec { + &self.specifiers + } + + pub fn identify(self) -> Box { + self.identifier + } +} + +#[async_trait] +pub trait ProtocolIdentifier: Sync + Send { + async fn identify( + &mut self, + hardware: Arc, + specifier: ProtocolCommunicationSpecifier, + ) -> Result<(UserDeviceIdentifier, Box), ButtplugDeviceError>; +} + +#[async_trait] +pub trait ProtocolInitializer: Sync + Send { + async fn initialize( + &mut self, + hardware: Arc, + device_definition: &DeviceDefinition, + ) -> Result, ButtplugDeviceError>; +} + +pub struct GenericProtocolIdentifier { + handler: Option>, + protocol_identifier: String, +} + +impl GenericProtocolIdentifier { + pub fn new(handler: Arc, protocol_identifier: &str) -> Self { + Self { + handler: Some(handler), + protocol_identifier: protocol_identifier.to_owned(), + } + } +} + +#[async_trait] +impl ProtocolIdentifier for GenericProtocolIdentifier { + async fn identify( + &mut self, + hardware: Arc, + _: ProtocolCommunicationSpecifier, + ) -> Result<(UserDeviceIdentifier, Box), ButtplugDeviceError> { + let device_identifier = UserDeviceIdentifier::new( + hardware.address(), + &self.protocol_identifier, + &Some(hardware.name().to_owned()), + ); + Ok(( + device_identifier, + Box::new(GenericProtocolInitializer::new( + self.handler.take().unwrap(), + )), + )) + } +} + +pub struct GenericProtocolInitializer { + handler: Option>, +} + +impl GenericProtocolInitializer { + pub fn new(handler: Arc) -> Self { + Self { + handler: Some(handler), + } + } +} + +#[async_trait] +impl ProtocolInitializer for GenericProtocolInitializer { + async fn initialize( + &mut self, + _: Arc, + _: &DeviceDefinition, + ) -> Result, ButtplugDeviceError> { + Ok(self.handler.take().unwrap()) + } +} + +pub trait ProtocolHandler: Sync + Send { + fn keepalive_strategy(&self) -> ProtocolKeepaliveStrategy { + ProtocolKeepaliveStrategy::HardwareRequiredRepeatLastPacketStrategy + } + + fn handle_message( + &self, + message: &ButtplugDeviceCommandMessageUnionV4, + ) -> Result, ButtplugDeviceError> { + self.command_unimplemented(print_type_of(&message)) + } + + // Allow here since this changes between debug/release + #[allow(unused_variables)] + fn command_unimplemented( + &self, + command: &str, + ) -> Result, ButtplugDeviceError> { + #[cfg(debug_assertions)] + unimplemented!("Command not implemented for this protocol"); + #[cfg(not(debug_assertions))] + Err(ButtplugDeviceError::UnhandledCommand(format!( + "Command not implemented for this protocol: {}", + command + ))) + } + + // The default scalar handler assumes that most devices require discrete commands per feature. If + // a protocol has commands that combine multiple features, either with matched or unmatched + // actuators, they should just implement their own version of this method. + fn handle_output_cmd( + &self, + cmd: &CheckedOutputCmdV4, + ) -> Result, ButtplugDeviceError> { + let output_command = cmd.output_command(); + match output_command { + OutputCommand::Constrict(x) => { + self.handle_output_constrict_cmd(cmd.feature_index(), cmd.feature_id(), x.value()) + } + OutputCommand::Spray(x) => { + self.handle_output_spray_cmd(cmd.feature_index(), cmd.feature_id(), x.value()) + } + OutputCommand::Oscillate(x) => { + self.handle_output_oscillate_cmd(cmd.feature_index(), cmd.feature_id(), x.value()) + } + OutputCommand::Rotate(x) => { + self.handle_output_rotate_cmd(cmd.feature_index(), cmd.feature_id(), x.value()) + } + OutputCommand::Vibrate(x) => { + self.handle_output_vibrate_cmd(cmd.feature_index(), cmd.feature_id(), x.value()) + } + OutputCommand::Position(x) => { + self.handle_output_position_cmd(cmd.feature_index(), cmd.feature_id(), x.value()) + } + OutputCommand::Heater(x) => { + self.handle_output_heater_cmd(cmd.feature_index(), cmd.feature_id(), x.value()) + } + OutputCommand::Led(x) => { + self.handle_output_led_cmd(cmd.feature_index(), cmd.feature_id(), x.value()) + } + OutputCommand::PositionWithDuration(x) => self.handle_position_with_duration_cmd( + cmd.feature_index(), + cmd.feature_id(), + x.position(), + x.duration(), + ), + OutputCommand::RotateWithDirection(x) => self.handle_rotation_with_direction_cmd( + cmd.feature_index(), + cmd.feature_id(), + x.speed(), + x.clockwise(), + ), + } + } + + fn handle_output_vibrate_cmd( + &self, + _feature_index: u32, + _feature_id: Uuid, + _speed: u32, + ) -> Result, ButtplugDeviceError> { + self.command_unimplemented("OutputCmd (Vibrate Actuator)") + } + + fn handle_output_rotate_cmd( + &self, + _feature_index: u32, + _feature_id: Uuid, + _speed: u32, + ) -> Result, ButtplugDeviceError> { + self.command_unimplemented("OutputCmd (Rotate Actuator)") + } + + fn handle_output_oscillate_cmd( + &self, + _feature_index: u32, + _feature_id: Uuid, + _speed: u32, + ) -> Result, ButtplugDeviceError> { + self.command_unimplemented("OutputCmd (Oscillate Actuator)") + } + + fn handle_output_spray_cmd( + &self, + _feature_index: u32, + _feature_id: Uuid, + _level: u32, + ) -> Result, ButtplugDeviceError> { + self.command_unimplemented("OutputCmd (Spray Actuator)") + } + + fn handle_output_constrict_cmd( + &self, + _feature_index: u32, + _feature_id: Uuid, + _level: u32, + ) -> Result, ButtplugDeviceError> { + self.command_unimplemented("OutputCmd (Constrict Actuator)") + } + + fn handle_output_heater_cmd( + &self, + _feature_index: u32, + _feature_id: Uuid, + _level: u32, + ) -> Result, ButtplugDeviceError> { + self.command_unimplemented("OutputCmd (Heater Actuator)") + } + + fn handle_output_led_cmd( + &self, + _feature_index: u32, + _feature_id: Uuid, + _level: u32, + ) -> Result, ButtplugDeviceError> { + self.command_unimplemented("OutputCmd (Led Actuator)") + } + + fn handle_output_position_cmd( + &self, + _feature_index: u32, + _feature_id: Uuid, + _position: u32, + ) -> Result, ButtplugDeviceError> { + self.command_unimplemented("OutputCmd (Position Actuator)") + } + + fn handle_position_with_duration_cmd( + &self, + _feature_index: u32, + _feature_id: Uuid, + _position: u32, + _duration: u32, + ) -> Result, ButtplugDeviceError> { + self.command_unimplemented("OutputCmd (Position w/ Duration Actuator)") + } + + fn handle_rotation_with_direction_cmd( + &self, + _feature_index: u32, + _feature_id: Uuid, + _speed: u32, + _clockwise: bool, + ) -> Result, ButtplugDeviceError> { + self.command_unimplemented("OutputCmd (Rotation w/ Direction Actuator)") + } + + fn handle_input_subscribe_cmd( + &self, + _device_index: u32, + _device: Arc, + _feature_index: u32, + _feature_id: Uuid, + _sensor_type: InputType, + ) -> BoxFuture> { + future::ready(Err(ButtplugDeviceError::UnhandledCommand( + "Command not implemented for this protocol: InputCmd (Subscribe)".to_string(), + ))) + .boxed() + } + + fn handle_input_unsubscribe_cmd( + &self, + _device: Arc, + _feature_index: u32, + _feature_id: Uuid, + _sensor_type: InputType, + ) -> BoxFuture> { + future::ready(Err(ButtplugDeviceError::UnhandledCommand( + "Command not implemented for this protocol: InputCmd (Unsubscribe)".to_string(), + ))) + .boxed() + } + + fn handle_input_read_cmd( + &self, + device_index: u32, + device: Arc, + feature_index: u32, + feature_id: Uuid, + sensor_type: InputType, + ) -> BoxFuture> { + match sensor_type { + InputType::Battery => { + self.handle_battery_level_cmd(device_index, device, feature_index, feature_id) + } + _ => future::ready(Err(ButtplugDeviceError::UnhandledCommand( + "Command not implemented for this protocol: InputCmd (Read)".to_string(), + ))) + .boxed(), + } + } + + // Handle Battery Level returns a SensorReading, as we'll always need to do a sensor index + // conversion on it. + fn handle_battery_level_cmd( + &self, + device_index: u32, + device: Arc, + feature_index: u32, + feature_id: Uuid, + ) -> BoxFuture> { + // If we have a standardized BLE Battery endpoint, handle that above the + // protocol, as it'll always be the same. + if device.endpoints().contains(&Endpoint::RxBLEBattery) { + debug!("Trying to get battery reading."); + let msg = HardwareReadCmd::new(feature_id, Endpoint::RxBLEBattery, 1, 0); + let fut = device.read_value(&msg); + async move { + let hw_msg = fut.await?; + let battery_level = hw_msg.data()[0] as i32; + let battery_reading = InputReadingV4::new( + device_index, + feature_index, + buttplug_core::message::InputTypeData::Battery(InputData::new(battery_level as u8)) + ); + debug!("Got battery reading: {}", battery_level); + Ok(battery_reading) + } + .boxed() + } else { + future::ready(Err(ButtplugDeviceError::UnhandledCommand( + "Command not implemented for this protocol: SensorReadCmd".to_string(), + ))) + .boxed() + } + } + + fn handle_rssi_level_cmd( + &self, + _device: Arc, + _feature_index: u32, + _feature_id: Uuid, + ) -> BoxFuture> { + future::ready(Err(ButtplugDeviceError::UnhandledCommand( + "Command not implemented for this protocol: SensorReadCmd".to_string(), + ))) + .boxed() + } + + fn event_stream( + &self, + ) -> Pin + Send>> { + tokio_stream::empty().boxed() + } +} + +#[macro_export] +macro_rules! generic_protocol_setup { + ( $protocol_name:ident, $protocol_identifier:tt) => { + paste::paste! { + pub mod setup { + use std::sync::Arc; + use $crate::device::protocol::{ + GenericProtocolIdentifier, ProtocolIdentifier, ProtocolIdentifierFactory, + }; + #[derive(Default)] + pub struct [< $protocol_name IdentifierFactory >] {} + + impl ProtocolIdentifierFactory for [< $protocol_name IdentifierFactory >] { + fn identifier(&self) -> &str { + $protocol_identifier + } + + fn create(&self) -> Box { + Box::new(GenericProtocolIdentifier::new( + Arc::new(super::$protocol_name::default()), + self.identifier(), + )) + } + } + } + } + }; +} + +#[macro_export] +macro_rules! generic_protocol_initializer_setup { + ( $protocol_name:ident, $protocol_identifier:tt) => { + paste::paste! { + pub mod setup { + use $crate::device::protocol::{ProtocolIdentifier, ProtocolIdentifierFactory}; + #[derive(Default)] + pub struct [< $protocol_name IdentifierFactory >] {} + + impl ProtocolIdentifierFactory for [< $protocol_name IdentifierFactory >] { + fn identifier(&self) -> &str { + $protocol_identifier + } + + fn create(&self) -> Box { + Box::new(super::[< $protocol_name Identifier >]::default()) + } + } + } + + #[derive(Default)] + pub struct [< $protocol_name Identifier >] {} + + #[async_trait] + impl ProtocolIdentifier for [< $protocol_name Identifier >] { + async fn identify( + &mut self, + hardware: Arc, + _: ProtocolCommunicationSpecifier, + ) -> Result<(UserDeviceIdentifier, Box), ButtplugDeviceError> { + Ok((UserDeviceIdentifier::new(hardware.address(), $protocol_identifier, &Some(hardware.name().to_owned())), Box::new([< $protocol_name Initializer >]::default()))) + } + } + } + }; +} + +pub use generic_protocol_initializer_setup; +pub use generic_protocol_setup; + +pub struct ProtocolManager { + // Map of protocol names to their respective protocol instance factories + protocol_map: HashMap>, +} + +impl Default for ProtocolManager { + fn default() -> Self { + Self { + protocol_map: get_default_protocol_map() + } + } +} + +impl ProtocolManager { + pub fn protocol_specializers( + &self, + specifier: &ProtocolCommunicationSpecifier, + base_communication_specifiers: &HashMap>, + user_communication_specifiers: &DashMap>, + ) -> Vec { + debug!( + "Looking for protocol that matches specifier: {:?}", + specifier + ); + let mut specializers = vec![]; + let mut update_specializer_map = + |name: &str, specifiers: &Vec| { + if specifiers.contains(specifier) { + info!( + "Found protocol {:?} for user specifier {:?}.", + name, specifier + ); + if self.protocol_map.contains_key(name) { + specializers.push(ProtocolSpecializer::new( + specifiers.clone(), + self + .protocol_map + .get(name) + .expect("already checked existence") + .create(), + )); + } else { + warn!( + "No protocol implementation for {:?} found for specifier {:?}.", + name, specifier + ); + } + } + }; + // Loop through both maps, as chaining between DashMap and HashMap gets kinda gross. + for spec in user_communication_specifiers.iter() { + update_specializer_map(spec.key(), spec.value()); + } + for (name, specifiers) in base_communication_specifiers.iter() { + update_specializer_map(name, specifiers); + } + specializers + } +} + + +/* +#[cfg(test)] +mod test { + use super::*; + use crate::{ + core::message::{OutputType, FeatureType}, + server::message::server_device_feature::{ServerDeviceFeature, ServerDeviceFeatureOutput}, + }; + use std::{ + collections::{HashMap, HashSet}, + ops::RangeInclusive, + }; + + fn create_unit_test_dcm() -> DeviceConfigurationManager { + let mut builder = DeviceConfigurationManagerBuilder::default(); + let specifiers = ProtocolCommunicationSpecifier::BluetoothLE(BluetoothLESpecifier::new( + HashSet::from(["LVS-*".to_owned(), "LovenseDummyTestName".to_owned()]), + vec![], + HashSet::new(), + HashMap::new(), + )); + let mut feature_actuator = HashMap::new(); + feature_actuator.insert( + OutputType::Vibrate, + ServerDeviceFeatureOutput::new(&RangeInclusive::new(0, 20), &RangeInclusive::new(0, 20)), + ); + builder + .communication_specifier("lovense", &[specifiers]) + .protocol_features( + &BaseDeviceIdentifier::new("lovense", &Some("P".to_owned())), + &BaseDeviceDefinition::new( + "Lovense Edge", + &uuid::Uuid::new_v4(), + &None, + &vec![ + ServerDeviceFeature::new( + "Edge Vibration 1", + &uuid::Uuid::new_v4(), + &None, + FeatureType::Vibrate, + &Some(feature_actuator.clone()), + &None, + ), + ServerDeviceFeature::new( + "Edge Vibration 2", + &uuid::Uuid::new_v4(), + &None, + FeatureType::Vibrate, + &Some(feature_actuator.clone()), + &None, + ), + ], + &None + ), + ) + .finish() + .unwrap() + } + + #[test] + fn test_config_equals() { + let config = create_unit_test_dcm(); + let spec = ProtocolCommunicationSpecifier::BluetoothLE(BluetoothLESpecifier::new_from_device( + "LVS-Something", + &HashMap::new(), + &[], + )); + assert!(!config.protocol_specializers(&spec).is_empty()); + } + + #[test] + fn test_config_wildcard_equals() { + let config = create_unit_test_dcm(); + let spec = ProtocolCommunicationSpecifier::BluetoothLE(BluetoothLESpecifier::new_from_device( + "LVS-Whatever", + &HashMap::new(), + &[], + )); + assert!(!config.protocol_specializers(&spec).is_empty()); + } + /* + #[test] + fn test_specific_device_config_creation() { + let dcm = create_unit_test_dcm(false); + let spec = ProtocolCommunicationSpecifier::BluetoothLE(BluetoothLESpecifier::new_from_device( + "LVS-Whatever", + &HashMap::new(), + &[], + )); + assert!(!dcm.protocol_specializers(&spec).is_empty()); + let config: ProtocolDeviceAttributes = dcm + .device_definition( + &UserDeviceIdentifier::new("Whatever", "lovense", &Some("P".to_owned())), + &[], + ) + .expect("Should be found") + .into(); + // Make sure we got the right name + assert_eq!(config.name(), "Lovense Edge"); + // Make sure we overwrote the default of 1 + assert_eq!( + config + .message_attributes() + .scalar_cmd() + .as_ref() + .expect("Test, assuming infallible") + .get(0) + .expect("Test, assuming infallible") + .step_count(), + 20 + ); + } + */ +} +*/ diff --git a/crates/buttplug_server/src/device/protocol_impl/activejoy.rs b/crates/buttplug_server/src/device/protocol_impl/activejoy.rs new file mode 100644 index 000000000..f14ecdb4b --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/activejoy.rs @@ -0,0 +1,44 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +generic_protocol_setup!(ActiveJoy, "activejoy"); + +#[derive(Default)] +pub struct ActiveJoy {} + +impl ProtocolHandler for ActiveJoy { + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + [ + 0xb0, // static header + 0x01, // mode: 1=vibe, 5=shock, 6=thrust, 7=suction, 8=rotation, 16=swing, + 0x00, // strong mode = 1 (thrust, suction, swing, rotate) + feature_index as u8, // 0 unless vibe2 + if speed == 0 { 0x00 } else { 0x01 }, + speed as u8, + ] + .to_vec(), + false, + ) + .into()]) + } +} diff --git a/buttplug/src/server/device/protocol/adrienlastic.rs b/crates/buttplug_server/src/device/protocol_impl/adrienlastic.rs similarity index 55% rename from buttplug/src/server/device/protocol/adrienlastic.rs rename to crates/buttplug_server/src/device/protocol_impl/adrienlastic.rs index 3502888e5..9353f3a7e 100644 --- a/buttplug/src/server/device/protocol/adrienlastic.rs +++ b/crates/buttplug_server/src/device/protocol_impl/adrienlastic.rs @@ -5,13 +5,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(AdrienLastic, "adrienlastic"); @@ -19,18 +20,16 @@ generic_protocol_setup!(AdrienLastic, "adrienlastic"); pub struct AdrienLastic {} impl ProtocolHandler for AdrienLastic { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, - format!("MotorValue:{:02};", scalar).as_bytes().to_vec(), + format!("MotorValue:{speed:02};").as_bytes().to_vec(), true, ) .into()]) diff --git a/buttplug/src/server/device/protocol/amorelie_joy.rs b/crates/buttplug_server/src/device/protocol_impl/amorelie_joy.rs similarity index 51% rename from buttplug/src/server/device/protocol/amorelie_joy.rs rename to crates/buttplug_server/src/device/protocol_impl/amorelie_joy.rs index 8a4fa9354..f30172de5 100644 --- a/buttplug/src/server/device/protocol/amorelie_joy.rs +++ b/crates/buttplug_server/src/device/protocol_impl/amorelie_joy.rs @@ -5,22 +5,25 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - configuration::{UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolCommunicationSpecifier, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use buttplug_server_device_config::{DeviceDefinition, UserDeviceIdentifier}; + +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, }, }; +use buttplug_server_device_config::ProtocolCommunicationSpecifier; use async_trait::async_trait; use std::sync::Arc; +use uuid::{uuid, Uuid}; + +const AMORELIE_JOY_PROTOCOL_UUID: Uuid = uuid!("0968017b-96f8-44ae-b113-39080dd7ed5f"); generic_protocol_initializer_setup!(AmorelieJoy, "amorelie-joy"); @@ -32,10 +35,15 @@ impl ProtocolInitializer for AmorelieJoyInitializer { async fn initialize( &mut self, hardware: Arc, - _: &UserDeviceDefinition, + _: &DeviceDefinition, ) -> Result, ButtplugDeviceError> { hardware - .write_value(&HardwareWriteCmd::new(Endpoint::Tx, vec![0x03], false)) + .write_value(&HardwareWriteCmd::new( + &[AMORELIE_JOY_PROTOCOL_UUID], + Endpoint::Tx, + vec![0x03], + false, + )) .await?; Ok(Arc::new(AmorelieJoy::default())) } @@ -45,21 +53,19 @@ impl ProtocolInitializer for AmorelieJoyInitializer { pub struct AmorelieJoy {} impl ProtocolHandler for AmorelieJoy { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, [ - 0x01, // static header - 0x01, // pattern (1 = steady), - scalar as u8, // speed 0-100 + 0x01, // static header + 0x01, // pattern (1 = steady), + speed as u8, // speed 0-100 ] .to_vec(), false, diff --git a/buttplug/src/server/device/protocol/aneros.rs b/crates/buttplug_server/src/device/protocol_impl/aneros.rs similarity index 54% rename from buttplug/src/server/device/protocol/aneros.rs rename to crates/buttplug_server/src/device/protocol_impl/aneros.rs index 69a1120ea..8fd689b8a 100644 --- a/buttplug/src/server/device/protocol/aneros.rs +++ b/crates/buttplug_server/src/device/protocol_impl/aneros.rs @@ -5,13 +5,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(Aneros, "aneros"); @@ -19,18 +20,16 @@ generic_protocol_setup!(Aneros, "aneros"); pub struct Aneros {} impl ProtocolHandler for Aneros { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - index: u32, - scalar: u32, + feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, - vec![0xF1 + (index as u8), scalar as u8], + vec![0xF1 + (feature_index as u8), speed as u8], false, ) .into()]) diff --git a/buttplug/src/server/device/protocol/ankni.rs b/crates/buttplug_server/src/device/protocol_impl/ankni.rs similarity index 74% rename from buttplug/src/server/device/protocol/ankni.rs rename to crates/buttplug_server/src/device/protocol_impl/ankni.rs index d75aacb9c..36bde4b5e 100644 --- a/buttplug/src/server/device/protocol/ankni.rs +++ b/crates/buttplug_server/src/device/protocol_impl/ankni.rs @@ -5,25 +5,30 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::server::device::configuration::ProtocolCommunicationSpecifier; -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - configuration::{UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareReadCmd, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareReadCmd, HardwareWriteCmd}, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, }, }; use async_trait::async_trait; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use buttplug_server_device_config::{ + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; use std::sync::Arc; +use uuid::{uuid, Uuid}; generic_protocol_initializer_setup!(Ankni, "ankni"); +const ANKNI_PROTOCOL_UUID: Uuid = uuid!("9859232d-57ee-4135-a93c-c8988bf8cbbf"); + #[derive(Default)] pub struct AnkniInitializer {} @@ -32,9 +37,9 @@ impl ProtocolInitializer for AnkniInitializer { async fn initialize( &mut self, hardware: Arc, - _: &UserDeviceDefinition, + _: &DeviceDefinition, ) -> Result, ButtplugDeviceError> { - let msg = HardwareReadCmd::new(Endpoint::Generic0, 16, 100); + let msg = HardwareReadCmd::new(ANKNI_PROTOCOL_UUID, Endpoint::Generic0, 16, 100); let reading = hardware.read_value(&msg).await?; // No mac address on PnP characteristic, assume no handshake required @@ -50,6 +55,7 @@ impl ProtocolInitializer for AnkniInitializer { debug!("Ankni Checksum: {:#02X}", check); let msg = HardwareWriteCmd::new( + &[ANKNI_PROTOCOL_UUID], Endpoint::Tx, vec![ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, @@ -59,6 +65,7 @@ impl ProtocolInitializer for AnkniInitializer { ); hardware.write_value(&msg).await?; let msg = HardwareWriteCmd::new( + &[ANKNI_PROTOCOL_UUID], Endpoint::Tx, vec![ 0x01, 0x02, check, check, check, check, check, check, check, check, check, check, check, @@ -75,21 +82,19 @@ impl ProtocolInitializer for AnkniInitializer { pub struct Ankni {} impl ProtocolHandler for Ankni { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, vec![ 0x03, 0x12, - scalar as u8, + speed as u8, 0x00, 0x00, 0x00, diff --git a/crates/buttplug_server/src/device/protocol_impl/bananasome.rs b/crates/buttplug_server/src/device/protocol_impl/bananasome.rs new file mode 100644 index 000000000..3c88b1e2c --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/bananasome.rs @@ -0,0 +1,71 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2025 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::sync::atomic::{AtomicU8, Ordering}; + +use uuid::{uuid, Uuid}; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; + +const BANANASOME_PROTOCOL_UUID: Uuid = uuid!("a0a2e5f8-3692-4f6b-8add-043513ed86f6"); +generic_protocol_setup!(Bananasome, "bananasome"); + +pub struct Bananasome { + current_commands: [AtomicU8; 3], +} + +impl Default for Bananasome { + fn default() -> Self { + Self { + current_commands: [AtomicU8::new(0), AtomicU8::new(0), AtomicU8::new(0)], + } + } +} + +impl Bananasome { + fn hardware_command(&self, feature_index: u32, speed: u32) -> Vec { + self.current_commands[feature_index as usize].store(speed as u8, Ordering::Relaxed); + vec![HardwareWriteCmd::new( + &[BANANASOME_PROTOCOL_UUID], + Endpoint::Tx, + vec![ + 0xa0, + 0x03, + self.current_commands[0].load(Ordering::Relaxed), + self.current_commands[1].load(Ordering::Relaxed), + self.current_commands[2].load(Ordering::Relaxed), + ], + false, + ) + .into()] + } +} + +impl ProtocolHandler for Bananasome { + fn handle_output_oscillate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + Ok(self.hardware_command(feature_index, speed)) + } + + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + Ok(self.hardware_command(feature_index, speed)) + } +} diff --git a/buttplug/src/server/device/protocol/cachito.rs b/crates/buttplug_server/src/device/protocol_impl/cachito.rs similarity index 53% rename from buttplug/src/server/device/protocol/cachito.rs rename to crates/buttplug_server/src/device/protocol_impl/cachito.rs index d5c0bea67..38dd21cd4 100644 --- a/buttplug/src/server/device/protocol/cachito.rs +++ b/crates/buttplug_server/src/device/protocol_impl/cachito.rs @@ -5,13 +5,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(Cachito, "cachito"); @@ -19,18 +20,21 @@ generic_protocol_setup!(Cachito, "cachito"); pub struct Cachito {} impl ProtocolHandler for Cachito { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - index: u32, - scalar: u32, + feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, - vec![2u8 + (index as u8), 1u8 + (index as u8), scalar as u8, 0u8], + vec![ + 2u8 + (feature_index as u8), + 1u8 + feature_index as u8, + speed as u8, + 0u8, + ], false, ) .into()]) diff --git a/crates/buttplug_server/src/device/protocol_impl/cowgirl.rs b/crates/buttplug_server/src/device/protocol_impl/cowgirl.rs new file mode 100644 index 000000000..0a503db23 --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/cowgirl.rs @@ -0,0 +1,71 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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::sync::atomic::{AtomicU8, Ordering}; + +use uuid::{uuid, Uuid}; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; + +const COWGIRL_PROTOCOL_UUID: Uuid = uuid!("0474d2fd-f566-4bed-8770-88e457a96144"); +generic_protocol_setup!(Cowgirl, "cowgirl"); + +pub struct Cowgirl { + speeds: [AtomicU8; 2], +} + +impl Default for Cowgirl { + fn default() -> Self { + Self { + speeds: [AtomicU8::new(0), AtomicU8::new(0)], + } + } +} + +impl Cowgirl { + fn hardware_commands(&self) -> Vec { + vec![HardwareWriteCmd::new( + &[COWGIRL_PROTOCOL_UUID], + Endpoint::Tx, + vec![ + 0x00, + 0x01, + self.speeds[0].load(Ordering::Relaxed), + self.speeds[1].load(Ordering::Relaxed), + ], + true, + ) + .into()] + } +} + +impl ProtocolHandler for Cowgirl { + fn handle_output_vibrate_cmd( + &self, + _feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.speeds[0].store(speed as u8, Ordering::Relaxed); + Ok(self.hardware_commands()) + } + + fn handle_output_rotate_cmd( + &self, + _feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.speeds[1].store(speed as u8, Ordering::Relaxed); + Ok(self.hardware_commands()) + } +} diff --git a/buttplug/src/server/device/protocol/cowgirl_cone.rs b/crates/buttplug_server/src/device/protocol_impl/cowgirl_cone.rs similarity index 61% rename from buttplug/src/server/device/protocol/cowgirl_cone.rs rename to crates/buttplug_server/src/device/protocol_impl/cowgirl_cone.rs index 8271290b7..f30098386 100644 --- a/buttplug/src/server/device/protocol/cowgirl_cone.rs +++ b/crates/buttplug_server/src/device/protocol_impl/cowgirl_cone.rs @@ -5,25 +5,30 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, }, - util::sleep, }; use async_trait::async_trait; +use buttplug_core::{errors::ButtplugDeviceError, util::sleep}; +use buttplug_server_device_config::{ + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, + Endpoint, +}; use std::{sync::Arc, time::Duration}; +use uuid::{uuid, Uuid}; generic_protocol_initializer_setup!(CowgirlCone, "cowgirl-cone"); +const COWGIRL_CONE_PROTOCOL_UUID: Uuid = uuid!("3054b443-eca7-41a6-8ba1-b93a646636a4"); + #[derive(Default)] pub struct CowgirlConeInitializer {} @@ -32,10 +37,11 @@ impl ProtocolInitializer for CowgirlConeInitializer { async fn initialize( &mut self, hardware: Arc, - _: &UserDeviceDefinition, + _: &DeviceDefinition, ) -> Result, ButtplugDeviceError> { hardware .write_value(&HardwareWriteCmd::new( + &[COWGIRL_CONE_PROTOCOL_UUID], Endpoint::Tx, vec![0xaa, 0x56, 0x00, 0x00], false, @@ -50,18 +56,16 @@ impl ProtocolInitializer for CowgirlConeInitializer { pub struct CowgirlCone {} impl ProtocolHandler for CowgirlCone { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, - vec![0xf1, 0x01, scalar as u8, 0x00], + vec![0xf1, 0x01, speed as u8, 0x00], false, ) .into()]) diff --git a/buttplug/src/server/device/protocol/cupido.rs b/crates/buttplug_server/src/device/protocol_impl/cupido.rs similarity index 79% rename from buttplug/src/server/device/protocol/cupido.rs rename to crates/buttplug_server/src/device/protocol_impl/cupido.rs index bfc678e33..02c15d24b 100644 --- a/buttplug/src/server/device/protocol/cupido.rs +++ b/crates/buttplug_server/src/device/protocol_impl/cupido.rs @@ -5,14 +5,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - generic_protocol_setup, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::ProtocolHandler, - }, +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(Cupido, "cupido"); @@ -20,18 +20,16 @@ generic_protocol_setup!(Cupido, "cupido"); pub struct Cupido {} impl ProtocolHandler for Cupido { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, - vec![0xb0, 0x03, 0, 0, 0, scalar as u8, 0xaa], + vec![0xb0, 0x03, 0, 0, 0, speed as u8, 0xaa], false, ) .into()]) @@ -60,7 +58,7 @@ impl ProtocolHandler for Cupido { let battery_reading = SensorReading::new( message.device_index(), *message.sensor_index(), - *message.sensor_type(), + *message.input_type(), vec![data[5] as i32], ); Ok(battery_reading.into()) diff --git a/buttplug/src/server/device/protocol/deepsire.rs b/crates/buttplug_server/src/device/protocol_impl/deepsire.rs similarity index 54% rename from buttplug/src/server/device/protocol/deepsire.rs rename to crates/buttplug_server/src/device/protocol_impl/deepsire.rs index ebdc7ac54..2b9ac179b 100644 --- a/buttplug/src/server/device/protocol/deepsire.rs +++ b/crates/buttplug_server/src/device/protocol_impl/deepsire.rs @@ -5,13 +5,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(DeepSire, "deepsire"); @@ -19,18 +20,16 @@ generic_protocol_setup!(DeepSire, "deepsire"); pub struct DeepSire {} impl ProtocolHandler for DeepSire { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, - vec![0x55, 0x04, 0x01, 0x00, 0x00, scalar as u8, 0xAA], + vec![0x55, 0x04, 0x01, 0x00, 0x00, speed as u8, 0xAA], false, ) .into()]) diff --git a/crates/buttplug_server/src/device/protocol_impl/feelingso.rs b/crates/buttplug_server/src/device/protocol_impl/feelingso.rs new file mode 100644 index 000000000..9983f72cd --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/feelingso.rs @@ -0,0 +1,75 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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::sync::atomic::{AtomicU8, Ordering}; + +use uuid::{uuid, Uuid}; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; + +const FEELINGSO_PROTOCOL_UUID: Uuid = uuid!("397d4cce-3173-4f66-b7ad-6ee21e59f854"); + +generic_protocol_setup!(FeelingSo, "feelingso"); + +pub struct FeelingSo { + speeds: [AtomicU8; 2], +} + +impl Default for FeelingSo { + fn default() -> Self { + Self { + speeds: [AtomicU8::new(0), AtomicU8::new(0)], + } + } +} + +impl FeelingSo { + fn hardware_command(&self) -> Vec { + vec![HardwareWriteCmd::new( + &[FEELINGSO_PROTOCOL_UUID], + Endpoint::Tx, + vec![ + 0xaa, + 0x40, + 0x03, + self.speeds[0].load(Ordering::Relaxed), + self.speeds[1].load(Ordering::Relaxed), + 0x14, // Oscillate range: 1 to 4 + 0x19, // Checksum? + ], + false, + ) + .into()] + } +} + +impl ProtocolHandler for FeelingSo { + fn handle_output_oscillate_cmd( + &self, + _feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.speeds[1].store(speed as u8, Ordering::Relaxed); + Ok(self.hardware_command()) + } + + fn handle_output_vibrate_cmd( + &self, + _feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.speeds[0].store(speed as u8, Ordering::Relaxed); + Ok(self.hardware_command()) + } +} diff --git a/buttplug/src/server/device/protocol/fleshlight_launch_helper.rs b/crates/buttplug_server/src/device/protocol_impl/fleshlight_launch_helper.rs similarity index 100% rename from buttplug/src/server/device/protocol/fleshlight_launch_helper.rs rename to crates/buttplug_server/src/device/protocol_impl/fleshlight_launch_helper.rs diff --git a/buttplug/src/server/device/protocol/fleshy_thrust.rs b/crates/buttplug_server/src/device/protocol_impl/fleshy_thrust.rs similarity index 51% rename from buttplug/src/server/device/protocol/fleshy_thrust.rs rename to crates/buttplug_server/src/device/protocol_impl/fleshy_thrust.rs index 65237faab..df63596f4 100644 --- a/buttplug/src/server/device/protocol/fleshy_thrust.rs +++ b/crates/buttplug_server/src/device/protocol_impl/fleshy_thrust.rs @@ -5,13 +5,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(FleshyThrust, "fleshy-thrust"); @@ -19,21 +20,20 @@ generic_protocol_setup!(FleshyThrust, "fleshy-thrust"); pub struct FleshyThrust {} impl ProtocolHandler for FleshyThrust { - fn handle_linear_cmd( + fn handle_position_with_duration_cmd( &self, - message: crate::core::message::LinearCmdV4, + _feature_index: u32, + feature_id: Uuid, + position: u32, + duration: u32, ) -> Result, ButtplugDeviceError> { - let current_cmd = message - .vectors() - .first() - .ok_or(ButtplugDeviceError::DeviceFeatureCountMismatch(1, 0))?; - Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, vec![ - (current_cmd.position() * 180f64).abs() as u8, - ((current_cmd.duration() & 0xff00) >> 8) as u8, - (current_cmd.duration() & 0xff) as u8, + position as u8, + ((duration & 0xff00) >> 8) as u8, + (duration & 0xff) as u8, ], false, ) diff --git a/buttplug/src/server/device/protocol/foreo.rs b/crates/buttplug_server/src/device/protocol_impl/foreo.rs similarity index 62% rename from buttplug/src/server/device/protocol/foreo.rs rename to crates/buttplug_server/src/device/protocol_impl/foreo.rs index 691a7c96c..c24e77edd 100644 --- a/buttplug/src/server/device/protocol/foreo.rs +++ b/crates/buttplug_server/src/device/protocol_impl/foreo.rs @@ -5,22 +5,25 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::server::device::configuration::ProtocolCommunicationSpecifier; -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - configuration::{UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, }, }; use async_trait::async_trait; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use buttplug_server_device_config::{ + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; use std::sync::Arc; +use uuid::Uuid; generic_protocol_initializer_setup!(Foreo, "foreo"); @@ -32,7 +35,7 @@ impl ProtocolInitializer for ForeoInitializer { async fn initialize( &mut self, hardware: Arc, - _: &UserDeviceDefinition, + _: &DeviceDefinition, ) -> Result, ButtplugDeviceError> { let lname = hardware.name().to_lowercase(); let mut ph = Foreo::default(); @@ -54,18 +57,16 @@ pub struct Foreo { } impl ProtocolHandler for Foreo { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, - vec![0x01, self.mode, scalar as u8], + vec![0x01, self.mode, speed as u8], true, ) .into()]) diff --git a/buttplug/src/server/device/protocol/fox.rs b/crates/buttplug_server/src/device/protocol_impl/fox.rs similarity index 54% rename from buttplug/src/server/device/protocol/fox.rs rename to crates/buttplug_server/src/device/protocol_impl/fox.rs index 2832728fc..552529665 100644 --- a/buttplug/src/server/device/protocol/fox.rs +++ b/crates/buttplug_server/src/device/protocol_impl/fox.rs @@ -5,13 +5,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(Fox, "fox"); @@ -19,18 +20,16 @@ generic_protocol_setup!(Fox, "fox"); pub struct Fox {} impl ProtocolHandler for Fox { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, - vec![0x03, 0x01, 0x01, 0xfe, scalar as u8], + vec![0x03, 0x01, 0x01, 0xfe, speed as u8], false, ) .into()]) diff --git a/buttplug/src/server/device/protocol/fredorch.rs b/crates/buttplug_server/src/device/protocol_impl/fredorch.rs similarity index 80% rename from buttplug/src/server/device/protocol/fredorch.rs rename to crates/buttplug_server/src/device/protocol_impl/fredorch.rs index fce727ce4..ce4895d28 100644 --- a/buttplug/src/server/device/protocol/fredorch.rs +++ b/crates/buttplug_server/src/device/protocol_impl/fredorch.rs @@ -5,25 +5,23 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::server::device::configuration::ProtocolCommunicationSpecifier; -use crate::{ - core::{ - errors::ButtplugDeviceError, - message::{self, Endpoint}, +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareEvent, HardwareSubscribeCmd, HardwareWriteCmd}, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, }, - server::device::{ - configuration::{UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareEvent, HardwareSubscribeCmd, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, - }, - util::sleep, }; use async_trait::async_trait; +use buttplug_core::{errors::ButtplugDeviceError, util::sleep}; +use buttplug_server_device_config::{ + Endpoint, + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; use futures::FutureExt; use std::{ sync::{ @@ -32,6 +30,8 @@ use std::{ }, time::Duration, }; +use tokio::select; +use uuid::{uuid, Uuid}; use super::fleshlight_launch_helper::calculate_speed; @@ -77,6 +77,8 @@ pub fn crc16(data: &[u8]) -> [u8; 2] { [n, o] } +const FREDORCH_PROTOCOL_UUID: Uuid = uuid!("f9a83f46-0af5-4766-84f0-a1cca6614115"); + generic_protocol_initializer_setup!(Fredorch, "fredorch"); #[derive(Default)] @@ -87,11 +89,14 @@ impl ProtocolInitializer for FredorchInitializer { async fn initialize( &mut self, hardware: Arc, - _: &UserDeviceDefinition, + _: &DeviceDefinition, ) -> Result, ButtplugDeviceError> { let mut event_receiver = hardware.event_stream(); hardware - .subscribe(&HardwareSubscribeCmd::new(Endpoint::Rx)) + .subscribe(&HardwareSubscribeCmd::new( + FREDORCH_PROTOCOL_UUID, + Endpoint::Rx, + )) .await?; let init: Vec<(String, Vec)> = vec![ @@ -145,7 +150,12 @@ impl ProtocolInitializer for FredorchInitializer { data.1.push(crc[1]); debug!("Fredorch: {} - sent {:?}", data.0, data.1); hardware - .write_value(&HardwareWriteCmd::new(Endpoint::Tx, data.1.clone(), false)) + .write_value(&HardwareWriteCmd::new( + &[FREDORCH_PROTOCOL_UUID], + Endpoint::Tx, + data.1.clone(), + false, + )) .await?; select! { @@ -182,29 +192,23 @@ pub struct Fredorch { } impl ProtocolHandler for Fredorch { - fn handle_linear_cmd( + fn handle_position_with_duration_cmd( &self, - message: message::LinearCmdV4, + _feature_index: u32, + _feature_id: Uuid, + position: u32, + duration: u32, ) -> Result, ButtplugDeviceError> { - let v = message.vectors()[0].clone(); // In the protocol, we know max speed is 99, so convert here. We have to // use AtomicU8 because there's no AtomicF64 yet. - let previous_position = self.previous_position.load(Ordering::SeqCst); - let distance = (previous_position as f64 - (v.position() * 99f64)).abs() / 99f64; - let fl_cmd = message::FleshlightLaunchFW12CmdV0::new( - 0, - (v.position() * 99f64) as u8, - (calculate_speed(distance, v.duration()) * 99f64) as u8, - ); - self.handle_fleshlight_launch_fw12_cmd(fl_cmd) - } + let previous_position = self.previous_position.load(Ordering::Relaxed); + let distance = (previous_position as f64 - position as f64).abs() / 99f64; - fn handle_fleshlight_launch_fw12_cmd( - &self, - message: message::FleshlightLaunchFW12CmdV0, - ) -> Result, ButtplugDeviceError> { - let position = ((message.position() as f64 / 99.0) * 150.0) as u8; - let speed = ((message.speed() as f64 / 99.0) * 15.0) as u8; + // TODO Clean this up, we do not need the conversions anymore since we'll have done the + // calculations before we get to the protocol layer. + let position = ((position as f64 / 99.0) * 150.0) as u8; + let converted_speed = calculate_speed(distance, duration.try_into().unwrap()) * 99f64; + let speed = ((converted_speed / 99.0) * 15.0) as u8; let mut data: Vec = vec![ 0x01, 0x10, 0x00, 0x6B, 0x00, 0x05, 0x0a, 0x00, speed, 0x00, speed, 0x00, position, 0x00, position, 0x00, 0x01, @@ -212,7 +216,13 @@ impl ProtocolHandler for Fredorch { let crc = crc16(&data); data.push(crc[0]); data.push(crc[1]); - self.previous_position.store(position, Ordering::SeqCst); - Ok(vec![HardwareWriteCmd::new(Endpoint::Tx, data, false).into()]) + self.previous_position.store(position, Ordering::Relaxed); + Ok(vec![HardwareWriteCmd::new( + &[FREDORCH_PROTOCOL_UUID], + Endpoint::Tx, + data, + false, + ) + .into()]) } } diff --git a/buttplug/src/server/device/protocol/fredorch_rotary.rs b/crates/buttplug_server/src/device/protocol_impl/fredorch_rotary.rs similarity index 77% rename from buttplug/src/server/device/protocol/fredorch_rotary.rs rename to crates/buttplug_server/src/device/protocol_impl/fredorch_rotary.rs index 8eed9ade1..831d04e94 100644 --- a/buttplug/src/server/device/protocol/fredorch_rotary.rs +++ b/crates/buttplug_server/src/device/protocol_impl/fredorch_rotary.rs @@ -5,22 +5,26 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::server::device::configuration::ProtocolCommunicationSpecifier; -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - configuration::{UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareEvent, HardwareSubscribeCmd, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareEvent, HardwareSubscribeCmd, HardwareWriteCmd}, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, }, - util::{async_manager, sleep}, }; use async_trait::async_trait; +use buttplug_core::{ + errors::ButtplugDeviceError, + util::{async_manager, sleep}, +}; +use buttplug_server_device_config::{ + Endpoint, + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; use futures::FutureExt; use std::{ sync::{ @@ -29,9 +33,11 @@ use std::{ }, time::Duration, }; +use tokio::select; +use uuid::{uuid, Uuid}; const FREDORCH_COMMAND_TIMEOUT_MS: u64 = 100; - +const FREDORCH_ROTORY_PROTOCOL_UUID: Uuid = uuid!("0ec6598a-bfd1-4f47-9738-e8cd8ace6473"); generic_protocol_initializer_setup!(FredorchRotary, "fredorch-rotary"); #[derive(Default)] @@ -42,7 +48,7 @@ impl ProtocolInitializer for FredorchRotaryInitializer { async fn initialize( &mut self, hardware: Arc, - _: &UserDeviceDefinition, + _: &DeviceDefinition, ) -> Result, ButtplugDeviceError> { warn!( "FredorchRotary device doesn't provide state feedback. If the device beeps twice, it is powered off and must be reconnected before it can be controlled!" @@ -50,7 +56,10 @@ impl ProtocolInitializer for FredorchRotaryInitializer { let mut event_receiver = hardware.event_stream(); hardware - .subscribe(&HardwareSubscribeCmd::new(Endpoint::Rx)) + .subscribe(&HardwareSubscribeCmd::new( + FREDORCH_ROTORY_PROTOCOL_UUID, + Endpoint::Rx, + )) .await?; let init: Vec<(String, Vec)> = vec![ @@ -77,7 +86,12 @@ impl ProtocolInitializer for FredorchRotaryInitializer { for data in init { debug!("FredorchRotary: {} - sent {:?}", data.0, data.1); hardware - .write_value(&HardwareWriteCmd::new(Endpoint::Tx, data.1.clone(), false)) + .write_value(&HardwareWriteCmd::new( + &[FREDORCH_ROTORY_PROTOCOL_UUID], + Endpoint::Tx, + data.1.clone(), + false, + )) .await?; select! { @@ -116,8 +130,8 @@ async fn speed_update_handler( info!("Entering FredorchRotary Control Loop"); loop { - let ts = target_speed.load(Ordering::SeqCst); - let cs = current_speed.load(Ordering::SeqCst); + let ts = target_speed.load(Ordering::Relaxed); + let cs = current_speed.load(Ordering::Relaxed); trace!("FredorchRotary: {}c vs {}t", cs, ts); @@ -131,6 +145,7 @@ async fn speed_update_handler( }; let update = device .write_value(&HardwareWriteCmd::new( + &[FREDORCH_ROTORY_PROTOCOL_UUID], Endpoint::Tx, vec![0x55u8, 0x03, cmd, cmd + 3, 0xaa], false, @@ -160,7 +175,7 @@ async fn speed_update_handler( }, 0, ), - Ordering::SeqCst, + Ordering::Relaxed, ); continue; } else { @@ -191,17 +206,18 @@ impl FredorchRotary { } impl ProtocolHandler for FredorchRotary { - fn handle_scalar_oscillate_cmd( + fn handle_output_oscillate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + _feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { - let speed: u8 = scalar as u8; - - self.target_speed.store(speed, Ordering::SeqCst); + let speed: u8 = speed as u8; + self.target_speed.store(speed, Ordering::Relaxed); if speed == 0 { - self.current_speed.store(speed, Ordering::SeqCst); + self.current_speed.store(speed, Ordering::Relaxed); Ok(vec![HardwareWriteCmd::new( + &[FREDORCH_ROTORY_PROTOCOL_UUID], Endpoint::Tx, vec![0x55, 0x03, 0x24, 0x27, 0xaa], false, diff --git a/buttplug/src/server/device/protocol/galaku.rs b/crates/buttplug_server/src/device/protocol_impl/galaku.rs similarity index 54% rename from buttplug/src/server/device/protocol/galaku.rs rename to crates/buttplug_server/src/device/protocol_impl/galaku.rs index 8a060b471..2ff58033a 100644 --- a/buttplug/src/server/device/protocol/galaku.rs +++ b/crates/buttplug_server/src/device/protocol_impl/galaku.rs @@ -6,33 +6,37 @@ // for full license information. use async_trait::async_trait; +use std::sync::atomic::{AtomicU8, Ordering}; use std::sync::Arc; +use uuid::{uuid, Uuid}; use futures_util::future::BoxFuture; use futures_util::{future, FutureExt}; -use crate::core::message::{ActuatorType, ButtplugDeviceMessage, SensorType}; -use crate::core::message::{ - SensorReadCmdV4, - SensorReadingV4, - SensorSubscribeCmdV4, - SensorUnsubscribeCmdV4, +use buttplug_core::message::{InputData, InputReadingV4, InputType, InputTypeData}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; + +use buttplug_server_device_config::{ + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, }; -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - generic_protocol_initializer_setup, - server::device::{ - configuration::UserDeviceIdentifier, - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition}, - hardware::{ - Hardware, - HardwareCommand, - HardwareEvent, - HardwareSubscribeCmd, - HardwareUnsubscribeCmd, - HardwareWriteCmd, - }, - protocol::{ProtocolHandler, ProtocolIdentifier, ProtocolInitializer}, + +use crate::device::{ + hardware::{ + Hardware, + HardwareCommand, + HardwareEvent, + HardwareSubscribeCmd, + HardwareUnsubscribeCmd, + HardwareWriteCmd, + }, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, }, }; @@ -45,7 +49,7 @@ static KEY_TAB: [[u32; 12]; 4] = [ fn get_tab_key(r: usize, t: usize) -> u32 { let e = 3 & r; - return KEY_TAB[e][t]; + KEY_TAB[e][t] } fn encrypt(data: Vec) -> Vec { @@ -55,17 +59,17 @@ fn encrypt(data: Vec) -> Vec { let u = (a ^ data[0] ^ data[i]) + a; new_data.push(u); } - return new_data; + new_data } fn decrypt(data: Vec) -> Vec { let mut new_data = vec![data[0]]; for i in 1..data.len() { let a = get_tab_key(data[i - 1] as usize, i); - let u = data[i] as i32 - a as i32 ^ data[0] as i32 ^ a as i32; + let u = (data[i] as i32 - a as i32) ^ data[0] as i32 ^ a as i32; new_data.push(if u < 0 { (u + 256) as u32 } else { u as u32 }); } - return new_data; + new_data } fn send_bytes(data: Vec) -> Vec { @@ -76,7 +80,7 @@ fn send_bytes(data: Vec) -> Vec { for value in encrypt(new_data) { uint8_array.push(value as u8); } - return uint8_array; + uint8_array } fn read_value(data: Vec) -> u32 { @@ -85,13 +89,14 @@ fn read_value(data: Vec) -> u32 { uint32_data.push(value as u32); } let decrypted_data = decrypt(uint32_data); - if decrypted_data.len() > 0 { + if !decrypted_data.is_empty() { decrypted_data[4] } else { 0 } } +const GALAKU_PROTOCOL_UUID: Uuid = uuid!("766d15d5-0f43-4768-a73a-96ff48bc389e"); generic_protocol_initializer_setup!(Galaku, "galaku"); #[derive(Default)] @@ -102,7 +107,7 @@ impl ProtocolInitializer for GalakuInitializer { async fn initialize( &mut self, hardware: Arc, - _: &UserDeviceDefinition, + _: &DeviceDefinition, ) -> Result, ButtplugDeviceError> { let mut protocol = Galaku::default(); protocol.is_caiping_pump_device = false; @@ -113,96 +118,93 @@ impl ProtocolInitializer for GalakuInitializer { } } -#[derive(Default)] pub struct Galaku { is_caiping_pump_device: bool, + speeds: [AtomicU8; 2], } -impl ProtocolHandler for Galaku { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn needs_full_command_set(&self) -> bool { - true - } - - fn handle_scalar_vibrate_cmd( - &self, - _index: u32, - scalar: u32, - ) -> Result, ButtplugDeviceError> { - let data: Vec = vec![90, 0, 0, 1, 49, scalar, 0, 0, 0, 0]; - Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - send_bytes(data), - false, - ) - .into()]) +impl Default for Galaku { + fn default() -> Self { + Self { + is_caiping_pump_device: false, + speeds: [AtomicU8::new(0), AtomicU8::new(0)], + } } +} - fn handle_scalar_cmd( +impl ProtocolHandler for Galaku { + fn handle_output_vibrate_cmd( &self, - commands: &[Option<(ActuatorType, u32)>], + feature_index: u32, + _feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { - if commands.len() == 1 { - if let Some(cmd) = commands[0] { - if self.is_caiping_pump_device { - let data: Vec = vec![ - 0xAA, - 1, - 10, - 3, - cmd.1 as u8, - if cmd.1 == 0 { 0 } else { 1 }, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - ]; - return Ok(vec![HardwareWriteCmd::new(Endpoint::Tx, data, false).into()]); - } else { - let data: Vec = vec![90, 0, 0, 1, 49, cmd.1, 0, 0, 0, 0]; - return Ok(vec![HardwareWriteCmd::new( - Endpoint::Tx, - send_bytes(data), - false, - ) - .into()]); - } - } + if self.is_caiping_pump_device { + let data: Vec = vec![ + 0xAA, + 1, + 10, + 3, + speed as u8, + if speed == 0 { 0 } else { 1 }, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ]; + Ok(vec![HardwareWriteCmd::new( + &[GALAKU_PROTOCOL_UUID], + Endpoint::Tx, + data, + false, + ) + .into()]) } else { - let cmd0 = commands[0].unwrap_or((ActuatorType::Vibrate, 0)); - let cmd1 = commands[1].unwrap_or((ActuatorType::Vibrate, 0)); - - let data: Vec = vec![90, 0, 0, 1, 64, 3, cmd0.1, cmd1.1, 0, 0]; - return Ok(vec![HardwareWriteCmd::new( + self.speeds[feature_index as usize].store(speed as u8, Ordering::Relaxed); + let data: Vec = vec![ + 90, + 0, + 0, + 1, + 49, + self.speeds[0].load(Ordering::Relaxed) as u32, + self.speeds[1].load(Ordering::Relaxed) as u32, + 0, + 0, + 0, + ]; + Ok(vec![HardwareWriteCmd::new( + &[GALAKU_PROTOCOL_UUID], Endpoint::Tx, send_bytes(data), false, ) - .into()]); + .into()]) } - Ok(vec![]) } - fn handle_sensor_subscribe_cmd( + fn handle_input_subscribe_cmd( &self, + _device_index: u32, device: Arc, - message: &SensorSubscribeCmdV4, + _feature_index: u32, + feature_id: Uuid, + sensor_type: InputType, ) -> BoxFuture> { - let message = message.clone(); - match message.sensor_type() { - SensorType::Battery => { + match sensor_type { + InputType::Battery => { async move { device - .subscribe(&HardwareSubscribeCmd::new(Endpoint::RxBLEBattery)) + .subscribe(&HardwareSubscribeCmd::new( + feature_id, + Endpoint::RxBLEBattery, + )) .await?; Ok(()) } @@ -215,17 +217,21 @@ impl ProtocolHandler for Galaku { } } - fn handle_sensor_unsubscribe_cmd( + fn handle_input_unsubscribe_cmd( &self, device: Arc, - message: &SensorUnsubscribeCmdV4, + _feature_index: u32, + feature_id: Uuid, + sensor_type: InputType, ) -> BoxFuture> { - let message = message.clone(); - match message.sensor_type() { - SensorType::Battery => { + match sensor_type { + InputType::Battery => { async move { device - .unsubscribe(&HardwareUnsubscribeCmd::new(Endpoint::RxBLEBattery)) + .unsubscribe(&HardwareUnsubscribeCmd::new( + feature_id, + Endpoint::RxBLEBattery, + )) .await?; Ok(()) } @@ -240,17 +246,27 @@ impl ProtocolHandler for Galaku { fn handle_battery_level_cmd( &self, + device_index: u32, device: Arc, - message: SensorReadCmdV4, - ) -> BoxFuture> { + feature_index: u32, + feature_id: Uuid, + ) -> BoxFuture> { let data: Vec = vec![90, 0, 0, 1, 19, 0, 0, 0, 0, 0]; let mut device_notification_receiver = device.event_stream(); async move { device - .subscribe(&HardwareSubscribeCmd::new(Endpoint::RxBLEBattery)) + .subscribe(&HardwareSubscribeCmd::new( + feature_id, + Endpoint::RxBLEBattery, + )) .await?; device - .write_value(&HardwareWriteCmd::new(Endpoint::Tx, send_bytes(data), true)) + .write_value(&HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + send_bytes(data), + true, + )) .await?; while let Ok(event) = device_notification_receiver.recv().await { return match event { @@ -258,13 +274,12 @@ impl ProtocolHandler for Galaku { if endpoint != Endpoint::RxBLEBattery { continue; } - let battery_reading = SensorReadingV4::new( - message.device_index(), - *message.feature_index(), - *message.sensor_type(), - vec![read_value(data) as i32], + let battery_reading = InputReadingV4::new( + device_index, + feature_index, + InputTypeData::Battery(InputData::new(data[0])) ); - Ok(battery_reading.into()) + Ok(battery_reading) } HardwareEvent::Disconnected(_) => Err(ButtplugDeviceError::ProtocolSpecificError( "Galaku".to_owned(), diff --git a/crates/buttplug_server/src/device/protocol_impl/galaku_pump.rs b/crates/buttplug_server/src/device/protocol_impl/galaku_pump.rs new file mode 100644 index 000000000..0e80e8488 --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/galaku_pump.rs @@ -0,0 +1,88 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2023 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 uuid::{uuid, Uuid}; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use std::num::Wrapping; +use std::sync::atomic::{AtomicU8, Ordering}; + +static KEY_TAB: [[u8; 12]; 4] = [ + [0, 24, 0x98, 0xf7, 0xa5, 61, 13, 41, 37, 80, 68, 70], + [0, 69, 110, 106, 111, 120, 32, 83, 45, 49, 46, 55], + [0, 101, 120, 32, 84, 111, 121, 115, 10, 0x8e, 0x9d, 0xa3], + [0, 0xc5, 0xd6, 0xe7, 0xf8, 10, 50, 32, 111, 98, 13, 10], +]; + +const GALAKU_PUMP_PROTOCOL_UUID: Uuid = uuid!("165ae3a9-33be-46a8-b438-9a6fc0f183cb"); +generic_protocol_setup!(GalakuPump, "galaku-pump"); + +pub struct GalakuPump { + speeds: [AtomicU8; 2], +} + +impl Default for GalakuPump { + fn default() -> Self { + Self { + speeds: [AtomicU8::new(0), AtomicU8::new(0)], + } + } +} + +impl GalakuPump { + fn hardware_command(&self) -> Vec { + let mut data: Vec = vec![ + 0x23, + 0x5a, + 0x00, + 0x00, + 0x01, + 0x60, + 0x03, + self.speeds[0].load(Ordering::Relaxed), + self.speeds[1].load(Ordering::Relaxed), + 0x00, + 0x00, + ]; + data.push(data.iter().fold(0u8, |c, b| (Wrapping(c) + Wrapping(*b)).0)); + + let mut data2: Vec = vec![0x23]; + for i in 1..data.len() { + let k = KEY_TAB[(data2[i - 1] & 3) as usize][i]; + data2.push((Wrapping((k ^ 0x23) ^ data[i]) + Wrapping(k)).0); + } + + vec![HardwareWriteCmd::new(&[GALAKU_PUMP_PROTOCOL_UUID], Endpoint::Tx, data2, true).into()] + } +} + +impl ProtocolHandler for GalakuPump { + fn handle_output_oscillate_cmd( + &self, + _feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.speeds[0].store(speed as u8, Ordering::Relaxed); + Ok(self.hardware_command()) + } + + fn handle_output_vibrate_cmd( + &self, + _feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.speeds[1].store(speed as u8, Ordering::Relaxed); + Ok(self.hardware_command()) + } +} diff --git a/buttplug/src/server/device/protocol/hgod.rs b/crates/buttplug_server/src/device/protocol_impl/hgod.rs similarity index 67% rename from buttplug/src/server/device/protocol/hgod.rs rename to crates/buttplug_server/src/device/protocol_impl/hgod.rs index 4485118f7..dab2c1a64 100644 --- a/buttplug/src/server/device/protocol/hgod.rs +++ b/crates/buttplug_server/src/device/protocol_impl/hgod.rs @@ -5,25 +5,26 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::server::device::configuration::ProtocolCommunicationSpecifier; -use crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, }, - server::device::{ - configuration::{UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, - }, - util::{async_manager, sleep}, }; use async_trait::async_trait; +use buttplug_core::{ + errors::ButtplugDeviceError, + util::{async_manager, sleep}, +}; +use buttplug_server_device_config::{ + Endpoint, + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; use std::{ sync::{ atomic::{AtomicU8, Ordering}, @@ -31,10 +32,12 @@ use std::{ }, time::Duration, }; +use uuid::{uuid, Uuid}; // Time between Hgod update commands, in milliseconds. const HGOD_COMMAND_DELAY_MS: u64 = 100; +const HGOD_PROTOCOL_UUID: Uuid = uuid!("0a086d5b-9918-4b73-b2dd-86ed66de6f51"); generic_protocol_initializer_setup!(Hgod, "hgod"); #[derive(Default)] @@ -45,7 +48,7 @@ impl ProtocolInitializer for HgodInitializer { async fn initialize( &mut self, hardware: Arc, - _: &UserDeviceDefinition, + _: &DeviceDefinition, ) -> Result, ButtplugDeviceError> { Ok(Arc::new(Hgod::new(hardware))) } @@ -71,11 +74,16 @@ impl Hgod { // HGod toys vibes only last ~100ms seconds. async fn send_hgod_updates(device: Arc, data: Arc) { loop { - let speed = data.load(Ordering::SeqCst); + let speed = data.load(Ordering::Relaxed); let command = vec![0x55, 0x04, 0, 0, 0, speed]; if speed > 0 { if let Err(e) = device - .write_value(&HardwareWriteCmd::new(Endpoint::Tx, command, false)) + .write_value(&HardwareWriteCmd::new( + &[HGOD_PROTOCOL_UUID], + Endpoint::Tx, + command, + false, + )) .await { error!( @@ -90,13 +98,13 @@ async fn send_hgod_updates(device: Arc, data: Arc) { } impl ProtocolHandler for Hgod { - fn handle_scalar_cmd( + fn handle_output_vibrate_cmd( &self, - commands: &[Option<(ActuatorType, u32)>], + _feature_index: u32, + _feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { - if let Some(cmd) = commands[0] { - self.last_command.store(cmd.1 as u8, Ordering::SeqCst); - } + self.last_command.store(speed as u8, Ordering::Relaxed); Ok(vec![]) } } diff --git a/buttplug/src/server/device/protocol/hismith.rs b/crates/buttplug_server/src/device/protocol_impl/hismith.rs similarity index 66% rename from buttplug/src/server/device/protocol/hismith.rs rename to crates/buttplug_server/src/device/protocol_impl/hismith.rs index f8441a344..7eea9144a 100644 --- a/buttplug/src/server/device/protocol/hismith.rs +++ b/crates/buttplug_server/src/device/protocol_impl/hismith.rs @@ -5,21 +5,26 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::server::device::configuration::ProtocolCommunicationSpecifier; -use crate::server::device::protocol::hismith_mini::HismithMiniInitializer; -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - configuration::{UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareReadCmd, HardwareWriteCmd}, - protocol::{ProtocolHandler, ProtocolIdentifier, ProtocolInitializer}, - }, +use super::hismith_mini::HismithMiniInitializer; +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareReadCmd, HardwareWriteCmd}, + protocol::{ProtocolHandler, ProtocolIdentifier, ProtocolInitializer}, }; use async_trait::async_trait; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use buttplug_server_device_config::{ + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; use std::sync::Arc; +use uuid::{uuid, Uuid}; + +const HISMITH_PROTOCOL_UUID: Uuid = uuid!("e59f9c5d-bb4a-4a9c-ab57-0ceb43af1da7"); pub mod setup { - use crate::server::device::protocol::{ProtocolIdentifier, ProtocolIdentifierFactory}; + use crate::device::protocol::{ProtocolIdentifier, ProtocolIdentifierFactory}; #[derive(Default)] pub struct HismithIdentifierFactory {} @@ -37,7 +42,7 @@ pub mod setup { #[derive(Default)] pub struct HismithIdentifier {} -const LEGACY_HISMITHS: [&'static str; 6] = ["1001", "1002", "1003", "3001", "2001", "1006"]; +const LEGACY_HISMITHS: [&str; 6] = ["1001", "1002", "1003", "3001", "2001", "1006"]; #[async_trait] impl ProtocolIdentifier for HismithIdentifier { @@ -47,13 +52,18 @@ impl ProtocolIdentifier for HismithIdentifier { _: ProtocolCommunicationSpecifier, ) -> Result<(UserDeviceIdentifier, Box), ButtplugDeviceError> { let result = hardware - .read_value(&HardwareReadCmd::new(Endpoint::RxBLEModel, 128, 500)) + .read_value(&HardwareReadCmd::new( + HISMITH_PROTOCOL_UUID, + Endpoint::RxBLEModel, + 128, + 500, + )) .await?; let identifier = result .data() .iter() - .map(|b| format!("{:02x}", b)) + .map(|b| format!("{b:02x}")) .collect::(); info!("Hismith Device Identifier: {}", identifier); @@ -80,7 +90,7 @@ impl ProtocolInitializer for HismithInitializer { async fn initialize( &mut self, _: Arc, - _: &UserDeviceDefinition, + _: &DeviceDefinition, ) -> Result, ButtplugDeviceError> { Ok(Arc::new(Hismith::default())) } @@ -90,19 +100,17 @@ impl ProtocolInitializer for HismithInitializer { pub struct Hismith {} impl ProtocolHandler for Hismith { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_oscillate_cmd( + fn handle_output_oscillate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { let idx: u8 = 0x04; - let speed: u8 = scalar as u8; + let speed: u8 = speed as u8; Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, vec![0xAA, idx, speed, speed + idx], false, @@ -110,21 +118,23 @@ impl ProtocolHandler for Hismith { .into()]) } - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - index: u32, - scalar: u32, + feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { // Wildolo has a vibe at index 0 using id 4 // The thrusting stroker has a vibe at index 1 using id 6 (and the weird 0xf0 off) - let idx: u8 = if index == 0 { 0x04 } else { 0x06 }; - let speed: u8 = if index != 0 && scalar == 0 { + let idx: u8 = if feature_index == 0 { 0x04 } else { 0x06 }; + let speed: u8 = if feature_index != 0 && speed == 0 { 0xf0 } else { - scalar as u8 + speed as u8 }; Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, vec![0xAA, idx, speed, speed + idx], false, diff --git a/buttplug/src/server/device/protocol/hismith_mini.rs b/crates/buttplug_server/src/device/protocol_impl/hismith_mini.rs similarity index 65% rename from buttplug/src/server/device/protocol/hismith_mini.rs rename to crates/buttplug_server/src/device/protocol_impl/hismith_mini.rs index 2dcffa0a4..ddb007bae 100644 --- a/buttplug/src/server/device/protocol/hismith_mini.rs +++ b/crates/buttplug_server/src/device/protocol_impl/hismith_mini.rs @@ -5,22 +5,28 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{ - errors::ButtplugDeviceError, - message::{Endpoint, FeatureType}, - }, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareReadCmd, HardwareWriteCmd}, - protocol::{ProtocolHandler, ProtocolIdentifier, ProtocolInitializer}, - }, +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareReadCmd, HardwareWriteCmd}, + protocol::{ProtocolHandler, ProtocolIdentifier, ProtocolInitializer}, }; use async_trait::async_trait; +use buttplug_core::{ + errors::ButtplugDeviceError, + message::FeatureType, +}; +use buttplug_server_device_config::{ + Endpoint, + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; use std::sync::Arc; +use uuid::{uuid, Uuid}; + +const HISMITH_MINI_PROTOCOL_UUID: Uuid = uuid!("94befc1a-9859-4bf6-99ee-5678c89237a7"); pub mod setup { - use crate::server::device::protocol::{ProtocolIdentifier, ProtocolIdentifierFactory}; + use crate::device::protocol::{ProtocolIdentifier, ProtocolIdentifierFactory}; #[derive(Default)] pub struct HismithMiniIdentifierFactory {} @@ -46,13 +52,18 @@ impl ProtocolIdentifier for HismithMiniIdentifier { _: ProtocolCommunicationSpecifier, ) -> Result<(UserDeviceIdentifier, Box), ButtplugDeviceError> { let result = hardware - .read_value(&HardwareReadCmd::new(Endpoint::RxBLEModel, 128, 500)) + .read_value(&HardwareReadCmd::new( + HISMITH_MINI_PROTOCOL_UUID, + Endpoint::RxBLEModel, + 128, + 500, + )) .await?; let identifier = result .data() .iter() - .map(|b| format!("{:02x}", b)) + .map(|b| format!("{b:02x}")) .collect::(); info!("Hismith Device Identifier: {}", identifier); @@ -71,19 +82,19 @@ impl ProtocolInitializer for HismithMiniInitializer { async fn initialize( &mut self, _: Arc, - device_definition: &UserDeviceDefinition, + device_definition: &DeviceDefinition, ) -> Result, ButtplugDeviceError> { Ok(Arc::new(HismithMini { dual_vibe: device_definition .features() .iter() - .filter(|x| *x.feature_type() == FeatureType::Vibrate) + .filter(|x| x.feature_type() == FeatureType::Vibrate) .count() >= 2, second_constrict: device_definition .features() .iter() - .position(|x| *x.feature_type() == FeatureType::Constrict) + .position(|x| x.feature_type() == FeatureType::Constrict) .unwrap_or(0) == 1, })) @@ -97,19 +108,17 @@ pub struct HismithMini { } impl ProtocolHandler for HismithMini { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_oscillate_cmd( + fn handle_output_oscillate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { let idx: u8 = 0x03; - let speed: u8 = scalar as u8; + let speed: u8 = speed as u8; Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, vec![0xCC, idx, speed, speed + idx], false, @@ -117,19 +126,21 @@ impl ProtocolHandler for HismithMini { .into()]) } - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - index: u32, - scalar: u32, + feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { - let idx: u8 = if !self.dual_vibe || index == 1 { + let idx: u8 = if !self.dual_vibe || feature_index == 1 { 0x05 } else { 0x03 }; - let speed: u8 = scalar as u8; + let speed: u8 = speed as u8; Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, vec![0xCC, idx, speed, speed + idx], false, @@ -137,15 +148,17 @@ impl ProtocolHandler for HismithMini { .into()]) } - fn handle_scalar_constrict_cmd( + fn handle_output_constrict_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + level: u32, ) -> Result, ButtplugDeviceError> { let idx: u8 = if self.second_constrict { 0x05 } else { 0x03 }; - let speed: u8 = scalar as u8; + let speed: u8 = level as u8; Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, vec![0xCC, idx, speed, speed + idx], false, diff --git a/crates/buttplug_server/src/device/protocol_impl/htk_bm.rs b/crates/buttplug_server/src/device/protocol_impl/htk_bm.rs new file mode 100644 index 000000000..e70acb3b7 --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/htk_bm.rs @@ -0,0 +1,61 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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::sync::atomic::{AtomicU8, Ordering}; + +use uuid::{uuid, Uuid}; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; + +const HTK_BM_PROTOCOL_UUID: Uuid = uuid!("4c70cb95-d3d9-4288-81ab-be845f9ad1fe"); +generic_protocol_setup!(HtkBm, "htk_bm"); + +pub struct HtkBm { + speeds: [AtomicU8; 2], +} + +impl Default for HtkBm { + fn default() -> Self { + Self { + speeds: [AtomicU8::new(0), AtomicU8::new(0)], + } + } +} + +impl ProtocolHandler for HtkBm { + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.speeds[feature_index as usize].store(speed as u8, Ordering::Relaxed); + + let mut data: u8 = 15; + let left = self.speeds[0].load(Ordering::Relaxed); + let right = self.speeds[1].load(Ordering::Relaxed); + if left != 0 && right != 0 { + data = 11 // both (normal mode) + } else if left != 0 { + data = 12 // left only + } else if right != 0 { + data = 13 // right only + } + Ok(vec![HardwareWriteCmd::new( + &[HTK_BM_PROTOCOL_UUID], + Endpoint::Tx, + vec![data], + false, + ) + .into()]) + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/itoys.rs b/crates/buttplug_server/src/device/protocol_impl/itoys.rs new file mode 100644 index 000000000..d92962b84 --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/itoys.rs @@ -0,0 +1,55 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; + +generic_protocol_setup!(IToys, "itoys"); + +#[derive(Default)] +pub struct IToys {} + +impl ProtocolHandler for IToys { + fn handle_output_vibrate_cmd( + &self, + _feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + vec![0xa0, 0x01, 0x00, 0x00, speed as u8, 0xff], + false, + ) + .into()]) + } + + + fn handle_output_oscillate_cmd(&self, _feature_index: u32, feature_id: Uuid, speed: u32) -> Result, ButtplugDeviceError> { + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + vec![ + 0xa0, + 0x06, + if speed == 0 { 0x00 } else { 0x01 }, + 0x00, + if speed == 0 { 0x00 } else { 0x01 }, + speed as u8, + ], + false, + ) + .into()]) + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/jejoue.rs b/crates/buttplug_server/src/device/protocol_impl/jejoue.rs new file mode 100644 index 000000000..0e98c51e9 --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/jejoue.rs @@ -0,0 +1,74 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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::sync::atomic::{AtomicU8, Ordering}; + +use uuid::{uuid, Uuid}; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; + +const JEJOUE_PROTOCOL_UUID: Uuid = uuid!("d3dd2bf5-b029-4bc1-9466-39f82c2e3258"); +generic_protocol_setup!(JeJoue, "jejoue"); + +pub struct JeJoue { + speeds: [AtomicU8; 2], +} + +impl Default for JeJoue { + fn default() -> Self { + Self { + speeds: [AtomicU8::new(0), AtomicU8::new(0)], + } + } +} + +impl ProtocolHandler for JeJoue { + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.speeds[feature_index as usize].store(speed as u8, Ordering::Relaxed); + + // Default to both vibes + let mut pattern: u8 = 1; + + // Use vibe 1 as speed + let mut speed = self.speeds[0].load(Ordering::Relaxed); + let vibe1_running = speed > 0; + let mut vibe2_running = false; + // Unless it's zero, then give vibe 2 a chance + if !vibe1_running { + speed = self.speeds[1].load(Ordering::Relaxed); + + // If we've vibing on 2 only, then change the pattern + if speed != 0 { + vibe2_running = true; + pattern = 3; + } + } + + // If we've vibing on 1 only, then change the pattern + if pattern == 1 && speed != 0 && !vibe2_running { + pattern = 2; + } + + Ok(vec![HardwareWriteCmd::new( + &[JEJOUE_PROTOCOL_UUID], + Endpoint::Tx, + vec![pattern, speed], + false, + ) + .into()]) + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/joyhub/joyhub.rs b/crates/buttplug_server/src/device/protocol_impl/joyhub/joyhub.rs new file mode 100644 index 000000000..4cc5512aa --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/joyhub/joyhub.rs @@ -0,0 +1,97 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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::sync::atomic::{AtomicU8, Ordering}; + +use uuid::{uuid, Uuid}; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; + +const JOYHUB_PROTOCOL_UUID: Uuid = uuid!("c0f6785a-0056-4a2a-a2a9-dc7ca4ae2a0d"); + +generic_protocol_setup!(JoyHub, "joyhub"); + +#[derive(Default)] +pub struct JoyHub { + last_cmds: [AtomicU8; 3] +} + +impl JoyHub { + fn form_hardware_command(&self, index: u32, speed: u32) -> Result, ButtplugDeviceError> { + self.last_cmds[index as usize].store(speed as u8, Ordering::Relaxed); + Ok(vec![HardwareWriteCmd::new( + &[JOYHUB_PROTOCOL_UUID], + Endpoint::Tx, + vec![ + 0xa0, + 0x03, + self.last_cmds[0].load(Ordering::Relaxed), + self.last_cmds[2].load(Ordering::Relaxed), + self.last_cmds[1].load(Ordering::Relaxed), + 0x00, + 0xaa, + ], + false, + ).into()]) + } +} + +impl ProtocolHandler for JoyHub { + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.form_hardware_command(feature_index, speed) + } + + fn handle_output_rotate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.form_hardware_command(feature_index, speed) + } + + fn handle_output_oscillate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.form_hardware_command(feature_index, speed) + } + + fn handle_output_constrict_cmd( + &self, + _feature_index: u32, + feature_id: Uuid, + level: u32, + ) -> Result, ButtplugDeviceError> { + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + vec![ + 0xa0, + 0x07, + if level == 0 { 0x00 } else { 0x01 }, + 0x00, + level as u8, + 0xff, + ], + false, + ) + .into()]) + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/joyhub/joyhub_v2.rs b/crates/buttplug_server/src/device/protocol_impl/joyhub/joyhub_v2.rs new file mode 100644 index 000000000..06dc84b44 --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/joyhub/joyhub_v2.rs @@ -0,0 +1,98 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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::sync::atomic::{AtomicU8, Ordering}; + +use uuid::{uuid, Uuid}; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; + +const JOYHUB_V2_PROTOCOL_UUID: Uuid = uuid!("3144b936-99c8-47f3-b85d-defa5fac9e6d"); +generic_protocol_setup!(JoyHubV2, "joyhub-v2"); + +#[derive(Default)] +pub struct JoyHubV2 { + last_cmds: [AtomicU8; 3] +} + +impl JoyHubV2 { + fn form_hardware_command(&self, index: u32, speed: u32) -> Result, ButtplugDeviceError> { + info!("GOT JOYHUB COMMAND"); + self.last_cmds[index as usize].store(speed as u8, Ordering::Relaxed); + Ok(vec![HardwareWriteCmd::new( + &[JOYHUB_V2_PROTOCOL_UUID], + Endpoint::Tx, + vec![ + 0xa0, + 0x03, + self.last_cmds[0].load(Ordering::Relaxed), + self.last_cmds[1].load(Ordering::Relaxed), + self.last_cmds[2].load(Ordering::Relaxed), + 0x00, + 0xaa, + ], + false, + ).into()]) + } +} + +impl ProtocolHandler for JoyHubV2 { + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.form_hardware_command(feature_index, speed) + } + + fn handle_output_rotate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.form_hardware_command(feature_index, speed) + } + + fn handle_output_oscillate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.form_hardware_command(feature_index, speed) + } + + fn handle_output_constrict_cmd( + &self, + _feature_index: u32, + feature_id: Uuid, + level: u32, + ) -> Result, ButtplugDeviceError> { + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + vec![ + 0xa0, + 0x0d, + 0x00, + 0x00, + level as u8, + 0xff + ], + false, + ) + .into()]) + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/joyhub/joyhub_v3.rs b/crates/buttplug_server/src/device/protocol_impl/joyhub/joyhub_v3.rs new file mode 100644 index 000000000..e2cb54937 --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/joyhub/joyhub_v3.rs @@ -0,0 +1,37 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; + +generic_protocol_setup!(JoyHubV3, "joyhub-v3"); + +#[derive(Default)] +pub struct JoyHubV3 {} + +impl ProtocolHandler for JoyHubV3 { + fn handle_output_vibrate_cmd( + &self, + _feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + vec![0xa0, 0x03, 0x00, 0x00, 0x00, speed as u8, 0xaa], + false, + ) + .into()]) + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/joyhub/joyhub_v4.rs b/crates/buttplug_server/src/device/protocol_impl/joyhub/joyhub_v4.rs new file mode 100644 index 000000000..c345de68c --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/joyhub/joyhub_v4.rs @@ -0,0 +1,97 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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::sync::atomic::{AtomicU8, Ordering}; + +use uuid::{uuid, Uuid}; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; + +const JOYHUB_V4_PROTOCOL_UUID: Uuid = uuid!("c99e8979-6f13-4556-9b6b-2061f527042b"); +generic_protocol_setup!(JoyHubV4, "joyhub-v4"); + +#[derive(Default)] +pub struct JoyHubV4 { + last_cmds: [AtomicU8; 3] +} + +impl JoyHubV4 { + fn form_hardware_command(&self, index: u32, speed: u32) -> Result, ButtplugDeviceError> { + self.last_cmds[index as usize].store(speed as u8, Ordering::Relaxed); + Ok(vec![HardwareWriteCmd::new( + &[JOYHUB_V4_PROTOCOL_UUID], + Endpoint::Tx, + vec![ + 0xa0, + 0x03, + self.last_cmds[0].load(Ordering::Relaxed), + 0x00, + self.last_cmds[2].load(Ordering::Relaxed), + self.last_cmds[1].load(Ordering::Relaxed), + 0xaa, + ], + false, + ).into()]) + } +} + +impl ProtocolHandler for JoyHubV4 { + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.form_hardware_command(feature_index, speed) + } + + fn handle_output_rotate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.form_hardware_command(feature_index, speed) + } + + fn handle_output_oscillate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.form_hardware_command(feature_index, speed) + } + + fn handle_output_constrict_cmd( + &self, + _feature_index: u32, + feature_id: Uuid, + level: u32, + ) -> Result, ButtplugDeviceError> { + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + vec![ + 0xa0, + 0x07, + if level == 0 { 0x00 } else { 0x01 }, + 0x00, + level as u8, + 0xff, + ], + false, + ) + .into()]) + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/joyhub/joyhub_v5.rs b/crates/buttplug_server/src/device/protocol_impl/joyhub/joyhub_v5.rs new file mode 100644 index 000000000..d65d45cc1 --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/joyhub/joyhub_v5.rs @@ -0,0 +1,96 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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::sync::atomic::{AtomicU8, Ordering}; + +use uuid::{uuid, Uuid}; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; + +const JOYHUB_V5_PROTOCOL_UUID: Uuid = uuid!("c99e8979-6f13-4556-9b6b-2061f527042b"); +generic_protocol_setup!(JoyHubV5, "joyhub-v5"); + +#[derive(Default)] +pub struct JoyHubV5 { + last_cmds: [AtomicU8; 3] +} + +impl JoyHubV5 { + fn form_hardware_command(&self, index: u32, speed: u32) -> Result, ButtplugDeviceError> { + self.last_cmds[index as usize].store(speed as u8, Ordering::Relaxed); + Ok(vec![HardwareWriteCmd::new( + &[JOYHUB_V5_PROTOCOL_UUID], + Endpoint::Tx, + vec![ + 0xa0, + 0x03, + self.last_cmds[1].load(Ordering::Relaxed), + 0x00, + self.last_cmds[0].load(Ordering::Relaxed), + 0x00, + 0xaa, + ], + false, + ).into()]) + } +} + +impl ProtocolHandler for JoyHubV5 { + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.form_hardware_command(feature_index, speed) + } + + fn handle_output_rotate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.form_hardware_command(feature_index, speed) + } + + fn handle_output_oscillate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.form_hardware_command(feature_index, speed) + } + + fn handle_output_constrict_cmd( + &self, + _feature_index: u32, + feature_id: Uuid, + level: u32, + ) -> Result, ButtplugDeviceError> { + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + vec![ + 0xa0, + 0x07, + if level == 0 { 0x00 } else { 0x01 }, + 0x00, + level as u8, + 0xff, + ], + false, + ) + .into()]) + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/joyhub/joyhub_v6.rs b/crates/buttplug_server/src/device/protocol_impl/joyhub/joyhub_v6.rs new file mode 100644 index 000000000..c66351330 --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/joyhub/joyhub_v6.rs @@ -0,0 +1,96 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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::sync::atomic::{AtomicU8, Ordering}; + +use uuid::{uuid, Uuid}; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; + +const JOYHUB_V6_PROTOCOL_UUID: Uuid = uuid!("c089952e-cb80-462b-8eeb-526f7ba21ff2"); +generic_protocol_setup!(JoyHubV6, "joyhub-v6"); + +#[derive(Default)] +pub struct JoyHubV6 { + last_cmds: [AtomicU8; 3] +} + +impl JoyHubV6 { + fn form_hardware_command(&self, index: u32, speed: u32) -> Result, ButtplugDeviceError> { + self.last_cmds[index as usize].store(speed as u8, Ordering::Relaxed); + Ok(vec![HardwareWriteCmd::new( + &[JOYHUB_V6_PROTOCOL_UUID], + Endpoint::Tx, + vec![ + 0xa0, + 0x03, + self.last_cmds[1].load(Ordering::Relaxed), + self.last_cmds[0].load(Ordering::Relaxed), + self.last_cmds[2].load(Ordering::Relaxed), + 0x00, + 0xaa, + ], + false, + ).into()]) + } +} + +impl ProtocolHandler for JoyHubV6 { + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.form_hardware_command(feature_index, speed) + } + + fn handle_output_rotate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.form_hardware_command(feature_index, speed) + } + + fn handle_output_oscillate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.form_hardware_command(feature_index, speed) + } + + fn handle_output_constrict_cmd( + &self, + _feature_index: u32, + feature_id: Uuid, + level: u32, + ) -> Result, ButtplugDeviceError> { + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + vec![ + 0xa0, + 0x07, + if level == 0 { 0x00 } else { 0x01 }, + 0x00, + level as u8, + 0xff, + ], + false, + ) + .into()]) + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/joyhub/mod.rs b/crates/buttplug_server/src/device/protocol_impl/joyhub/mod.rs new file mode 100644 index 000000000..6bb97bece --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/joyhub/mod.rs @@ -0,0 +1,6 @@ +pub mod joyhub; +pub mod joyhub_v2; +pub mod joyhub_v3; +pub mod joyhub_v4; +pub mod joyhub_v5; +pub mod joyhub_v6; diff --git a/buttplug/src/server/device/protocol/kgoal_boost.rs b/crates/buttplug_server/src/device/protocol_impl/kgoal_boost.rs similarity index 74% rename from buttplug/src/server/device/protocol/kgoal_boost.rs rename to crates/buttplug_server/src/device/protocol_impl/kgoal_boost.rs index d9da62736..b31d13385 100644 --- a/buttplug/src/server/device/protocol/kgoal_boost.rs +++ b/crates/buttplug_server/src/device/protocol_impl/kgoal_boost.rs @@ -6,23 +6,18 @@ // for full license information. use crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ - self, - ButtplugDeviceMessage, - ButtplugServerDeviceMessage, - Endpoint, - SensorReadingV4, - SensorType, - }, - }, - server::device::{ + device::{ hardware::{Hardware, HardwareEvent, HardwareSubscribeCmd, HardwareUnsubscribeCmd}, protocol::{generic_protocol_setup, ProtocolHandler}, }, + message::ButtplugServerDeviceMessage, +}; +use buttplug_core::{ + errors::ButtplugDeviceError, + message::{InputData, InputReadingV4, InputType}, util::{async_manager, stream::convert_broadcast_receiver_to_stream}, }; +use buttplug_server_device_config::Endpoint; use dashmap::DashSet; use futures::{ future::{self, BoxFuture}, @@ -31,6 +26,7 @@ use futures::{ }; use std::{pin::Pin, sync::Arc}; use tokio::sync::broadcast; +use uuid::Uuid; generic_protocol_setup!(KGoalBoost, "kgoal-boost"); @@ -57,15 +53,17 @@ impl ProtocolHandler for KGoalBoost { convert_broadcast_receiver_to_stream(self.event_stream.subscribe()).boxed() } - fn handle_sensor_subscribe_cmd( + fn handle_input_subscribe_cmd( &self, + device_index: u32, device: Arc, - message: &message::SensorSubscribeCmdV4, + feature_index: u32, + feature_id: Uuid, + _sensor_type: InputType, ) -> BoxFuture> { - if self.subscribed_sensors.contains(message.feature_index()) { + if self.subscribed_sensors.contains(&feature_index) { return future::ready(Ok(())).boxed(); } - let message = message.clone(); let sensors = self.subscribed_sensors.clone(); // Readout value: 0x000104000005d3 // Byte 0: Always 0x00 @@ -78,12 +76,11 @@ impl ProtocolHandler for KGoalBoost { // characteristic subscription. if sensors.is_empty() { device - .subscribe(&HardwareSubscribeCmd::new(Endpoint::RxPressure)) + .subscribe(&HardwareSubscribeCmd::new(feature_id, Endpoint::RxPressure)) .await?; let sender = self.event_stream.clone(); let mut hardware_stream = device.event_stream(); let stream_sensors = sensors.clone(); - let device_index = message.device_index(); // If we subscribe successfully, we need to set up our event handler. async_manager::spawn(async move { while let Ok(info) = hardware_stream.recv().await { @@ -99,13 +96,17 @@ impl ProtocolHandler for KGoalBoost { continue; } // Extract our two pressure values. - let normalized = (data[3] as i32) << 8 | data[4] as i32; - let unnormalized = (data[5] as i32) << 8 | data[6] as i32; + let normalized = (data[3] as u32) << 8 | data[4] as u32; + let unnormalized = (data[5] as u32) << 8 | data[6] as u32; if stream_sensors.contains(&0) && sender .send( - SensorReadingV4::new(device_index, 0, SensorType::Pressure, vec![normalized]) - .into(), + InputReadingV4::new( + device_index, + feature_index, + buttplug_core::message::InputTypeData::Pressure(InputData::new(normalized)) + ) + .into(), ) .is_err() { @@ -117,11 +118,10 @@ impl ProtocolHandler for KGoalBoost { if stream_sensors.contains(&1) && sender .send( - SensorReadingV4::new( + InputReadingV4::new( device_index, - 0, - SensorType::Pressure, - vec![unnormalized], + feature_index, + buttplug_core::message::InputTypeData::Pressure(InputData::new(unnormalized)) ) .into(), ) @@ -137,29 +137,33 @@ impl ProtocolHandler for KGoalBoost { } }); } - sensors.insert(*message.feature_index()); + sensors.insert(feature_index); Ok(()) } .boxed() } - fn handle_sensor_unsubscribe_cmd( + fn handle_input_unsubscribe_cmd( &self, device: Arc, - message: &message::SensorUnsubscribeCmdV4, + feature_index: u32, + feature_id: Uuid, + _sensor_type: InputType, ) -> BoxFuture> { - if !self.subscribed_sensors.contains(message.feature_index()) { + if !self.subscribed_sensors.contains(&feature_index) { return future::ready(Ok(())).boxed(); } - let message = message.clone(); let sensors = self.subscribed_sensors.clone(); async move { // If we have no sensors we're currently subscribed to, we'll need to bring up our BLE // characteristic subscription. - sensors.remove(message.feature_index()); + sensors.remove(&feature_index); if sensors.is_empty() { device - .unsubscribe(&HardwareUnsubscribeCmd::new(Endpoint::RxPressure)) + .unsubscribe(&HardwareUnsubscribeCmd::new( + feature_id, + Endpoint::RxPressure, + )) .await?; } Ok(()) diff --git a/buttplug/src/server/device/protocol/kiiroo_prowand.rs b/crates/buttplug_server/src/device/protocol_impl/kiiroo_prowand.rs similarity index 51% rename from buttplug/src/server/device/protocol/kiiroo_prowand.rs rename to crates/buttplug_server/src/device/protocol_impl/kiiroo_prowand.rs index 65f46d4fe..ab5565a39 100644 --- a/buttplug/src/server/device/protocol/kiiroo_prowand.rs +++ b/crates/buttplug_server/src/device/protocol_impl/kiiroo_prowand.rs @@ -5,18 +5,18 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{ - errors::ButtplugDeviceError, - message::{self, ButtplugDeviceMessage, Endpoint, SensorReadingV4}, - }, - server::device::{ - hardware::{Hardware, HardwareCommand, HardwareReadCmd, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareReadCmd, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; +use buttplug_core::{ + errors::ButtplugDeviceError, + message::{self, InputData, InputReadingV4, InputType, InputTypeData}, +}; +use buttplug_server_device_config::Endpoint; use futures::{future::BoxFuture, FutureExt}; use std::{default::Default, sync::Arc}; +use uuid::Uuid; generic_protocol_setup!(KiirooProWand, "kiiroo-prowand"); @@ -24,20 +24,22 @@ generic_protocol_setup!(KiirooProWand, "kiiroo-prowand"); pub struct KiirooProWand {} impl ProtocolHandler for KiirooProWand { - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, vec![ 0x00, 0x00, 0x64, - if scalar == 0 { 0x00 } else { 0xff }, - scalar as u8, - scalar as u8, + if speed == 0 { 0x00 } else { 0xff }, + speed as u8, + speed as u8, ], false, ) @@ -46,24 +48,23 @@ impl ProtocolHandler for KiirooProWand { fn handle_battery_level_cmd( &self, + device_index: u32, device: Arc, - message: message::SensorReadCmdV4, - ) -> BoxFuture> { + feature_index: u32, + feature_id: Uuid, + ) -> BoxFuture> { debug!("Trying to get battery reading."); - let message = message.clone(); - let msg = HardwareReadCmd::new(Endpoint::RxBLEBattery, 20, 0); + let msg = HardwareReadCmd::new(feature_id, Endpoint::RxBLEBattery, 20, 0); let fut = device.read_value(&msg); async move { let hw_msg = fut.await?; let data = hw_msg.data(); - let battery_level = data[0] as i32; - let battery_reading = message::SensorReadingV4::new( - message.device_index(), - *message.feature_index(), - *message.sensor_type(), - vec![battery_level], + let battery_reading = message::InputReadingV4::new( + device_index, + feature_index, + InputTypeData::Battery(InputData::new(data[0])) ); - debug!("Got battery reading: {}", battery_level); + debug!("Got battery reading: {}", data[0]); Ok(battery_reading) } .boxed() diff --git a/buttplug/src/server/device/protocol/kiiroo_spot.rs b/crates/buttplug_server/src/device/protocol_impl/kiiroo_spot.rs similarity index 50% rename from buttplug/src/server/device/protocol/kiiroo_spot.rs rename to crates/buttplug_server/src/device/protocol_impl/kiiroo_spot.rs index b65a5e44b..460c5ed33 100644 --- a/buttplug/src/server/device/protocol/kiiroo_spot.rs +++ b/crates/buttplug_server/src/device/protocol_impl/kiiroo_spot.rs @@ -5,18 +5,18 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{ - errors::ButtplugDeviceError, - message::{self, ButtplugDeviceMessage, Endpoint, SensorReadingV4}, - }, - server::device::{ - hardware::{Hardware, HardwareCommand, HardwareReadCmd, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareReadCmd, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; +use buttplug_core::{ + errors::ButtplugDeviceError, + message::{self, InputData, InputReadingV4, InputType, InputTypeData}, +}; +use buttplug_server_device_config::Endpoint; use futures::{future::BoxFuture, FutureExt}; use std::{default::Default, sync::Arc}; +use uuid::Uuid; generic_protocol_setup!(KiirooSpot, "kiiroo-spot"); @@ -24,14 +24,16 @@ generic_protocol_setup!(KiirooSpot, "kiiroo-spot"); pub struct KiirooSpot {} impl ProtocolHandler for KiirooSpot { - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, - vec![0x00, 0xff, 0x00, 0x00, 0x00, scalar as u8], + vec![0x00, 0xff, 0x00, 0x00, 0x00, speed as u8], false, ) .into()]) @@ -39,24 +41,23 @@ impl ProtocolHandler for KiirooSpot { fn handle_battery_level_cmd( &self, + device_index: u32, device: Arc, - message: message::SensorReadCmdV4, - ) -> BoxFuture> { + feature_index: u32, + feature_id: Uuid, + ) -> BoxFuture> { debug!("Trying to get battery reading."); - let message = message.clone(); - let msg = HardwareReadCmd::new(Endpoint::RxBLEBattery, 20, 0); + let msg = HardwareReadCmd::new(feature_id, Endpoint::RxBLEBattery, 20, 0); let fut = device.read_value(&msg); async move { let hw_msg = fut.await?; let data = hw_msg.data(); - let battery_level = data[0] as i32; - let battery_reading = message::SensorReadingV4::new( - message.device_index(), - *message.feature_index(), - *message.sensor_type(), - vec![battery_level], + let battery_reading = message::InputReadingV4::new( + device_index, + feature_index, + InputTypeData::Battery(InputData::new(data[0])) ); - debug!("Got battery reading: {}", battery_level); + debug!("Got battery reading: {}", data[0]); Ok(battery_reading) } .boxed() diff --git a/crates/buttplug_server/src/device/protocol_impl/kiiroo_v2.rs b/crates/buttplug_server/src/device/protocol_impl/kiiroo_v2.rs new file mode 100644 index 000000000..f48379b5d --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/kiiroo_v2.rs @@ -0,0 +1,84 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 super::fleshlight_launch_helper::calculate_speed; +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, + }, +}; +use async_trait::async_trait; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use buttplug_server_device_config::{ + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; +use std::sync::{ + atomic::{AtomicU8, Ordering}, + Arc, +}; +use uuid::{uuid, Uuid}; + +const KIIROO_V2_PROTOCOL_UUID: Uuid = uuid!("05ab9d57-5e65-47b2-add4-5bad3e8663e5"); +generic_protocol_initializer_setup!(KiirooV2, "kiiroo-v2"); + +#[derive(Default)] +pub struct KiirooV2Initializer {} + +#[async_trait] +impl ProtocolInitializer for KiirooV2Initializer { + async fn initialize( + &mut self, + hardware: Arc, + _: &DeviceDefinition, + ) -> Result, ButtplugDeviceError> { + let msg = HardwareWriteCmd::new( + &[KIIROO_V2_PROTOCOL_UUID], + Endpoint::Firmware, + vec![0x0u8], + true, + ); + hardware.write_value(&msg).await?; + Ok(Arc::new(KiirooV2::default())) + } +} + +#[derive(Default)] +pub struct KiirooV2 { + previous_position: Arc, +} + +impl ProtocolHandler for KiirooV2 { + fn handle_position_with_duration_cmd( + &self, + _feature_index: u32, + feature_id: Uuid, + position: u32, + duration: u32, + ) -> Result, ButtplugDeviceError> { + // In the protocol, we know max speed is 99, so convert here. We have to + // use AtomicU8 because there's no AtomicF64 yet. + let previous_position = self.previous_position.load(Ordering::Relaxed); + let distance = (previous_position as f64 - (position as f64)).abs() / 99f64; + let position = position as u8; + let calculated_speed = (calculate_speed(distance, duration) * 99f64) as u8; + self.previous_position.store(position, Ordering::Relaxed); + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + [position, calculated_speed].to_vec(), + false, + ) + .into()]) + } +} diff --git a/buttplug/src/server/device/protocol/kiiroo_v21.rs b/crates/buttplug_server/src/device/protocol_impl/kiiroo_v21.rs similarity index 69% rename from buttplug/src/server/device/protocol/kiiroo_v21.rs rename to crates/buttplug_server/src/device/protocol_impl/kiiroo_v21.rs index dce4585d5..7d1fc98e1 100644 --- a/buttplug/src/server/device/protocol/kiiroo_v21.rs +++ b/crates/buttplug_server/src/device/protocol_impl/kiiroo_v21.rs @@ -7,18 +7,7 @@ use super::fleshlight_launch_helper::calculate_speed; use crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ - self, - ButtplugDeviceMessage, - ButtplugServerDeviceMessage, - Endpoint, - SensorReadingV4, - SensorType, - }, - }, - server::device::{ + device::{ hardware::{ Hardware, HardwareCommand, @@ -30,8 +19,14 @@ use crate::{ }, protocol::{generic_protocol_setup, ProtocolHandler}, }, + message::ButtplugServerDeviceMessage, +}; +use buttplug_core::{ + errors::ButtplugDeviceError, + message::{InputData, InputReadingV4, InputType, InputTypeData}, util::{async_manager, stream::convert_broadcast_receiver_to_stream}, }; +use buttplug_server_device_config::Endpoint; use dashmap::DashSet; use futures::{ future::{self, BoxFuture}, @@ -42,11 +37,12 @@ use std::{ default::Default, pin::Pin, sync::{ - atomic::{AtomicU8, Ordering::SeqCst}, + atomic::{AtomicU8, Ordering::Relaxed}, Arc, }, }; use tokio::sync::broadcast; +use uuid::Uuid; generic_protocol_setup!(KiirooV21, "kiiroo-v21"); @@ -69,46 +65,39 @@ impl Default for KiirooV21 { } impl ProtocolHandler for KiirooV21 { - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, - vec![0x01, scalar as u8], + vec![0x01, speed as u8], false, ) .into()]) } - fn handle_linear_cmd( + fn handle_position_with_duration_cmd( &self, - message: message::LinearCmdV4, + _feature_index: u32, + feature_id: Uuid, + position: u32, + duration: u32, ) -> Result, ButtplugDeviceError> { - let v = message.vectors()[0].clone(); // In the protocol, we know max speed is 99, so convert here. We have to // use AtomicU8 because there's no AtomicF64 yet. - let previous_position = self.previous_position.load(SeqCst); - let distance = (previous_position as f64 - (v.position() * 99f64)).abs() / 99f64; - let fl_cmd = message::FleshlightLaunchFW12CmdV0::new( - message.device_index(), - (v.position() * 99f64) as u8, - (calculate_speed(distance, v.duration()) * 99f64) as u8, - ); - self.handle_fleshlight_launch_fw12_cmd(fl_cmd) - } - - fn handle_fleshlight_launch_fw12_cmd( - &self, - message: message::FleshlightLaunchFW12CmdV0, - ) -> Result, ButtplugDeviceError> { - let previous_position = self.previous_position.clone(); - let position = message.position(); - previous_position.store(position, SeqCst); + let previous_position = self.previous_position.load(Relaxed); + let distance = (previous_position as f64 - (position as f64)).abs() / 99f64; + let position = position as u8; + let speed = (calculate_speed(distance, duration) * 99f64) as u8; + self.previous_position.store(position, Relaxed); Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, - [0x03, 0x00, message.speed(), message.position()].to_vec(), + [0x03, 0x00, speed, position].to_vec(), false, ) .into()]) @@ -116,14 +105,15 @@ impl ProtocolHandler for KiirooV21 { fn handle_battery_level_cmd( &self, + device_index: u32, device: Arc, - message: message::SensorReadCmdV4, - ) -> BoxFuture> { + feature_index: u32, + feature_id: Uuid, + ) -> BoxFuture> { debug!("Trying to get battery reading."); - let message = message.clone(); // Reading the "whitelist" endpoint for this device retrieves the battery level, // which is byte 5. All other bytes of the 20-byte result are unknown. - let msg = HardwareReadCmd::new(Endpoint::Whitelist, 20, 0); + let msg = HardwareReadCmd::new(feature_id, Endpoint::Whitelist, 20, 0); let fut = device.read_value(&msg); async move { let hw_msg = fut.await?; @@ -134,12 +124,11 @@ impl ProtocolHandler for KiirooV21 { "Kiiroo battery data not expected length!".to_owned(), )); } - let battery_level = data[5] as i32; - let battery_reading = message::SensorReadingV4::new( - message.device_index(), - *message.feature_index(), - *message.sensor_type(), - vec![battery_level], + let battery_level = data[5]; + let battery_reading = InputReadingV4::new( + device_index, + feature_index, + InputTypeData::Battery(InputData::new(battery_level)) ); debug!("Got battery reading: {}", battery_level); Ok(battery_reading) @@ -153,13 +142,16 @@ impl ProtocolHandler for KiirooV21 { convert_broadcast_receiver_to_stream(self.event_stream.subscribe()).boxed() } - fn handle_sensor_subscribe_cmd( + /* + fn handle_input_subscribe_cmd( &self, + device_index: u32, device: Arc, - message: &message::SensorSubscribeCmdV4, + feature_index: u32, + feature_id: Uuid, + _sensor_type: InputType, ) -> BoxFuture> { - let message = message.clone(); - if self.subscribed_sensors.contains(message.feature_index()) { + if self.subscribed_sensors.contains(&feature_index) { return future::ready(Ok(())).boxed(); } let sensors = self.subscribed_sensors.clone(); @@ -178,12 +170,11 @@ impl ProtocolHandler for KiirooV21 { // characteristic subscription. if sensors.is_empty() { device - .subscribe(&HardwareSubscribeCmd::new(Endpoint::Rx)) + .subscribe(&HardwareSubscribeCmd::new(feature_id, Endpoint::Rx)) .await?; let sender = self.event_stream.clone(); let mut hardware_stream = device.event_stream(); let stream_sensors = sensors.clone(); - let device_index = message.device_index(); // If we subscribe successfully, we need to set up our event handler. async_manager::spawn(async move { while let Ok(info) = hardware_stream.recv().await { @@ -201,23 +192,19 @@ impl ProtocolHandler for KiirooV21 { // Extract our pressure values. // Invert analog values so that the value increases with pressure. let analog: Vec = (0..4) - .into_iter() .map(|i| { (u16::MAX as i32) - ((data[2 * i] as i32) << 8 | (data[2 * i + 1] as i32)) }) .collect(); - let digital: Vec = (0..4) - .into_iter() - .map(|i| ((data[8] as i32) >> i) & 1) - .collect(); + let digital: Vec = (0..4).map(|i| ((data[8] as i32) >> i) & 1).collect(); for ((sensor_index, sensor_type), sensor_data) in (0u32..) - .zip([SensorType::Pressure, SensorType::Button]) + .zip([InputType::Pressure, InputType::Button]) .zip([analog, digital]) { if stream_sensors.contains(&sensor_index) && sender .send( - SensorReadingV4::new(device_index, sensor_index, sensor_type, sensor_data) + InputReadingV4::new(device_index, sensor_index, sensor_type, sensor_data) .into(), ) .is_err() @@ -233,34 +220,35 @@ impl ProtocolHandler for KiirooV21 { } }); } - sensors.insert(*message.feature_index()); + sensors.insert(feature_index); Ok(()) } .boxed() } - fn handle_sensor_unsubscribe_cmd( + fn handle_input_unsubscribe_cmd( &self, device: Arc, - message: &message::SensorUnsubscribeCmdV4, + feature_index: u32, + feature_id: Uuid, + _sensor_type: InputType, ) -> BoxFuture> { - let message = message.clone(); - - if !self.subscribed_sensors.contains(message.feature_index()) { + if !self.subscribed_sensors.contains(&feature_index) { return future::ready(Ok(())).boxed(); } let sensors = self.subscribed_sensors.clone(); async move { // If we have no sensors we're currently subscribed to, we'll need to end our BLE // characteristic subscription. - sensors.remove(message.feature_index()); + sensors.remove(&feature_index); if sensors.is_empty() { device - .unsubscribe(&HardwareUnsubscribeCmd::new(Endpoint::Rx)) + .unsubscribe(&HardwareUnsubscribeCmd::new(feature_id, Endpoint::Rx)) .await?; } Ok(()) } .boxed() } + */ } diff --git a/buttplug/src/server/device/protocol/kiiroo_v21_initialized.rs b/crates/buttplug_server/src/device/protocol_impl/kiiroo_v21_initialized.rs similarity index 54% rename from buttplug/src/server/device/protocol/kiiroo_v21_initialized.rs rename to crates/buttplug_server/src/device/protocol_impl/kiiroo_v21_initialized.rs index 0056b0388..093c6f364 100644 --- a/buttplug/src/server/device/protocol/kiiroo_v21_initialized.rs +++ b/crates/buttplug_server/src/device/protocol_impl/kiiroo_v21_initialized.rs @@ -5,28 +5,32 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{ - errors::ButtplugDeviceError, - message::{self, Endpoint}, - }, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ - fleshlight_launch_helper::calculate_speed, - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, +use super::fleshlight_launch_helper::calculate_speed; + +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, }, }; use async_trait::async_trait; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use buttplug_server_device_config::{ + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; use std::sync::{ atomic::{AtomicU8, Ordering}, Arc, }; +use uuid::{uuid, Uuid}; + +const KIIROO_V21_INITIALIZED_PROTOCOL_UUID: Uuid = uuid!("22329023-5464-41b6-a0de-673d7e993055"); generic_protocol_initializer_setup!(KiirooV21Initialized, "kiiroo-v21-initialized"); @@ -38,11 +42,12 @@ impl ProtocolInitializer for KiirooV21InitializedInitializer { async fn initialize( &mut self, hardware: Arc, - _: &UserDeviceDefinition, + _: &DeviceDefinition, ) -> Result, ButtplugDeviceError> { debug!("calling Onyx+ init"); hardware .write_value(&HardwareWriteCmd::new( + &[KIIROO_V21_INITIALIZED_PROTOCOL_UUID], Endpoint::Tx, vec![0x03u8, 0x00u8, 0x64u8, 0x19u8], true, @@ -50,6 +55,7 @@ impl ProtocolInitializer for KiirooV21InitializedInitializer { .await?; hardware .write_value(&HardwareWriteCmd::new( + &[KIIROO_V21_INITIALIZED_PROTOCOL_UUID], Endpoint::Tx, vec![0x03u8, 0x00u8, 0x64u8, 0x00u8], true, @@ -65,49 +71,41 @@ pub struct KiirooV21Initialized { } impl ProtocolHandler for KiirooV21Initialized { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, - vec![0x01, scalar as u8], + vec![0x01, speed as u8], false, ) .into()]) } - fn handle_linear_cmd( + fn handle_position_with_duration_cmd( &self, - message: message::LinearCmdV4, + _feature_index: u32, + feature_id: Uuid, + position: u32, + duration: u32, ) -> Result, ButtplugDeviceError> { - let v = message.vectors()[0].clone(); // In the protocol, we know max speed is 99, so convert here. We have to // use AtomicU8 because there's no AtomicF64 yet. - let previous_position = self.previous_position.load(Ordering::SeqCst); - let distance = (previous_position as f64 - (v.position() * 99f64)).abs() / 99f64; - let fl_cmd = message::FleshlightLaunchFW12CmdV0::new( - 0, - (v.position() * 99f64) as u8, - (calculate_speed(distance, v.duration()) * 99f64) as u8, - ); - self.handle_fleshlight_launch_fw12_cmd(fl_cmd) - } + let previous_position = self.previous_position.load(Ordering::Relaxed); + let distance = (previous_position as f64 - (position as f64)).abs() / 99f64; + let calculated_speed = (calculate_speed(distance, duration) * 99f64) as u8; - fn handle_fleshlight_launch_fw12_cmd( - &self, - message: message::FleshlightLaunchFW12CmdV0, - ) -> Result, ButtplugDeviceError> { - let position = message.position(); - self.previous_position.store(position, Ordering::SeqCst); + self + .previous_position + .store(position as u8, Ordering::Relaxed); Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, - [0x03, 0x00, message.speed(), message.position()].to_vec(), + [0x03, 0x00, calculated_speed, position as u8].to_vec(), false, ) .into()]) diff --git a/crates/buttplug_server/src/device/protocol_impl/kiiroo_v2_vibrator.rs b/crates/buttplug_server/src/device/protocol_impl/kiiroo_v2_vibrator.rs new file mode 100644 index 000000000..1ea647b73 --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/kiiroo_v2_vibrator.rs @@ -0,0 +1,53 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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::sync::atomic::{AtomicU8, Ordering}; + +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; + +generic_protocol_setup!(KiirooV2Vibrator, "kiiroo-v2-vibrator"); + +pub struct KiirooV2Vibrator { + speeds: [AtomicU8; 3], +} + +impl Default for KiirooV2Vibrator { + fn default() -> Self { + Self { + speeds: [AtomicU8::new(0), AtomicU8::new(0), AtomicU8::new(0)], + } + } +} + +impl ProtocolHandler for KiirooV2Vibrator { + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.speeds[feature_index as usize].store(speed as u8, Ordering::Relaxed); + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + self + .speeds + .iter() + .map(|v| v.load(Ordering::Relaxed)) + .collect(), + false, + ) + .into()]) + } +} diff --git a/buttplug/src/server/device/protocol/kizuna.rs b/crates/buttplug_server/src/device/protocol_impl/kizuna.rs similarity index 54% rename from buttplug/src/server/device/protocol/kizuna.rs rename to crates/buttplug_server/src/device/protocol_impl/kizuna.rs index b1c5b2d95..b8a02f6a8 100644 --- a/buttplug/src/server/device/protocol/kizuna.rs +++ b/crates/buttplug_server/src/device/protocol_impl/kizuna.rs @@ -5,13 +5,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(Kizuna, "kizuna"); @@ -19,18 +20,16 @@ generic_protocol_setup!(Kizuna, "kizuna"); pub struct Kizuna {} impl ProtocolHandler for Kizuna { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_rotate_cmd( + fn handle_output_rotate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, - vec![48 + scalar as u8, '\r' as u8, '\n' as u8], + vec![48 + speed as u8, b'\r', b'\n'], false, ) .into()]) diff --git a/buttplug/src/server/device/protocol/lelo_harmony.rs b/crates/buttplug_server/src/device/protocol_impl/lelo_harmony.rs similarity index 56% rename from buttplug/src/server/device/protocol/lelo_harmony.rs rename to crates/buttplug_server/src/device/protocol_impl/lelo_harmony.rs index 50f5e7030..f6ad3c6a3 100644 --- a/buttplug/src/server/device/protocol/lelo_harmony.rs +++ b/crates/buttplug_server/src/device/protocol_impl/lelo_harmony.rs @@ -5,32 +5,34 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, +use crate::device::{ + hardware::{ + Hardware, + HardwareCommand, + HardwareEvent, + HardwareSubscribeCmd, + HardwareUnsubscribeCmd, + HardwareWriteCmd, }, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{ - Hardware, - HardwareCommand, - HardwareEvent, - HardwareSubscribeCmd, - HardwareUnsubscribeCmd, - HardwareWriteCmd, - }, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, }, }; use async_trait::async_trait; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use buttplug_server_device_config::{ + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; use std::sync::Arc; +use uuid::{uuid, Uuid}; +const LELO_HARMONY_PROTOCOL_UUID: Uuid = uuid!("220e180a-e6d5-4fd1-963e-43a6f990b717"); generic_protocol_initializer_setup!(LeloHarmony, "lelo-harmony"); #[derive(Default)] @@ -41,7 +43,7 @@ impl ProtocolInitializer for LeloHarmonyInitializer { async fn initialize( &mut self, hardware: Arc, - _: &UserDeviceDefinition, + _: &DeviceDefinition, ) -> Result, ButtplugDeviceError> { // The Lelo Harmony has a very specific pairing flow: // * First the device is turned on in BLE mode (long press) @@ -55,7 +57,10 @@ impl ProtocolInitializer for LeloHarmonyInitializer { // * If it returns 0x00,00,00,00,00,00,00,00 the connection is authorised let mut event_receiver = hardware.event_stream(); hardware - .subscribe(&HardwareSubscribeCmd::new(Endpoint::Whitelist)) + .subscribe(&HardwareSubscribeCmd::new( + LELO_HARMONY_PROTOCOL_UUID, + Endpoint::Whitelist, + )) .await?; loop { @@ -72,15 +77,26 @@ impl ProtocolInitializer for LeloHarmonyInitializer { debug!("Lelo Harmony gave us a password: {:?}", n); // Can't send whilst subscribed hardware - .unsubscribe(&HardwareUnsubscribeCmd::new(Endpoint::Whitelist)) + .unsubscribe(&HardwareUnsubscribeCmd::new( + LELO_HARMONY_PROTOCOL_UUID, + Endpoint::Whitelist, + )) .await?; // Send with response hardware - .write_value(&HardwareWriteCmd::new(Endpoint::Whitelist, n, true)) + .write_value(&HardwareWriteCmd::new( + &[LELO_HARMONY_PROTOCOL_UUID], + Endpoint::Whitelist, + n, + true, + )) .await?; // Get back to the loop hardware - .subscribe(&HardwareSubscribeCmd::new(Endpoint::Whitelist)) + .subscribe(&HardwareSubscribeCmd::new( + LELO_HARMONY_PROTOCOL_UUID, + Endpoint::Whitelist, + )) .await?; } } else { @@ -96,35 +112,50 @@ impl ProtocolInitializer for LeloHarmonyInitializer { #[derive(Default)] pub struct LeloHarmony {} +impl LeloHarmony { + fn handle_input_cmd( + &self, + feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + vec![ + 0x0a, + 0x12, + feature_index as u8 + 1, + 0x08, + 0x00, + 0x00, + 0x00, + 0x00, + speed as u8, + 0x00, + ], + false, + ) + .into()]) + } +} + impl ProtocolHandler for LeloHarmony { - fn handle_scalar_cmd( + fn handle_output_rotate_cmd( &self, - cmds: &[Option<(ActuatorType, u32)>], + feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { - let mut cmd_vec: Vec = vec![]; - for (i, cmd) in cmds.iter().enumerate() { - if let Some(pair) = cmd { - cmd_vec.push( - HardwareWriteCmd::new( - Endpoint::Tx, - vec![ - 0x0a, - 0x12, - i as u8 + 1, - 0x08, - 0x00, - 0x00, - 0x00, - 0x00, - pair.1 as u8, - 0x00, - ], - false, - ) - .into(), - ); - } - } - Ok(cmd_vec) + self.handle_input_cmd(feature_index, feature_id, speed) + } + + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.handle_input_cmd(feature_index, feature_id, speed) } } diff --git a/crates/buttplug_server/src/device/protocol_impl/lelof1s.rs b/crates/buttplug_server/src/device/protocol_impl/lelof1s.rs new file mode 100644 index 000000000..f27d8964d --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/lelof1s.rs @@ -0,0 +1,92 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareSubscribeCmd, HardwareWriteCmd}, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, + }, +}; +use async_trait::async_trait; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use buttplug_server_device_config::{ + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; +use std::sync::{ + atomic::{AtomicU8, Ordering}, + Arc, +}; +use uuid::{uuid, Uuid}; + +const LELO_F1S_PROTOCOL_UUID: Uuid = uuid!("4987f232-40f9-47a3-8d0c-e30b74e75310"); +generic_protocol_initializer_setup!(LeloF1s, "lelo-f1s"); + +#[derive(Default)] +pub struct LeloF1sInitializer {} + +#[async_trait] +impl ProtocolInitializer for LeloF1sInitializer { + async fn initialize( + &mut self, + hardware: Arc, + _: &DeviceDefinition, + ) -> Result, ButtplugDeviceError> { + // The Lelo F1s needs you to hit the power button after connection + // before it'll accept any commands. Unless we listen for event on + // the button, this is more likely to turn the device off. + hardware + .subscribe(&HardwareSubscribeCmd::new( + LELO_F1S_PROTOCOL_UUID, + Endpoint::Rx, + )) + .await?; + Ok(Arc::new(LeloF1s::new(false))) + } +} + +pub struct LeloF1s { + speeds: [AtomicU8; 2], + write_with_response: bool, +} + +impl LeloF1s { + pub fn new(write_with_response: bool) -> Self { + Self { + write_with_response, + speeds: [AtomicU8::new(0), AtomicU8::new(0)], + } + } +} + +impl ProtocolHandler for LeloF1s { + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.speeds[feature_index as usize].store(speed as u8, Ordering::Relaxed); + let mut cmd_vec = vec![0x1]; + self + .speeds + .iter() + .for_each(|v| cmd_vec.push(v.load(Ordering::Relaxed))); + Ok(vec![HardwareWriteCmd::new( + &[LELO_F1S_PROTOCOL_UUID], + Endpoint::Tx, + cmd_vec, + self.write_with_response, + ) + .into()]) + } +} diff --git a/buttplug/src/server/device/protocol/lelof1sv2.rs b/crates/buttplug_server/src/device/protocol_impl/lelof1sv2.rs similarity index 53% rename from buttplug/src/server/device/protocol/lelof1sv2.rs rename to crates/buttplug_server/src/device/protocol_impl/lelof1sv2.rs index 0859ea331..83341dab4 100644 --- a/buttplug/src/server/device/protocol/lelof1sv2.rs +++ b/crates/buttplug_server/src/device/protocol_impl/lelof1sv2.rs @@ -5,32 +5,37 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, +use super::{ + lelo_harmony::LeloHarmony, + lelof1s::LeloF1s, +}; +use crate::device::{ + hardware::{ + Hardware, + HardwareEvent, + HardwareSubscribeCmd, + HardwareUnsubscribeCmd, + HardwareWriteCmd, }, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{ - Hardware, - HardwareCommand, - HardwareEvent, - HardwareSubscribeCmd, - HardwareUnsubscribeCmd, - HardwareWriteCmd, - }, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, }, }; use async_trait::async_trait; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use buttplug_server_device_config::{ + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; use std::sync::Arc; +use uuid::{uuid, Uuid}; +const LELO_F1S_V2_PROTOCOL_UUID: Uuid = uuid!("85c59ac5-89ee-4549-8958-ce5449226a5c"); generic_protocol_initializer_setup!(LeloF1sV2, "lelo-f1sv2"); #[derive(Default)] @@ -41,7 +46,7 @@ impl ProtocolInitializer for LeloF1sV2Initializer { async fn initialize( &mut self, hardware: Arc, - _: &UserDeviceDefinition, + _: &DeviceDefinition, ) -> Result, ButtplugDeviceError> { let use_harmony = !hardware.endpoints().contains(&Endpoint::Whitelist); let sec_endpoint = if use_harmony { @@ -62,7 +67,10 @@ impl ProtocolInitializer for LeloF1sV2Initializer { // * If it returns 0x00,00,00,00,00,00,00,00 the connection is authorised let mut event_receiver = hardware.event_stream(); hardware - .subscribe(&HardwareSubscribeCmd::new(sec_endpoint)) + .subscribe(&HardwareSubscribeCmd::new( + LELO_F1S_V2_PROTOCOL_UUID, + sec_endpoint, + )) .await?; let noauth: Vec = vec![0; 8]; let authed: Vec = vec![1, 0, 0, 0, 0, 0, 0, 0]; @@ -76,20 +84,35 @@ impl ProtocolInitializer for LeloF1sV2Initializer { ) } else if n.eq(&authed) { debug!("Lelo F1s V2 is authorised!"); - return Ok(Arc::new(LeloF1sV2::new(use_harmony))); + if use_harmony { + return Ok(Arc::new(LeloHarmony::default())); + } else { + return Ok(Arc::new(LeloF1s::new(true))); + } } else { debug!("Lelo F1s V2 gave us a password: {:?}", n); // Can't send whilst subscribed hardware - .unsubscribe(&HardwareUnsubscribeCmd::new(sec_endpoint)) + .unsubscribe(&HardwareUnsubscribeCmd::new( + LELO_F1S_V2_PROTOCOL_UUID, + sec_endpoint, + )) .await?; // Send with response hardware - .write_value(&HardwareWriteCmd::new(sec_endpoint, n, true)) + .write_value(&HardwareWriteCmd::new( + &[LELO_F1S_V2_PROTOCOL_UUID], + sec_endpoint, + n, + true, + )) .await?; // Get back to the loop hardware - .subscribe(&HardwareSubscribeCmd::new(sec_endpoint)) + .subscribe(&HardwareSubscribeCmd::new( + LELO_F1S_V2_PROTOCOL_UUID, + sec_endpoint, + )) .await?; } } else { @@ -101,63 +124,3 @@ impl ProtocolInitializer for LeloF1sV2Initializer { } } } - -pub struct LeloF1sV2 { - use_harmony: bool, -} - -impl LeloF1sV2 { - fn new(use_harmony: bool) -> Self { - Self { use_harmony } - } -} - -impl ProtocolHandler for LeloF1sV2 { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn needs_full_command_set(&self) -> bool { - !self.use_harmony - } - - fn handle_scalar_cmd( - &self, - cmds: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { - if self.use_harmony { - let mut cmd_vec: Vec = vec![]; - for (i, cmd) in cmds.iter().enumerate() { - if let Some(pair) = cmd { - cmd_vec.push( - HardwareWriteCmd::new( - Endpoint::TxVibrate, - vec![ - 0x0a, - 0x12, - i as u8 + 1, - 0x08, - 0x00, - 0x00, - 0x00, - 0x00, - pair.1 as u8, - 0x00, - ], - false, - ) - .into(), - ); - } - } - return Ok(cmd_vec); - } - let mut cmd_vec = vec![0x1]; - for cmd in cmds.iter() { - cmd_vec.push(cmd.expect("LeloF1s should always send all values").1 as u8); - } - Ok(vec![ - HardwareWriteCmd::new(Endpoint::Tx, cmd_vec, true).into() - ]) - } -} diff --git a/crates/buttplug_server/src/device/protocol_impl/leten.rs b/crates/buttplug_server/src/device/protocol_impl/leten.rs new file mode 100644 index 000000000..7686b42fd --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/leten.rs @@ -0,0 +1,84 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, ProtocolKeepaliveStrategy, + }, +}; +use async_trait::async_trait; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use buttplug_server_device_config::{ + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; +use std::sync::Arc; +use std::time::Duration; +use uuid::{uuid, Uuid}; + +const LETEN_PROTOCOL_UUID: Uuid = uuid!("7d899f44-2676-4a00-9c68-0c800055ee2a"); + +generic_protocol_initializer_setup!(Leten, "leten"); +#[derive(Default)] +pub struct LetenInitializer {} + +#[async_trait] +impl ProtocolInitializer for LetenInitializer { + async fn initialize( + &mut self, + hardware: Arc, + _: &DeviceDefinition, + ) -> Result, ButtplugDeviceError> { + // There's a more complex auth flow that the app "sometimes" goes through where it + // sends [0x04, 0x00] and waits for [0x01] on Rx before calling [0x04, 0x01] + hardware + .write_value(&HardwareWriteCmd::new( + &[LETEN_PROTOCOL_UUID], + Endpoint::Tx, + vec![0x04, 0x01], + true, + )) + .await?; + // Sometimes sending this causes Rx to receive [0x0a] + Ok(Arc::new(Leten::default())) + } +} + +const LETEN_COMMAND_DELAY_MS: u64 = 1000; + +#[derive(Default)] +pub struct Leten {} + +impl ProtocolHandler for Leten { + fn keepalive_strategy(&self) -> ProtocolKeepaliveStrategy { + // Leten keepalive is shorter + ProtocolKeepaliveStrategy::RepeatLastPacketStrategyWithTiming(Duration::from_millis( + LETEN_COMMAND_DELAY_MS, + )) + } + + fn handle_output_vibrate_cmd( + &self, + _feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + vec![0x02, speed as u8], + true, + ) + .into()]) + } +} diff --git a/buttplug/src/server/device/protocol/libo_elle.rs b/crates/buttplug_server/src/device/protocol_impl/libo_elle.rs similarity index 53% rename from buttplug/src/server/device/protocol/libo_elle.rs rename to crates/buttplug_server/src/device/protocol_impl/libo_elle.rs index aa2a5f1e9..328db3710 100644 --- a/buttplug/src/server/device/protocol/libo_elle.rs +++ b/crates/buttplug_server/src/device/protocol_impl/libo_elle.rs @@ -5,13 +5,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(LiboElle, "libo-elle"); @@ -19,29 +20,26 @@ generic_protocol_setup!(LiboElle, "libo-elle"); pub struct LiboElle {} impl ProtocolHandler for LiboElle { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - index: u32, - scalar: u32, + feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![{ - let speed = scalar as u8; - if index == 1 { + let speed = speed as u8; + if feature_index == 1 { let mut data = 0u8; - if speed as u8 > 0 && speed <= 7 { + if speed > 0 && speed <= 7 { data |= (speed - 1) << 4; data |= 1; // Set the mode too } else if speed > 7 { data |= (speed - 8) << 4; data |= 4; // Set the mode too } - HardwareWriteCmd::new(Endpoint::Tx, vec![data], false).into() + HardwareWriteCmd::new(&[feature_id], Endpoint::Tx, vec![data], false).into() } else { - HardwareWriteCmd::new(Endpoint::TxMode, vec![speed], false).into() + HardwareWriteCmd::new(&[feature_id], Endpoint::TxMode, vec![speed], false).into() } }]) } diff --git a/crates/buttplug_server/src/device/protocol_impl/libo_shark.rs b/crates/buttplug_server/src/device/protocol_impl/libo_shark.rs new file mode 100644 index 000000000..a88af7a5a --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/libo_shark.rs @@ -0,0 +1,44 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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::sync::atomic::{AtomicU8, Ordering}; + +use uuid::{uuid, Uuid}; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; + +const LIBO_SHARK_PROTOCOL_UUID: Uuid = uuid!("c0044425-b59c-4037-a702-0438afcaad3e"); +generic_protocol_setup!(LiboShark, "libo-shark"); + +#[derive(Default)] +pub struct LiboShark { + values: [AtomicU8; 2], +} + +impl ProtocolHandler for LiboShark { + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.values[feature_index as usize].store(speed as u8, Ordering::Relaxed); + let data = self.values[0].load(Ordering::Relaxed) << 4 | self.values[1].load(Ordering::Relaxed); + Ok(vec![HardwareWriteCmd::new( + &[LIBO_SHARK_PROTOCOL_UUID], + Endpoint::Tx, + vec![data], + false, + ) + .into()]) + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/libo_vibes.rs b/crates/buttplug_server/src/device/protocol_impl/libo_vibes.rs new file mode 100644 index 000000000..b73a9b604 --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/libo_vibes.rs @@ -0,0 +1,66 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 uuid::{uuid, Uuid}; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; + +const LIBO_VIBES_PROTOCOL_UUID: Uuid = uuid!("72a3d029-cf33-4fff-beec-1c45b85cc8ae"); +generic_protocol_setup!(LiboVibes, "libo-vibes"); + +#[derive(Default)] +pub struct LiboVibes {} + +impl ProtocolHandler for LiboVibes { + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + let mut msg_vec = vec![]; + if feature_index == 0 { + msg_vec.push( + HardwareWriteCmd::new( + &[LIBO_VIBES_PROTOCOL_UUID], + Endpoint::Tx, + vec![speed as u8], + false, + ) + .into(), + ); + // If this is a single vibe device, we need to send stop to TxMode too + if speed as u8 == 0 { + msg_vec.push( + HardwareWriteCmd::new( + &[LIBO_VIBES_PROTOCOL_UUID], + Endpoint::TxMode, + vec![0u8], + false, + ) + .into(), + ); + } + } else if feature_index == 1 { + msg_vec.push( + HardwareWriteCmd::new( + &[LIBO_VIBES_PROTOCOL_UUID], + Endpoint::TxMode, + vec![speed as u8], + false, + ) + .into(), + ); + } + Ok(msg_vec) + } +} diff --git a/buttplug/src/server/device/protocol/lioness.rs b/crates/buttplug_server/src/device/protocol_impl/lioness.rs similarity index 59% rename from buttplug/src/server/device/protocol/lioness.rs rename to crates/buttplug_server/src/device/protocol_impl/lioness.rs index bfab158e8..53be356d2 100644 --- a/buttplug/src/server/device/protocol/lioness.rs +++ b/crates/buttplug_server/src/device/protocol_impl/lioness.rs @@ -5,23 +5,27 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::server::device::configuration::ProtocolCommunicationSpecifier; -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - configuration::{UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareSubscribeCmd, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareSubscribeCmd, HardwareWriteCmd}, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, }, }; use async_trait::async_trait; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use buttplug_server_device_config::{ + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; use std::sync::Arc; +use uuid::{uuid, Uuid}; +const LIONESS_PROTOCOL_UUID: Uuid = uuid!("1912c626-f611-4569-9d62-fb40ff8e1474"); generic_protocol_initializer_setup!(Lioness, "lioness"); #[derive(Default)] @@ -32,14 +36,18 @@ impl ProtocolInitializer for LionessInitializer { async fn initialize( &mut self, hardware: Arc, - _: &UserDeviceDefinition, + _: &DeviceDefinition, ) -> Result, ButtplugDeviceError> { hardware - .subscribe(&HardwareSubscribeCmd::new(Endpoint::Rx)) + .subscribe(&HardwareSubscribeCmd::new( + LIONESS_PROTOCOL_UUID, + Endpoint::Rx, + )) .await?; let res = hardware .write_value(&HardwareWriteCmd::new( + &[LIONESS_PROTOCOL_UUID], Endpoint::Tx, vec![0x01, 0xAA, 0xAA, 0xBB, 0xCC, 0x10], true, @@ -59,18 +67,16 @@ impl ProtocolInitializer for LionessInitializer { pub struct Lioness {} impl ProtocolHandler for Lioness { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, - vec![0x02, 0xAA, 0xBB, 0xCC, 0xCC, scalar as u8], + vec![0x02, 0xAA, 0xBB, 0xCC, 0xCC, speed as u8], false, ) .into()]) diff --git a/crates/buttplug_server/src/device/protocol_impl/loob.rs b/crates/buttplug_server/src/device/protocol_impl/loob.rs new file mode 100644 index 000000000..a806f8228 --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/loob.rs @@ -0,0 +1,78 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2025 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 crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, + }, +}; +use async_trait::async_trait; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use buttplug_server_device_config::{ + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; +use std::cmp::{max, min}; +use std::sync::Arc; +use uuid::{uuid, Uuid}; + +const LOOB_PROTOCOL_UUID: Uuid = uuid!("b3a02457-3bda-4c5b-8363-aead6eda74ae"); +generic_protocol_initializer_setup!(Loob, "loob"); + +#[derive(Default)] +pub struct LoobInitializer {} + +#[async_trait] +impl ProtocolInitializer for LoobInitializer { + async fn initialize( + &mut self, + hardware: Arc, + _: &DeviceDefinition, + ) -> Result, ButtplugDeviceError> { + let msg = HardwareWriteCmd::new( + &[LOOB_PROTOCOL_UUID], + Endpoint::Tx, + vec![0x00, 0x01, 0x01, 0xf4], + true, + ); + hardware.write_value(&msg).await?; + Ok(Arc::new(Loob::default())) + } +} + +#[derive(Default)] +pub struct Loob {} + +impl ProtocolHandler for Loob { + fn handle_position_with_duration_cmd( + &self, + _feature_index: u32, + feature_id: Uuid, + position: u32, + duration: u32, + ) -> Result, ButtplugDeviceError> { + let pos: u16 = max(min(position as u16, 1000), 1); + let time: u16 = max(duration as u16, 1); + let mut data = pos.to_be_bytes().to_vec(); + for b in time.to_be_bytes() { + data.push(b); + } + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + data, + false, + ) + .into()]) + } +} diff --git a/buttplug/src/server/device/protocol/lovedistance.rs b/crates/buttplug_server/src/device/protocol_impl/lovedistance.rs similarity index 51% rename from buttplug/src/server/device/protocol/lovedistance.rs rename to crates/buttplug_server/src/device/protocol_impl/lovedistance.rs index 69d48e354..a1b68bfd4 100644 --- a/buttplug/src/server/device/protocol/lovedistance.rs +++ b/crates/buttplug_server/src/device/protocol_impl/lovedistance.rs @@ -5,22 +5,27 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, }, }; use async_trait::async_trait; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use buttplug_server_device_config::{ + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; use std::sync::Arc; +use uuid::{uuid, Uuid}; +const LOVEDISTANCE_PROTOCOL_UUID: Uuid = uuid!("a5f50cd5-7985-438c-a5bc-f8ff72bc0117"); generic_protocol_initializer_setup!(LoveDistance, "lovedistance"); #[derive(Default)] @@ -31,11 +36,21 @@ impl ProtocolInitializer for LoveDistanceInitializer { async fn initialize( &mut self, hardware: Arc, - _: &UserDeviceDefinition, + _: &DeviceDefinition, ) -> Result, ButtplugDeviceError> { - let msg = HardwareWriteCmd::new(Endpoint::Tx, vec![0xf3, 0, 0], false); + let msg = HardwareWriteCmd::new( + &[LOVEDISTANCE_PROTOCOL_UUID], + Endpoint::Tx, + vec![0xf3, 0, 0], + false, + ); hardware.write_value(&msg).await?; - let msg = HardwareWriteCmd::new(Endpoint::Tx, vec![0xf4, 1], false); + let msg = HardwareWriteCmd::new( + &[LOVEDISTANCE_PROTOCOL_UUID], + Endpoint::Tx, + vec![0xf4, 1], + false, + ); hardware.write_value(&msg).await?; Ok(Arc::new(LoveDistance::default())) } @@ -45,18 +60,16 @@ impl ProtocolInitializer for LoveDistanceInitializer { pub struct LoveDistance {} impl ProtocolHandler for LoveDistance { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, - vec![0xf3, 0x00, scalar as u8], + vec![0xf3, 0x00, speed as u8], false, ) .into()]) diff --git a/crates/buttplug_server/src/device/protocol_impl/lovehoney_desire.rs b/crates/buttplug_server/src/device/protocol_impl/lovehoney_desire.rs new file mode 100644 index 000000000..78c09cc15 --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/lovehoney_desire.rs @@ -0,0 +1,124 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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::sync::{ + atomic::{AtomicU8, Ordering}, + Arc, +}; + +use async_trait::async_trait; +use uuid::{uuid, Uuid}; + +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, + }, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use buttplug_server_device_config::{DeviceDefinition, ProtocolCommunicationSpecifier, UserDeviceIdentifier}; +const LOVEHONEY_DESIRE_PROTOCOL_UUID: Uuid = uuid!("5dcd8487-4814-44cb-a768-13bf81d545c0"); +const LOVEHONEY_DESIRE_VIBE2_PROTOCOL_UUID: Uuid = uuid!("d44a99fe-903b-4fff-bee7-1141767c9cca"); + +generic_protocol_initializer_setup!(LovehoneyDesire, "lovehoney-desire"); + +#[derive(Default)] +pub struct LovehoneyDesireInitializer {} + +#[async_trait] +impl ProtocolInitializer for LovehoneyDesireInitializer { + async fn initialize( + &mut self, + _: Arc, + def: &DeviceDefinition, + ) -> Result, ButtplugDeviceError> { + Ok(Arc::new(LovehoneyDesire::new( + def + .features() + .iter() + .filter(|x| x.output().is_some()) + .count() as u8, + ))) + } +} + +pub struct LovehoneyDesire { + current_commands: Vec, +} + +impl LovehoneyDesire { + fn new(num_vibrators: u8) -> Self { + Self { + current_commands: std::iter::repeat_with(|| AtomicU8::default()) + .take(num_vibrators as usize) + .collect(), + } + } +} + +impl ProtocolHandler for LovehoneyDesire { + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + // The Lovehoney Desire has 2 types of commands + // + // - Set both motors with one command + // - Set each motor separately + // + // We'll need to check what we got back and write our + // commands accordingly. + if self.current_commands.len() == 1 { + Ok(vec![HardwareWriteCmd::new( + &[LOVEHONEY_DESIRE_PROTOCOL_UUID], + Endpoint::Tx, + vec![0xF3, 0, speed as u8], + true, + ) + .into()]) + } else { + self.current_commands[feature_index as usize].store(speed as u8, Ordering::Relaxed); + let speed0 = self.current_commands[0].load(Ordering::Relaxed); + let speed1 = self.current_commands[1].load(Ordering::Relaxed); + if speed0 == speed1 { + Ok(vec![HardwareWriteCmd::new( + &[ + LOVEHONEY_DESIRE_PROTOCOL_UUID, + LOVEHONEY_DESIRE_VIBE2_PROTOCOL_UUID, + ], + Endpoint::Tx, + vec![0xF3, 0, speed0 as u8], + true, + ) + .into()]) + } else { + Ok(vec![ + HardwareWriteCmd::new( + &[LOVEHONEY_DESIRE_PROTOCOL_UUID], + Endpoint::Tx, + vec![0xF3, 1, speed0 as u8], + true, + ) + .into(), + HardwareWriteCmd::new( + &[LOVEHONEY_DESIRE_VIBE2_PROTOCOL_UUID], + Endpoint::Tx, + vec![0xF3, 2, speed1 as u8], + true, + ) + .into(), + ]) + } + } + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/lovense/lovense_max.rs b/crates/buttplug_server/src/device/protocol_impl/lovense/lovense_max.rs new file mode 100644 index 000000000..5ed7f5573 --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/lovense/lovense_max.rs @@ -0,0 +1,65 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 super::{form_lovense_command, form_vibrate_command}; +use crate::device::{ + hardware::{Hardware, HardwareCommand}, + protocol::{ + ProtocolHandler, + ProtocolKeepaliveStrategy, + }, +}; +use buttplug_core::{errors::ButtplugDeviceError, message::InputReadingV4}; +use futures::future::BoxFuture; +use std::sync::Arc; +use uuid::Uuid; + +#[derive(Default)] +pub struct LovenseMax {} + +impl ProtocolHandler for LovenseMax { + fn keepalive_strategy(&self) -> ProtocolKeepaliveStrategy { + super::keepalive_strategy() + } + + fn handle_output_vibrate_cmd( + &self, + _feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + form_vibrate_command(feature_id, speed) + } + + fn handle_output_oscillate_cmd( + &self, + _feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + form_vibrate_command(feature_id, speed) + } + + fn handle_output_constrict_cmd( + &self, + _feature_index: u32, + feature_id: Uuid, + level: u32, + ) -> Result, ButtplugDeviceError> { + form_lovense_command(feature_id, &format!("Air:Level:{level};")) + } + + fn handle_battery_level_cmd( + &self, + device_index: u32, + device: Arc, + feature_index: u32, + feature_id: Uuid, + ) -> BoxFuture<'static, Result> { + super::handle_battery_level_cmd(device_index, device, feature_index, feature_id) + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/lovense/lovense_multi_actuator.rs b/crates/buttplug_server/src/device/protocol_impl/lovense/lovense_multi_actuator.rs new file mode 100644 index 000000000..2714db847 --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/lovense/lovense_multi_actuator.rs @@ -0,0 +1,78 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 super::form_vibrate_command; +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, + protocol::{ProtocolHandler, ProtocolKeepaliveStrategy}, +}; +use buttplug_core::{ + errors::ButtplugDeviceError, + message::InputReadingV4, +}; +use buttplug_server_device_config::Endpoint; +use futures::future::BoxFuture; +use std::sync::{atomic::AtomicU32, Arc}; +use uuid::Uuid; + +#[derive(Default)] +pub struct LovenseMultiActuator { + _vibrator_values: Vec, +} + +impl LovenseMultiActuator { + pub fn new(num_vibrators: u32) -> Self { + Self { + _vibrator_values: std::iter::repeat_with(|| AtomicU32::new(0)) + .take(num_vibrators as usize) + .collect(), + } + } +} + +impl ProtocolHandler for LovenseMultiActuator { + fn keepalive_strategy(&self) -> ProtocolKeepaliveStrategy { + super::keepalive_strategy() + } + + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + let lovense_cmd = format!("Vibrate{}:{};", feature_index + 1, speed) + .as_bytes() + .to_vec(); + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + lovense_cmd, + false, + ) + .into()]) + } + + fn handle_output_oscillate_cmd( + &self, + _feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + form_vibrate_command(feature_id, speed) + } + + fn handle_battery_level_cmd( + &self, + device_index: u32, + device: Arc, + feature_index: u32, + feature_id: Uuid, + ) -> BoxFuture<'static, Result> { + super::handle_battery_level_cmd(device_index, device, feature_index, feature_id) + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/lovense/lovense_rotate_vibrator.rs b/crates/buttplug_server/src/device/protocol_impl/lovense/lovense_rotate_vibrator.rs new file mode 100644 index 000000000..f6f626a5e --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/lovense/lovense_rotate_vibrator.rs @@ -0,0 +1,78 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 super::{form_rotate_with_direction_command, form_vibrate_command}; + +use crate::device::{ + hardware::{Hardware, HardwareCommand}, + protocol::{ + ProtocolHandler, + ProtocolKeepaliveStrategy, + }, +}; +use buttplug_core::{errors::ButtplugDeviceError, message::InputReadingV4}; +use futures::future::BoxFuture; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; +use uuid::Uuid; + +#[derive(Default)] +pub struct LovenseRotateVibrator { + clockwise: AtomicBool, +} + +impl ProtocolHandler for LovenseRotateVibrator { + fn keepalive_strategy(&self) -> ProtocolKeepaliveStrategy { + super::keepalive_strategy() + } + + fn handle_output_vibrate_cmd( + &self, + _feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + form_vibrate_command(feature_id, speed) + } + + fn handle_output_rotate_cmd( + &self, + _feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + form_rotate_with_direction_command(speed, false) + } + + fn handle_rotation_with_direction_cmd( + &self, + _feature_index: u32, + _feature_id: Uuid, + speed: u32, + clockwise: bool, + ) -> Result, ButtplugDeviceError> { + let change = if clockwise != self.clockwise.load(Ordering::Relaxed) { + self.clockwise.store(clockwise, Ordering::Relaxed); + true + } else { + false + }; + form_rotate_with_direction_command(speed, change) + } + + fn handle_battery_level_cmd( + &self, + device_index: u32, + device: Arc, + feature_index: u32, + feature_id: Uuid, + ) -> BoxFuture<'static, Result> { + super::handle_battery_level_cmd(device_index, device, feature_index, feature_id) + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/lovense/lovense_single_actuator.rs b/crates/buttplug_server/src/device/protocol_impl/lovense/lovense_single_actuator.rs new file mode 100644 index 000000000..b14e1fd8d --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/lovense/lovense_single_actuator.rs @@ -0,0 +1,53 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 super::form_vibrate_command; +use crate::device::{ + hardware::{Hardware, HardwareCommand}, + protocol::{ProtocolHandler, ProtocolKeepaliveStrategy}, +}; +use buttplug_core::{errors::ButtplugDeviceError, message::InputReadingV4}; +use futures::future::BoxFuture; +use std::sync::Arc; +use uuid::Uuid; + +#[derive(Default)] +pub struct LovenseSingleActuator {} + +impl ProtocolHandler for LovenseSingleActuator { + fn keepalive_strategy(&self) -> ProtocolKeepaliveStrategy { + super::keepalive_strategy() + } + + fn handle_output_vibrate_cmd( + &self, + _feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + form_vibrate_command(feature_id, speed) + } + + fn handle_output_oscillate_cmd( + &self, + _feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + form_vibrate_command(feature_id, speed) + } + + fn handle_battery_level_cmd( + &self, + device_index: u32, + device: Arc, + feature_index: u32, + feature_id: Uuid, + ) -> BoxFuture<'static, Result> { + super::handle_battery_level_cmd(device_index, device, feature_index, feature_id) + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/lovense/lovense_stroker.rs b/crates/buttplug_server/src/device/protocol_impl/lovense/lovense_stroker.rs new file mode 100644 index 000000000..929f14c6f --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/lovense/lovense_stroker.rs @@ -0,0 +1,118 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, + protocol::{ProtocolHandler, ProtocolKeepaliveStrategy}, +}; +use buttplug_core::{ + errors::ButtplugDeviceError, + message::InputReadingV4, + util::{async_manager, sleep}, +}; +use buttplug_server_device_config::Endpoint; +use futures::future::BoxFuture; +use std::{ + sync::{ + atomic::{AtomicU32, Ordering}, + Arc, + }, + time::Duration, +}; +use uuid::{uuid, Uuid}; + +const LOVENSE_STROKER_PROTOCOL_UUID: Uuid = uuid!("a97fc354-5561-459a-bc62-110d7c2868ac"); + +pub struct LovenseStroker { + linear_info: Arc<(AtomicU32, AtomicU32)>, +} + +impl LovenseStroker { + pub fn new(hardware: Arc) -> Self { + let linear_info = Arc::new((AtomicU32::new(0), AtomicU32::new(0))); + async_manager::spawn(update_linear_movement( + hardware.clone(), + linear_info.clone(), + )); + Self { linear_info } + } +} + +impl ProtocolHandler for LovenseStroker { + fn keepalive_strategy(&self) -> ProtocolKeepaliveStrategy { + super::keepalive_strategy() + } + + fn handle_position_with_duration_cmd( + &self, + _feature_index: u32, + _feature_id: Uuid, + position: u32, + duration: u32, + ) -> Result, ButtplugDeviceError> { + self.linear_info.0.store(position, Ordering::Relaxed); + self.linear_info.1.store(duration, Ordering::Relaxed); + Ok(vec![]) + } + + fn handle_battery_level_cmd( + &self, + device_index: u32, + device: Arc, + feature_index: u32, + feature_id: Uuid, + ) -> BoxFuture<'static, Result> { + super::handle_battery_level_cmd(device_index, device, feature_index, feature_id) + } +} + +async fn update_linear_movement(device: Arc, linear_info: Arc<(AtomicU32, AtomicU32)>) { + let mut last_goal_position = 0i32; + let mut current_move_amount = 0i32; + let mut current_position = 0i32; + loop { + // See if we've updated our goal position + let goal_position = linear_info.0.load(Ordering::Relaxed) as i32; + // If we have and it's not the same, recalculate based on current status. + if last_goal_position != goal_position { + last_goal_position = goal_position; + // We move every 100ms, so divide the movement into that many chunks. + // If we're moving so fast it'd be under our 100ms boundary, just move in 1 step. + let move_steps = (linear_info.1.load(Ordering::Relaxed) / 100).max(1); + current_move_amount = (goal_position - current_position) / move_steps as i32; + } + + // If we aren't going anywhere, just pause then restart + if current_position == last_goal_position { + sleep(Duration::from_millis(100)).await; + continue; + } + + // Update our position, make sure we don't overshoot + current_position += current_move_amount; + if current_move_amount < 0 { + if current_position < last_goal_position { + current_position = last_goal_position; + } + } else if current_position > last_goal_position { + current_position = last_goal_position; + } + + let lovense_cmd = format!("FSetSite:{current_position};"); + + let hardware_cmd: HardwareWriteCmd = HardwareWriteCmd::new( + &[LOVENSE_STROKER_PROTOCOL_UUID], + Endpoint::Tx, + lovense_cmd.into_bytes(), + false, + ); + if device.write_value(&hardware_cmd).await.is_err() { + return; + } + sleep(Duration::from_millis(100)).await; + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/lovense/mod.rs b/crates/buttplug_server/src/device/protocol_impl/lovense/mod.rs new file mode 100644 index 000000000..3fe32885a --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/lovense/mod.rs @@ -0,0 +1,553 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 Nonpolynomial Labs LLC. All rights reserved. +// +// Licensed under the BSD 3-Clause license. See LICENSE file in the project root +// for full license information. + +mod lovense_max; +mod lovense_multi_actuator; +mod lovense_rotate_vibrator; +mod lovense_single_actuator; +mod lovense_stroker; + + +use lovense_max::LovenseMax; +use lovense_multi_actuator::LovenseMultiActuator; +use lovense_rotate_vibrator::LovenseRotateVibrator; +use lovense_single_actuator::LovenseSingleActuator; +use lovense_stroker::LovenseStroker; + +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareEvent, HardwareSubscribeCmd, HardwareWriteCmd}, + protocol::{ + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, ProtocolKeepaliveStrategy, + }, +}; +use async_trait::async_trait; +use buttplug_core::{ + errors::ButtplugDeviceError, + message::{self, FeatureType, InputData, InputReadingV4, InputTypeData}, + util::sleep, +}; +use buttplug_server_device_config::{ + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, + Endpoint +}; +use futures::{future::BoxFuture, FutureExt}; +use regex::Regex; +use std::{sync::Arc, time::Duration}; +use tokio::select; +use uuid::{uuid, Uuid}; + +// Constants for dealing with the Lovense subscript/write race condition. The +// timeout needs to be VERY long, otherwise this trips up old lovense serial +// adapters. +// +// Just buy new adapters, people. +const LOVENSE_COMMAND_TIMEOUT_MS: u64 = 500; +const LOVENSE_COMMAND_RETRY: u64 = 5; + +const LOVENSE_PROTOCOL_UUID: Uuid = uuid!("cfa3fac5-48bb-4d87-817e-a439965956e1"); + +pub mod setup { + use crate::device::protocol::{ProtocolIdentifier, ProtocolIdentifierFactory}; + #[derive(Default)] + pub struct LovenseIdentifierFactory {} + + impl ProtocolIdentifierFactory for LovenseIdentifierFactory { + fn identifier(&self) -> &str { + "lovense" + } + + fn create(&self) -> Box { + Box::new(super::LovenseIdentifier::default()) + } + } +} + +#[derive(Default)] +pub struct LovenseIdentifier {} + +fn lovense_model_resolver(type_response: String) -> String { + let parts = type_response.split(':').collect::>(); + if parts.len() < 2 { + warn!( + "Lovense Device returned invalid DeviceType info: {}", + type_response + ); + return "lovense".to_string(); + } + + let identifier = parts[0].to_owned(); + let version = parts[1].to_owned().parse::().unwrap_or(0); + + info!("Identified device type {} version {}", identifier, version); + + // Flexer: version must be 3+ to control actuators separately + if identifier == "EI" && version >= 3 { + return "EI-FW3".to_string(); + } + + identifier +} + +#[async_trait] +impl ProtocolIdentifier for LovenseIdentifier { + async fn identify( + &mut self, + hardware: Arc, + _: ProtocolCommunicationSpecifier, + ) -> Result<(UserDeviceIdentifier, Box), ButtplugDeviceError> { + let mut event_receiver = hardware.event_stream(); + let mut count = 0; + hardware + .subscribe(&HardwareSubscribeCmd::new( + LOVENSE_PROTOCOL_UUID, + Endpoint::Rx, + )) + .await?; + + loop { + let msg = HardwareWriteCmd::new( + &[LOVENSE_PROTOCOL_UUID], + Endpoint::Tx, + b"DeviceType;".to_vec(), + false, + ); + hardware.write_value(&msg).await?; + + select! { + event = event_receiver.recv().fuse() => { + if let Ok(HardwareEvent::Notification(_, _, n)) = event { + let type_response = std::str::from_utf8(&n).map_err(|_| ButtplugDeviceError::ProtocolSpecificError("lovense".to_owned(), "Lovense device init got back non-UTF8 string.".to_owned()))?.to_owned(); + debug!("Lovense Device Type Response: {}", type_response); + let ident = lovense_model_resolver(type_response); + return Ok((UserDeviceIdentifier::new(hardware.address(), "lovense", &Some(ident.clone())), Box::new(LovenseInitializer::new(ident)))); + } else { + return Err( + ButtplugDeviceError::ProtocolSpecificError( + "Lovense".to_owned(), + "Lovense Device disconnected while getting DeviceType info.".to_owned(), + ), + ); + } + } + _ = sleep(Duration::from_millis(LOVENSE_COMMAND_TIMEOUT_MS)).fuse() => { + count += 1; + if count > LOVENSE_COMMAND_RETRY { + warn!("Lovense Device timed out while getting DeviceType info. ({} retries)", LOVENSE_COMMAND_RETRY); + let re = Regex::new(r"LVS-([A-Z]+)\d+").expect("Static regex shouldn't fail"); + if let Some(caps) = re.captures(hardware.name()) { + info!("Lovense Device identified by BLE name"); + return Ok((UserDeviceIdentifier::new(hardware.address(), "lovense", &Some(caps[1].to_string())), Box::new(LovenseInitializer::new(caps[1].to_string())))); + }; + return Ok((UserDeviceIdentifier::new(hardware.address(), "lovense", &None), Box::new(LovenseInitializer::new("".to_string())))); + } + } + } + } + } +} +pub struct LovenseInitializer { + device_type: String, +} + +impl LovenseInitializer { + pub fn new(device_type: String) -> Self { + Self { device_type } + } +} + +#[async_trait] +impl ProtocolInitializer for LovenseInitializer { + async fn initialize( + &mut self, + hardware: Arc, + device_definition: &DeviceDefinition, + ) -> Result, ButtplugDeviceError> { + let device_type = self.device_type.clone(); + + let vibrator_count = device_definition + .features() + .iter() + .filter(|x| [FeatureType::Vibrate, FeatureType::Oscillate].contains(&x.feature_type())) + .count(); + + let output_count = device_definition + .features() + .iter() + .filter(|x| x.output().is_some()) + .count(); + + let vibrator_rotator = output_count == 2 + && device_definition + .features() + .iter() + .filter(|x| x.feature_type() == FeatureType::Vibrate) + .count() + == 1 + && device_definition + .features() + .iter() + .filter(|x| x.feature_type() == FeatureType::RotateWithDirection) + .count() + == 1; + + let lovense_max = output_count == 2 + && device_definition + .features() + .iter() + .filter(|x| x.feature_type() == FeatureType::Vibrate) + .count() + == 1 + && device_definition + .features() + .iter() + .filter(|x| x.feature_type() == FeatureType::Constrict) + .count() + == 1; + + // This might need better tuning if other complex Lovenses are released + // Currently this only applies to the Flexer/Lapis/Solace + let use_mply = + (vibrator_count == 2 && output_count > 2) || vibrator_count > 2 || device_type == "H"; + + // New Lovense devices seem to be moving to the simplified LVS:; command format. + // I'm not sure if there's a good way to detect this. + let use_lvs = device_type == "OC"; + + debug!( + "Device type {} initialized with {} vibrators {} using Mply", + device_type, + vibrator_count, + if use_mply { "" } else { "not " } + ); + + if device_type == "BA" { + Ok(Arc::new(LovenseStroker::new(hardware))) + } else if output_count == 1 { + Ok(Arc::new(LovenseSingleActuator::default())) + } else if lovense_max { + Ok(Arc::new(LovenseMax::default())) + } else if vibrator_rotator { + Ok(Arc::new(LovenseRotateVibrator::default())) + } else { + Ok(Arc::new(LovenseMultiActuator::new(vibrator_count as u32))) + } + } +} +/* +pub struct Lovense { + rotation_direction: AtomicBool, + vibrator_values: Vec, + use_mply: bool, + use_lvs: bool, + device_type: String, + value_cache: DashMap, + linear_info: Arc<(AtomicU32, AtomicU32)>, +} + +impl Lovense { + pub fn new( + device_type: &str, + vibrator_count: usize, + use_mply: bool, + use_lvs: bool, + ) -> Self { + let linear_info = Arc::new((AtomicU32::new(0), AtomicU32::new(0))); + + let mut vibrator_values = vec![]; + for _ in 0..vibrator_count { + vibrator_values.push(AtomicU32::new(0)); + } + + Self { + rotation_direction: AtomicBool::new(false), + vibrator_values, + use_mply, + use_lvs, + device_type: device_type.to_owned(), + value_cache: DashMap::new(), + linear_info, + } + } + + fn handle_lvs_cmd(&self) -> Result, ButtplugDeviceError> { + let mut speeds = "LVS:{}".as_bytes().to_vec(); + for i in self.vibrator_values.iter() { + speeds.push(i.load(Ordering::Relaxed) as u8); + } + speeds.push(0x3b); + + Ok(vec![HardwareWriteCmd::new( + LOVENSE_PROTOCOL_UUID, + Endpoint::Tx, + speeds, + false, + ) + .into()]) + } + + fn handle_mply_cmd(&self) -> Result, ButtplugDeviceError> { + /* + let mut speeds = cmds + .iter() + .map(|x| { + if let Some(val) = x { + val.1.to_string() + } else { + "-1".to_string() + } + }) + .collect::>(); + + if speeds.len() == 1 && self.device_type == "H" { + // Max range unless stopped + speeds.push(if speeds[0] == "0" { + "0".to_string() + } else { + "20".to_string() + }); + } + + let lovense_cmd = format!("Mply:{};", speeds.join(":")).as_bytes().to_vec(); + + Ok(vec![HardwareWriteCmd::new( + Endpoint::Tx, + lovense_cmd, + false, + ) + .into()]) + */ + Ok(vec![]) + } +} + +impl ProtocolHandler for Lovense { + fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { + // For Lovense, we'll just repeat the device type packet and drop the result. + super::ProtocolKeepaliveStrategy::HardwareRequiredRepeatPacketStrategy(HardwareWriteCmd::new( + LOVENSE_PROTOCOL_UUID, + Endpoint::Tx, + b"DeviceType;".to_vec(), + false, + )) + } + + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + let current_vibrator_value = + self.vibrator_values[feature_index as usize].load(Ordering::Relaxed); + if current_vibrator_value == speed { + Ok(vec![]) + } else { + self.vibrator_values[feature_index as usize].store(speed, Ordering::Relaxed); + let speeds: Vec = self + .vibrator_values + .iter() + .map(|v| v.load(Ordering::Relaxed)) + .collect(); + if self.use_lvs { + self.handle_lvs_cmd() + } else if self.use_mply { + self.handle_mply_cmd() + } else { + let lovense_cmd = if self.vibrator_values.len() == 1 { + format!("Vibrate:{speed};").as_bytes().to_vec() + } else { + format!("Vibrate{}:{};", feature_index + 1, speed) + .as_bytes() + .to_vec() + }; + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + lovense_cmd, + false, + ) + .into()]) + } + } + /* + if self.use_lvs { + self.handle_lvs_cmd(cmd) + } else if self.use_mply { + self.handle_mply_cmd(cmd) + } else { + // Handle vibration commands, these will be by far the most common. Fucking machine oscillation + // uses lovense vibrate commands internally too, so we can include them here. + let vibrate_cmds: Vec<> = cmds + .iter() + .filter(|x| { + if let Some(val) = x { + [ActuatorType::Vibrate, ActuatorType::Oscillate].contains(&val.0) + } else { + false + } + }) + .map(|x| x.as_ref().expect("Already verified is some")) + .collect(); + if !vibrate_cmds.is_empty() { + // Lovense is the same situation as the Lovehoney Desire, where commands + // are different if we're addressing all motors or seperate motors. + // Difference here being that there's Lovense variants with different + // numbers of motors. + // + // Neat way of checking if everything is the same via + // https://sts10.github.io/2019/06/06/is-all-equal-function.html. + // + // Just make sure we're not matching on None, 'cause if that's the case + // we ain't got shit to do. + // + // Note that the windowed comparison causes mixed types as well as mixed + // speeds to fall back to separate commands. This is because the Gravity's + // thruster on Vibrate2 is independent of Vibrate + if self.vibrator_count == vibrate_cmds.len() + && (self.vibrator_count == 1 + || vibrate_cmds + .windows(2) + .all(|w| w[0].0 == w[1].0 && w[0].1 == w[1].1)) + { + let lovense_cmd = format!("Vibrate:{};", vibrate_cmds[0].1) + .as_bytes() + .to_vec(); + hardware_cmds.push(HardwareWriteCmd::new(Endpoint::Tx, lovense_cmd, false).into()); + } else { + for (i, cmd) in cmds.iter().enumerate() { + if let Some((actuator, speed)) = cmd { + if ![ActuatorType::Vibrate, ActuatorType::Oscillate].contains(actuator) { + continue; + } + let lovense_cmd = format!("Vibrate{}:{};", i + 1, speed).as_bytes().to_vec(); + hardware_cmds.push(HardwareWriteCmd::new(Endpoint::Tx, lovense_cmd, false).into()); + } + } + } + } + */ + } +} + */ + +fn handle_battery_level_cmd( + device_index: u32, + device: Arc, + feature_index: u32, + feature_id: Uuid, +) -> BoxFuture<'static, Result> { + let mut device_notification_receiver = device.event_stream(); + async move { + let write_fut = device.write_value(&HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + b"Battery;".to_vec(), + false, + )); + write_fut.await?; + while let Ok(event) = device_notification_receiver.recv().await { + match event { + HardwareEvent::Notification(_, _, data) => { + if let Ok(data_str) = std::str::from_utf8(&data) { + debug!("Lovense event received: {}", data_str); + let len = data_str.len(); + // Depending on the state of the toy, we may get an initial + // character of some kind, i.e. if the toy is currently vibrating + // then battery level comes up as "s89;" versus just "89;". We'll + // need to chop the semicolon and make sure we only read the + // numbers in the string. + // + // Contains() is casting a wider net than we need here, but it'll + // do for now. + let start_pos = usize::from(data_str.contains('s')); + if let Ok(level) = data_str[start_pos..(len - 1)].parse::() { + return Ok(message::InputReadingV4::new( + device_index, + feature_index, + InputTypeData::Battery(InputData::new(level)) + )); + } + } + } + HardwareEvent::Disconnected(_) => { + return Err(ButtplugDeviceError::ProtocolSpecificError( + "Lovense".to_owned(), + "Lovense Device disconnected while getting Battery info.".to_owned(), + )) + } + } + } + Err(ButtplugDeviceError::ProtocolSpecificError( + "Lovense".to_owned(), + "Lovense Device disconnected while getting Battery info.".to_owned(), + )) + } + .boxed() +} + +pub(super) fn keepalive_strategy() -> ProtocolKeepaliveStrategy { + // For Lovense, we'll just repeat the device type packet and drop the result. + ProtocolKeepaliveStrategy::HardwareRequiredRepeatPacketStrategy(HardwareWriteCmd::new( + &[LOVENSE_PROTOCOL_UUID], + Endpoint::Tx, + b"DeviceType;".to_vec(), + false, + )) +} + +pub(super) fn form_lovense_command( + feature_id: Uuid, + command: &str, +) -> Result, ButtplugDeviceError> { + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + command.as_bytes().to_vec(), + false, + ) + .into()]) +} + +pub(super) fn form_vibrate_command( + feature_id: Uuid, + speed: u32, +) -> Result, ButtplugDeviceError> { + form_lovense_command(feature_id, &format!("Vibrate:{speed};")) +} + +// Due to swapping direction with lovense requiring a seperate command, we have to treat these like +// two seperate outputs, otherwise we'll stomp on ourselves. Luckily Lovense devices currently only +// have one rotation mechanism. +const LOVENSE_ROTATE_UUID: Uuid = uuid!("4a741489-922f-4f0b-a594-175b75482849"); +const LOVENSE_ROTATE_DIRECTION_UUID: Uuid = uuid!("4ad23456-2ba8-4916-bd91-9b603811f253"); + +pub(super) fn form_rotate_with_direction_command( + speed: u32, + change_direction: bool, +) -> Result, ButtplugDeviceError> { + let mut hardware_cmds = vec![]; + let lovense_cmd = format!("Rotate:{speed};").as_bytes().to_vec(); + hardware_cmds + .push(HardwareWriteCmd::new(&[LOVENSE_ROTATE_UUID], Endpoint::Tx, lovense_cmd, false).into()); + if change_direction { + hardware_cmds.push( + HardwareWriteCmd::new( + &[LOVENSE_ROTATE_DIRECTION_UUID], + Endpoint::Tx, + b"RotateChange;".to_vec(), + false, + ) + .into(), + ); + } + trace!("{:?}", hardware_cmds); + Ok(hardware_cmds) +} diff --git a/buttplug/src/server/device/protocol/lovense_connect_service.rs b/crates/buttplug_server/src/device/protocol_impl/lovense_connect_service.rs similarity index 88% rename from buttplug/src/server/device/protocol/lovense_connect_service.rs rename to crates/buttplug_server/src/device/protocol_impl/lovense_connect_service.rs index 312f87485..d6585555f 100644 --- a/buttplug/src/server/device/protocol/lovense_connect_service.rs +++ b/crates/buttplug_server/src/device/protocol_impl/lovense_connect_service.rs @@ -8,17 +8,20 @@ use crate::{ core::{ errors::ButtplugDeviceError, - message::{self, ActuatorType, ButtplugDeviceMessage, Endpoint, FeatureType, SensorReadingV4}, + message::{self, ActuatorType, Endpoint, FeatureType, SensorReadingV4}, }, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareReadCmd, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, +use crate::{ + device::{ + configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, + hardware::{Hardware, HardwareCommand, HardwareReadCmd, HardwareWriteCmd}, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, + }, }, + message::checked_sensor_read_cmd::CheckedSensorReadCmdV4, }, }; use async_trait::async_trait; @@ -68,7 +71,7 @@ impl ProtocolInitializer for LovenseConnectServiceInitializer { .to_vec(); hardware - .write_value(&HardwareWriteCmd::new(Endpoint::Tx, lovense_cmd, false).into()) + .write_value(&HardwareWriteCmd::new(Endpoint::Tx, lovense_cmd, false)) .await?; protocol.vibrator_count = 0; @@ -97,15 +100,15 @@ impl LovenseConnectService { } impl ProtocolHandler for LovenseConnectService { - fn handle_scalar_cmd( + fn handle_value_cmd( &self, - cmds: &[Option<(ActuatorType, u32)>], + cmds: &[Option<(ActuatorType, i32)>], ) -> Result, ButtplugDeviceError> { let mut hardware_cmds = vec![]; // Handle vibration commands, these will be by far the most common. Fucking machine oscillation // uses lovense vibrate commands internally too, so we can include them here. - let vibrate_cmds: Vec<&(ActuatorType, u32)> = cmds + let vibrate_cmds: Vec<&(ActuatorType, i32)> = cmds .iter() .filter(|x| { if let Some(val) = x { @@ -160,7 +163,7 @@ impl ProtocolHandler for LovenseConnectService { } // Handle constriction commands. - let thrusting_cmds: Vec<&(ActuatorType, u32)> = cmds + let thrusting_cmds: Vec<&(ActuatorType, i32)> = cmds .iter() .filter(|x| { if let Some(val) = x { @@ -180,7 +183,7 @@ impl ProtocolHandler for LovenseConnectService { } // Handle constriction commands. - let constrict_cmds: Vec<&(ActuatorType, u32)> = cmds + let constrict_cmds: Vec<&(ActuatorType, i32)> = cmds .iter() .filter(|x| { if let Some(val) = x { @@ -208,7 +211,7 @@ impl ProtocolHandler for LovenseConnectService { } // Handle "rotation" commands: Currently just applicable as the Flexer's Fingering command - let rotation_cmds: Vec<&(ActuatorType, u32)> = cmds + let rotation_cmds: Vec<&(ActuatorType, i32)> = cmds .iter() .filter(|x| { if let Some(val) = x { @@ -273,15 +276,15 @@ impl ProtocolHandler for LovenseConnectService { cmds: &[Option<(u32, bool)>], ) -> Result, ButtplugDeviceError> { let mut hardware_cmds = vec![]; - if let Some(Some((speed, clockwise))) = cmds.get(0) { + if let Some(Some((speed, clockwise))) = cmds.first() { let lovense_cmd = format!("/Rotate?v={}&t={}", speed, self.address) .as_bytes() .to_vec(); hardware_cmds.push(HardwareWriteCmd::new(Endpoint::Tx, lovense_cmd, false).into()); - let dir = self.rotation_direction.load(Ordering::SeqCst); + let dir = self.rotation_direction.load(Ordering::Relaxed); // TODO Should we store speed and direction as an option for rotation caching? This is weird. if dir != *clockwise { - self.rotation_direction.store(*clockwise, Ordering::SeqCst); + self.rotation_direction.store(*clockwise, Ordering::Relaxed); hardware_cmds .push(HardwareWriteCmd::new(Endpoint::Tx, b"RotateChange?".to_vec(), false).into()); } @@ -292,7 +295,7 @@ impl ProtocolHandler for LovenseConnectService { fn handle_battery_level_cmd( &self, device: Arc, - msg: message::SensorReadCmdV4, + msg: CheckedSensorReadCmdV4, ) -> BoxFuture> { async move { // This is a dummy read. We just store the battery level in the device @@ -301,15 +304,12 @@ impl ProtocolHandler for LovenseConnectService { .read_value(&HardwareReadCmd::new(Endpoint::Rx, 0, 0)) .await?; debug!("Battery level: {}", reading.data()[0]); - Ok( - message::SensorReadingV4::new( - msg.device_index(), - *msg.feature_index(), - *msg.sensor_type(), - vec![reading.data()[0] as i32], - ) - .into(), - ) + Ok(message::SensorReadingV4::new( + msg.device_index(), + msg.feature_index(), + msg.input_type(), + vec![reading.data()[0] as i32], + )) } .boxed() } diff --git a/buttplug/src/server/device/protocol/lovenuts.rs b/crates/buttplug_server/src/device/protocol_impl/lovenuts.rs similarity index 51% rename from buttplug/src/server/device/protocol/lovenuts.rs rename to crates/buttplug_server/src/device/protocol_impl/lovenuts.rs index 82fadf963..fe4ef1dca 100644 --- a/buttplug/src/server/device/protocol/lovenuts.rs +++ b/crates/buttplug_server/src/device/protocol_impl/lovenuts.rs @@ -5,13 +5,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(LoveNuts, "lovenuts"); @@ -19,20 +20,23 @@ generic_protocol_setup!(LoveNuts, "lovenuts"); pub struct LoveNuts {} impl ProtocolHandler for LoveNuts { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { let mut data: Vec = vec![0x45, 0x56, 0x4f, 0x4c]; - data.append(&mut [scalar as u8 | (scalar as u8) << 4; 10].to_vec()); + data.append(&mut [speed as u8 | (speed as u8) << 4; 10].to_vec()); data.push(0x00); data.push(0xff); - Ok(vec![HardwareWriteCmd::new(Endpoint::Tx, data, false).into()]) + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + data, + false, + ) + .into()]) } } diff --git a/crates/buttplug_server/src/device/protocol_impl/luvmazer.rs b/crates/buttplug_server/src/device/protocol_impl/luvmazer.rs new file mode 100644 index 000000000..ec12d3885 --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/luvmazer.rs @@ -0,0 +1,53 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2025 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 uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{ProtocolHandler, generic_protocol_setup} +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; + + +generic_protocol_setup!(Luvmazer, "luvmazer"); + +#[derive(Default)] +pub struct Luvmazer {} + +impl ProtocolHandler for Luvmazer { + fn handle_output_vibrate_cmd( + &self, + _feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + vec![0xa0, 0x01, 0x00, 0x00, 0x64, speed as u8], + false, + ) + .into()]) + } + + fn handle_output_rotate_cmd( + &self, + _feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + vec![0xa0, 0x0f, 0x00, 0x00, 0x64, speed as u8], + false, + ) + .into()]) + } +} diff --git a/buttplug/src/server/device/protocol/magic_motion_v1.rs b/crates/buttplug_server/src/device/protocol_impl/magic_motion_v1.rs similarity index 67% rename from buttplug/src/server/device/protocol/magic_motion_v1.rs rename to crates/buttplug_server/src/device/protocol_impl/magic_motion_v1.rs index d8d5d0349..8b3907fec 100644 --- a/buttplug/src/server/device/protocol/magic_motion_v1.rs +++ b/crates/buttplug_server/src/device/protocol_impl/magic_motion_v1.rs @@ -5,13 +5,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(MagicMotionV1, "magic-motion-1"); @@ -19,16 +20,14 @@ generic_protocol_setup!(MagicMotionV1, "magic-motion-1"); pub struct MagicMotionV1 {} impl ProtocolHandler for MagicMotionV1 { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, vec![ 0x0b, @@ -40,7 +39,7 @@ impl ProtocolHandler for MagicMotionV1 { 0x00, 0x04, 0x08, - scalar as u8, + speed as u8, 0x64, 0x00, ], @@ -49,12 +48,14 @@ impl ProtocolHandler for MagicMotionV1 { .into()]) } - fn handle_scalar_oscillate_cmd( + fn handle_output_oscillate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, vec![ 0x0b, @@ -66,7 +67,7 @@ impl ProtocolHandler for MagicMotionV1 { 0x00, 0x04, 0x08, - scalar as u8, + speed as u8, 0x64, 0x00, ], diff --git a/crates/buttplug_server/src/device/protocol_impl/magic_motion_v2.rs b/crates/buttplug_server/src/device/protocol_impl/magic_motion_v2.rs new file mode 100644 index 000000000..258e08c3d --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/magic_motion_v2.rs @@ -0,0 +1,70 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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::sync::atomic::{AtomicU8, Ordering}; + +use uuid::{uuid, Uuid}; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; + +const MAGIC_MOTION_2_PROTOCOL_UUID: Uuid = uuid!("4d6e9297-c57e-4ce7-a63c-24cc7d117a47"); + +generic_protocol_setup!(MagicMotionV2, "magic-motion-2"); + +pub struct MagicMotionV2 { + speeds: [AtomicU8; 2], +} + +impl Default for MagicMotionV2 { + fn default() -> Self { + Self { + speeds: [AtomicU8::new(0), AtomicU8::new(0)], + } + } +} + +impl ProtocolHandler for MagicMotionV2 { + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.speeds[feature_index as usize].store(speed as u8, Ordering::Relaxed); + let data = vec![ + 0x10, + 0xff, + 0x04, + 0x0a, + 0x32, + 0x0a, + 0x00, + 0x04, + 0x08, + self.speeds[0].load(Ordering::Relaxed), + 0x64, + 0x00, + 0x04, + 0x08, + self.speeds[1].load(Ordering::Relaxed), + 0x64, + 0x01, + ]; + Ok(vec![HardwareWriteCmd::new( + &[MAGIC_MOTION_2_PROTOCOL_UUID], + Endpoint::Tx, + data, + false, + ) + .into()]) + } +} diff --git a/buttplug/src/server/device/protocol/magic_motion_v3.rs b/crates/buttplug_server/src/device/protocol_impl/magic_motion_v3.rs similarity index 63% rename from buttplug/src/server/device/protocol/magic_motion_v3.rs rename to crates/buttplug_server/src/device/protocol_impl/magic_motion_v3.rs index f644910f7..defc6c97b 100644 --- a/buttplug/src/server/device/protocol/magic_motion_v3.rs +++ b/crates/buttplug_server/src/device/protocol_impl/magic_motion_v3.rs @@ -5,13 +5,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(MagicMotionV3, "magic-motion-3"); @@ -19,16 +20,14 @@ generic_protocol_setup!(MagicMotionV3, "magic-motion-3"); pub struct MagicMotionV3 {} impl ProtocolHandler for MagicMotionV3 { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, vec![ 0x0b, @@ -40,7 +39,7 @@ impl ProtocolHandler for MagicMotionV3 { 0x00, 0x04, 0x08, - scalar as u8, + speed as u8, 0x64, 0x00, ], 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 new file mode 100644 index 000000000..139e34897 --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/magic_motion_v4.rs @@ -0,0 +1,111 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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::sync::{ + atomic::{AtomicU8, Ordering}, + Arc, +}; + +use async_trait::async_trait; +use uuid::{uuid, Uuid}; + +use buttplug_server_device_config::DeviceDefinition; +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, + }, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use buttplug_server_device_config::{ProtocolCommunicationSpecifier, UserDeviceIdentifier}; +const MAGICMOTIONV4_PROTOCOL_UUID: Uuid = uuid!("d4d62d09-c3e1-44c9-8eba-caa15de5b2a7"); + +generic_protocol_initializer_setup!(MagicMotionV4, "magic-motion-4"); + +#[derive(Default)] +pub struct MagicMotionV4Initializer {} + +#[async_trait] +impl ProtocolInitializer for MagicMotionV4Initializer { + async fn initialize( + &mut self, + _: Arc, + def: &DeviceDefinition, + ) -> Result, ButtplugDeviceError> { + Ok(Arc::new(MagicMotionV4::new( + def + .features() + .iter() + .filter(|x| x.output().is_some()) + .count() as u8, + ))) + } +} + +pub struct MagicMotionV4 { + current_commands: Vec, +} + +impl MagicMotionV4 { + fn new(num_vibrators: u8) -> Self { + Self { + current_commands: std::iter::repeat_with(|| AtomicU8::default()) + .take(num_vibrators as usize) + .collect(), + } + } +} + +impl ProtocolHandler for MagicMotionV4 { + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + let data = if self.current_commands.len() == 1 { + vec![ + 0x10, + 0xff, + 0x04, + 0x0a, + 0x32, + 0x32, + 0x00, + 0x04, + 0x08, + speed as u8, + 0x64, + 0x00, + 0x04, + 0x08, + speed as u8, + 0x64, + 0x01, + ] + } else { + self.current_commands[feature_index as usize].store(speed as u8, Ordering::Relaxed); + let speed0 = self.current_commands[0].load(Ordering::Relaxed); + let speed1 = self.current_commands[1].load(Ordering::Relaxed); + vec![ + 0x10, 0xff, 0x04, 0x0a, 0x32, 0x32, 0x00, 0x04, 0x08, speed0, 0x64, 0x00, 0x04, 0x08, + speed1, 0x64, 0x01, + ] + }; + Ok(vec![HardwareWriteCmd::new( + &[MAGICMOTIONV4_PROTOCOL_UUID], + Endpoint::Tx, + data, + true, + ) + .into()]) + } +} diff --git a/buttplug/src/server/device/protocol/mannuo.rs b/crates/buttplug_server/src/device/protocol_impl/mannuo.rs similarity index 52% rename from buttplug/src/server/device/protocol/mannuo.rs rename to crates/buttplug_server/src/device/protocol_impl/mannuo.rs index 909848821..3824af578 100644 --- a/buttplug/src/server/device/protocol/mannuo.rs +++ b/crates/buttplug_server/src/device/protocol_impl/mannuo.rs @@ -5,13 +5,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(ManNuo, "mannuo"); @@ -19,22 +20,25 @@ generic_protocol_setup!(ManNuo, "mannuo"); pub struct ManNuo {} impl ProtocolHandler for ManNuo { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { - let mut data = vec![0xAA, 0x55, 0x06, 0x01, 0x01, 0x01, scalar as u8, 0xFA]; + let mut data = vec![0xAA, 0x55, 0x06, 0x01, 0x01, 0x01, speed as u8, 0xFA]; // Simple XOR of everything up to the 9th byte for CRC. let mut crc: u8 = 0; for b in data.clone() { crc ^= b; } data.push(crc); - Ok(vec![HardwareWriteCmd::new(Endpoint::Tx, data, true).into()]) + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + data, + true, + ) + .into()]) } } diff --git a/buttplug/src/server/device/protocol/maxpro.rs b/crates/buttplug_server/src/device/protocol_impl/maxpro.rs similarity index 57% rename from buttplug/src/server/device/protocol/maxpro.rs rename to crates/buttplug_server/src/device/protocol_impl/maxpro.rs index ccd379451..7820fc5a5 100644 --- a/buttplug/src/server/device/protocol/maxpro.rs +++ b/crates/buttplug_server/src/device/protocol_impl/maxpro.rs @@ -5,13 +5,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(Maxpro, "maxpro"); @@ -19,14 +20,11 @@ generic_protocol_setup!(Maxpro, "maxpro"); pub struct Maxpro {} impl ProtocolHandler for Maxpro { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { let mut data = vec![ 0x55u8, @@ -35,9 +33,9 @@ impl ProtocolHandler for Maxpro { 0xff, 0xff, 0x3f, - scalar as u8, + speed as u8, 0x5f, - scalar as u8, + speed as u8, 0x00, ]; let mut crc: u8 = 0; @@ -47,6 +45,12 @@ impl ProtocolHandler for Maxpro { } data[9] = crc; - Ok(vec![HardwareWriteCmd::new(Endpoint::Tx, data, false).into()]) + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + data, + false, + ) + .into()]) } } diff --git a/buttplug/src/server/device/protocol/meese.rs b/crates/buttplug_server/src/device/protocol_impl/meese.rs similarity index 53% rename from buttplug/src/server/device/protocol/meese.rs rename to crates/buttplug_server/src/device/protocol_impl/meese.rs index 46e155440..b4b34b3e7 100644 --- a/buttplug/src/server/device/protocol/meese.rs +++ b/crates/buttplug_server/src/device/protocol_impl/meese.rs @@ -5,13 +5,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(Meese, "meese"); @@ -19,18 +20,16 @@ generic_protocol_setup!(Meese, "meese"); pub struct Meese {} impl ProtocolHandler for Meese { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - index: u32, - scalar: u32, + feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, - vec![0x01, 0x80, 0x01 + (index as u8), (scalar as u8)], + vec![0x01, 0x80, 0x01 + (feature_index as u8), (speed as u8)], true, ) .into()]) diff --git a/crates/buttplug_server/src/device/protocol_impl/metaxsire.rs b/crates/buttplug_server/src/device/protocol_impl/metaxsire.rs new file mode 100644 index 000000000..02e9bd7a0 --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/metaxsire.rs @@ -0,0 +1,142 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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::sync::atomic::{AtomicU8, Ordering}; +use std::sync::Arc; + +use async_trait::async_trait; +use uuid::{uuid, Uuid}; + +use buttplug_core::{ + errors::ButtplugDeviceError, + message::OutputType, +}; +use buttplug_server_device_config::{DeviceDefinition, Endpoint, ProtocolCommunicationSpecifier, UserDeviceIdentifier}; + +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, + }, +}; + +generic_protocol_initializer_setup!(MetaXSire, "metaxsire"); + +#[derive(Default)] +pub struct MetaXSireInitializer {} + +#[async_trait] +impl ProtocolInitializer for MetaXSireInitializer { + async fn initialize( + &mut self, + _: Arc, + def: &DeviceDefinition, + ) -> Result, ButtplugDeviceError> { + let mut commands = vec![]; + def.features().iter().for_each(|x| { + if x.output().is_some() { + commands.push((x.feature_type().try_into().unwrap(), AtomicU8::default())) + } + }); + Ok(Arc::new(MetaXSire::new(commands))) + } +} + +const METAXSIRE_PROTOCOL_UUID: Uuid = uuid!("6485a762-2ea7-48c1-a4ba-ab724e618348"); + +#[derive(Default)] +pub struct MetaXSire { + commands: Vec<(OutputType, AtomicU8)>, +} + +impl MetaXSire { + fn new(commands: Vec<(OutputType, AtomicU8)>) -> Self { + Self { commands } + } + + fn form_command( + &self, + feature_index: u32, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.commands[feature_index as usize] + .1 + .store(speed as u8, Ordering::Relaxed); + let mut data: Vec = vec![0x23, 0x07]; + data.push((self.commands.len() * 3) as u8); + + for (i, (output_type, speed)) in self.commands.iter().enumerate() { + // motor number + data.push(0x80 | ((i + 1) as u8)); + // motor type: 03=vibe 04=pump 06=rotate + data.push(if *output_type == OutputType::Rotate { + 0x06 + } else if *output_type == OutputType::Constrict || *output_type == OutputType::Oscillate { + 0x04 + } else { + // Vibrate + 0x03 + }); + data.push(speed.load(Ordering::Relaxed)); + } + + let mut crc: u8 = 0; + for b in data.clone() { + crc ^= b; + } + data.push(crc); + + Ok(vec![HardwareWriteCmd::new( + &[METAXSIRE_PROTOCOL_UUID], + Endpoint::Tx, + data, + false, + ) + .into()]) + } +} + +impl ProtocolHandler for MetaXSire { + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.form_command(feature_index, speed) + } + + fn handle_output_oscillate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.form_command(feature_index, speed) + } + + fn handle_output_rotate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.form_command(feature_index, speed) + } + + fn handle_output_constrict_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + level: u32, + ) -> Result, ButtplugDeviceError> { + self.form_command(feature_index, level) + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/metaxsire_v2.rs b/crates/buttplug_server/src/device/protocol_impl/metaxsire_v2.rs new file mode 100644 index 000000000..4e508d7d1 --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/metaxsire_v2.rs @@ -0,0 +1,95 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2023 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 crate::device::hardware::Hardware; +use crate::device::protocol::ProtocolInitializer; +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_initializer_setup, ProtocolHandler, ProtocolIdentifier}, +}; +use async_trait::async_trait; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use buttplug_server_device_config::{ + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; +use std::sync::Arc; +use uuid::{uuid, Uuid}; + +const METAXSIRE_V2_PROTOCOL_ID: Uuid = uuid!("28b934b4-ca45-4e14-85e7-4c1524b2b4c1"); +generic_protocol_initializer_setup!(MetaXSireV2, "metaxsire-v2"); + +#[derive(Default)] +pub struct MetaXSireV2Initializer {} + +#[async_trait] +impl ProtocolInitializer for MetaXSireV2Initializer { + async fn initialize( + &mut self, + hardware: Arc, + _: &DeviceDefinition, + ) -> Result, ButtplugDeviceError> { + hardware + .write_value(&HardwareWriteCmd::new( + &[METAXSIRE_V2_PROTOCOL_ID], + Endpoint::Tx, + vec![0xaa, 0x04], + true, + )) + .await?; + Ok(Arc::new(MetaXSireV2::default())) + } +} + +#[derive(Default)] +pub struct MetaXSireV2 {} + +impl MetaXSireV2 { + fn form_command( + &self, + feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + vec![ + 0xaa, + 0x03, + 0x01, + (feature_index + 1) as u8, + 0x64, + speed as u8, + ], + true, + ) + .into()]) + } +} + +impl ProtocolHandler for MetaXSireV2 { + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.form_command(feature_index, feature_id, speed) + } + + fn handle_output_oscillate_cmd( + &self, + feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.form_command(feature_index, feature_id, speed) + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/metaxsire_v3.rs b/crates/buttplug_server/src/device/protocol_impl/metaxsire_v3.rs new file mode 100644 index 000000000..a0412ee16 --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/metaxsire_v3.rs @@ -0,0 +1,65 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler, ProtocolKeepaliveStrategy}, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use std::time::Duration; +use uuid::Uuid; + +generic_protocol_setup!(MetaXSireV3, "metaxsire-v3"); + +const METAXSIRE_COMMAND_DELAY_MS: u64 = 100; + +#[derive(Default)] +pub struct MetaXSireV3 {} + +impl MetaXSireV3 { + fn form_command( + &self, + feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + vec![0xa1, 0x04, speed as u8, feature_index as u8 + 1], + true, + ) + .into()]) + } +} + +impl ProtocolHandler for MetaXSireV3 { + fn keepalive_strategy(&self) -> ProtocolKeepaliveStrategy { + ProtocolKeepaliveStrategy::RepeatLastPacketStrategyWithTiming(Duration::from_millis( + METAXSIRE_COMMAND_DELAY_MS, + )) + } + + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + feature_id: uuid::Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.form_command(feature_index, feature_id, speed) + } + + fn handle_output_rotate_cmd( + &self, + feature_index: u32, + feature_id: uuid::Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.form_command(feature_index, feature_id, speed) + } +} diff --git a/buttplug/src/server/device/protocol/metaxsire_v4.rs b/crates/buttplug_server/src/device/protocol_impl/metaxsire_v4.rs similarity index 55% rename from buttplug/src/server/device/protocol/metaxsire_v4.rs rename to crates/buttplug_server/src/device/protocol_impl/metaxsire_v4.rs index d79610d6d..9c20dff64 100644 --- a/buttplug/src/server/device/protocol/metaxsire_v4.rs +++ b/crates/buttplug_server/src/device/protocol_impl/metaxsire_v4.rs @@ -5,13 +5,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(MetaXSireV4, "metaxsire-v4"); @@ -19,18 +20,16 @@ generic_protocol_setup!(MetaXSireV4, "metaxsire-v4"); pub struct MetaXSireV4 {} impl ProtocolHandler for MetaXSireV4 { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, - vec![0xbb, 0x01, scalar as u8, 0x66], + vec![0xbb, 0x01, speed as u8, 0x66], true, ) .into()]) diff --git a/crates/buttplug_server/src/device/protocol_impl/metaxsire_v5.rs b/crates/buttplug_server/src/device/protocol_impl/metaxsire_v5.rs new file mode 100644 index 000000000..215e7de51 --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/metaxsire_v5.rs @@ -0,0 +1,37 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2025 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 uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; + +generic_protocol_setup!(MetaXSireV5, "metaxsire-v5"); + +#[derive(Default)] +pub struct MetaXSireV5 {} + +impl ProtocolHandler for MetaXSireV5 { + fn handle_output_vibrate_cmd( + &self, + _feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + vec![0xaa, 0x03, 0x03, speed as u8, 0x00, 0x00, 0x00], + false, + ) + .into()]) + } +} diff --git a/buttplug/src/server/device/protocol/mizzzee.rs b/crates/buttplug_server/src/device/protocol_impl/mizzzee.rs similarity index 57% rename from buttplug/src/server/device/protocol/mizzzee.rs rename to crates/buttplug_server/src/device/protocol_impl/mizzzee.rs index c31831c90..1c2287447 100644 --- a/buttplug/src/server/device/protocol/mizzzee.rs +++ b/crates/buttplug_server/src/device/protocol_impl/mizzzee.rs @@ -5,13 +5,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(MizzZee, "mizzzee"); @@ -19,24 +20,22 @@ generic_protocol_setup!(MizzZee, "mizzzee"); pub struct MizzZee {} impl ProtocolHandler for MizzZee { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, vec![ 0x69, 0x96, 0x03, 0x01, - if scalar == 0 { 0x00 } else { 0x01 }, - scalar as u8, + if speed == 0 { 0x00 } else { 0x01 }, + speed as u8, ], false, ) diff --git a/buttplug/src/server/device/protocol/mizzzee_v2.rs b/crates/buttplug_server/src/device/protocol_impl/mizzzee_v2.rs similarity index 54% rename from buttplug/src/server/device/protocol/mizzzee_v2.rs rename to crates/buttplug_server/src/device/protocol_impl/mizzzee_v2.rs index f1705c9ec..ca4e48762 100644 --- a/buttplug/src/server/device/protocol/mizzzee_v2.rs +++ b/crates/buttplug_server/src/device/protocol_impl/mizzzee_v2.rs @@ -5,13 +5,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(MizzZeeV2, "mizzzee-v2"); @@ -19,18 +20,16 @@ generic_protocol_setup!(MizzZeeV2, "mizzzee-v2"); pub struct MizzZeeV2 {} impl ProtocolHandler for MizzZeeV2 { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, - vec![0x69, 0x96, 0x04, 0x02, scalar as u8, 0x2c, scalar as u8], + vec![0x69, 0x96, 0x04, 0x02, speed as u8, 0x2c, speed as u8], false, ) .into()]) diff --git a/crates/buttplug_server/src/device/protocol_impl/mizzzee_v3.rs b/crates/buttplug_server/src/device/protocol_impl/mizzzee_v3.rs new file mode 100644 index 000000000..85bfc36a7 --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/mizzzee_v3.rs @@ -0,0 +1,80 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler, ProtocolKeepaliveStrategy}, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use std::time::Duration; +use uuid::Uuid; + +generic_protocol_setup!(MizzZeeV3, "mizzzee-v3"); + +// Time between MizzZee v3 update commands, in milliseconds. +const MIZZZEE3_COMMAND_DELAY_MS: u64 = 200; + +fn handle_scale(scale: f32) -> f32 { + if scale == 0.0 { + return 0.0; + } + scale * 0.7 + 0.3 +} + +fn scalar_to_vector(scalar: u32) -> Vec { + if scalar == 0 { + return vec![ + 0x03, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + } + + const HEADER: [u8; 3] = [0x03, 0x12, 0xf3]; + const FILL_VEC: [u8; 6] = [0x00, 0xfc, 0x00, 0xfe, 0x40, 0x01]; + + let scale: f32 = handle_scale(scalar as f32 / 1000.0) * 1023.0; + let modded_scale: u16 = ((scale as u16) << 6) | 60; + + let bytes = modded_scale.to_le_bytes(); + + let mut data: Vec = Vec::new(); + data.extend_from_slice(&HEADER); + data.extend_from_slice(&FILL_VEC); + data.extend_from_slice(&bytes); + data.extend_from_slice(&FILL_VEC); + data.extend_from_slice(&bytes); + data.push(0x00); + + data +} + +#[derive(Default)] +pub struct MizzZeeV3 {} + +impl ProtocolHandler for MizzZeeV3 { + fn keepalive_strategy(&self) -> ProtocolKeepaliveStrategy { + ProtocolKeepaliveStrategy::RepeatLastPacketStrategyWithTiming(Duration::from_millis( + MIZZZEE3_COMMAND_DELAY_MS, + )) + } + + fn handle_output_vibrate_cmd( + &self, + _feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + scalar_to_vector(speed), + true, + ) + .into()]) + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/mod.rs b/crates/buttplug_server/src/device/protocol_impl/mod.rs new file mode 100644 index 000000000..01ee8579d --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/mod.rs @@ -0,0 +1,571 @@ +use std::{collections::HashMap, sync::Arc}; + +use crate::device::protocol::ProtocolIdentifierFactory; + + +// Utility mods +pub mod fleshlight_launch_helper; + +// Since users can pick and choose protocols, we need all of these to be public. +pub mod activejoy; +pub mod adrienlastic; +pub mod amorelie_joy; +pub mod aneros; +pub mod ankni; +pub mod bananasome; +pub mod cachito; +pub mod cowgirl; +pub mod cowgirl_cone; +pub mod cupido; +pub mod deepsire; +pub mod feelingso; +pub mod fleshy_thrust; +pub mod foreo; +pub mod fox; +pub mod fredorch; +pub mod fredorch_rotary; +pub mod galaku; +pub mod galaku_pump; +pub mod hgod; +pub mod hismith; +pub mod hismith_mini; +pub mod htk_bm; +pub mod itoys; +pub mod jejoue; +pub mod joyhub; +pub mod kgoal_boost; +pub mod kiiroo_prowand; +pub mod kiiroo_spot; +pub mod kiiroo_v2; +pub mod kiiroo_v21; +pub mod kiiroo_v21_initialized; +pub mod kiiroo_v2_vibrator; +pub mod kizuna; +pub mod lelo_harmony; +pub mod lelof1s; +pub mod lelof1sv2; +pub mod leten; +pub mod libo_elle; +pub mod libo_shark; +pub mod libo_vibes; +pub mod lioness; +pub mod loob; +pub mod lovedistance; +pub mod lovehoney_desire; +pub mod lovense; +// pub mod lovense_connect_service; +pub mod lovenuts; +pub mod luvmazer; +pub mod magic_motion_v1; +pub mod magic_motion_v2; +pub mod magic_motion_v3; +pub mod magic_motion_v4; +pub mod mannuo; +pub mod maxpro; +pub mod meese; +pub mod metaxsire; +pub mod metaxsire_v2; +pub mod metaxsire_v3; +pub mod metaxsire_v4; +pub mod metaxsire_v5; +pub mod mizzzee; +pub mod mizzzee_v2; +pub mod mizzzee_v3; +pub mod monsterpub; +pub mod motorbunny; +pub mod mysteryvibe; +pub mod mysteryvibe_v2; +pub mod nextlevelracing; +pub mod nexus_revo; +pub mod nintendo_joycon; +pub mod nobra; +pub mod omobo; +pub mod patoo; +pub mod picobong; +pub mod pink_punch; +pub mod prettylove; +pub mod raw_protocol; +pub mod realov; +pub mod sakuraneko; +pub mod satisfyer; +pub mod sensee; +pub mod sensee_capsule; +pub mod sensee_v2; +pub mod serveu; +pub mod sexverse_lg389; +pub mod svakom; +pub mod synchro; +pub mod tcode_v03; +pub mod thehandy; +pub mod tryfun; +pub mod tryfun_blackhole; +pub mod tryfun_meta2; +pub mod vibcrafter; +pub mod vibratissimo; +pub mod vorze_sa; +pub mod wetoy; +pub mod wevibe; +pub mod wevibe8bit; +pub mod wevibe_chorus; +pub mod xibao; +pub mod xinput; +pub mod xiuxiuda; +pub mod xuanhuan; +pub mod youcups; +pub mod youou; +pub mod zalo; + +pub fn get_default_protocol_map() -> HashMap> { + let mut map = HashMap::new(); + fn add_to_protocol_map( + map: &mut HashMap>, + factory: T, + ) where + T: ProtocolIdentifierFactory + 'static, + { + let factory = Arc::new(factory); + map.insert(factory.identifier().to_owned(), factory); + } + + add_to_protocol_map( + &mut map, + activejoy::setup::ActiveJoyIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + adrienlastic::setup::AdrienLasticIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + amorelie_joy::setup::AmorelieJoyIdentifierFactory::default(), + ); + add_to_protocol_map(&mut map, aneros::setup::AnerosIdentifierFactory::default()); + add_to_protocol_map(&mut map, ankni::setup::AnkniIdentifierFactory::default()); + add_to_protocol_map( + &mut map, + bananasome::setup::BananasomeIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + cachito::setup::CachitoIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + cowgirl::setup::CowgirlIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + cowgirl_cone::setup::CowgirlConeIdentifierFactory::default(), + ); + add_to_protocol_map(&mut map, cupido::setup::CupidoIdentifierFactory::default()); + add_to_protocol_map( + &mut map, + deepsire::setup::DeepSireIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + lovense::setup::LovenseIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + hismith::setup::HismithIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + hismith_mini::setup::HismithMiniIdentifierFactory::default(), + ); + add_to_protocol_map(&mut map, htk_bm::setup::HtkBmIdentifierFactory::default()); + add_to_protocol_map( + &mut map, + thehandy::setup::TheHandyIdentifierFactory::default(), + ); + + add_to_protocol_map( + &mut map, + feelingso::setup::FeelingSoIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + fleshy_thrust::setup::FleshyThrustIdentifierFactory::default(), + ); + add_to_protocol_map(&mut map, foreo::setup::ForeoIdentifierFactory::default()); + add_to_protocol_map(&mut map, fox::setup::FoxIdentifierFactory::default()); + add_to_protocol_map( + &mut map, + fredorch::setup::FredorchIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + fredorch_rotary::setup::FredorchRotaryIdentifierFactory::default(), + ); + + add_to_protocol_map(&mut map, hgod::setup::HgodIdentifierFactory::default()); + + add_to_protocol_map( + &mut map, + galaku_pump::setup::GalakuPumpIdentifierFactory::default(), + ); + + add_to_protocol_map(&mut map, galaku::setup::GalakuIdentifierFactory::default()); + + add_to_protocol_map(&mut map, itoys::setup::IToysIdentifierFactory::default()); + add_to_protocol_map(&mut map, jejoue::setup::JeJoueIdentifierFactory::default()); + add_to_protocol_map(&mut map, joyhub::joyhub::setup::JoyHubIdentifierFactory::default()); + add_to_protocol_map( + &mut map, + joyhub::joyhub_v2::setup::JoyHubV2IdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + joyhub::joyhub_v3::setup::JoyHubV3IdentifierFactory::default(), + ); + + add_to_protocol_map( + &mut map, + joyhub::joyhub_v4::setup::JoyHubV4IdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + joyhub::joyhub_v5::setup::JoyHubV5IdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + joyhub::joyhub_v6::setup::JoyHubV6IdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + kiiroo_prowand::setup::KiirooProWandIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + kiiroo_spot::setup::KiirooSpotIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + kiiroo_v2::setup::KiirooV2IdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + kiiroo_v2_vibrator::setup::KiirooV2VibratorIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + kiiroo_v21::setup::KiirooV21IdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + kiiroo_v21_initialized::setup::KiirooV21InitializedIdentifierFactory::default(), + ); + add_to_protocol_map(&mut map, kizuna::setup::KizunaIdentifierFactory::default()); + add_to_protocol_map( + &mut map, + lelof1s::setup::LeloF1sIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + lelof1sv2::setup::LeloF1sV2IdentifierFactory::default(), + ); + add_to_protocol_map(&mut map, leten::setup::LetenIdentifierFactory::default()); + add_to_protocol_map( + &mut map, + lelo_harmony::setup::LeloHarmonyIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + libo_elle::setup::LiboElleIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + libo_shark::setup::LiboSharkIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + libo_vibes::setup::LiboVibesIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + lioness::setup::LionessIdentifierFactory::default(), + ); + add_to_protocol_map(&mut map, loob::setup::LoobIdentifierFactory::default()); + add_to_protocol_map( + &mut map, + lovehoney_desire::setup::LovehoneyDesireIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + lovedistance::setup::LoveDistanceIdentifierFactory::default(), + ); + // add_to_protocol_map( + // &mut map, + // lovense_connect_service::setup::LovenseConnectServiceIdentifierFactory::default(), + // ); + add_to_protocol_map( + &mut map, + lovenuts::setup::LoveNutsIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + luvmazer::setup::LuvmazerIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + magic_motion_v1::setup::MagicMotionV1IdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + magic_motion_v2::setup::MagicMotionV2IdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + magic_motion_v3::setup::MagicMotionV3IdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + magic_motion_v4::setup::MagicMotionV4IdentifierFactory::default(), + ); + add_to_protocol_map(&mut map, mannuo::setup::ManNuoIdentifierFactory::default()); + add_to_protocol_map(&mut map, maxpro::setup::MaxproIdentifierFactory::default()); + add_to_protocol_map(&mut map, meese::setup::MeeseIdentifierFactory::default()); + add_to_protocol_map( + &mut map, + metaxsire::setup::MetaXSireIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + metaxsire_v2::setup::MetaXSireV2IdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + metaxsire_v3::setup::MetaXSireV3IdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + metaxsire_v4::setup::MetaXSireV4IdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + metaxsire_v5::setup::MetaXSireV5IdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + mizzzee::setup::MizzZeeIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + mizzzee_v2::setup::MizzZeeV2IdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + mizzzee_v3::setup::MizzZeeV3IdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + monsterpub::setup::MonsterPubIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + motorbunny::setup::MotorbunnyIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + mysteryvibe::setup::MysteryVibeIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + mysteryvibe_v2::setup::MysteryVibeV2IdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + nexus_revo::setup::NexusRevoIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + nextlevelracing::setup::NextLevelRacingIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + nintendo_joycon::setup::NintendoJoyconIdentifierFactory::default(), + ); + add_to_protocol_map(&mut map, nobra::setup::NobraIdentifierFactory::default()); + add_to_protocol_map(&mut map, omobo::setup::OmoboIdentifierFactory::default()); + add_to_protocol_map(&mut map, patoo::setup::PatooIdentifierFactory::default()); + add_to_protocol_map( + &mut map, + picobong::setup::PicobongIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + pink_punch::setup::PinkPunchIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + prettylove::setup::PrettyLoveIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + raw_protocol::setup::RawProtocolIdentifierFactory::default(), + ); + add_to_protocol_map(&mut map, realov::setup::RealovIdentifierFactory::default()); + add_to_protocol_map( + &mut map, + sakuraneko::setup::SakuranekoIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + satisfyer::setup::SatisfyerIdentifierFactory::default(), + ); + add_to_protocol_map(&mut map, sensee::setup::SenseeIdentifierFactory::default()); + add_to_protocol_map( + &mut map, + sensee_capsule::setup::SenseeCapsuleIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + sensee_v2::setup::SenseeV2IdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + sexverse_lg389::setup::SexverseLG389IdentifierFactory::default(), + ); + add_to_protocol_map(&mut map, serveu::setup::ServeUIdentifierFactory::default()); + add_to_protocol_map( + &mut map, + svakom::svakom_avaneo::setup::SvakomAvaNeoIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + svakom::svakom_alex::setup::SvakomAlexIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + svakom::svakom_alex_v2::setup::SvakomAlexV2IdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + svakom::svakom_barnard::setup::SvakomBarnardIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + svakom::svakom_barney::setup::SvakomBarneyIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + svakom::svakom_dice::setup::SvakomDiceIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + svakom::svakom_dt250a::setup::SvakomDT250AIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + svakom::svakom_iker::setup::SvakomIkerIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + svakom::svakom_jordan::setup::SvakomJordanIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + svakom::svakom_pulse::setup::SvakomPulseIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + svakom::svakom_sam::setup::SvakomSamIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + svakom::svakom_sam2::setup::SvakomSam2IdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + svakom::svakom_suitcase::setup::SvakomSuitcaseIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + svakom::svakom_tarax::setup::SvakomTaraXIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + svakom::svakom_v1::setup::SvakomV1IdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + svakom::svakom_v2::setup::SvakomV2IdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + svakom::svakom_v3::setup::SvakomV3IdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + svakom::svakom_v4::setup::SvakomV4IdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + svakom::svakom_v5::setup::SvakomV5IdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + svakom::svakom_v6::setup::SvakomV6IdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + synchro::setup::SynchroIdentifierFactory::default(), + ); + add_to_protocol_map(&mut map, tryfun::setup::TryFunIdentifierFactory::default()); + add_to_protocol_map( + &mut map, + tryfun_blackhole::setup::TryFunBlackHoleIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + tryfun_meta2::setup::TryFunMeta2IdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + tcode_v03::setup::TCodeV03IdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + vibcrafter::setup::VibCrafterIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + vibratissimo::setup::VibratissimoIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + vorze_sa::setup::VorzeSAIdentifierFactory::default(), + ); + add_to_protocol_map(&mut map, wetoy::setup::WeToyIdentifierFactory::default()); + add_to_protocol_map(&mut map, wevibe::setup::WeVibeIdentifierFactory::default()); + add_to_protocol_map( + &mut map, + wevibe8bit::setup::WeVibe8BitIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + wevibe_chorus::setup::WeVibeChorusIdentifierFactory::default(), + ); + add_to_protocol_map(&mut map, xibao::setup::XibaoIdentifierFactory::default()); + add_to_protocol_map(&mut map, xinput::setup::XInputIdentifierFactory::default()); + add_to_protocol_map( + &mut map, + xiuxiuda::setup::XiuxiudaIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + xuanhuan::setup::XuanhuanIdentifierFactory::default(), + ); + add_to_protocol_map( + &mut map, + youcups::setup::YoucupsIdentifierFactory::default(), + ); + add_to_protocol_map(&mut map, youou::setup::YououIdentifierFactory::default()); + add_to_protocol_map(&mut map, zalo::setup::ZaloIdentifierFactory::default()); + add_to_protocol_map( + &mut map, + kgoal_boost::setup::KGoalBoostIdentifierFactory::default(), + ); + map +} diff --git a/buttplug/src/server/device/protocol/monsterpub.rs b/crates/buttplug_server/src/device/protocol_impl/monsterpub.rs similarity index 60% rename from buttplug/src/server/device/protocol/monsterpub.rs rename to crates/buttplug_server/src/device/protocol_impl/monsterpub.rs index 0a949ed66..5dd724d24 100644 --- a/buttplug/src/server/device/protocol/monsterpub.rs +++ b/crates/buttplug_server/src/device/protocol_impl/monsterpub.rs @@ -5,22 +5,26 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, - }, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareReadCmd, HardwareWriteCmd}, - protocol::{ProtocolHandler, ProtocolIdentifier, ProtocolInitializer}, - }, +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareReadCmd, HardwareWriteCmd}, + protocol::{ProtocolHandler, ProtocolIdentifier, ProtocolInitializer}, }; use async_trait::async_trait; -use std::sync::Arc; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use buttplug_server_device_config::{ + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; +use std::sync::{ + atomic::{AtomicU8, Ordering}, + Arc, +}; +use uuid::{uuid, Uuid}; pub mod setup { - use crate::server::device::protocol::{ProtocolIdentifier, ProtocolIdentifierFactory}; + use crate::device::protocol::{ProtocolIdentifier, ProtocolIdentifierFactory}; #[derive(Default)] pub struct MonsterPubIdentifierFactory {} @@ -35,6 +39,8 @@ pub mod setup { } } +const MONSTERPUB_PROTOCOL_UUID: Uuid = uuid!("c7fe6c69-e7c2-4fa9-822a-6bb337dece1a"); + #[derive(Default)] pub struct MonsterPubIdentifier {} @@ -46,10 +52,15 @@ impl ProtocolIdentifier for MonsterPubIdentifier { _: ProtocolCommunicationSpecifier, ) -> Result<(UserDeviceIdentifier, Box), ButtplugDeviceError> { let read_resp = hardware - .read_value(&HardwareReadCmd::new(Endpoint::RxBLEModel, 32, 500)) + .read_value(&HardwareReadCmd::new( + MONSTERPUB_PROTOCOL_UUID, + Endpoint::RxBLEModel, + 32, + 500, + )) .await; let ident = match read_resp { - Ok(data) => std::str::from_utf8(&data.data()) + Ok(data) => std::str::from_utf8(data.data()) .map_err(|_| { ButtplugDeviceError::ProtocolSpecificError( "monsterpub".to_owned(), @@ -75,11 +86,16 @@ impl ProtocolInitializer for MonsterPubInitializer { async fn initialize( &mut self, hardware: Arc, - _: &UserDeviceDefinition, + def: &DeviceDefinition, ) -> Result, ButtplugDeviceError> { if hardware.endpoints().contains(&Endpoint::Rx) { let value = hardware - .read_value(&HardwareReadCmd::new(Endpoint::Rx, 16, 200)) + .read_value(&HardwareReadCmd::new( + MONSTERPUB_PROTOCOL_UUID, + Endpoint::Rx, + 16, + 200, + )) .await?; let keys = [ [ @@ -111,9 +127,20 @@ impl ProtocolInitializer for MonsterPubInitializer { ); hardware - .write_value(&HardwareWriteCmd::new(Endpoint::Rx, auth, true)) + .write_value(&HardwareWriteCmd::new( + &[MONSTERPUB_PROTOCOL_UUID], + Endpoint::Rx, + auth, + true, + )) .await?; } + let output_count = def + .features() + .iter() + .filter(|x| x.output().is_some()) + .count(); + Ok(Arc::new(MonsterPub::new( if hardware.endpoints().contains(&Endpoint::TxVibrate) { Endpoint::TxVibrate @@ -122,44 +149,36 @@ impl ProtocolInitializer for MonsterPubInitializer { } else { Endpoint::Generic0 // tracy's dog 3 vibe }, + output_count as u32, ))) } } pub struct MonsterPub { tx: Endpoint, + speeds: Vec, } impl MonsterPub { - pub fn new(tx: Endpoint) -> Self { - Self { tx } - } -} - -impl ProtocolHandler for MonsterPub { - fn needs_full_command_set(&self) -> bool { - true - } - - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy + pub fn new(tx: Endpoint, num_outputs: u32) -> Self { + let speeds: Vec = std::iter::repeat_with(|| AtomicU8::default()) + .take(num_outputs as usize) + .collect(); + Self { tx, speeds } } - fn handle_scalar_cmd( - &self, - cmds: &[Option<(ActuatorType, u32)>], - ) -> Result, ButtplugDeviceError> { + fn form_command(&self) -> Result, ButtplugDeviceError> { let mut data = vec![]; let mut stop = true; + if self.tx == Endpoint::Generic0 { data.push(3u8); } - for (_, cmd) in cmds.iter().enumerate() { - if let Some((_, speed)) = cmd { - data.push(*speed as u8); - if *speed != 0 { - stop = false; - } + for cmd in self.speeds.iter() { + let speed = cmd.load(Ordering::Relaxed); + data.push(speed as u8); + if speed != 0 { + stop = false; } } let tx = if self.tx == Endpoint::Tx && stop { @@ -168,6 +187,7 @@ impl ProtocolHandler for MonsterPub { self.tx }; Ok(vec![HardwareWriteCmd::new( + &[MONSTERPUB_PROTOCOL_UUID], tx, data, tx == Endpoint::TxMode, @@ -175,3 +195,25 @@ impl ProtocolHandler for MonsterPub { .into()]) } } + +impl ProtocolHandler for MonsterPub { + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.speeds[feature_index as usize].store(speed as u8, Ordering::Relaxed); + self.form_command() + } + + fn handle_output_oscillate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.speeds[feature_index as usize].store(speed as u8, Ordering::Relaxed); + self.form_command() + } +} diff --git a/buttplug/src/server/device/protocol/motorbunny.rs b/crates/buttplug_server/src/device/protocol_impl/motorbunny.rs similarity index 70% rename from buttplug/src/server/device/protocol/motorbunny.rs rename to crates/buttplug_server/src/device/protocol_impl/motorbunny.rs index d8d06e38a..60a2a62c9 100644 --- a/buttplug/src/server/device/protocol/motorbunny.rs +++ b/crates/buttplug_server/src/device/protocol_impl/motorbunny.rs @@ -12,13 +12,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(Motorbunny, "motorbunny"); @@ -26,21 +27,18 @@ generic_protocol_setup!(Motorbunny, "motorbunny"); pub struct Motorbunny {} impl ProtocolHandler for Motorbunny { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { let mut command_vec: Vec; - if scalar == 0 { + if speed == 0 { command_vec = vec![0xf0, 0x00, 0x00, 0x00, 0x00, 0xec]; } else { command_vec = vec![0xff]; - let mut vibe_commands = [scalar as u8, 0x14].repeat(7); + let mut vibe_commands = [speed as u8, 0x14].repeat(7); let crc = vibe_commands .iter() .fold(0u8, |a, b| a.overflowing_add(*b).0); @@ -48,6 +46,7 @@ impl ProtocolHandler for Motorbunny { command_vec.append(&mut vec![crc, 0xec]); } Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, command_vec, false, @@ -55,17 +54,19 @@ impl ProtocolHandler for Motorbunny { .into()]) } - fn handle_rotate_cmd( + fn handle_rotation_with_direction_cmd( &self, - commands: &[Option<(u32, bool)>], + _feature_index: u32, + feature_id: Uuid, + speed: u32, + clockwise: bool, ) -> Result, ButtplugDeviceError> { - let rotate = commands[0].unwrap_or((0, false)); let mut command_vec: Vec; - if rotate.0 == 0 { + if speed == 0 { command_vec = vec![0xa0, 0x00, 0x00, 0x00, 0x00, 0xec]; } else { command_vec = vec![0xaf]; - let mut rotate_command = vec![if rotate.1 { 0x2a } else { 0x29 }, rotate.0 as u8].repeat(7); + let mut rotate_command = [if clockwise { 0x2a } else { 0x29 }, speed as u8].repeat(7); let crc = rotate_command .iter() .fold(0u8, |a, b| a.overflowing_add(*b).0); @@ -73,6 +74,7 @@ impl ProtocolHandler for Motorbunny { command_vec.append(&mut vec![crc, 0xec]); } Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, command_vec, false, diff --git a/crates/buttplug_server/src/device/protocol_impl/mysteryvibe.rs b/crates/buttplug_server/src/device/protocol_impl/mysteryvibe.rs new file mode 100644 index 000000000..3a44cd9fc --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/mysteryvibe.rs @@ -0,0 +1,112 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, ProtocolKeepaliveStrategy, + }, +}; +use async_trait::async_trait; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use buttplug_server_device_config::{ + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; +use std::{ + sync::{ + atomic::{AtomicU8, Ordering}, + Arc, + }, + time::Duration, +}; +use uuid::{uuid, Uuid}; + +generic_protocol_initializer_setup!(MysteryVibe, "mysteryvibe"); + +const MYSTERYVIBE_PROTOCOL_UUID: Uuid = uuid!("53bca658-2efe-4388-8ced-333789bac20b"); + +#[derive(Default)] +pub struct MysteryVibeInitializer {} + +#[async_trait] +impl ProtocolInitializer for MysteryVibeInitializer { + async fn initialize( + &mut self, + hardware: Arc, + def: &DeviceDefinition, + ) -> Result, ButtplugDeviceError> { + let msg = HardwareWriteCmd::new( + &[MYSTERYVIBE_PROTOCOL_UUID], + Endpoint::TxMode, + vec![0x43u8, 0x02u8, 0x00u8], + true, + ); + hardware.write_value(&msg).await?; + let vibrator_count = def + .features() + .iter() + .filter(|x| x.output().is_some()) + .count(); + Ok(Arc::new(MysteryVibe::new(vibrator_count as u8))) + } +} + +// Time between Mysteryvibe update commands, in milliseconds. This is basically +// a best guess derived from watching packet timing a few years ago. +// +// Thelemic vibrator. Neat. +// +const MYSTERYVIBE_COMMAND_DELAY_MS: u64 = 93; + +#[derive(Default)] +pub struct MysteryVibe { + speeds: Vec, +} + +impl MysteryVibe { + pub fn new(vibrator_count: u8) -> Self { + Self { + speeds: std::iter::repeat_with(|| AtomicU8::default()) + .take(vibrator_count as usize) + .collect(), + } + } +} + +impl ProtocolHandler for MysteryVibe { + fn keepalive_strategy(&self) -> ProtocolKeepaliveStrategy { + ProtocolKeepaliveStrategy::RepeatLastPacketStrategyWithTiming(Duration::from_millis( + MYSTERYVIBE_COMMAND_DELAY_MS, + )) + } + + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.speeds[feature_index as usize].store(speed as u8, Ordering::Relaxed); + Ok(vec![HardwareWriteCmd::new( + &[MYSTERYVIBE_PROTOCOL_UUID], + Endpoint::TxVibrate, + self + .speeds + .iter() + .map(|x| x.load(Ordering::Relaxed)) + .collect(), + false, + ) + .into()]) + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/mysteryvibe_v2.rs b/crates/buttplug_server/src/device/protocol_impl/mysteryvibe_v2.rs new file mode 100644 index 000000000..d24d15d30 --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/mysteryvibe_v2.rs @@ -0,0 +1,59 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 super::mysteryvibe::MysteryVibe; +use crate::device::{ + hardware::{Hardware, HardwareWriteCmd}, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, + }, +}; +use async_trait::async_trait; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use buttplug_server_device_config::{ + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; +use std::sync::Arc; +use uuid::{uuid, Uuid}; + +generic_protocol_initializer_setup!(MysteryVibeV2, "mysteryvibe-v2"); + +const MYSTERYVIBE_V2_PROTOCOL_UUID: Uuid = uuid!("215a2c34-11fa-419a-84d2-60ac6acbc9f8"); + +#[derive(Default)] +pub struct MysteryVibeV2Initializer {} + +#[async_trait] +impl ProtocolInitializer for MysteryVibeV2Initializer { + async fn initialize( + &mut self, + hardware: Arc, + def: &DeviceDefinition, + ) -> Result, ButtplugDeviceError> { + // The only thing that's different about MysteryVibeV2 from v1 is the initialization packet. + // Just send that then return the older protocol version. + let msg = HardwareWriteCmd::new( + &[MYSTERYVIBE_V2_PROTOCOL_UUID], + Endpoint::TxMode, + vec![0x03u8, 0x02u8, 0x40u8], + true, + ); + hardware.write_value(&msg).await?; + let vibrator_count = def + .features() + .iter() + .filter(|x| x.output().is_some()) + .count(); + Ok(Arc::new(MysteryVibe::new(vibrator_count as u8))) + } +} diff --git a/buttplug/src/server/device/protocol/nextlevelracing.rs b/crates/buttplug_server/src/device/protocol_impl/nextlevelracing.rs similarity index 59% rename from buttplug/src/server/device/protocol/nextlevelracing.rs rename to crates/buttplug_server/src/device/protocol_impl/nextlevelracing.rs index d8cf3b409..c8c8d826d 100644 --- a/buttplug/src/server/device/protocol/nextlevelracing.rs +++ b/crates/buttplug_server/src/device/protocol_impl/nextlevelracing.rs @@ -5,13 +5,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(NextLevelRacing, "nextlevelracing"); @@ -19,18 +20,16 @@ generic_protocol_setup!(NextLevelRacing, "nextlevelracing"); pub struct NextLevelRacing {} impl ProtocolHandler for NextLevelRacing { - fn needs_full_command_set(&self) -> bool { - false - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - index: u32, - scalar: u32, + feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, - format!("M{}{}\r", index, scalar).into_bytes(), + format!("M{feature_index}{speed}\r").into_bytes(), false, ) .into()]) diff --git a/crates/buttplug_server/src/device/protocol_impl/nexus_revo.rs b/crates/buttplug_server/src/device/protocol_impl/nexus_revo.rs new file mode 100644 index 000000000..0f34cf22d --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/nexus_revo.rs @@ -0,0 +1,60 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2025 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 uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; + +generic_protocol_setup!(NexusRevo, "nexus-revo"); + +#[derive(Default)] +pub struct NexusRevo {} + +impl ProtocolHandler for NexusRevo { + fn handle_output_vibrate_cmd( + &self, + _feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + vec![0xaa, 0x01, 0x01, 0x00, 0x01, speed as u8], + true, + ) + .into()]) + } + + fn handle_rotation_with_direction_cmd( + &self, + _feature_index: u32, + feature_id: Uuid, + speed: u32, + clockwise: bool, + ) -> Result, ButtplugDeviceError> { + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + vec![ + 0xaa, + 0x01, + 0x02, + 0x00, + speed as u8 + if speed != 0 && clockwise { 2 } else { 0 }, + 0x00, + ], + true, + ) + .into()]) + } +} diff --git a/buttplug/src/server/device/protocol/nintendo_joycon.rs b/crates/buttplug_server/src/device/protocol_impl/nintendo_joycon.rs similarity index 84% rename from buttplug/src/server/device/protocol/nintendo_joycon.rs rename to crates/buttplug_server/src/device/protocol_impl/nintendo_joycon.rs index 6367856d1..58ea09cdb 100644 --- a/buttplug/src/server/device/protocol/nintendo_joycon.rs +++ b/crates/buttplug_server/src/device/protocol_impl/nintendo_joycon.rs @@ -5,19 +5,23 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -#[cfg(feature = "wasm")] -use crate::util; -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - generic_protocol_initializer_setup, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ProtocolHandler, ProtocolIdentifier, ProtocolInitializer}, +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, }, - util::async_manager, }; use async_trait::async_trait; +use buttplug_core::{errors::ButtplugDeviceError, util::{self, async_manager}}; +use buttplug_server_device_config::{ + Endpoint, + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; use std::{ sync::{ atomic::{AtomicBool, AtomicU16, Ordering}, @@ -26,6 +30,9 @@ use std::{ time::Duration, }; use tokio::sync::Notify; +use uuid::{uuid, Uuid}; + +const NINTENDO_JOYCON_PROTOCOL_UUID: Uuid = uuid!("de9cce17-abb7-4ad5-9754-f1872733c197"); /// Send command, sub-command, and data (sub-command's arguments) with u8 integers /// This returns ACK packet for the command or Error. @@ -61,7 +68,12 @@ async fn send_command_raw( // send command device - .write_value(&HardwareWriteCmd::new(Endpoint::Tx, buf.to_vec(), false)) + .write_value(&HardwareWriteCmd::new( + &[NINTENDO_JOYCON_PROTOCOL_UUID], + Endpoint::Tx, + buf.to_vec(), + false, + )) .await } @@ -112,7 +124,7 @@ async fn send_sub_command( sub_command: u8, data: &[u8], ) -> Result<(), ButtplugDeviceError> { - send_sub_command_raw(device, packet_number, sub_command as u8, data).await + send_sub_command_raw(device, packet_number, sub_command, data).await } /// Rumble data for vibration. @@ -175,19 +187,19 @@ impl Rumble { } } -impl Into<[u8; 4]> for Rumble { - fn into(self) -> [u8; 4] { - let encoded_hex_freq = f32::round(f32::log2(self.frequency / 10.0) * 32.0) as u8; +impl From for [u8; 4] { + fn from(val: Rumble) -> Self { + let encoded_hex_freq = f32::round(f32::log2(val.frequency / 10.0) * 32.0) as u8; let hf_freq: u16 = (encoded_hex_freq as u16).saturating_sub(0x60) * 4; let lf_freq: u8 = encoded_hex_freq.saturating_sub(0x41) + 1; - let encoded_hex_amp = if self.amplitude > 0.23 { - f32::round(f32::log2(self.amplitude * 8.7) * 32.0) as u8 - } else if self.amplitude > 0.12 { - f32::round(f32::log2(self.amplitude * 17.0) * 16.0) as u8 + let encoded_hex_amp = if val.amplitude > 0.23 { + f32::round(f32::log2(val.amplitude * 8.7) * 32.0) as u8 + } else if val.amplitude > 0.12 { + f32::round(f32::log2(val.amplitude * 17.0) * 16.0) as u8 } else { - f32::round(((f32::log2(self.amplitude) * 32.0) - 96.0) / (4.0 - 2.0 * self.amplitude)) as u8 + f32::round(((f32::log2(val.amplitude) * 32.0) - 96.0) / (4.0 - 2.0 * val.amplitude)) as u8 }; let hf_amp: u16 = { @@ -232,7 +244,7 @@ impl ProtocolInitializer for NintendoJoyconInitializer { async fn initialize( &mut self, hardware: Arc, - _: &UserDeviceDefinition, + _: &DeviceDefinition, ) -> Result, ButtplugDeviceError> { send_sub_command(hardware.clone(), 0, 72, &[0x01]) .await @@ -297,12 +309,13 @@ impl NintendoJoycon { } impl ProtocolHandler for NintendoJoycon { - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _: u32, - scalar: u32, + _feature_index: u32, + _feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { - self.speed_val.store(scalar as u16, Ordering::Relaxed); + self.speed_val.store(speed as u16, Ordering::Relaxed); Ok(vec![]) } } diff --git a/buttplug/src/server/device/protocol/nobra.rs b/crates/buttplug_server/src/device/protocol_impl/nobra.rs similarity index 52% rename from buttplug/src/server/device/protocol/nobra.rs rename to crates/buttplug_server/src/device/protocol_impl/nobra.rs index d27586557..26fb02ede 100644 --- a/buttplug/src/server/device/protocol/nobra.rs +++ b/crates/buttplug_server/src/device/protocol_impl/nobra.rs @@ -5,22 +5,27 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, }, }; use async_trait::async_trait; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use buttplug_server_device_config::{ + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; use std::sync::Arc; +use uuid::{uuid, Uuid}; +const NOBRA_PROTOCOL_UUID: Uuid = uuid!("166e7d2b-b9ed-4769-aaaf-66127e4e14eb"); generic_protocol_initializer_setup!(Nobra, "nobra"); #[derive(Default)] @@ -31,10 +36,15 @@ impl ProtocolInitializer for NobraInitializer { async fn initialize( &mut self, hardware: Arc, - _: &UserDeviceDefinition, + _: &DeviceDefinition, ) -> Result, ButtplugDeviceError> { hardware - .write_value(&HardwareWriteCmd::new(Endpoint::Tx, vec![0x70], false)) + .write_value(&HardwareWriteCmd::new( + &[NOBRA_PROTOCOL_UUID], + Endpoint::Tx, + vec![0x70], + false, + )) .await?; Ok(Arc::new(Nobra::default())) } @@ -44,17 +54,15 @@ impl ProtocolInitializer for NobraInitializer { pub struct Nobra {} impl ProtocolHandler for Nobra { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { - let output_speed = if scalar == 0 { 0x70 } else { 0x60 + scalar }; + let output_speed = if speed == 0 { 0x70 } else { 0x60 + speed }; Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, vec![output_speed as u8], false, diff --git a/buttplug/src/server/device/protocol/omobo.rs b/crates/buttplug_server/src/device/protocol_impl/omobo.rs similarity index 53% rename from buttplug/src/server/device/protocol/omobo.rs rename to crates/buttplug_server/src/device/protocol_impl/omobo.rs index 852ef214b..50b66f7d9 100644 --- a/buttplug/src/server/device/protocol/omobo.rs +++ b/crates/buttplug_server/src/device/protocol_impl/omobo.rs @@ -5,13 +5,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(Omobo, "omobo"); @@ -19,18 +20,16 @@ generic_protocol_setup!(Omobo, "omobo"); pub struct Omobo {} impl ProtocolHandler for Omobo { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, - vec![0xa1, 0x04, 0x04, 0x01, scalar as u8, 0xff, 0x55], + vec![0xa1, 0x04, 0x04, 0x01, speed as u8, 0xff, 0x55], true, ) .into()]) diff --git a/buttplug/src/server/device/protocol/patoo.rs b/crates/buttplug_server/src/device/protocol_impl/patoo.rs similarity index 58% rename from buttplug/src/server/device/protocol/patoo.rs rename to crates/buttplug_server/src/device/protocol_impl/patoo.rs index 137ddb636..d89bf63b4 100644 --- a/buttplug/src/server/device/protocol/patoo.rs +++ b/crates/buttplug_server/src/device/protocol_impl/patoo.rs @@ -5,22 +5,27 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, - }, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ProtocolHandler, ProtocolIdentifier, ProtocolInitializer}, - }, +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use buttplug_server_device_config::{ + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; + +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, + protocol::{ProtocolHandler, ProtocolIdentifier, ProtocolInitializer}, }; use async_trait::async_trait; -use std::sync::Arc; +use std::sync::{ + atomic::{AtomicU8, Ordering}, + Arc, +}; +use uuid::{uuid, Uuid}; pub mod setup { - use crate::server::device::protocol::{ProtocolIdentifier, ProtocolIdentifierFactory}; + use crate::device::protocol::{ProtocolIdentifier, ProtocolIdentifierFactory}; #[derive(Default)] pub struct PatooIdentifierFactory {} @@ -68,47 +73,57 @@ impl ProtocolInitializer for PatooInitializer { async fn initialize( &mut self, _: Arc, - _: &UserDeviceDefinition, + _: &DeviceDefinition, ) -> Result, ButtplugDeviceError> { Ok(Arc::new(Patoo::default())) } } +const PATOO_TX_PROTOCOL_UUID: Uuid = uuid!("2366a70f-9a7c-4fea-8ba6-8b21a7d5d641"); +const PATOO_TX_MODE_PROTOCOL_UUID: Uuid = uuid!("b17714be-fc66-4d9b-bf52-afb3b91212a4"); + #[derive(Default)] -pub struct Patoo {} +pub struct Patoo { + speeds: [AtomicU8; 2], +} impl ProtocolHandler for Patoo { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_cmd( + fn handle_output_vibrate_cmd( &self, - cmds: &[Option<(ActuatorType, u32)>], + feature_index: u32, + _feature_id: uuid::Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { + self.speeds[feature_index as usize].store(speed as u8, Ordering::Relaxed); let mut msg_vec = vec![]; // Default to vibes let mut mode: u8 = 4u8; // Use vibe 1 as speed - let mut speed = cmds[0].unwrap_or((ActuatorType::Vibrate, 0)).1 as u8; + let mut speed = self.speeds[0].load(Ordering::Relaxed); if speed == 0 { mode = 0; + let speed2 = self.speeds[1].load(Ordering::Relaxed); // If we have a second vibe and it's not also 0, use that - if cmds.len() > 1 { - speed = cmds[1].unwrap_or((ActuatorType::Vibrate, 0)).1 as u8; - if speed != 0 { - mode |= 0x80; - } + if speed2 != 0 { + speed = speed2; + mode |= 0x80; } - } else if cmds.len() > 1 && cmds[1].unwrap_or((ActuatorType::Vibrate, 0)).1 as u8 != 0 { - // Enable second vibe if it's not at 0 - mode |= 0x80; } - msg_vec.push(HardwareWriteCmd::new(Endpoint::Tx, vec![speed], true).into()); - msg_vec.push(HardwareWriteCmd::new(Endpoint::TxMode, vec![mode], true).into()); + msg_vec.push( + HardwareWriteCmd::new(&[PATOO_TX_PROTOCOL_UUID], Endpoint::Tx, vec![speed], true).into(), + ); + msg_vec.push( + HardwareWriteCmd::new( + &[PATOO_TX_MODE_PROTOCOL_UUID], + Endpoint::TxMode, + vec![mode], + true, + ) + .into(), + ); Ok(msg_vec) } diff --git a/buttplug/src/server/device/protocol/picobong.rs b/crates/buttplug_server/src/device/protocol_impl/picobong.rs similarity index 53% rename from buttplug/src/server/device/protocol/picobong.rs rename to crates/buttplug_server/src/device/protocol_impl/picobong.rs index 49bece192..91fbe55c6 100644 --- a/buttplug/src/server/device/protocol/picobong.rs +++ b/crates/buttplug_server/src/device/protocol_impl/picobong.rs @@ -5,13 +5,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(Picobong, "picobong"); @@ -19,19 +20,17 @@ generic_protocol_setup!(Picobong, "picobong"); pub struct Picobong {} impl ProtocolHandler for Picobong { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { - let mode: u8 = if scalar == 0 { 0xff } else { 0x01 }; + let mode: u8 = if speed == 0 { 0xff } else { 0x01 }; Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, - [0x01, mode, scalar as u8].to_vec(), + [0x01, mode, speed as u8].to_vec(), false, ) .into()]) diff --git a/buttplug/src/server/device/protocol/pink_punch.rs b/crates/buttplug_server/src/device/protocol_impl/pink_punch.rs similarity index 56% rename from buttplug/src/server/device/protocol/pink_punch.rs rename to crates/buttplug_server/src/device/protocol_impl/pink_punch.rs index f95dc911c..1a539fe4f 100644 --- a/buttplug/src/server/device/protocol/pink_punch.rs +++ b/crates/buttplug_server/src/device/protocol_impl/pink_punch.rs @@ -5,13 +5,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(PinkPunch, "pink_punch"); @@ -19,18 +20,16 @@ generic_protocol_setup!(PinkPunch, "pink_punch"); pub struct PinkPunch {} impl ProtocolHandler for PinkPunch { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, - vec![0x09, scalar as u8], + vec![0x09, speed as u8], true, ) .into()]) diff --git a/buttplug/src/server/device/protocol/prettylove.rs b/crates/buttplug_server/src/device/protocol_impl/prettylove.rs similarity index 71% rename from buttplug/src/server/device/protocol/prettylove.rs rename to crates/buttplug_server/src/device/protocol_impl/prettylove.rs index 852b382c4..00a595b99 100644 --- a/buttplug/src/server/device/protocol/prettylove.rs +++ b/crates/buttplug_server/src/device/protocol_impl/prettylove.rs @@ -5,19 +5,23 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ProtocolHandler, ProtocolIdentifier, ProtocolInitializer}, - }, +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, + protocol::{ProtocolHandler, ProtocolIdentifier, ProtocolInitializer}, }; use async_trait::async_trait; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use buttplug_server_device_config::{ + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; use std::sync::Arc; +use uuid::Uuid; pub mod setup { - use crate::server::device::protocol::{ProtocolIdentifier, ProtocolIdentifierFactory}; + use crate::device::protocol::{ProtocolIdentifier, ProtocolIdentifierFactory}; #[derive(Default)] pub struct PrettyLoveIdentifierFactory {} @@ -61,7 +65,7 @@ impl ProtocolInitializer for PrettyLoveInitializer { async fn initialize( &mut self, _: Arc, - _: &UserDeviceDefinition, + _: &DeviceDefinition, ) -> Result, ButtplugDeviceError> { Ok(Arc::new(PrettyLove::default())) } @@ -71,18 +75,16 @@ impl ProtocolInitializer for PrettyLoveInitializer { pub struct PrettyLove {} impl ProtocolHandler for PrettyLove { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, - vec![0x00u8, scalar as u8], + vec![0x00u8, speed as u8], true, ) .into()]) diff --git a/buttplug/src/server/device/protocol/raw_protocol.rs b/crates/buttplug_server/src/device/protocol_impl/raw_protocol.rs similarity index 83% rename from buttplug/src/server/device/protocol/raw_protocol.rs rename to crates/buttplug_server/src/device/protocol_impl/raw_protocol.rs index 918b1807e..a34e35ac0 100644 --- a/buttplug/src/server/device/protocol/raw_protocol.rs +++ b/crates/buttplug_server/src/device/protocol_impl/raw_protocol.rs @@ -5,7 +5,7 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::server::device::protocol::{generic_protocol_setup, ProtocolHandler}; +use crate::device::protocol::{generic_protocol_setup, ProtocolHandler}; generic_protocol_setup!(RawProtocol, "raw"); diff --git a/buttplug/src/server/device/protocol/realov.rs b/crates/buttplug_server/src/device/protocol_impl/realov.rs similarity index 55% rename from buttplug/src/server/device/protocol/realov.rs rename to crates/buttplug_server/src/device/protocol_impl/realov.rs index 3aa868028..bc92d2265 100644 --- a/buttplug/src/server/device/protocol/realov.rs +++ b/crates/buttplug_server/src/device/protocol_impl/realov.rs @@ -5,13 +5,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(Realov, "realov"); @@ -19,18 +20,16 @@ generic_protocol_setup!(Realov, "realov"); pub struct Realov {} impl ProtocolHandler for Realov { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, - [0xc5u8, 0x55, scalar as u8, 0xaa].to_vec(), + [0xc5u8, 0x55, speed as u8, 0xaa].to_vec(), false, ) .into()]) diff --git a/buttplug/src/server/device/protocol/sakuraneko.rs b/crates/buttplug_server/src/device/protocol_impl/sakuraneko.rs similarity index 67% rename from buttplug/src/server/device/protocol/sakuraneko.rs rename to crates/buttplug_server/src/device/protocol_impl/sakuraneko.rs index a66b1531b..d77c31567 100644 --- a/buttplug/src/server/device/protocol/sakuraneko.rs +++ b/crates/buttplug_server/src/device/protocol_impl/sakuraneko.rs @@ -5,13 +5,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(Sakuraneko, "sakuraneko"); @@ -19,16 +20,14 @@ generic_protocol_setup!(Sakuraneko, "sakuraneko"); pub struct Sakuraneko {} impl ProtocolHandler for Sakuraneko { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, vec![ 0xa1, @@ -38,7 +37,7 @@ impl ProtocolHandler for Sakuraneko { 0x00, 0x00, 0x64, - scalar as u8, + speed as u8, 0x00, 0x64, 0xdf, @@ -49,12 +48,14 @@ impl ProtocolHandler for Sakuraneko { .into()]) } - fn handle_scalar_rotate_cmd( + fn handle_output_rotate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, vec![ 0xa2, @@ -64,7 +65,7 @@ impl ProtocolHandler for Sakuraneko { 0x00, 0x00, 0x64, - scalar as u8, + speed as u8, 0x00, 0x32, 0xdf, diff --git a/buttplug/src/server/device/protocol/satisfyer.rs b/crates/buttplug_server/src/device/protocol_impl/satisfyer.rs similarity index 58% rename from buttplug/src/server/device/protocol/satisfyer.rs rename to crates/buttplug_server/src/device/protocol_impl/satisfyer.rs index 5ea4f09f2..9f0000aca 100644 --- a/buttplug/src/server/device/protocol/satisfyer.rs +++ b/crates/buttplug_server/src/device/protocol_impl/satisfyer.rs @@ -5,19 +5,18 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{ - errors::ButtplugDeviceError, - message::{self, Endpoint}, - }, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareReadCmd, HardwareWriteCmd}, - protocol::{ProtocolHandler, ProtocolIdentifier, ProtocolInitializer}, - }, - util::{async_manager, sleep}, +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareReadCmd, HardwareWriteCmd}, + protocol::{ProtocolHandler, ProtocolIdentifier, ProtocolInitializer, ProtocolKeepaliveStrategy}, }; use async_trait::async_trait; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use buttplug_server_device_config::{ + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; use std::{ sync::{ atomic::{AtomicU8, Ordering}, @@ -25,9 +24,12 @@ use std::{ }, time::Duration, }; +use uuid::{uuid, Uuid}; + +const SATISFYER_PROTOCOL_UUID: Uuid = uuid!("79a0ed0d-f392-4c48-967e-f4467438c344"); pub mod setup { - use crate::server::device::protocol::{ProtocolIdentifier, ProtocolIdentifierFactory}; + use crate::device::protocol::{ProtocolIdentifier, ProtocolIdentifierFactory}; #[derive(Default)] pub struct SatisfyerIdentifierFactory {} @@ -73,7 +75,12 @@ impl ProtocolIdentifier for SatisfyerIdentifier { } let result = hardware - .read_value(&HardwareReadCmd::new(Endpoint::RxBLEModel, 128, 500)) + .read_value(&HardwareReadCmd::new( + SATISFYER_PROTOCOL_UUID, + Endpoint::RxBLEModel, + 128, + 500, + )) .await?; let device_identifier = format!( "{}", @@ -99,18 +106,24 @@ impl ProtocolInitializer for SatisfyerInitializer { async fn initialize( &mut self, hardware: Arc, - device_definition: &UserDeviceDefinition, + device_definition: &DeviceDefinition, ) -> Result, ButtplugDeviceError> { - let msg = HardwareWriteCmd::new(Endpoint::Command, vec![0x01], true); + let msg = HardwareWriteCmd::new( + &[SATISFYER_PROTOCOL_UUID], + Endpoint::Command, + vec![0x01], + true, + ); let info_fut = hardware.write_value(&msg); info_fut.await?; let feature_count = device_definition .features() .iter() - .filter(|x| x.actuator().is_some()) + .filter(|x| x.output().is_some()) .count(); - Ok(Arc::new(Satisfyer::new(hardware, feature_count))) + + Ok(Arc::new(Satisfyer::new(feature_count))) } } @@ -122,45 +135,18 @@ pub struct Satisfyer { fn form_command(feature_count: usize, data: Arc>) -> Vec { data[0..feature_count] .iter() - .map(|d| vec![d.load(Ordering::SeqCst); 4]) + .map(|d| vec![d.load(Ordering::Relaxed); 4]) .collect::>>() .concat() } -// Satisfyer toys will drop their connections if they don't get an update within ~10 seconds. -// Therefore we try to send a command every ~1s unless something is sent/updated sooner. -async fn send_satisfyer_updates( - device: Arc, - feature_count: usize, - data: Arc>, -) { - loop { - let command = form_command(feature_count, data.clone()); - if let Err(e) = device - .write_value(&HardwareWriteCmd::new(Endpoint::Tx, command, false)) - .await - { - error!( - "Got an error from a satisfyer device, exiting control loop: {:?}", - e - ); - break; - } - sleep(Duration::from_secs(1)).await; - } -} - impl Satisfyer { - fn new(hardware: Arc, feature_count: usize) -> Self { + fn new(feature_count: usize) -> Self { let last_command = Arc::new( (0..feature_count) .map(|_| AtomicU8::new(0)) .collect::>(), ); - let last_command_clone = last_command.clone(); - async_manager::spawn(async move { - send_satisfyer_updates(hardware, feature_count, last_command_clone).await; - }); Self { feature_count, @@ -170,26 +156,25 @@ impl Satisfyer { } impl ProtocolHandler for Satisfyer { - fn needs_full_command_set(&self) -> bool { - true + fn keepalive_strategy(&self) -> ProtocolKeepaliveStrategy { + ProtocolKeepaliveStrategy::RepeatLastPacketStrategyWithTiming(Duration::from_secs(3)) } - fn handle_scalar_cmd( + fn handle_output_vibrate_cmd( &self, - commands: &[Option<(message::ActuatorType, u32)>], + feature_index: u32, + _feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { - if self.feature_count != commands.len() { - return Err(ButtplugDeviceError::DeviceFeatureCountMismatch( - self.feature_count as u32, - commands.len() as u32, - )); - } - for (i, item) in commands.iter().enumerate() { - let command_val = item.as_ref().unwrap().1 as u8; - self.last_command[i].store(command_val, Ordering::SeqCst); - } + self.last_command[feature_index as usize].store(speed as u8, Ordering::Relaxed); let data = form_command(self.feature_count, self.last_command.clone()); - Ok(vec![HardwareWriteCmd::new(Endpoint::Tx, data, false).into()]) + Ok(vec![HardwareWriteCmd::new( + &[SATISFYER_PROTOCOL_UUID], + Endpoint::Tx, + data, + false, + ) + .into()]) } } diff --git a/buttplug/src/server/device/protocol/sensee.rs b/crates/buttplug_server/src/device/protocol_impl/sensee.rs similarity index 62% rename from buttplug/src/server/device/protocol/sensee.rs rename to crates/buttplug_server/src/device/protocol_impl/sensee.rs index 6c0ae8ea3..4fe415b34 100644 --- a/buttplug/src/server/device/protocol/sensee.rs +++ b/crates/buttplug_server/src/device/protocol_impl/sensee.rs @@ -5,13 +5,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(Sensee, "sensee"); @@ -19,16 +20,14 @@ generic_protocol_setup!(Sensee, "sensee"); pub struct Sensee {} impl ProtocolHandler for Sensee { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, vec![ 0x55, @@ -41,7 +40,7 @@ impl ProtocolHandler for Sensee { 0xf7, 0x01, 0x01, - scalar as u8, + speed as u8, ], false, ) diff --git a/buttplug/src/server/device/protocol/sensee_capsule.rs b/crates/buttplug_server/src/device/protocol_impl/sensee_capsule.rs similarity index 66% rename from buttplug/src/server/device/protocol/sensee_capsule.rs rename to crates/buttplug_server/src/device/protocol_impl/sensee_capsule.rs index 466a96c26..e3684ee7f 100644 --- a/buttplug/src/server/device/protocol/sensee_capsule.rs +++ b/crates/buttplug_server/src/device/protocol_impl/sensee_capsule.rs @@ -5,13 +5,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(SenseeCapsule, "sensee-capsule"); @@ -19,16 +20,14 @@ generic_protocol_setup!(SenseeCapsule, "sensee-capsule"); pub struct SenseeCapsule {} impl ProtocolHandler for SenseeCapsule { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, vec![ 0x55, @@ -39,19 +38,21 @@ impl ProtocolHandler for SenseeCapsule { 0x12, 0x66, 0xf9, - 0xf0 | scalar as u8, + 0xf0 | speed as u8, ], false, ) .into()]) } - fn handle_scalar_constrict_cmd( + fn handle_output_constrict_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + level: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, vec![ 0x55, @@ -62,7 +63,7 @@ impl ProtocolHandler for SenseeCapsule { 0x11, 0x66, 0xf2, - 0xf0 | scalar as u8, + 0xf0 | level as u8, 0x00, 0x00, ], diff --git a/crates/buttplug_server/src/device/protocol_impl/sensee_v2.rs b/crates/buttplug_server/src/device/protocol_impl/sensee_v2.rs new file mode 100644 index 000000000..05f4555dc --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/sensee_v2.rs @@ -0,0 +1,205 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 buttplug_core::{ + errors::ButtplugDeviceError, + message::OutputType, +}; +use buttplug_server_device_config::{ + DeviceDefinition, Endpoint, ProtocolCommunicationSpecifier, UserDeviceIdentifier +}; + +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareReadCmd, HardwareWriteCmd}, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, + }, +}; +use async_trait::async_trait; +use std::{ + collections::HashMap, + sync::{ + atomic::{AtomicU8, Ordering}, + Arc, + }, +}; +use uuid::{uuid, Uuid}; + +generic_protocol_initializer_setup!(SenseeV2, "sensee-v2"); + +const SENSEE_V2_PROTOCOL_UUID: Uuid = uuid!("6e68d015-6e83-484b-9dbc-de7684cf8c29"); + +#[derive(Default)] +pub struct SenseeV2Initializer {} + +#[async_trait] +impl ProtocolInitializer for SenseeV2Initializer { + async fn initialize( + &mut self, + hardware: Arc, + device_definition: &DeviceDefinition, + ) -> Result, ButtplugDeviceError> { + let res = hardware + .read_value(&HardwareReadCmd::new( + SENSEE_V2_PROTOCOL_UUID, + Endpoint::Tx, + 128, + 500, + )) + .await?; + info!("Sensee model data: {:X?}", res.data()); + + let device_type = if res.data().len() >= 6 { + res.data()[6] + } else { + 0x66 + }; + + let feature_map = |output_type| { + let mut map = HashMap::new(); + device_definition + .features() + .iter() + .enumerate() + .for_each(|(i, x)| { + if let Some(output_map) = x.output() { + if output_map.contains_key(&output_type) { + map.insert(i as u32, AtomicU8::new(0)); + } + } + }); + map + }; + + let vibe_map = feature_map(OutputType::Vibrate); + let thrust_map = feature_map(OutputType::Oscillate); + let suck_map = feature_map(OutputType::Constrict); + + Ok(Arc::new(SenseeV2::new( + device_type, + vibe_map, + thrust_map, + suck_map, + ))) + } +} + +pub struct SenseeV2 { + device_type: u8, + vibe_map: HashMap, + thrust_map: HashMap, + suck_map: HashMap, +} + +fn make_cmd(dtype: u8, func: u8, cmd: Vec) -> Vec { + let mut out = vec![0x55, 0xAA, 0xF0]; // fixed start code + out.push(0x02); // version + out.push(0x00); // package numer? + out.push(0x04 + cmd.len() as u8); // Data length + out.push(dtype); // Device type - always 0x66? + out.push(func); // Function code + out.extend(cmd); + + let cdc = vec![0, 0]; + // ToDo: CDC not yet used + out.extend(cdc); + + out +} + +impl SenseeV2 { + fn new( + device_type: u8, + vibe_map: HashMap, + thrust_map: HashMap, + suck_map: HashMap, + ) -> Self { + Self { + device_type, + vibe_map, + thrust_map, + suck_map, + } + } + + fn compile_command(&self) -> Result, ButtplugDeviceError> { + let mut data = vec![]; + data.push( + if self.vibe_map.len() != 0 { 1 } else { 0 } + + if self.thrust_map.len() != 0 { 1 } else { 0 } + + if self.suck_map.len() != 0 { 1 } else { 0 } as u8, + ); + let mut data_add = |i, m: &HashMap| { + if m.len() > 0 { + data.push(i); + data.push(m.len() as u8); + for (i, (_, v)) in m.iter().enumerate() { + data.push((i + 1) as u8); + data.push(v.load(Ordering::Relaxed)); + } + } + }; + data_add(0, &self.vibe_map); + data_add(1, &self.thrust_map); + data_add(2, &self.suck_map); + + Ok(vec![HardwareWriteCmd::new( + &[SENSEE_V2_PROTOCOL_UUID], + Endpoint::Tx, + make_cmd(self.device_type, 0xf1, data), + false, + ) + .into()]) + } +} + +impl ProtocolHandler for SenseeV2 { + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self + .vibe_map + .get(&feature_index) + .unwrap() + .store(speed as u8, Ordering::Relaxed); + self.compile_command() + } + + fn handle_output_oscillate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self + .thrust_map + .get(&feature_index) + .unwrap() + .store(speed as u8, Ordering::Relaxed); + self.compile_command() + } + + fn handle_output_constrict_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + level: u32, + ) -> Result, ButtplugDeviceError> { + self + .suck_map + .get(&feature_index) + .unwrap() + .store(level as u8, Ordering::Relaxed); + self.compile_command() + } +} diff --git a/buttplug/src/server/device/protocol/serveu.rs b/crates/buttplug_server/src/device/protocol_impl/serveu.rs similarity index 69% rename from buttplug/src/server/device/protocol/serveu.rs rename to crates/buttplug_server/src/device/protocol_impl/serveu.rs index d32b54b1d..fac47d79a 100644 --- a/buttplug/src/server/device/protocol/serveu.rs +++ b/crates/buttplug_server/src/device/protocol_impl/serveu.rs @@ -5,13 +5,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; use std::sync::{ atomic::{AtomicU8, Ordering}, Arc, @@ -25,21 +26,19 @@ pub struct ServeU { } impl ProtocolHandler for ServeU { - fn handle_linear_cmd( + fn handle_position_with_duration_cmd( &self, - message: crate::core::message::LinearCmdV4, + _feature_index: u32, + feature_id: Uuid, + position: u32, + duration: u32, ) -> Result, ButtplugDeviceError> { let last_pos = self.last_position.load(Ordering::Relaxed); - let current_cmd = message - .vectors() - .first() - .ok_or(ButtplugDeviceError::DeviceFeatureCountMismatch(1, 0))?; // Need to get "units" (abstracted steps 0-100) per second, so calculate how far we need to move over our goal duration. - let goal_pos = (current_cmd.position() * 100f64).ceil() as u8; + let goal_pos = position as u8; self.last_position.store(goal_pos, Ordering::Relaxed); - let speed_threshold = ((((goal_pos as i8) - last_pos as i8).abs()) as f64 - / ((current_cmd.duration() as f64) / 1000f64)) - .ceil(); + let speed_threshold = + ((((goal_pos as i8) - last_pos as i8).abs()) as f64 / ((duration as f64) / 1000f64)).ceil(); let speed = if speed_threshold <= 0.00001 { // Stop device @@ -56,6 +55,7 @@ impl ProtocolHandler for ServeU { }; Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, vec![0x01, goal_pos, speed], false, diff --git a/crates/buttplug_server/src/device/protocol_impl/sexverse_lg389.rs b/crates/buttplug_server/src/device/protocol_impl/sexverse_lg389.rs new file mode 100644 index 000000000..97f5ca4dc --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/sexverse_lg389.rs @@ -0,0 +1,65 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2025 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::sync::atomic::{AtomicU8, Ordering}; + +use uuid::{uuid, Uuid}; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; + +generic_protocol_setup!(SexverseLG389, "sexverse-lg389"); + +const SEXVERSE_PROTOCOL_UUID: Uuid = uuid!("575b2394-8f88-4367-a355-11321efda686"); + +#[derive(Default)] +pub struct SexverseLG389 { + vibe_speed: AtomicU8, + osc_speed: AtomicU8, +} + +impl SexverseLG389 { + fn generate_command(&self) -> Result, ButtplugDeviceError> { + let vibe = self.vibe_speed.load(Ordering::Relaxed); + let osc = self.osc_speed.load(Ordering::Relaxed); + let range = if osc == 0 { 0 } else { 4u8 }; // Full range + let anchor = if osc == 0 { 0 } else { 1u8 }; // Anchor to base + Ok(vec![HardwareWriteCmd::new( + &[SEXVERSE_PROTOCOL_UUID], + Endpoint::Tx, + vec![0xaa, 0x05, vibe, 0x14, anchor, 0x00, range, 0x00, osc, 0x00], + true, + ) + .into()]) + } +} + +impl ProtocolHandler for SexverseLG389 { + fn handle_output_vibrate_cmd( + &self, + _feature_index: u32, + _feature_id: uuid::Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.vibe_speed.store(speed as u8, Ordering::Relaxed); + self.generate_command() + } + + fn handle_output_oscillate_cmd( + &self, + _feature_index: u32, + _feature_id: uuid::Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.osc_speed.store(speed as u8, Ordering::Relaxed); + self.generate_command() + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/svakom/mod.rs b/crates/buttplug_server/src/device/protocol_impl/svakom/mod.rs new file mode 100644 index 000000000..ee75bf14e --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/svakom/mod.rs @@ -0,0 +1,91 @@ +pub mod svakom_alex; +pub mod svakom_alex_v2; +pub mod svakom_avaneo; +pub mod svakom_barnard; +pub mod svakom_barney; +pub mod svakom_dice; +pub mod svakom_dt250a; +pub mod svakom_iker; +pub mod svakom_jordan; +pub mod svakom_pulse; +pub mod svakom_sam; +pub mod svakom_sam2; +pub mod svakom_suitcase; +pub mod svakom_tarax; +pub mod svakom_v1; +pub mod svakom_v2; +pub mod svakom_v3; +pub mod svakom_v4; +pub mod svakom_v5; +pub mod svakom_v6; + +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::{ + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; + +use crate::device::{ + hardware::Hardware, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, + }, +}; +use async_trait::async_trait; +use std::sync::Arc; + +generic_protocol_initializer_setup!(Svakom, "svakom"); + +#[derive(Default)] +pub struct SvakomInitializer {} + +#[async_trait] +impl ProtocolInitializer for SvakomInitializer { + async fn initialize( + &mut self, + hardware: Arc, + def: &DeviceDefinition, + ) -> Result, ButtplugDeviceError> { + if let Some(variant) = def.protocol_variant() { + match variant.as_str() { + "svakom_alex" => Ok(Arc::new(svakom_alex::SvakomAlex::default())), + "svakom_alex_v2" => Ok(Arc::new(svakom_alex_v2::SvakomAlexV2::default())), + //"svakom_avaneo" => Ok(Arc::new(svakom_avaneo::SvakomAvaNeo::default())), + "svakom_barnard" => Ok(Arc::new(svakom_barnard::SvakomBarnard::default())), + "svakom_barney" => Ok(Arc::new(svakom_barney::SvakomBarney::default())), + "svakom_dice" => Ok(Arc::new(svakom_dice::SvakomDice::default())), + //"svakom_dt250a" => svakom_dt250a::SvakomDT250AInitializer::default().initialize(hardware, def).await, + "svakom_iker" => Ok(Arc::new(svakom_iker::SvakomIker::default())), + "svakom_jordan" => Ok(Arc::new(svakom_jordan::SvakomJordan::default())), + "svakom_pulse" => Ok(Arc::new(svakom_pulse::SvakomPulse::default())), + "svakom_sam" => { + svakom_sam::SvakomSamInitializer::default() + .initialize(hardware, def) + .await + } + "svakom_sam2" => Ok(Arc::new(svakom_sam2::SvakomSam2::default())), + //"svakom_suitcase" => Ok(Arc::new(svakom_suitcase::SvakomSuitcase::default())), + //"svakom_tarax" => Ok(Arc::new(svakom_tarax::SvakomTaraX::default())), + "svakom_v1" => Ok(Arc::new(svakom_v1::SvakomV1::default())), + "svakom_v2" => Ok(Arc::new(svakom_v2::SvakomV2::default())), + "svakom_v3" => Ok(Arc::new(svakom_v3::SvakomV3::default())), + "svakom_v4" => Ok(Arc::new(svakom_v4::SvakomV4::default())), + "svakom_v5" => Ok(Arc::new(svakom_v5::SvakomV5::default())), + "svakom_v6" => Ok(Arc::new(svakom_v6::SvakomV6::default())), + _ => Err(ButtplugDeviceError::ProtocolNotImplemented(format!( + "No protocol implementation for Vorze Device {}", + hardware.name() + ))), + } + } else { + Err(ButtplugDeviceError::ProtocolNotImplemented(format!( + "No protocol implementation for Vorze Device {}", + hardware.name() + ))) + } + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_alex.rs b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_alex.rs new file mode 100644 index 000000000..30313318d --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_alex.rs @@ -0,0 +1,41 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler, ProtocolKeepaliveStrategy}, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; + +generic_protocol_setup!(SvakomAlex, "svakom-alex"); + +#[derive(Default)] +pub struct SvakomAlex {} + +impl ProtocolHandler for SvakomAlex { + fn keepalive_strategy(&self) -> ProtocolKeepaliveStrategy { + ProtocolKeepaliveStrategy::HardwareRequiredRepeatLastPacketStrategy + } + + fn handle_output_vibrate_cmd( + &self, + _feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + [18, 1, 3, 0, if speed == 0 { 0xFF } else { speed as u8 }, 0].to_vec(), + false, + ) + .into()]) + } +} diff --git a/buttplug/src/server/device/protocol/svakom_alex_v2.rs b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_alex_v2.rs similarity index 50% rename from buttplug/src/server/device/protocol/svakom_alex_v2.rs rename to crates/buttplug_server/src/device/protocol_impl/svakom/svakom_alex_v2.rs index db5e6df42..105e463b5 100644 --- a/buttplug/src/server/device/protocol/svakom_alex_v2.rs +++ b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_alex_v2.rs @@ -5,13 +5,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler, ProtocolKeepaliveStrategy}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(SvakomAlexV2, "svakom-alex-v2"); @@ -19,18 +20,20 @@ generic_protocol_setup!(SvakomAlexV2, "svakom-alex-v2"); pub struct SvakomAlexV2 {} impl ProtocolHandler for SvakomAlexV2 { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy + fn keepalive_strategy(&self) -> ProtocolKeepaliveStrategy { + ProtocolKeepaliveStrategy::HardwareRequiredRepeatLastPacketStrategy } - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, - [0x55, 3, 3, 0, scalar as u8, scalar as u8 + 5].to_vec(), + [0x55, 3, 3, 0, speed as u8, speed as u8 + 5].to_vec(), false, ) .into()]) diff --git a/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_avaneo.rs b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_avaneo.rs new file mode 100644 index 000000000..8a1602511 --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_avaneo.rs @@ -0,0 +1,69 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2023 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 buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, +}; + + +generic_protocol_setup!(SvakomAvaNeo, "svakom-avaneo"); + +#[derive(Default)] +pub struct SvakomAvaNeo {} + +impl SvakomAvaNeo { + fn form_hardware_command( + &self, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + [ + 0x55, + 0x03, + 0x00, + 0x00, + if speed == 0 { 0x00 } else { 0x01 }, + speed as u8, + ] + .to_vec(), + false, + ) + .into()]) + } +} + +impl ProtocolHandler for SvakomAvaNeo { + // Note: This protocol used to have a mode byte that was set in cases where multiple commands were + // sent at the same time. This has been removed in the v10 line, but may cause issues. If we get + // bug reports on that, we may need to revisit this implementation. + + fn handle_output_vibrate_cmd( + &self, + _feature_index: u32, + feature_id: uuid::Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.form_hardware_command(feature_id, speed) + } + + fn handle_output_oscillate_cmd( + &self, + _feature_index: u32, + feature_id: uuid::Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.form_hardware_command(feature_id, speed) + } +} diff --git a/buttplug/src/server/device/protocol/svakom_barnard.rs b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_barnard.rs similarity index 64% rename from buttplug/src/server/device/protocol/svakom_barnard.rs rename to crates/buttplug_server/src/device/protocol_impl/svakom/svakom_barnard.rs index c89aa36a4..506302995 100644 --- a/buttplug/src/server/device/protocol/svakom_barnard.rs +++ b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_barnard.rs @@ -5,13 +5,12 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler, ProtocolKeepaliveStrategy}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(SvakomBarnard, "svakom-barnard"); @@ -19,16 +18,18 @@ generic_protocol_setup!(SvakomBarnard, "svakom-barnard"); pub struct SvakomBarnard {} impl ProtocolHandler for SvakomBarnard { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy + fn keepalive_strategy(&self) -> ProtocolKeepaliveStrategy { + ProtocolKeepaliveStrategy::HardwareRequiredRepeatLastPacketStrategy } - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, + _feature_index: u32, + feature_id: uuid::Uuid, speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, [ 0x55, @@ -44,12 +45,14 @@ impl ProtocolHandler for SvakomBarnard { .into()]) } - fn handle_scalar_oscillate_cmd( + fn handle_output_oscillate_cmd( &self, - _index: u32, + _feature_index: u32, + feature_id: uuid::Uuid, speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, [ 0x55, 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 new file mode 100644 index 000000000..5512d057a --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_barney.rs @@ -0,0 +1,47 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler, ProtocolKeepaliveStrategy}, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; + +generic_protocol_setup!(SvakomBarney, "svakom-barney"); + +#[derive(Default)] +pub struct SvakomBarney {} + +impl ProtocolHandler for SvakomBarney { + fn keepalive_strategy(&self) -> ProtocolKeepaliveStrategy { + ProtocolKeepaliveStrategy::HardwareRequiredRepeatLastPacketStrategy + } + + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + feature_id: uuid::Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + [ + 0x55, + 0x03, + feature_index as u8 + 1, + 0x00, + if speed == 0 { 0x00 } else { 0x03 }, + speed as u8, + ] + .to_vec(), + false, + ) + .into()]) + } +} diff --git a/buttplug/src/server/device/protocol/svakom_dice.rs b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_dice.rs similarity index 56% rename from buttplug/src/server/device/protocol/svakom_dice.rs rename to crates/buttplug_server/src/device/protocol_impl/svakom/svakom_dice.rs index 696f56da7..681c587ae 100644 --- a/buttplug/src/server/device/protocol/svakom_dice.rs +++ b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_dice.rs @@ -5,13 +5,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler, ProtocolKeepaliveStrategy}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(SvakomDice, "svakom-dice"); @@ -19,18 +20,20 @@ generic_protocol_setup!(SvakomDice, "svakom-dice"); pub struct SvakomDice {} impl ProtocolHandler for SvakomDice { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy + fn keepalive_strategy(&self) -> ProtocolKeepaliveStrategy { + ProtocolKeepaliveStrategy::HardwareRequiredRepeatLastPacketStrategy } - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, - [0x55, 0x04, 0x00, 0x00, 01, scalar as u8, 0xaa].to_vec(), + [0x55, 0x04, 0x00, 0x00, 01, speed as u8, 0xaa].to_vec(), false, ) .into()]) diff --git a/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_dt250a.rs b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_dt250a.rs new file mode 100644 index 000000000..fb50dcb1e --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_dt250a.rs @@ -0,0 +1,70 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, +}; + + +generic_protocol_setup!(SvakomDT250A, "svakom-dt250a"); + +#[derive(Default)] +pub struct SvakomDT250A {} + +impl SvakomDT250A { + // Note: This protocol used to have a mode byte that was set in cases where multiple commands were + // sent at the same time. This has been removed in the v10 line, but may cause issues. If we get + // bug reports on that, we may need to revisit this implementation. + + fn form_hardware_command( + &self, + mode: u8, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + [ + 0x55, + mode, + 0x00, + 0x00, + if speed == 0 { 0x00 } else { 0x01 }, + speed as u8, + ] + .to_vec(), + false, + ) + .into()]) + } +} + +impl ProtocolHandler for SvakomDT250A { + fn handle_output_vibrate_cmd( + &self, + _feature_index: u32, + feature_id: uuid::Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.form_hardware_command(0x03, feature_id, speed) + } + + fn handle_output_constrict_cmd( + &self, + _feature_index: u32, + feature_id: uuid::Uuid, + level: u32, + ) -> Result, ButtplugDeviceError> { + self.form_hardware_command(0x08, feature_id, level) + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_iker.rs b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_iker.rs new file mode 100644 index 000000000..cae13c15a --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_iker.rs @@ -0,0 +1,71 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler, ProtocolKeepaliveStrategy}, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use std::sync::atomic::{AtomicU8, Ordering}; +use std::sync::Arc; + +generic_protocol_setup!(SvakomIker, "svakom-iker"); + +#[derive(Default)] +pub struct SvakomIker { + last_speeds: Arc<[AtomicU8; 2]>, +} + +impl ProtocolHandler for SvakomIker { + fn keepalive_strategy(&self) -> ProtocolKeepaliveStrategy { + ProtocolKeepaliveStrategy::HardwareRequiredRepeatLastPacketStrategy + } + + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + feature_id: uuid::Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.last_speeds[feature_index as usize].store(speed as u8, Ordering::Relaxed); + let vibe0 = self.last_speeds[0].load(Ordering::Relaxed); + let vibe1 = self.last_speeds[1].load(Ordering::Relaxed); + if vibe0 == 0 && vibe1 == 0 { + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + [0x55, 0x07, 0x00, 0x00, 0x00, 0x00].to_vec(), + false, + ) + .into()]) + } else { + let mut msgs = vec![]; + msgs.push( + HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + [0x55, 0x03, 0x03, 0x00, 0x01, vibe0 as u8].to_vec(), + false, + ) + .into(), + ); + if vibe1 > 0 { + msgs.push( + HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + [0x55, 0x07, 0x00, 0x00, vibe1 as u8, 0x00].to_vec(), + false, + ) + .into(), + ); + } + Ok(msgs) + } + } +} diff --git a/buttplug/src/server/device/protocol/svakom_jordan.rs b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_jordan.rs similarity index 53% rename from buttplug/src/server/device/protocol/svakom_jordan.rs rename to crates/buttplug_server/src/device/protocol_impl/svakom/svakom_jordan.rs index 8f86e9809..d5cb467d4 100644 --- a/buttplug/src/server/device/protocol/svakom_jordan.rs +++ b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_jordan.rs @@ -5,58 +5,62 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler, ProtocolKeepaliveStrategy}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(SvakomJordan, "svakom-jordan"); #[derive(Default)] -struct SvakomJordan {} +pub struct SvakomJordan {} impl ProtocolHandler for SvakomJordan { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy + fn keepalive_strategy(&self) -> ProtocolKeepaliveStrategy { + ProtocolKeepaliveStrategy::HardwareRequiredRepeatLastPacketStrategy } - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: uuid::Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, vec![ 0x55, 0x03, 0x00, 0x00, - if scalar == 0 { 0x00 } else { 0x01 }, - scalar as u8, + if speed == 0 { 0x00 } else { 0x01 }, + speed as u8, 0x00, ], false, ) .into()]) } - fn handle_scalar_oscillate_cmd( + + fn handle_output_oscillate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: uuid::Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, vec![ 0x55, 0x08, 0x00, 0x00, - if scalar == 0 { 0x00 } else { 0x01 }, - scalar as u8, + if speed == 0 { 0x00 } else { 0x01 }, + speed as u8, 0x00, ], false, diff --git a/buttplug/src/server/device/protocol/svakom_pulse.rs b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_pulse.rs similarity index 53% rename from buttplug/src/server/device/protocol/svakom_pulse.rs rename to crates/buttplug_server/src/device/protocol_impl/svakom/svakom_pulse.rs index ef17659f6..3bfdf0f08 100644 --- a/buttplug/src/server/device/protocol/svakom_pulse.rs +++ b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_pulse.rs @@ -5,13 +5,12 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler, ProtocolKeepaliveStrategy}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(SvakomPulse, "svakom-pulse"); @@ -19,24 +18,26 @@ generic_protocol_setup!(SvakomPulse, "svakom-pulse"); pub struct SvakomPulse {} impl ProtocolHandler for SvakomPulse { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy + fn keepalive_strategy(&self) -> ProtocolKeepaliveStrategy { + ProtocolKeepaliveStrategy::HardwareRequiredRepeatLastPacketStrategy } - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: uuid::Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, [ 0x55, 0x03, 0x03, 0x00, - if scalar == 0 { 0x00 } else { 0x01 }, - scalar as u8 + 1, + if speed == 0 { 0x00 } else { 0x01 }, + speed as u8 + 1, ] .to_vec(), false, diff --git a/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_sam.rs b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_sam.rs new file mode 100644 index 000000000..837c8a116 --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_sam.rs @@ -0,0 +1,109 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2023 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 crate::device::protocol::ProtocolKeepaliveStrategy; +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareSubscribeCmd, HardwareWriteCmd}, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, + }, +}; +use async_trait::async_trait; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use buttplug_server_device_config::{ + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; +use std::sync::Arc; +use uuid::{uuid, Uuid}; + +generic_protocol_initializer_setup!(SvakomSam, "svakom-sam"); +const SVAKOM_SAM_PROTOCOL_UUID: Uuid = uuid!("e39a6b4a-230a-4669-be94-68135f97f166"); + +#[derive(Default)] +pub struct SvakomSamInitializer {} + +#[async_trait] +impl ProtocolInitializer for SvakomSamInitializer { + async fn initialize( + &mut self, + hardware: Arc, + _: &DeviceDefinition, + ) -> Result, ButtplugDeviceError> { + hardware + .subscribe(&HardwareSubscribeCmd::new( + SVAKOM_SAM_PROTOCOL_UUID, + Endpoint::Rx, + )) + .await?; + let mut gen2 = hardware.endpoints().contains(&Endpoint::TxMode); + if !gen2 && hardware.endpoints().contains(&Endpoint::Firmware) { + gen2 = true; + warn!("Svakom Sam model without speed control detected - This device will only vibrate at 1 speed"); + } + + Ok(Arc::new(SvakomSam::new(gen2))) + } +} + +pub struct SvakomSam { + gen2: bool, +} + +impl SvakomSam { + pub fn new(gen2: bool) -> Self { + Self { gen2 } + } +} + +impl ProtocolHandler for SvakomSam { + fn keepalive_strategy(&self) -> ProtocolKeepaliveStrategy { + ProtocolKeepaliveStrategy::HardwareRequiredRepeatLastPacketStrategy + } + + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + if feature_index == 0 { + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + if self.gen2 { + [ + 18, + 1, + 3, + 0, + if speed == 0 { 0x00 } else { 0x04 }, + speed as u8, + ] + .to_vec() + } else { + [18, 1, 3, 0, 5, speed as u8].to_vec() + }, + false, + ) + .into()]) + } else { + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + [18, 6, 1, speed as u8].to_vec(), + false, + ) + .into()]) + } + } +} diff --git a/buttplug/src/server/device/protocol/svakom_sam2.rs b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_sam2.rs similarity index 55% rename from buttplug/src/server/device/protocol/svakom_sam2.rs rename to crates/buttplug_server/src/device/protocol_impl/svakom/svakom_sam2.rs index e3f03acca..805035992 100644 --- a/buttplug/src/server/device/protocol/svakom_sam2.rs +++ b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_sam2.rs @@ -5,13 +5,12 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler, ProtocolKeepaliveStrategy}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(SvakomSam2, "svakom-sam2"); @@ -19,24 +18,26 @@ generic_protocol_setup!(SvakomSam2, "svakom-sam2"); pub struct SvakomSam2 {} impl ProtocolHandler for SvakomSam2 { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy + fn keepalive_strategy(&self) -> ProtocolKeepaliveStrategy { + ProtocolKeepaliveStrategy::HardwareRequiredRepeatLastPacketStrategy } - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: uuid::Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, [ 0x55, 0x03, 0x00, 0x00, - if scalar == 0 { 0x00 } else { 0x05 }, - scalar as u8, + if speed == 0 { 0x00 } else { 0x05 }, + speed as u8, 0x00, ] .to_vec(), @@ -45,20 +46,22 @@ impl ProtocolHandler for SvakomSam2 { .into()]) } - fn handle_scalar_constrict_cmd( + fn handle_output_constrict_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: uuid::Uuid, + level: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, [ 0x55, 0x09, 0x00, 0x00, - if scalar == 0 { 0x00 } else { 0x01 }, - scalar as u8, + if level == 0 { 0x00 } else { 0x01 }, + level as u8, 0x00, ] .to_vec(), diff --git a/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_suitcase.rs b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_suitcase.rs new file mode 100644 index 000000000..d2cc8b811 --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_suitcase.rs @@ -0,0 +1,52 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, +}; + + +generic_protocol_setup!(SvakomSuitcase, "svakom-suitcase"); + +#[derive(Default)] +pub struct SvakomSuitcase {} + +impl ProtocolHandler for SvakomSuitcase { + // I am like 90% sure this is wrong since this device has two vibrators, but the original + // implementation made no sense in terms of knowing which command addressed which index. Putting + // in a best effort here and we'll see if anyone complains. + fn handle_output_vibrate_cmd( + &self, + _feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + let scalar = speed; + let mut speed = (scalar % 10) as u8; + let mut intensity = if scalar == 0 { + 0u8 + } else { + (scalar as f32 / 10.0).floor() as u8 + 1 + }; + if speed == 0 && intensity != 0 { + // 10 -> 2,0 -> 1,A + speed = 10; + intensity -= 1; + } + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + [0x55, 0x03, 0x00, 0x00, intensity, speed].to_vec(), + false, + ).into()]) + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_tarax.rs b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_tarax.rs new file mode 100644 index 000000000..87dde57cd --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_tarax.rs @@ -0,0 +1,47 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2023 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 buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, +}; + +generic_protocol_setup!(SvakomTaraX, "svakom-tarax"); + +#[derive(Default)] +pub struct SvakomTaraX {} + +impl ProtocolHandler for SvakomTaraX { + // I am like 90% sure this is wrong since this device has two vibrators, but the original + // implementation made no sense in terms of knowing which command addressed which index. Putting + // in a best effort here and we'll see if anyone complains. + fn handle_output_vibrate_cmd( + &self, + _feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + Ok(vec!(HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + [ + 0x55, + 0x03, + 0x00, + 0x00, + if speed == 0 { 0x01 } else { speed as u8 }, + if speed == 0 { 0x01 } else { 0x02 }, + ] + .to_vec(), + false, + ).into())) + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_v1.rs b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_v1.rs new file mode 100644 index 000000000..88acc9095 --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_v1.rs @@ -0,0 +1,42 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler, ProtocolKeepaliveStrategy}, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; + +generic_protocol_setup!(SvakomV1, "svakom-v1"); + +#[derive(Default)] +pub struct SvakomV1 {} + +impl ProtocolHandler for SvakomV1 { + fn keepalive_strategy(&self) -> ProtocolKeepaliveStrategy { + ProtocolKeepaliveStrategy::HardwareRequiredRepeatLastPacketStrategy + } + + fn handle_output_vibrate_cmd( + &self, + _feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + let multiplier: u8 = if speed == 0 { 0x00 } else { 0x01 }; + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + [0x55, 0x04, 0x03, 0x00, multiplier, speed as u8].to_vec(), + false, + ) + .into()]) + } +} diff --git a/buttplug/src/server/device/protocol/svakom_v2.rs b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_v2.rs similarity index 52% rename from buttplug/src/server/device/protocol/svakom_v2.rs rename to crates/buttplug_server/src/device/protocol_impl/svakom/svakom_v2.rs index 2ed68357a..ae8e82ee2 100644 --- a/buttplug/src/server/device/protocol/svakom_v2.rs +++ b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_v2.rs @@ -5,13 +5,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler, ProtocolKeepaliveStrategy}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(SvakomV2, "svakom-v2"); @@ -19,32 +20,35 @@ generic_protocol_setup!(SvakomV2, "svakom-v2"); pub struct SvakomV2 {} impl ProtocolHandler for SvakomV2 { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy + fn keepalive_strategy(&self) -> ProtocolKeepaliveStrategy { + ProtocolKeepaliveStrategy::HardwareRequiredRepeatLastPacketStrategy } - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - index: u32, - scalar: u32, + feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { - if index == 1 { + if feature_index == 1 { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, - [0x55, 0x06, 0x01, 0x00, scalar as u8, scalar as u8].to_vec(), + [0x55, 0x06, 0x01, 0x00, speed as u8, speed as u8].to_vec(), true, ) .into()]) } else { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, [ 0x55, 0x03, 0x03, 0x00, - if scalar == 0 { 0x00 } else { 0x01 }, - scalar as u8, + if speed == 0 { 0x00 } else { 0x01 }, + speed as u8, ] .to_vec(), true, diff --git a/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_v3.rs b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_v3.rs new file mode 100644 index 000000000..e9629d548 --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_v3.rs @@ -0,0 +1,64 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2023 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 uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler, ProtocolKeepaliveStrategy}, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; + +generic_protocol_setup!(SvakomV3, "svakom-v3"); + +#[derive(Default)] +pub struct SvakomV3 {} + +impl ProtocolHandler for SvakomV3 { + fn keepalive_strategy(&self) -> ProtocolKeepaliveStrategy { + ProtocolKeepaliveStrategy::HardwareRequiredRepeatLastPacketStrategy + } + + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + [ + 0x55, + if feature_index == 0 { 0x03 } else { 0x09 }, + if feature_index == 0 { 0x03 } else { 0x00 }, + 0x00, + if speed == 0 { 0x00 } else { 0x01 }, + speed as u8, + ] + .to_vec(), + false, + ) + .into()]) + } + + fn handle_output_rotate_cmd( + &self, + _feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + [0x55, 0x08, 0x00, 0x00, speed as u8, 0xff].to_vec(), + false, + ) + .into()]) + } +} 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 new file mode 100644 index 000000000..0d170730d --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_v4.rs @@ -0,0 +1,48 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2023 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 crate::device::protocol::ProtocolKeepaliveStrategy; +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; + +generic_protocol_setup!(SvakomV4, "svakom-v4"); + +#[derive(Default)] +pub struct SvakomV4 {} + +impl ProtocolHandler for SvakomV4 { + fn keepalive_strategy(&self) -> ProtocolKeepaliveStrategy { + ProtocolKeepaliveStrategy::HardwareRequiredRepeatLastPacketStrategy + } + + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + feature_id: uuid::Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + [ + 0x55, + 0x03, + feature_index as u8 + 1, + 0x00, + if speed == 0 { 0x00 } else { 0x03 }, + speed as u8, + ] + .to_vec(), + false, + ) + .into()]) + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_v5.rs b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_v5.rs new file mode 100644 index 000000000..7d4b22211 --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_v5.rs @@ -0,0 +1,81 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 uuid::{uuid, Uuid}; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler, ProtocolKeepaliveStrategy}, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use std::sync::atomic::{AtomicU8, Ordering}; +generic_protocol_setup!(SvakomV5, "svakom-v5"); + +const SVAKOM_V5_VIBRATOR_UUID: Uuid = uuid!("d19af460-3d81-483b-a87f-b2781d972bac"); + +#[derive(Default)] +pub struct SvakomV5 { + last_vibrator_speeds: [AtomicU8; 2], +} + +impl ProtocolHandler for SvakomV5 { + fn keepalive_strategy(&self) -> ProtocolKeepaliveStrategy { + ProtocolKeepaliveStrategy::HardwareRequiredRepeatLastPacketStrategy + } + + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + _feature_id: uuid::Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.last_vibrator_speeds[feature_index as usize].store(speed as u8, Ordering::Relaxed); + let vibe1 = self.last_vibrator_speeds[0].load(Ordering::Relaxed); + let vibe2 = self.last_vibrator_speeds[1].load(Ordering::Relaxed); + Ok(vec![HardwareWriteCmd::new( + &[SVAKOM_V5_VIBRATOR_UUID], + Endpoint::Tx, + [ + 0x55, + 0x03, + if (vibe1 > 0 && vibe2 > 0) || vibe1 == vibe2 { + 0x00 + } else if vibe1 > 0 { + 0x01 + } else { + 0x02 + }, + 0x00, + if vibe1 == vibe2 && vibe1 == 0 { + 0x00 + } else { + 0x01 + }, + vibe1.max(vibe2) as u8, + ] + .to_vec(), + false, + ) + .into()]) + } + + fn handle_output_oscillate_cmd( + &self, + _feature_index: u32, + feature_id: uuid::Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + [0x55, 0x09, 0x00, 0x00, speed as u8, 0x00].to_vec(), + false, + ) + .into()]) + } +} 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 new file mode 100644 index 000000000..db401cd5c --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/svakom/svakom_v6.rs @@ -0,0 +1,137 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2025 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 async_trait::async_trait; +use uuid::{uuid, Uuid}; + +use buttplug_core::{ + errors::ButtplugDeviceError, + message::{OutputType}, +}; + +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, + ProtocolKeepaliveStrategy, + }, +}; +use buttplug_server_device_config::{DeviceDefinition, UserDeviceIdentifier, ProtocolCommunicationSpecifier, Endpoint}; +use std::sync::{ + atomic::{AtomicU8, Ordering}, + Arc, +}; + +const SVAKOM_V6_VIBRATOR_UUID: Uuid = uuid!("4cf33d95-a3d1-4ed4-9ac6-9ba6d6ccb091"); + +generic_protocol_initializer_setup!(SvakomV6, "svakom-v6"); + +#[derive(Default)] +pub struct SvakomV6Initializer {} + +#[async_trait] +impl ProtocolInitializer for SvakomV6Initializer { + async fn initialize( + &mut self, + _: Arc, + def: &DeviceDefinition, + ) -> Result, ButtplugDeviceError> { + let num_vibrators = def + .features() + .iter() + .filter(|x| { + if let Some(output_map) = x.output() { + output_map.contains_key(&OutputType::Vibrate) + } else { + false + } + }) + .count() as u8; + Ok(Arc::new(SvakomV6::new(num_vibrators))) + } +} + +#[derive(Default)] +pub struct SvakomV6 { + num_vibrators: u8, + last_vibrator_speeds: [AtomicU8; 3], +} + +impl SvakomV6 { + fn new(num_vibrators: u8) -> Self { + Self { + num_vibrators, + ..Default::default() + } + } +} + +impl ProtocolHandler for SvakomV6 { + fn keepalive_strategy(&self) -> ProtocolKeepaliveStrategy { + ProtocolKeepaliveStrategy::HardwareRequiredRepeatLastPacketStrategy + } + + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + feature_id: uuid::Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.last_vibrator_speeds[feature_index as usize].store(speed as u8, Ordering::Relaxed); + if feature_index < 2 { + let vibe1 = self.last_vibrator_speeds[0].load(Ordering::Relaxed); + let vibe2 = self.last_vibrator_speeds[1].load(Ordering::Relaxed); + Ok(vec![HardwareWriteCmd::new( + &[SVAKOM_V6_VIBRATOR_UUID], + Endpoint::Tx, + [ + 0x55, + 0x03, + if self.num_vibrators == 1 || (vibe1 > 0 && vibe2 > 0) || vibe1 == vibe2 { + 0x00 + } else if vibe1 > 0 { + 0x01 + } else { + 0x02 + }, + 0x00, + if vibe1 == vibe2 && vibe1 == 0 { + 0x00 + } else { + 0x01 + }, + vibe1.max(vibe2) as u8, + 0x00, + ] + .to_vec(), + false, + ) + .into()]) + } else { + let vibe3 = self.last_vibrator_speeds[2].load(Ordering::Relaxed); + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + [ + 0x55, + 0x07, + 0x00, + 0x00, + if vibe3 == 0 { 0x00 } else { 0x01 }, + vibe3 as u8, + 0x00, + ] + .to_vec(), + false, + ) + .into()]) + } + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/synchro.rs b/crates/buttplug_server/src/device/protocol_impl/synchro.rs new file mode 100644 index 000000000..de22ccc3d --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/synchro.rs @@ -0,0 +1,44 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; + +generic_protocol_setup!(Synchro, "synchro"); + +#[derive(Default)] +pub struct Synchro {} + +impl ProtocolHandler for Synchro { + fn handle_rotation_with_direction_cmd( + &self, + _feature_index: u32, + feature_id: Uuid, + speed: u32, + clockwise: bool, + ) -> Result, ButtplugDeviceError> { + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + vec![ + 0xa1, + 0x01, + speed as u8 | if clockwise || speed == 0 { 0x00 } else { 0x80 }, + 0x77, + 0x55, + ], + false, + ) + .into()]) + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/tcode_v03.rs b/crates/buttplug_server/src/device/protocol_impl/tcode_v03.rs new file mode 100644 index 000000000..206bd5bfb --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/tcode_v03.rs @@ -0,0 +1,82 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; + +generic_protocol_setup!(TCodeV03, "tcode-v03"); + +#[derive(Default)] +pub struct TCodeV03 {} + +impl ProtocolHandler for TCodeV03 { + fn handle_output_position_cmd( + &self, + _feature_index: u32, + feature_id: Uuid, + position: u32, + ) -> Result, ButtplugDeviceError> { + let mut msg_vec = vec![]; + + let command = format!("L0{position:03}\nR0{position:03}\n"); + msg_vec.push( + HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + command.as_bytes().to_vec(), + false, + ) + .into(), + ); + + Ok(msg_vec) + } + + fn handle_position_with_duration_cmd( + &self, + feature_index: u32, + feature_id: Uuid, + position: u32, + duration: u32, + ) -> Result, ButtplugDeviceError> { + let mut msg_vec = vec![]; + + let command = format!("L{feature_index}{position:02}I{duration}\n"); + msg_vec.push( + HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + command.as_bytes().to_vec(), + false, + ) + .into(), + ); + + Ok(msg_vec) + } + + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + format!("V{feature_index}{speed:02}\n").as_bytes().to_vec(), + false, + ) + .into()]) + } +} diff --git a/buttplug/src/server/device/protocol/thehandy/handyplug.proto b/crates/buttplug_server/src/device/protocol_impl/thehandy/handyplug.proto similarity index 100% rename from buttplug/src/server/device/protocol/thehandy/handyplug.proto rename to crates/buttplug_server/src/device/protocol_impl/thehandy/handyplug.proto diff --git a/buttplug/src/server/device/protocol/thehandy/handyplug.rs b/crates/buttplug_server/src/device/protocol_impl/thehandy/handyplug.rs similarity index 100% rename from buttplug/src/server/device/protocol/thehandy/handyplug.rs rename to crates/buttplug_server/src/device/protocol_impl/thehandy/handyplug.rs diff --git a/buttplug/src/server/device/protocol/thehandy/mod.rs b/crates/buttplug_server/src/device/protocol_impl/thehandy/mod.rs similarity index 69% rename from buttplug/src/server/device/protocol/thehandy/mod.rs rename to crates/buttplug_server/src/device/protocol_impl/thehandy/mod.rs index 787e57929..3257167cf 100644 --- a/buttplug/src/server/device/protocol/thehandy/mod.rs +++ b/crates/buttplug_server/src/device/protocol_impl/thehandy/mod.rs @@ -7,29 +7,26 @@ use self::handyplug::Ping; -use super::fleshlight_launch_helper; -use crate::{ - core::{ - errors::ButtplugDeviceError, - message::{self, ButtplugDeviceMessage, Endpoint}, - }, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, ProtocolKeepaliveStrategy, }, }; use async_trait::async_trait; -use prost::Message; -use std::sync::{ - atomic::{AtomicU8, Ordering}, - Arc, +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use buttplug_server_device_config::{ + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, }; +use prost::Message; +use std::sync::Arc; +use uuid::{uuid, Uuid}; mod protocomm { include!("./protocomm.rs"); @@ -39,6 +36,7 @@ mod handyplug { include!("./handyplug.rs"); } +const THEHANDY_PROTOCOL_UUID: Uuid = uuid!("e7c3ba93-ddbf-4f38-a960-30a332739d02"); generic_protocol_initializer_setup!(TheHandy, "thehandy"); #[derive(Default)] @@ -49,7 +47,7 @@ impl ProtocolInitializer for TheHandyInitializer { async fn initialize( &mut self, _hardware: Arc, - _: &UserDeviceDefinition, + _: &DeviceDefinition, ) -> Result, ButtplugDeviceError> { // Ok, somehow this whole function has been basically a no-op. The read/write lines never had an // await on them, so they were never run. But only now, in Rust 1.75/Buttplug 7.1.15, have we @@ -108,15 +106,10 @@ impl ProtocolInitializer for TheHandyInitializer { } #[derive(Default)] -pub struct TheHandy { - // The generic command manager would normally handle this storage, but the only reason we're - // retaining tracking information is to build our fucking timing calculation for the fleshlight - // command backport. I am so mad right now. - previous_position: Arc, -} +pub struct TheHandy {} impl ProtocolHandler for TheHandy { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { + fn keepalive_strategy(&self) -> ProtocolKeepaliveStrategy { let ping_payload = handyplug::Payload { messages: vec![handyplug::Message { message: Some(handyplug::message::Message::Ping(Ping { id: 999 })), @@ -127,51 +120,23 @@ impl ProtocolHandler for TheHandy { .encode(&mut ping_buf) .expect("Infallible encode."); - super::ProtocolKeepaliveStrategy::RepeatPacketStrategy(HardwareWriteCmd::new( + ProtocolKeepaliveStrategy::HardwareRequiredRepeatPacketStrategy(HardwareWriteCmd::new( + &[THEHANDY_PROTOCOL_UUID], Endpoint::Tx, ping_buf, true, )) } - fn handle_fleshlight_launch_fw12_cmd( - &self, - message: message::FleshlightLaunchFW12CmdV0, - ) -> Result, ButtplugDeviceError> { - // Oh good. ScriptPlayer hasn't updated to LinearCmd yet so now I have to - // work backward from fleshlight to my own Linear format that Handy uses. - // - // Building this library was a mistake. - let goal_position = message.position() as f64 / 100f64; - let previous_position = self.previous_position.load(Ordering::SeqCst) as f64 / 100f64; - self - .previous_position - .store(message.position(), Ordering::SeqCst); - let distance = (goal_position - previous_position).abs(); - let duration = - fleshlight_launch_helper::calculate_duration(distance, message.speed() as f64 / 99f64) as u32; - self.handle_linear_cmd(message::LinearCmdV4::new( - message.device_index(), - vec![message::VectorSubcommandV4::new(0, duration, goal_position)], - )) - } - - fn handle_linear_cmd( + fn handle_position_with_duration_cmd( &self, - message: message::LinearCmdV4, + _feature_index: u32, + _feature_id: Uuid, + position: u32, + duration: u32, ) -> Result, ButtplugDeviceError> { // What is "How not to implement a command structure for your device that does one thing", Alex? - // First make sure we only have one vector. - // - // TODO Use the command manager to check this. - if message.vectors().len() != 1 { - return Err(ButtplugDeviceError::DeviceFeatureCountMismatch( - 1, - message.vectors().len() as u32, - )); - } - let linear = handyplug::LinearCmd { // You know when message IDs are important? When you have a protocol that handles multiple // asynchronous commands. You know what doesn't handle multiple asynchronous commands? The @@ -197,8 +162,8 @@ impl ProtocolHandler for TheHandy { // The handy. It's the handy. vectors: vec![handyplug::linear_cmd::Vector { index: 0, - duration: message.vectors()[0].duration(), - position: message.vectors()[0].position(), + position: position as f64 / 100f64, + duration, }], }; let linear_payload = handyplug::Payload { @@ -210,8 +175,12 @@ impl ProtocolHandler for TheHandy { linear_payload .encode(&mut linear_buf) .expect("Infallible encode."); - Ok(vec![ - HardwareWriteCmd::new(Endpoint::Tx, linear_buf, true).into() - ]) + Ok(vec![HardwareWriteCmd::new( + &[THEHANDY_PROTOCOL_UUID], + Endpoint::Tx, + linear_buf, + true, + ) + .into()]) } } diff --git a/buttplug/src/server/device/protocol/thehandy/protocomm.proto b/crates/buttplug_server/src/device/protocol_impl/thehandy/protocomm.proto similarity index 100% rename from buttplug/src/server/device/protocol/thehandy/protocomm.proto rename to crates/buttplug_server/src/device/protocol_impl/thehandy/protocomm.proto diff --git a/buttplug/src/server/device/protocol/thehandy/protocomm.rs b/crates/buttplug_server/src/device/protocol_impl/thehandy/protocomm.rs similarity index 100% rename from buttplug/src/server/device/protocol/thehandy/protocomm.rs rename to crates/buttplug_server/src/device/protocol_impl/thehandy/protocomm.rs diff --git a/buttplug/src/server/device/protocol/tryfun.rs b/crates/buttplug_server/src/device/protocol_impl/tryfun.rs similarity index 51% rename from buttplug/src/server/device/protocol/tryfun.rs rename to crates/buttplug_server/src/device/protocol_impl/tryfun.rs index 83dc93897..a0eeb2f83 100644 --- a/buttplug/src/server/device/protocol/tryfun.rs +++ b/crates/buttplug_server/src/device/protocol_impl/tryfun.rs @@ -5,13 +5,12 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - generic_protocol_setup, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::ProtocolHandler, - }, +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; generic_protocol_setup!(TryFun, "tryfun"); @@ -20,17 +19,14 @@ generic_protocol_setup!(TryFun, "tryfun"); pub struct TryFun {} impl ProtocolHandler for TryFun { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_oscillate_cmd( + fn handle_output_oscillate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: uuid::Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { let mut sum: u8 = 0xff; - let mut data = vec![0xAA, 0x02, 0x07, scalar as u8]; + let mut data = vec![0xAA, 0x02, 0x07, speed as u8]; let mut count = 0; for item in data.iter().skip(1) { sum -= item; @@ -39,16 +35,23 @@ impl ProtocolHandler for TryFun { sum += count; data.push(sum); - Ok(vec![HardwareWriteCmd::new(Endpoint::Tx, data, true).into()]) + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + data, + true, + ) + .into()]) } - fn handle_scalar_rotate_cmd( + fn handle_output_rotate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: uuid::Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { let mut sum: u8 = 0xff; - let mut data = vec![0xAA, 0x02, 0x08, scalar as u8]; + let mut data = vec![0xAA, 0x02, 0x08, speed as u8]; let mut count = 0; for item in data.iter().skip(1) { sum -= item; @@ -57,26 +60,34 @@ impl ProtocolHandler for TryFun { sum += count; data.push(sum); - Ok(vec![HardwareWriteCmd::new(Endpoint::Tx, data, true).into()]) + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + data, + true, + ) + .into()]) } - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: uuid::Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, vec![ 0x00, 0x02, 0x00, 0x05, - if scalar == 0 { 1u8 } else { 2u8 }, - if scalar == 0 { 2u8 } else { scalar as u8 }, + if speed == 0 { 1u8 } else { 2u8 }, + if speed == 0 { 2u8 } else { speed as u8 }, 0x01, - if scalar == 0 { 1u8 } else { 0u8 }, - 0xfd - (scalar as u8).max(1), + if speed == 0 { 1u8 } else { 0u8 }, + 0xfd - (speed as u8).max(1), ], true, ) diff --git a/buttplug/src/server/device/protocol/tryfun_blackhole.rs b/crates/buttplug_server/src/device/protocol_impl/tryfun_blackhole.rs similarity index 63% rename from buttplug/src/server/device/protocol/tryfun_blackhole.rs rename to crates/buttplug_server/src/device/protocol_impl/tryfun_blackhole.rs index 04c208b5c..31612032a 100644 --- a/buttplug/src/server/device/protocol/tryfun_blackhole.rs +++ b/crates/buttplug_server/src/device/protocol_impl/tryfun_blackhole.rs @@ -5,13 +5,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - generic_protocol_setup, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::ProtocolHandler, - }, +use uuid::Uuid; + +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; use std::sync::atomic::{AtomicU8, Ordering}; @@ -23,14 +24,11 @@ pub struct TryFunBlackHole { } impl ProtocolHandler for TryFunBlackHole { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_oscillate_cmd( + fn handle_output_oscillate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { let mut sum: u8 = 0xff; let mut data = vec![ @@ -39,7 +37,7 @@ impl ProtocolHandler for TryFunBlackHole { 0x00, 0x03, 0x0c, - scalar as u8, + speed as u8, ]; let mut count = 1; for item in data.iter().skip(1) { @@ -49,13 +47,20 @@ impl ProtocolHandler for TryFunBlackHole { sum += count; data.push(sum); - Ok(vec![HardwareWriteCmd::new(Endpoint::Tx, data, false).into()]) + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + data, + false, + ) + .into()]) } - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { let mut sum: u8 = 0xff; let mut data = vec![ @@ -64,7 +69,7 @@ impl ProtocolHandler for TryFunBlackHole { 0x00, 0x03, 0x09, - scalar as u8, + speed as u8, ]; let mut count = 1; for item in data.iter().skip(1) { @@ -74,6 +79,12 @@ impl ProtocolHandler for TryFunBlackHole { sum += count; data.push(sum); - Ok(vec![HardwareWriteCmd::new(Endpoint::Tx, data, false).into()]) + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + data, + false, + ) + .into()]) } } diff --git a/crates/buttplug_server/src/device/protocol_impl/tryfun_meta2.rs b/crates/buttplug_server/src/device/protocol_impl/tryfun_meta2.rs new file mode 100644 index 000000000..59c1f42be --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/tryfun_meta2.rs @@ -0,0 +1,133 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2025 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 uuid::Uuid; + +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, +}; +use std::sync::atomic::{AtomicU8, Ordering}; + +generic_protocol_setup!(TryFunMeta2, "tryfun-meta2"); + +#[derive(Default)] +pub struct TryFunMeta2 { + packet_id: AtomicU8, +} + +impl ProtocolHandler for TryFunMeta2 { + fn handle_output_oscillate_cmd( + &self, + _feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + let mut sum: u8 = 0xff; + let mut data = vec![ + self.packet_id.fetch_add(1, Ordering::Relaxed), + 0x02, + 0x00, + 0x05, + 0x21, + 0x05, + 0x0b, + speed as u8, + ]; + let mut count = 1; + for item in data.iter().skip(1) { + sum -= item; + count += 1; + } + sum += count; + data.push(sum); + + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + data, + false, + ) + .into()]) + } + + fn handle_rotation_with_direction_cmd( + &self, + _feature_index: u32, + feature_id: Uuid, + speed: u32, + clockwise: bool, + ) -> Result, ButtplugDeviceError> { + let mut speed = speed as i8; + if clockwise { + speed += 1; + speed *= -1; + } + let mut sum: u8 = 0xff; + let mut data = vec![ + self.packet_id.fetch_add(1, Ordering::Relaxed), + 0x02, + 0x00, + 0x05, + 0x21, + 0x05, + 0x0e, + speed as u8, + ]; + let mut count = 1; + for item in data.iter().skip(1) { + sum = sum.wrapping_sub(*item); + count += 1; + } + sum += count; + data.push(sum); + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + data, + false, + ) + .into()]) + } + + fn handle_output_vibrate_cmd( + &self, + _feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + let mut sum: u8 = 0xff; + let mut data = vec![ + self.packet_id.fetch_add(1, Ordering::Relaxed), + 0x02, + 0x00, + 0x05, + 0x21, + 0x05, + 0x08, + speed as u8, + ]; + let mut count = 1; + for item in data.iter().skip(1) { + sum -= item; + count += 1; + } + sum += count; + data.push(sum); + + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + data, + false, + ) + .into()]) + } +} diff --git a/buttplug/src/server/device/protocol/vibcrafter.rs b/crates/buttplug_server/src/device/protocol_impl/vibcrafter.rs similarity index 69% rename from buttplug/src/server/device/protocol/vibcrafter.rs rename to crates/buttplug_server/src/device/protocol_impl/vibcrafter.rs index d8eb0838c..c0cdb240e 100644 --- a/buttplug/src/server/device/protocol/vibcrafter.rs +++ b/crates/buttplug_server/src/device/protocol_impl/vibcrafter.rs @@ -5,27 +5,31 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, - }, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareEvent, HardwareSubscribeCmd, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareEvent, HardwareSubscribeCmd, HardwareWriteCmd}, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, }, }; use aes::Aes128; use async_trait::async_trait; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use buttplug_server_device_config::{ + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; use ecb::cipher::block_padding::Pkcs7; use ecb::cipher::{BlockDecryptMut, BlockEncryptMut, KeyInit}; -use std::sync::Arc; +use std::sync::{ + atomic::{AtomicU8, Ordering}, + Arc, +}; +use uuid::{uuid, Uuid}; use rand::distributions::Alphanumeric; use rand::{thread_rng, Rng}; @@ -35,6 +39,7 @@ use sha2::{Digest, Sha256}; type Aes128EcbEnc = ecb::Encryptor; type Aes128EcbDec = ecb::Decryptor; +const VIBCRAFTER_PROTOCOL_UUID: Uuid = uuid!("d3721a71-a81d-461a-b404-8599ce50c00b"); const VIBCRAFTER_KEY: [u8; 16] = *b"jdk#Cra%f5Vib28r"; generic_protocol_initializer_setup!(VibCrafter, "vibcrafter"); @@ -47,7 +52,7 @@ fn encrypt(command: String) -> Vec { let res = enc.encrypt_padded_vec_mut::(command.as_bytes()); info!("Encoded {} to {:?}", command, res); - return res; + res } fn decrypt(data: Vec) -> String { @@ -55,7 +60,7 @@ fn decrypt(data: Vec) -> String { let res = String::from_utf8(dec.decrypt_padded_vec_mut::(&data).unwrap()).unwrap(); info!("Decoded {} from {:?}", res, data); - return res; + res } #[async_trait] @@ -63,11 +68,14 @@ impl ProtocolInitializer for VibCrafterInitializer { async fn initialize( &mut self, hardware: Arc, - _: &UserDeviceDefinition, + _: &DeviceDefinition, ) -> Result, ButtplugDeviceError> { let mut event_receiver = hardware.event_stream(); hardware - .subscribe(&HardwareSubscribeCmd::new(Endpoint::Rx)) + .subscribe(&HardwareSubscribeCmd::new( + VIBCRAFTER_PROTOCOL_UUID, + Endpoint::Rx, + )) .await?; let auth_str = thread_rng() @@ -78,6 +86,7 @@ impl ProtocolInitializer for VibCrafterInitializer { let auth_msg = format!("Auth:{};", auth_str); hardware .write_value(&HardwareWriteCmd::new( + &[VIBCRAFTER_PROTOCOL_UUID], Endpoint::Tx, encrypt(auth_msg), false, @@ -99,12 +108,13 @@ impl ProtocolInitializer for VibCrafterInitializer { if let Some(to_hash) = parts.get(1) { debug!("VibCrafter to hash {:?}", to_hash); let mut sha256 = Sha256::new(); - sha256.update(&to_hash.as_str().as_bytes()); + sha256.update(to_hash.as_str().as_bytes()); let result = &sha256.finalize(); let auth_msg = format!("Auth:{:02x}{:02x};", result[0], result[1]); hardware .write_value(&HardwareWriteCmd::new( + &[VIBCRAFTER_PROTOCOL_UUID], Endpoint::Tx, encrypt(auth_msg), false, @@ -113,19 +123,19 @@ impl ProtocolInitializer for VibCrafterInitializer { } else { return Err(ButtplugDeviceError::ProtocolSpecificError( "VibCrafter".to_owned(), - "VibCrafter didn't provided valid security handshake".to_owned(), + "VibCrafter didn't provide a valid security handshake".to_owned(), )); } } else { return Err(ButtplugDeviceError::ProtocolSpecificError( "VibCrafter".to_owned(), - "VibCrafter didn't provided valid security handshake".to_owned(), + "VibCrafter didn't provide a valid security handshake".to_owned(), )); } } else { return Err(ButtplugDeviceError::ProtocolSpecificError( "VibCrafter".to_owned(), - "VibCrafter didn't provided valid security handshake".to_owned(), + "VibCrafter didn't provide a valid security handshake".to_owned(), )); } } @@ -133,31 +143,27 @@ impl ProtocolInitializer for VibCrafterInitializer { } #[derive(Default)] -pub struct VibCrafter {} +pub struct VibCrafter { + speeds: [AtomicU8; 2], +} impl ProtocolHandler for VibCrafter { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn needs_full_command_set(&self) -> bool { - true - } - - fn handle_scalar_cmd( + fn handle_output_vibrate_cmd( &self, - commands: &[Option<(ActuatorType, u32)>], + feature_index: u32, + feature_id: uuid::Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { - let speed0 = commands[0].unwrap_or((ActuatorType::Vibrate, 0)).1; - let speed1 = if commands.len() > 1 { - commands[1].unwrap_or((ActuatorType::Vibrate, 0)).1 - } else { - speed0 - }; + self.speeds[feature_index as usize].store(speed as u8, Ordering::Relaxed); Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, - encrypt(format!("MtInt:{:02}{:02};", speed0, speed1)), + encrypt(format!( + "MtInt:{:02}{:02};", + self.speeds[0].load(Ordering::Relaxed), + self.speeds[1].load(Ordering::Relaxed) + )), false, ) .into()]) diff --git a/crates/buttplug_server/src/device/protocol_impl/vibratissimo.rs b/crates/buttplug_server/src/device/protocol_impl/vibratissimo.rs new file mode 100644 index 000000000..3f6f9290b --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/vibratissimo.rs @@ -0,0 +1,129 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 buttplug_core::message::OutputType; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use buttplug_server_device_config::{ + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; + +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareReadCmd, HardwareWriteCmd}, + protocol::{ProtocolHandler, ProtocolIdentifier, ProtocolInitializer}, +}; +use async_trait::async_trait; +use std::sync::atomic::{AtomicU8, Ordering}; +use std::sync::Arc; +use uuid::{uuid, Uuid}; + +const VIBRATISSIMO_PROTOCOL_UUID: Uuid = uuid!("66ef7aa4-1e6a-4067-9066-dcb53c7647f2"); + +pub mod setup { + use crate::device::protocol::{ProtocolIdentifier, ProtocolIdentifierFactory}; + #[derive(Default)] + pub struct VibratissimoIdentifierFactory {} + + impl ProtocolIdentifierFactory for VibratissimoIdentifierFactory { + fn identifier(&self) -> &str { + "vibratissimo" + } + + fn create(&self) -> Box { + Box::new(super::VibratissimoIdentifier::default()) + } + } +} + +#[derive(Default)] +pub struct VibratissimoIdentifier {} + +#[async_trait] +impl ProtocolIdentifier for VibratissimoIdentifier { + async fn identify( + &mut self, + hardware: Arc, + _: ProtocolCommunicationSpecifier, + ) -> Result<(UserDeviceIdentifier, Box), ButtplugDeviceError> { + let result = hardware + .read_value(&HardwareReadCmd::new( + VIBRATISSIMO_PROTOCOL_UUID, + Endpoint::RxBLEModel, + 128, + 500, + )) + .await?; + let ident = + String::from_utf8(result.data().to_vec()).unwrap_or_else(|_| hardware.name().to_owned()); + Ok(( + UserDeviceIdentifier::new(hardware.address(), "vibratissimo", &Some(ident)), + Box::new(VibratissimoInitializer::default()), + )) + } +} + +#[derive(Default)] +pub struct VibratissimoInitializer {} + +#[async_trait] +impl ProtocolInitializer for VibratissimoInitializer { + async fn initialize( + &mut self, + _: Arc, + def: &DeviceDefinition, + ) -> Result, ButtplugDeviceError> { + let num_vibrators: u8 = def + .features() + .iter() + .filter(|x| { + x.output() + .as_ref() + .map_or(false, |x| x.contains_key(&OutputType::Vibrate)) + }) + .count() as u8; + Ok(Arc::new(Vibratissimo::new(num_vibrators as u8))) + } +} + +pub struct Vibratissimo { + speeds: Vec, +} + +impl Vibratissimo { + fn new(num_vibrators: u8) -> Self { + let speeds: Vec = std::iter::repeat_with(|| AtomicU8::default()) + .take(num_vibrators as usize) + .collect(); + Self { speeds } + } +} + +impl ProtocolHandler for Vibratissimo { + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.speeds[feature_index as usize].store(speed as u8, Ordering::Relaxed); + let mut data = vec![]; + for cmd in &self.speeds { + data.push(cmd.load(std::sync::atomic::Ordering::Relaxed)); + } + if data.len() == 1 { + data.push(0x00); + } + + // Put the device in write mode + Ok(vec![ + HardwareWriteCmd::new(&[feature_id], Endpoint::TxMode, vec![0x03, 0xff], false).into(), + HardwareWriteCmd::new(&[feature_id], Endpoint::TxVibrate, data, false).into(), + ]) + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/vorze_sa/dual_rotator.rs b/crates/buttplug_server/src/device/protocol_impl/vorze_sa/dual_rotator.rs new file mode 100644 index 000000000..cbe1fe380 --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/vorze_sa/dual_rotator.rs @@ -0,0 +1,56 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 uuid::{uuid, Uuid}; + +use super::VorzeDevice; + +use crate::device::{ + protocol::ProtocolHandler, + hardware::{HardwareCommand, HardwareWriteCmd}, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use std::sync::atomic::{AtomicI8, Ordering}; + +// Vorze UFO needs a unified protocol UUID since we update both outputs in the same packet. +const VORZE_UFO_PROTOCOL_UUID: Uuid = uuid!("013c2d1f-b3c0-4372-9cf6-e5fafd3b7631"); + +#[derive(Default)] +pub struct VorzeSADualRotator { + speeds: [AtomicI8; 2], +} + +impl ProtocolHandler for VorzeSADualRotator { + fn handle_rotation_with_direction_cmd( + &self, + feature_index: u32, + _feature_id: uuid::Uuid, + speed: u32, + clockwise: bool, + ) -> Result, ButtplugDeviceError> { + self.speeds[feature_index as usize].store( + if clockwise { + speed as i8 + } else { + -(speed as i8) + }, + Ordering::Relaxed, + ); + let speed_left = self.speeds[0].load(Ordering::Relaxed); + let data_left = ((speed_left >= 0) as u8) << 7 | (speed_left.unsigned_abs()); + let speed_right = self.speeds[1].load(Ordering::Relaxed); + let data_right = ((speed_right >= 0) as u8) << 7 | (speed_right.unsigned_abs()); + Ok(vec![HardwareWriteCmd::new( + &[VORZE_UFO_PROTOCOL_UUID], + Endpoint::Tx, + vec![VorzeDevice::UfoTw as u8, data_left, data_right], + true, + ) + .into()]) + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/vorze_sa/mod.rs b/crates/buttplug_server/src/device/protocol_impl/vorze_sa/mod.rs new file mode 100644 index 000000000..8bc7efeff --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/vorze_sa/mod.rs @@ -0,0 +1,107 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 Nonpolynomial Labs LLC. All rights reserved. +// +// Licensed under the BSD 3-Clause license. See LICENSE file in the project root +// for full license information. + +mod dual_rotator; +mod piston; +mod single_rotator; +mod vibrator; + +use crate::device::{ + hardware::Hardware, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, + }, +}; +use async_trait::async_trait; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::{ + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; +use std::sync::Arc; + +generic_protocol_initializer_setup!(VorzeSA, "vorze-sa"); + +#[derive(Default)] +pub struct VorzeSAInitializer {} + +#[async_trait] +impl ProtocolInitializer for VorzeSAInitializer { + async fn initialize( + &mut self, + hardware: Arc, + def: &DeviceDefinition, + ) -> Result, ButtplugDeviceError> { + if let Some(variant) = def.protocol_variant() { + let hwname = hardware.name().to_ascii_lowercase(); + match variant.as_str() { + "vorze-sa-single-rotator" => { + if hwname.contains("cycsa") { + Ok(Arc::new(single_rotator::VorzeSASingleRotator::new( + VorzeDevice::Cyclone, + ))) + } else if hwname.contains("ufo") { + Ok(Arc::new(single_rotator::VorzeSASingleRotator::new( + VorzeDevice::Ufo, + ))) + } else { + Err(ButtplugDeviceError::ProtocolNotImplemented(format!( + "No protocol implementation for Vorze Device {}", + hardware.name() + ))) + } + } + "vorze-sa-dual-rotator" => Ok(Arc::new(dual_rotator::VorzeSADualRotator::default())), + "vorze-sa-vibrator" => { + if hwname.contains("bach") { + Ok(Arc::new(vibrator::VorzeSAVibrator::new(VorzeDevice::Bach))) + } else if hwname.contains("rocket") { + Ok(Arc::new(vibrator::VorzeSAVibrator::new( + VorzeDevice::Rocket, + ))) + } else { + Err(ButtplugDeviceError::ProtocolNotImplemented(format!( + "No protocol implementation for Vorze Device {}", + hardware.name() + ))) + } + } + "vorze-sa-piston" => Ok(Arc::new(piston::VorzeSAPiston::default())), + _ => Err(ButtplugDeviceError::ProtocolNotImplemented(format!( + "No protocol implementation for Vorze Device {}", + hardware.name() + ))), + } + } else { + Err(ButtplugDeviceError::ProtocolNotImplemented(format!( + "No protocol implementation for Vorze Device {}", + hardware.name() + ))) + } + } +} + +#[repr(u8)] +#[derive(PartialEq, Eq, Clone, Copy)] +pub enum VorzeDevice { + Bach = 6, + Piston = 3, + Cyclone = 1, + Rocket = 7, + Ufo = 2, + UfoTw = 5, +} + +#[repr(u8)] +enum VorzeActions { + Rotate = 1, + Vibrate = 3, +} diff --git a/crates/buttplug_server/src/device/protocol_impl/vorze_sa/piston.rs b/crates/buttplug_server/src/device/protocol_impl/vorze_sa/piston.rs new file mode 100644 index 000000000..104ff6084 --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/vorze_sa/piston.rs @@ -0,0 +1,70 @@ +use super::VorzeDevice; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::ProtocolHandler, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use std::sync::{ + atomic::{AtomicU8, Ordering}, + Arc, +}; + +#[derive(Default)] +pub struct VorzeSAPiston { + previous_position: Arc, +} + +pub fn get_piston_speed(mut distance: f64, mut duration: f64) -> u8 { + if distance <= 0f64 { + return 100; + } + + if distance > 200f64 { + distance = 200f64; + } + + // Convert duration to max length + duration = 200f64 * duration / distance; + + let mut speed = (duration / 6658f64).powf(-1.21); + + if speed > 100f64 { + speed = 100f64; + } + + if speed < 0f64 { + speed = 0f64; + } + + speed as u8 +} + +impl ProtocolHandler for VorzeSAPiston { + fn handle_position_with_duration_cmd( + &self, + _feature_index: u32, + feature_id: uuid::Uuid, + position: u32, + duration: u32, + ) -> Result, ButtplugDeviceError> { + let previous_position = self.previous_position.load(Ordering::Relaxed); + let position = position as u8; + let distance = (previous_position as f64 - position as f64).abs(); + + let speed = get_piston_speed(distance, duration as f64); + + self + .previous_position + .store(position as u8, Ordering::Relaxed); + + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + vec![VorzeDevice::Piston as u8, position as u8, speed], + true, + ) + .into()]) + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/vorze_sa/single_rotator.rs b/crates/buttplug_server/src/device/protocol_impl/vorze_sa/single_rotator.rs new file mode 100644 index 000000000..84eb4a11b --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/vorze_sa/single_rotator.rs @@ -0,0 +1,45 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 super::{VorzeActions, VorzeDevice}; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::ProtocolHandler +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; + +pub struct VorzeSASingleRotator { + device_type: VorzeDevice, +} + +impl VorzeSASingleRotator { + pub fn new(device_type: VorzeDevice) -> Self { + Self { device_type } + } +} + +impl ProtocolHandler for VorzeSASingleRotator { + fn handle_rotation_with_direction_cmd( + &self, + _feature_index: u32, + feature_id: uuid::Uuid, + speed: u32, + clockwise: bool, + ) -> Result, ButtplugDeviceError> { + let clockwise = if clockwise { 1u8 } else { 0 }; + let data: u8 = (clockwise) << 7 | (speed as u8); + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + vec![self.device_type as u8, VorzeActions::Rotate as u8, data], + true, + ) + .into()]) + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/vorze_sa/vibrator.rs b/crates/buttplug_server/src/device/protocol_impl/vorze_sa/vibrator.rs new file mode 100644 index 000000000..cb06a3655 --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/vorze_sa/vibrator.rs @@ -0,0 +1,48 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 super::{VorzeActions, VorzeDevice}; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::ProtocolHandler, +}; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; + +pub struct VorzeSAVibrator { + device_type: VorzeDevice, +} + +impl VorzeSAVibrator { + pub fn new(device_type: VorzeDevice) -> Self { + Self { device_type } + } +} + +impl ProtocolHandler for VorzeSAVibrator { + fn handle_output_vibrate_cmd( + &self, + _feature_index: u32, + feature_id: uuid::Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + Ok(vec![{ + HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + vec![ + self.device_type as u8, + VorzeActions::Vibrate as u8, + speed as u8, + ], + true, + ) + .into() + }]) + } +} diff --git a/buttplug/src/server/device/protocol/wetoy.rs b/crates/buttplug_server/src/device/protocol_impl/wetoy.rs similarity index 53% rename from buttplug/src/server/device/protocol/wetoy.rs rename to crates/buttplug_server/src/device/protocol_impl/wetoy.rs index 57e9e47d9..4482ab551 100644 --- a/buttplug/src/server/device/protocol/wetoy.rs +++ b/crates/buttplug_server/src/device/protocol_impl/wetoy.rs @@ -5,23 +5,27 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::server::device::configuration::ProtocolCommunicationSpecifier; -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - configuration::{UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, }, }; use async_trait::async_trait; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use buttplug_server_device_config::{ + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; use std::sync::Arc; +use uuid::{uuid, Uuid}; +const WETOY_PROTOCOL_ID: Uuid = uuid!("9868762e-4203-4876-abf5-83c992e024b4"); generic_protocol_initializer_setup!(WeToy, "wetoy"); #[derive(Default)] @@ -32,10 +36,15 @@ impl ProtocolInitializer for WeToyInitializer { async fn initialize( &mut self, hardware: Arc, - _: &UserDeviceDefinition, + _: &DeviceDefinition, ) -> Result, ButtplugDeviceError> { hardware - .write_value(&HardwareWriteCmd::new(Endpoint::Tx, vec![0x80, 0x03], true)) + .write_value(&HardwareWriteCmd::new( + &[WETOY_PROTOCOL_ID], + Endpoint::Tx, + vec![0x80, 0x03], + true, + )) .await?; Ok(Arc::new(WeToy::default())) } @@ -45,21 +54,19 @@ impl ProtocolInitializer for WeToyInitializer { pub struct WeToy {} impl ProtocolHandler for WeToy { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, - if scalar == 0 { + if speed == 0 { vec![0x80, 0x03] } else { - vec![0xb2, scalar as u8 - 1] + vec![0xb2, speed as u8 - 1] }, true, ) diff --git a/crates/buttplug_server/src/device/protocol_impl/wevibe.rs b/crates/buttplug_server/src/device/protocol_impl/wevibe.rs new file mode 100644 index 000000000..c27b156ad --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/wevibe.rs @@ -0,0 +1,120 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, + }, +}; +use async_trait::async_trait; +use buttplug_core::message::OutputType; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use buttplug_server_device_config::{ + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; +use std::sync::atomic::{AtomicU8, Ordering}; +use std::sync::Arc; +use uuid::{uuid, Uuid}; + +const WEVIBE_PROTOCOL_UUID: Uuid = uuid!("3658e33d-086d-401e-9dce-8e9e88ff791f"); +generic_protocol_initializer_setup!(WeVibe, "wevibe"); + +#[derive(Default)] +pub struct WeVibeInitializer {} + +#[async_trait] +impl ProtocolInitializer for WeVibeInitializer { + async fn initialize( + &mut self, + hardware: Arc, + def: &DeviceDefinition, + ) -> Result, ButtplugDeviceError> { + debug!("calling WeVibe init"); + hardware + .write_value(&HardwareWriteCmd::new( + &[WEVIBE_PROTOCOL_UUID], + Endpoint::Tx, + vec![0x0f, 0x03, 0x00, 0x99, 0x00, 0x03, 0x00, 0x00], + true, + )) + .await?; + hardware + .write_value(&HardwareWriteCmd::new( + &[WEVIBE_PROTOCOL_UUID], + Endpoint::Tx, + vec![0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + true, + )) + .await?; + let num_vibrators = def + .features() + .iter() + .filter(|x| { + x.output() + .as_ref() + .map_or(false, |x| x.contains_key(&OutputType::Vibrate)) + }) + .count() as u8; + Ok(Arc::new(WeVibe::new(num_vibrators))) + } +} + +pub struct WeVibe { + num_vibrators: u8, + speeds: [AtomicU8; 2], +} + +impl WeVibe { + fn new(num_vibrators: u8) -> Self { + Self { + num_vibrators, + speeds: [AtomicU8::default(), AtomicU8::default()], + } + } +} + +impl ProtocolHandler for WeVibe { + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + _feature_id: uuid::Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.speeds[feature_index as usize].store(speed as u8, Ordering::Relaxed); + let max_vibrators = if self.num_vibrators > 1 { 1 } else { 0 }; + let r_speed_int = self.speeds[0].load(Ordering::Relaxed); + let r_speed_ext = self.speeds[max_vibrators].load(Ordering::Relaxed); + let data = if r_speed_int == 0 && r_speed_ext == 0 { + vec![0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + } else { + vec![ + 0x0f, + 0x03, + 0x00, + r_speed_ext | (r_speed_int << 4), + 0x00, + 0x03, + 0x00, + 0x00, + ] + }; + Ok(vec![HardwareWriteCmd::new( + &[WEVIBE_PROTOCOL_UUID], + Endpoint::Tx, + data, + true, + ) + .into()]) + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/wevibe8bit.rs b/crates/buttplug_server/src/device/protocol_impl/wevibe8bit.rs new file mode 100644 index 000000000..c545dc900 --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/wevibe8bit.rs @@ -0,0 +1,107 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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::sync::{ + atomic::{AtomicU8, Ordering}, + Arc, +}; + +use async_trait::async_trait; +use uuid::{uuid, Uuid}; + +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, + }, +}; +use buttplug_core::{ + errors::ButtplugDeviceError, + message::OutputType, +}; +use buttplug_server_device_config::{Endpoint, DeviceDefinition, UserDeviceIdentifier, ProtocolCommunicationSpecifier}; + +generic_protocol_initializer_setup!(WeVibe8Bit, "wevibe-8bit"); + +const WEVIBE8BIT_PROTOCOL_UUID: Uuid = uuid!("f5e48973-09e9-4063-8177-487f6292e2ed"); + +#[derive(Default)] +pub struct WeVibe8BitInitializer {} + +#[async_trait] +impl ProtocolInitializer for WeVibe8BitInitializer { + async fn initialize( + &mut self, + _hardware: Arc, + def: &DeviceDefinition, + ) -> Result, ButtplugDeviceError> { + let num_vibrators = def + .features() + .iter() + .filter(|x| { + x.output() + .as_ref() + .map_or(false, |x| x.contains_key(&OutputType::Vibrate)) + }) + .count() as u8; + Ok(Arc::new(WeVibe8Bit::new(num_vibrators))) + } +} + +pub struct WeVibe8Bit { + num_vibrators: u8, + speeds: [AtomicU8; 2], +} + +impl WeVibe8Bit { + fn new(num_vibrators: u8) -> Self { + Self { + num_vibrators, + speeds: [AtomicU8::default(), AtomicU8::default()], + } + } +} + +impl ProtocolHandler for WeVibe8Bit { + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + _feature_id: uuid::Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.speeds[feature_index as usize].store(speed as u8, Ordering::Relaxed); + let max_vibrators = if self.num_vibrators > 1 { 1 } else { 0 }; + let r_speed_int = self.speeds[0].load(Ordering::Relaxed); + let r_speed_ext = self.speeds[max_vibrators].load(Ordering::Relaxed); + let data = if r_speed_int == 0 && r_speed_ext == 0 { + vec![0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + } else { + let status_byte: u8 = + (if r_speed_ext == 0 { 0 } else { 2 }) | (if r_speed_int == 0 { 0 } else { 1 }); + vec![ + 0x0f, + 0x03, + 0x00, + r_speed_ext + 3, + r_speed_int + 3, + status_byte, + 0x00, + 0x00, + ] + }; + Ok(vec![HardwareWriteCmd::new( + &[WEVIBE8BIT_PROTOCOL_UUID], + Endpoint::Tx, + data, + true, + ) + .into()]) + } +} diff --git a/crates/buttplug_server/src/device/protocol_impl/wevibe_chorus.rs b/crates/buttplug_server/src/device/protocol_impl/wevibe_chorus.rs new file mode 100644 index 000000000..e165531ea --- /dev/null +++ b/crates/buttplug_server/src/device/protocol_impl/wevibe_chorus.rs @@ -0,0 +1,108 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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::sync::{ + atomic::{AtomicU8, Ordering}, + Arc, +}; + +use async_trait::async_trait; +use uuid::{uuid, Uuid}; + +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, + }, +}; +use buttplug_core::{ + errors::ButtplugDeviceError, + message::OutputType, +}; +use buttplug_server_device_config::{DeviceDefinition, Endpoint, ProtocolCommunicationSpecifier, UserDeviceIdentifier}; + +generic_protocol_initializer_setup!(WeVibeChorus, "wevibe-chorus"); + +const WEVIBE_CHORUS_PROTOCOL_UUID: Uuid = uuid!("cdeadd1c-b913-4305-a255-bd8834c4e37f"); + +#[derive(Default)] +pub struct WeVibeChorusInitializer {} + +#[async_trait] +impl ProtocolInitializer for WeVibeChorusInitializer { + async fn initialize( + &mut self, + _hardware: Arc, + def: &DeviceDefinition, + ) -> Result, ButtplugDeviceError> { + let num_vibrators = def + .features() + .iter() + .filter(|x| { + x.output() + .as_ref() + .map_or(false, |x| x.contains_key(&OutputType::Vibrate)) + }) + .count() as u8; + Ok(Arc::new(WeVibeChorus::new(num_vibrators))) + } +} + +pub struct WeVibeChorus { + num_vibrators: u8, + speeds: [AtomicU8; 2], +} + +impl WeVibeChorus { + fn new(num_vibrators: u8) -> Self { + Self { + num_vibrators, + speeds: [AtomicU8::default(), AtomicU8::default()], + } + } +} + +impl ProtocolHandler for WeVibeChorus { + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + _feature_id: uuid::Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.speeds[feature_index as usize].store(speed as u8, Ordering::Relaxed); + let max_vibrators = if self.num_vibrators > 1 { 1 } else { 0 }; + let r_speed_int = self.speeds[0].load(Ordering::Relaxed); + let r_speed_ext = self.speeds[max_vibrators].load(Ordering::Relaxed); + let data = if r_speed_int == 0 && r_speed_ext == 0 { + vec![0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + } else { + // Note the motor order is flipped for the Chorus + let status_byte: u8 = + (if r_speed_ext == 0 { 0 } else { 2 }) | (if r_speed_int == 0 { 0 } else { 1 }); + vec![ + 0x0f, + 0x03, + 0x00, + r_speed_int, + r_speed_ext, + status_byte, + 0x00, + 0x00, + ] + }; + Ok(vec![HardwareWriteCmd::new( + &[WEVIBE_CHORUS_PROTOCOL_UUID], + Endpoint::Tx, + data, + true, + ) + .into()]) + } +} diff --git a/buttplug/src/server/device/protocol/xibao.rs b/crates/buttplug_server/src/device/protocol_impl/xibao.rs similarity index 60% rename from buttplug/src/server/device/protocol/xibao.rs rename to crates/buttplug_server/src/device/protocol_impl/xibao.rs index a3394cddd..bf0f5283f 100644 --- a/buttplug/src/server/device/protocol/xibao.rs +++ b/crates/buttplug_server/src/device/protocol_impl/xibao.rs @@ -5,13 +5,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; use std::num::Wrapping; generic_protocol_setup!(Xibao, "xibao"); @@ -20,16 +21,14 @@ generic_protocol_setup!(Xibao, "xibao"); pub struct Xibao {} impl ProtocolHandler for Xibao { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_oscillate_cmd( + fn handle_output_oscillate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, vec![ 0x66, @@ -43,8 +42,8 @@ impl ProtocolHandler for Xibao { 0x00, 0x02, 0x04, - scalar as u8, - (Wrapping(scalar as u8) + Wrapping(0xb5)).0, + speed as u8, + (Wrapping(speed as u8) + Wrapping(0xb5)).0, ], false, ) diff --git a/buttplug/src/server/device/protocol/xinput.rs b/crates/buttplug_server/src/device/protocol_impl/xinput.rs similarity index 52% rename from buttplug/src/server/device/protocol/xinput.rs rename to crates/buttplug_server/src/device/protocol_impl/xinput.rs index 32244fd5d..f612cf804 100644 --- a/buttplug/src/server/device/protocol/xinput.rs +++ b/crates/buttplug_server/src/device/protocol_impl/xinput.rs @@ -5,54 +5,49 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. +use buttplug_server_device_config::Endpoint; use byteorder::LittleEndian; -use crate::{ - core::{ - errors::ButtplugDeviceError, - message::{self, ActuatorType, ButtplugDeviceMessage, Endpoint, SensorReadingV4}, - }, - server::device::{ - hardware::{Hardware, HardwareCommand, HardwareReadCmd, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareReadCmd, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, +}; +use buttplug_core::{ + errors::ButtplugDeviceError, + message::{self, InputData, InputReadingV4, InputTypeData}, }; use byteorder::WriteBytesExt; use futures::future::{BoxFuture, FutureExt}; -use std::sync::Arc; +use std::sync::{ + atomic::{AtomicU16, Ordering}, + Arc, +}; generic_protocol_setup!(XInput, "xinput"); #[derive(Default)] -pub struct XInput {} +pub struct XInput { + speeds: [AtomicU16; 2], +} impl ProtocolHandler for XInput { - fn needs_full_command_set(&self) -> bool { - true - } - - fn handle_scalar_cmd( + fn handle_output_vibrate_cmd( &self, - cmds: &[Option<(ActuatorType, u32)>], + feature_index: u32, + feature_id: uuid::Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { + self.speeds[feature_index as usize].store(speed as u16, Ordering::Relaxed); // XInput is fast enough that we can ignore the commands handed // back by the manager and just form our own packet. This means // we'll just use the manager's return for command validity // checking. let mut cmd = vec![]; if cmd - .write_u16::( - cmds[1] - .expect("GCM uses match_all, we'll always get 2 values") - .1 as u16, - ) + .write_u16::(self.speeds[1].load(Ordering::Relaxed)) .is_err() || cmd - .write_u16::( - cmds[0] - .expect("GCM uses match_all, we'll always get 2 values") - .1 as u16, - ) + .write_u16::(self.speeds[0].load(Ordering::Relaxed)) .is_err() { return Err(ButtplugDeviceError::ProtocolSpecificError( @@ -60,20 +55,29 @@ impl ProtocolHandler for XInput { "Cannot convert XInput value for processing".to_owned(), )); } - Ok(vec![HardwareWriteCmd::new(Endpoint::Tx, cmd, false).into()]) + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + cmd, + false, + ) + .into()]) } - fn handle_battery_level_cmd( + fn handle_input_read_cmd( &self, + device_index: u32, device: Arc, - msg: message::SensorReadCmdV4, - ) -> BoxFuture> { + feature_index: u32, + feature_id: uuid::Uuid, + _sensor_type: message::InputType, + ) -> BoxFuture> { async move { let reading = device - .read_value(&HardwareReadCmd::new(Endpoint::Rx, 0, 0)) + .read_value(&HardwareReadCmd::new(feature_id, Endpoint::Rx, 0, 0)) .await?; let battery = match reading.data()[0] { - 0 => 0i32, + 0 => 0u8, 1 => 33, 2 => 66, 3 => 100, @@ -83,11 +87,10 @@ impl ProtocolHandler for XInput { )) } }; - Ok(message::SensorReadingV4::new( - msg.device_index(), - *msg.feature_index(), - *msg.sensor_type(), - vec![battery], + Ok(message::InputReadingV4::new( + device_index, + feature_index, + InputTypeData::Battery(InputData::new(battery)), )) } .boxed() diff --git a/buttplug/src/server/device/protocol/xiuxiuda.rs b/crates/buttplug_server/src/device/protocol_impl/xiuxiuda.rs similarity index 53% rename from buttplug/src/server/device/protocol/xiuxiuda.rs rename to crates/buttplug_server/src/device/protocol_impl/xiuxiuda.rs index 89d7e6f23..cb1a78728 100644 --- a/buttplug/src/server/device/protocol/xiuxiuda.rs +++ b/crates/buttplug_server/src/device/protocol_impl/xiuxiuda.rs @@ -5,13 +5,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(Xiuxiuda, "xiuxiuda"); @@ -19,18 +20,16 @@ generic_protocol_setup!(Xiuxiuda, "xiuxiuda"); pub struct Xiuxiuda {} impl ProtocolHandler for Xiuxiuda { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, - [0x00, 0x00, 0x00, 0x00, 0x65, 0x3a, 0x30, scalar as u8, 0x64].to_vec(), + [0x00, 0x00, 0x00, 0x00, 0x65, 0x3a, 0x30, speed as u8, 0x64].to_vec(), false, ) .into()]) diff --git a/buttplug/src/server/device/protocol/xuanhuan.rs b/crates/buttplug_server/src/device/protocol_impl/xuanhuan.rs similarity index 51% rename from buttplug/src/server/device/protocol/xuanhuan.rs rename to crates/buttplug_server/src/device/protocol_impl/xuanhuan.rs index 688c35527..53a181f03 100644 --- a/buttplug/src/server/device/protocol/xuanhuan.rs +++ b/crates/buttplug_server/src/device/protocol_impl/xuanhuan.rs @@ -5,24 +5,36 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ - generic_protocol_initializer_setup, - ProtocolHandler, - ProtocolIdentifier, - ProtocolInitializer, - }, +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, + protocol::{ + generic_protocol_initializer_setup, + ProtocolHandler, + ProtocolIdentifier, + ProtocolInitializer, }, - util::{async_manager, sleep}, }; use async_trait::async_trait; -use std::{sync::Arc, time::Duration}; -use tokio::sync::RwLock; +use buttplug_core::{ + errors::ButtplugDeviceError, + util::{async_manager, sleep}, +}; +use buttplug_server_device_config::{ + Endpoint, + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; +use std::{ + sync::{ + atomic::{AtomicU8, Ordering}, + Arc, + }, + time::Duration, +}; +use uuid::{uuid, Uuid}; +const XUANHUAN_PROTOCOL_ID: Uuid = uuid!("e9f9f8ab-4fd5-4573-a4ec-ab542568849b"); generic_protocol_initializer_setup!(Xuanhuan, "xuanhuan"); #[derive(Default)] @@ -33,34 +45,43 @@ impl ProtocolInitializer for XuanhuanInitializer { async fn initialize( &mut self, hardware: Arc, - _: &UserDeviceDefinition, + _: &DeviceDefinition, ) -> Result, ButtplugDeviceError> { Ok(Arc::new(Xuanhuan::new(hardware))) } } -async fn vibration_update_handler(device: Arc, command_holder: Arc>>) { +async fn vibration_update_handler(device: Arc, command_holder: Arc) { info!("Entering Xuanhuan Control Loop"); - let mut current_command = command_holder.read().await.clone(); - while current_command == vec![0x03, 0x02, 0x00, 0x00] - || device - .write_value(&HardwareWriteCmd::new(Endpoint::Tx, current_command, true)) - .await - .is_ok() - { + loop { + let speed = command_holder.load(Ordering::Relaxed); + if speed != 0 { + let current_command = vec![0x03, 0x02, 0x00, speed]; + if device + .write_value(&HardwareWriteCmd::new( + &[XUANHUAN_PROTOCOL_ID], + Endpoint::Tx, + current_command, + true, + )) + .await + .is_err() + { + break; + } + } sleep(Duration::from_millis(300)).await; - current_command = command_holder.read().await.clone(); } info!("Xuanhuan control loop exiting, most likely due to device disconnection."); } pub struct Xuanhuan { - current_command: Arc>>, + current_command: Arc, } impl Xuanhuan { fn new(device: Arc) -> Self { - let current_command = Arc::new(RwLock::new(vec![0x03, 0x02, 0x00, 0x00])); + let current_command = Arc::new(AtomicU8::new(0)); let current_command_clone = current_command.clone(); async_manager::spawn( async move { vibration_update_handler(device, current_command_clone).await }, @@ -70,20 +91,19 @@ impl Xuanhuan { } impl ProtocolHandler for Xuanhuan { - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + _feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { - let current_command = self.current_command.clone(); - async_manager::spawn(async move { - let write_mutex = current_command.clone(); - let mut command_writer = write_mutex.write().await; - *command_writer = vec![0x03, 0x02, 0x00, scalar as u8]; - }); + let speed = speed as u8; + self.current_command.store(speed, Ordering::Relaxed); + Ok(vec![HardwareWriteCmd::new( + &[XUANHUAN_PROTOCOL_ID], Endpoint::Tx, - vec![0x03, 0x02, 0x00, scalar as u8], + vec![0x03, 0x02, 0x00, speed], true, ) .into()]) diff --git a/buttplug/src/server/device/protocol/youcups.rs b/crates/buttplug_server/src/device/protocol_impl/youcups.rs similarity index 54% rename from buttplug/src/server/device/protocol/youcups.rs rename to crates/buttplug_server/src/device/protocol_impl/youcups.rs index ed94de373..cebe9a267 100644 --- a/buttplug/src/server/device/protocol/youcups.rs +++ b/crates/buttplug_server/src/device/protocol_impl/youcups.rs @@ -5,13 +5,14 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use uuid::Uuid; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(Youcups, "youcups"); @@ -19,18 +20,16 @@ generic_protocol_setup!(Youcups, "youcups"); pub struct Youcups {} impl ProtocolHandler for Youcups { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, - format!("$SYS,{}?", scalar as u8).as_bytes().to_vec(), + format!("$SYS,{}?", speed as u8).as_bytes().to_vec(), false, ) .into()]) diff --git a/buttplug/src/server/device/protocol/youou.rs b/crates/buttplug_server/src/device/protocol_impl/youou.rs similarity index 75% rename from buttplug/src/server/device/protocol/youou.rs rename to crates/buttplug_server/src/device/protocol_impl/youou.rs index 21a1d0518..950fdfc8d 100644 --- a/buttplug/src/server/device/protocol/youou.rs +++ b/crates/buttplug_server/src/device/protocol_impl/youou.rs @@ -5,23 +5,27 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::server::device::configuration::ProtocolCommunicationSpecifier; -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - configuration::{UserDeviceDefinition, UserDeviceIdentifier}, - hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, - protocol::{ProtocolHandler, ProtocolIdentifier, ProtocolInitializer}, - }, +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; +use buttplug_server_device_config::{ + DeviceDefinition, + ProtocolCommunicationSpecifier, + UserDeviceIdentifier, +}; + +use crate::device::{ + hardware::{Hardware, HardwareCommand, HardwareWriteCmd}, + protocol::{ProtocolHandler, ProtocolIdentifier, ProtocolInitializer}, }; use async_trait::async_trait; use std::sync::{ atomic::{AtomicU8, Ordering}, Arc, }; +use uuid::Uuid; pub mod setup { - use crate::server::device::protocol::{ProtocolIdentifier, ProtocolIdentifierFactory}; + use crate::device::protocol::{ProtocolIdentifier, ProtocolIdentifierFactory}; #[derive(Default)] pub struct YououIdentifierFactory {} @@ -61,7 +65,7 @@ impl ProtocolInitializer for YououInitializer { async fn initialize( &mut self, _: Arc, - _: &UserDeviceDefinition, + _: &DeviceDefinition, ) -> Result, ButtplugDeviceError> { Ok(Arc::new(Youou::default())) } @@ -73,17 +77,18 @@ pub struct Youou { } impl ProtocolHandler for Youou { - fn handle_scalar_vibrate_cmd( + fn handle_output_vibrate_cmd( &self, - _index: u32, - scalar: u32, + _feature_index: u32, + feature_id: Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { // Byte 2 seems to be a monotonically increasing packet id of some kind // // Speed seems to be 0-247 or so. // // Anything above that sets a pattern which isn't what we want here. - let state = u8::from(scalar > 0); + let state = u8::from(speed > 0); // Scope the packet id set so we can unlock ASAP. let mut data = vec![ @@ -93,7 +98,7 @@ impl ProtocolHandler for Youou { 0x02, 0x03, 0x01, - scalar as u8, + speed as u8, state, ]; self.packet_id.store( @@ -110,6 +115,12 @@ impl ProtocolHandler for Youou { let mut data2 = vec![crc, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; data.append(&mut data2); - Ok(vec![HardwareWriteCmd::new(Endpoint::Tx, data, false).into()]) + Ok(vec![HardwareWriteCmd::new( + &[feature_id], + Endpoint::Tx, + data, + false, + ) + .into()]) } } diff --git a/buttplug/src/server/device/protocol/zalo.rs b/crates/buttplug_server/src/device/protocol_impl/zalo.rs similarity index 50% rename from buttplug/src/server/device/protocol/zalo.rs rename to crates/buttplug_server/src/device/protocol_impl/zalo.rs index fa503de7d..46263ea65 100644 --- a/buttplug/src/server/device/protocol/zalo.rs +++ b/crates/buttplug_server/src/device/protocol_impl/zalo.rs @@ -5,39 +5,34 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{ - errors::ButtplugDeviceError, - message::{ActuatorType, Endpoint}, - }, - server::device::{ - hardware::{HardwareCommand, HardwareWriteCmd}, - protocol::{generic_protocol_setup, ProtocolHandler}, - }, +use std::sync::atomic::{AtomicU8, Ordering}; + +use crate::device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + protocol::{generic_protocol_setup, ProtocolHandler}, }; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server_device_config::Endpoint; generic_protocol_setup!(Zalo, "zalo"); #[derive(Default)] -pub struct Zalo {} +pub struct Zalo { + speeds: [AtomicU8; 2], +} impl ProtocolHandler for Zalo { - fn keepalive_strategy(&self) -> super::ProtocolKeepaliveStrategy { - super::ProtocolKeepaliveStrategy::RepeatLastPacketStrategy - } - - fn handle_scalar_cmd( + fn handle_output_vibrate_cmd( &self, - cmds: &[Option<(ActuatorType, u32)>], + feature_index: u32, + feature_id: uuid::Uuid, + speed: u32, ) -> Result, ButtplugDeviceError> { - // Store off result before the match, so we drop the lock ASAP. - let speed0: u8 = cmds[0].unwrap_or((ActuatorType::Vibrate, 0)).1 as u8; - let speed1: u8 = if cmds.len() == 1 { - 0 - } else { - cmds[1].unwrap_or((ActuatorType::Vibrate, 0)).1 as u8 - }; + self.speeds[feature_index as usize].store(speed as u8, Ordering::Relaxed); + let speed0: u8 = self.speeds[0].load(Ordering::Relaxed); + let speed1: u8 = self.speeds[1].load(Ordering::Relaxed); Ok(vec![HardwareWriteCmd::new( + &[feature_id], Endpoint::Tx, vec![ if speed0 == 0 && speed1 == 0 { diff --git a/crates/buttplug_server/src/device/server_device.rs b/crates/buttplug_server/src/device/server_device.rs new file mode 100644 index 000000000..3de2480e4 --- /dev/null +++ b/crates/buttplug_server/src/device/server_device.rs @@ -0,0 +1,678 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 Nonpolynomial Labs LLC. All rights reserved. +// +// Licensed under the BSD 3-Clause license. See LICENSE file in the project root +// for full license information. + +//! Server Device Implementation +//! +//! This struct manages the trip from buttplug protocol actuator/sensor message to hardware +//! communication. This involves: +//! +//! - Taking buttplug device command messages from the exposed server +//! - Converting older spec version messages to the newest spec version, which usually requires +//! device information for actuation/sensor messages. +//! - Validity checking the messages to make sure they match the capabilities of the hardware +//! - Turning the buttplug messages into hardware commands via the associated protocol +//! - Sending them to the hardware +//! - Possibly receiving back information (in the case of sensors), possibly firing and forgetting +//! (in terms of almost everything else) +//! +//! We make a lot of assumptions in here based on the devices we support right now, including: +//! +//! - Devices will only ever have one directional rotation actuator (we have no device that supports +//! two rotational components currently) +//! - Devices will only ever have one linear actuator (we have no device that supports multiple +//! linear actuators currently) +//! - Devices scalar command ordering is explicitly set by the device config file +//! - This means that we rely on the config file to know which vibrator is which on a device with +//! multiple vibrators. In protocols, especially for toy brands that release a large line of +//! different toys all using the same protocols (lovense, wevibe, etc), the order of features in +//! the config file MATTERS and needs to be tested against an actual device to make sure we're +//! controlling the actuator we think we are. +//! - This situation sucks and we should have better definitions, a problem outlined at +//! https://github.com/buttplugio/buttplug/issues/646 +//! +//! In order to handle multiple message spec versions + +use std::{ + collections::VecDeque, + fmt::{self, Debug}, + sync::Arc, + time::Duration, +}; + +use buttplug_core::{ + ButtplugResultFuture, + errors::{ButtplugDeviceError, ButtplugError}, + message::{ + self, ButtplugServerMessageV4, DeviceFeature, DeviceMessageInfoV4, InputCommandType, InputType, + OutputRotateWithDirection, OutputType, OutputValue, + }, + util::{self, async_manager, stream::convert_broadcast_receiver_to_stream}, +}; +use buttplug_server_device_config::{ + DeviceConfigurationManager, DeviceDefinition, UserDeviceIdentifier, +}; + +use crate::{ + ButtplugServerResultFuture, + device::{ + hardware::{Hardware, HardwareCommand, HardwareConnector, HardwareEvent}, + protocol::{ProtocolHandler, ProtocolKeepaliveStrategy, ProtocolSpecializer}, + }, + message::{ + ButtplugServerDeviceMessage, checked_input_cmd::CheckedInputCmdV4, + checked_output_cmd::CheckedOutputCmdV4, server_device_attributes::ServerDeviceAttributes, + spec_enums::ButtplugDeviceCommandMessageUnionV4, + }, +}; +use core::hash::{Hash, Hasher}; +use dashmap::DashMap; +use futures::future::{self, BoxFuture, FutureExt}; +use getset::Getters; +use tokio::{ + select, + sync::{ + Mutex, + mpsc::{Sender, channel}, + }, + time::Instant, +}; +use tokio_stream::StreamExt; +use uuid::Uuid; + +#[derive(Debug)] +pub enum ServerDeviceEvent { + Connected(Arc), + Notification(UserDeviceIdentifier, ButtplugServerDeviceMessage), + Disconnected(UserDeviceIdentifier), +} + +#[derive(Getters)] +pub struct ServerDevice { + hardware: Arc, + handler: Arc, + #[getset(get = "pub")] + definition: DeviceDefinition, + //output_command_manager: ActuatorCommandManager, + /// Unique identifier for the device + #[getset(get = "pub")] + identifier: UserDeviceIdentifier, + #[getset(get = "pub")] + legacy_attributes: ServerDeviceAttributes, + last_output_command: DashMap, + + stop_commands: Vec, + internal_hw_msg_sender: Sender>, +} +impl Debug for ServerDevice { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ButtplugDevice") + .field("name", &self.name()) + .field("identifier", &self.identifier) + .finish() + } +} + +impl Hash for ServerDevice { + fn hash(&self, state: &mut H) { + self.identifier.hash(state); + } +} + +impl Eq for ServerDevice {} + +impl PartialEq for ServerDevice { + fn eq(&self, other: &Self) -> bool { + self.identifier == *other.identifier() + } +} + +impl ServerDevice { + pub(super) async fn build( + device_config_manager: Arc, + mut hardware_connector: Box, + protocol_specializers: Vec, + ) -> Result { + // We've already checked to make sure we have specializers in the server device manager event + // loop. That check used to be here for sake of continuity in building devices in this method, but + // having that done before we get here fixes issues with some device advertisement timing (See + // #462 for more info.) + + // At this point, we know we've got hardware that is waiting to connect, and enough protocol + // info to actually do something after we connect. So go ahead and connect. + trace!("Connecting to {:?}", hardware_connector); + let mut hardware_specializer = hardware_connector.connect().await?; + + // We can't run these in parallel because we need to only accept one specializer. + let mut protocol_identifier = None; + let mut hardware_out = None; + for protocol_specializer in protocol_specializers { + if let Ok(specialized_hardware) = hardware_specializer + .specialize(protocol_specializer.specifiers()) + .await + { + protocol_identifier = Some(protocol_specializer.identify()); + hardware_out = Some(specialized_hardware); + break; + } + } + + if protocol_identifier.is_none() { + return Err(ButtplugDeviceError::DeviceConfigurationError( + "No protocols with viable communication matches for hardware.".to_owned(), + )); + } + + let mut protocol_identifier_stage = protocol_identifier.unwrap(); + let hardware = Arc::new(hardware_out.unwrap()); + + let (identifier, mut protocol_initializer) = protocol_identifier_stage + .identify(hardware.clone(), hardware_connector.specifier()) + .await?; + + // Now we have an identifier. After this point, if anything fails, consider it a complete + // connection failure, as identify may have already run commands on the device, and therefore + // put it in an unknown state if anything fails. + + // Check in the DeviceConfigurationManager to make sure we have attributes for this device. + let attrs = if let Some(attrs) = device_config_manager.device_definition(&identifier) { + attrs + } else { + return Err(ButtplugDeviceError::DeviceConfigurationError(format!( + "No protocols with viable protocol attributes for hardware {identifier:?}." + ))); + }; + + // If we have attributes, go ahead and initialize, handing us back our hardware instance that + // is now ready to use with the protocol handler. + + // Build the server device and return. + let handler = protocol_initializer + .initialize(hardware.clone(), &attrs.clone()) + .await?; + + let requires_keepalive = hardware.requires_keepalive(); + let strategy = handler.keepalive_strategy(); + + // We now have fully initialized hardware, return a server device. + let device = Self::new(identifier, handler, hardware, &attrs); + + // If we need a keepalive with a packet replay, set this up via stopping the device on connect. + if (requires_keepalive + && matches!( + strategy, + ProtocolKeepaliveStrategy::HardwareRequiredRepeatLastPacketStrategy + )) + || matches!( + strategy, + ProtocolKeepaliveStrategy::RepeatLastPacketStrategyWithTiming(_) + ) + { + if let Err(e) = device.handle_stop_device_cmd().await { + return Err(ButtplugDeviceError::DeviceConnectionError(format!( + "Error setting up keepalive: {e}" + ))); + } + } + + Ok(device) + } + + /// Given a protocol and a device impl, create a new ButtplugDevice instance + fn new( + identifier: UserDeviceIdentifier, + handler: Arc, + hardware: Arc, + definition: &DeviceDefinition, + ) -> Self { + let (internal_hw_msg_sender, mut internal_hw_msg_recv) = channel::>(1024); + + let device_wait_duration = if definition.message_gap().is_some() { + definition.message_gap() + } else { + hardware.message_gap() + }; + + // Set up and start the packet send task + { + let hardware = hardware.clone(); + let strategy = handler.keepalive_strategy(); + let strategy_duration = + if let ProtocolKeepaliveStrategy::RepeatLastPacketStrategyWithTiming(duration) = strategy { + Some(duration) + } else { + None + }; + async_manager::spawn(async move { + let mut hardware_events = hardware.event_stream(); + let keepalive_packet = Mutex::new(None); + // TODO This needs to throw system error messages + let send_hw_cmd = async |command| { + let _ = hardware.parse_message(&command).await; + if hardware.requires_keepalive() + && matches!( + strategy, + ProtocolKeepaliveStrategy::HardwareRequiredRepeatLastPacketStrategy + ) + { + if let HardwareCommand::Write(command) = command { + *keepalive_packet.lock().await = Some(command); + } + }; + }; + loop { + let requires_keepalive = hardware.requires_keepalive(); + let wait_duration_fut = async move { + if let Some(duration) = strategy_duration { + util::sleep(duration).await; + } else if requires_keepalive { + // This is really only for iOS Bluetooth + util::sleep(Duration::from_secs(5)).await; + } else { + future::pending::<()>().await; + }; + }; + select! { + hw_event = hardware_events.recv() => { + if let Ok(hw_event) = hw_event { + if matches!(hw_event, HardwareEvent::Disconnected(_)) { + info!("Hardware disconnected, shutting down keepalive"); + return; + } + } else { + info!("Hardware disconnected, shutting down keepalive"); + return; + } + } + msg = internal_hw_msg_recv.recv() => { + if msg.is_none() { + info!("No longer receiving message from device parent, breaking"); + break; + } + let hardware_cmd = msg.unwrap(); + if device_wait_duration.is_none() { + trace!("No wait duration specified, sending hardware commands {:?}", hardware_cmd); + // send and continue + for cmd in hardware_cmd { + send_hw_cmd(cmd).await; + } + continue; + } + // Run commands in order, otherwise we may end up sending out of order. This may take a while, + // but it's what 99% of protocols expect. If they want something else, they can implement it + // themselves. + // + // If anything errors out, just bail on the command series. This most likely means the device + // disconnected. + let mut local_commands: VecDeque = VecDeque::new(); + local_commands.append(&mut VecDeque::from(hardware_cmd)); + + let sleep_until = Instant::now() + *device_wait_duration.as_ref().unwrap(); + loop { + select! { + hw_event = hardware_events.recv() => { + if let Ok(hw_event) = hw_event { + if matches!(hw_event, HardwareEvent::Disconnected(_)) { + info!("Hardware disconnected, shutting down keepalive"); + return; + } + } else { + info!("Hardware disconnected, shutting down keepalive"); + return; + } + } + msg = internal_hw_msg_recv.recv() => { + if msg.is_none() { + info!("No longer receiving message from device parent, breaking"); + local_commands.clear(); + break; + } + // Run commands in order, otherwise we may end up sending out of order. This may take a while, + // but it's what 99% of protocols expect. If they want something else, they can implement it + // themselves. + // + // If anything errors out, just bail on the command series. This most likely means the device + // disconnected. + for command in msg.unwrap() { + local_commands.retain(|v| !command.overlaps(v)); + local_commands.push_back(command); + } + } + _ = util::sleep(sleep_until - Instant::now()) => { + break; + } + } + if sleep_until < Instant::now() { + break; + } + } + while let Some(command) = local_commands.pop_front() { + debug!("Sending hardware command {:?}", command); + send_hw_cmd(command).await; + } + } + _ = wait_duration_fut => { + let keepalive_packet = keepalive_packet.lock().await.clone(); + match &strategy { + ProtocolKeepaliveStrategy::RepeatLastPacketStrategyWithTiming(duration) => { + if hardware.time_since_last_write().await > *duration { + if let Some(packet) = keepalive_packet { + if let Err(e) = hardware.write_value(&packet).await { + warn!("Error writing keepalive packet: {:?}", e); + break; + } + } + } + } + ProtocolKeepaliveStrategy::HardwareRequiredRepeatPacketStrategy(packet) => { + if let Err(e) = hardware.write_value(packet).await { + warn!("Error writing keepalive packet: {:?}", e); + break; + } + } + ProtocolKeepaliveStrategy::HardwareRequiredRepeatLastPacketStrategy => { + if let Some(packet) = keepalive_packet { + if let Err(e) = hardware.write_value(&packet).await { + warn!("Error writing keepalive packet: {:?}", e); + break; + } + } + } + } + } + } + } + info!("Leaving keepalive task for {}", hardware.name()); + }); + } + + let mut stop_commands: Vec = vec![]; + // We consider the feature's FeatureType to be the "main" capability of a feature. Use that to + // calculate stop commands. + for (index, feature) in definition.features().iter().enumerate() { + if let Some(output_map) = feature.output() { + for actuator_type in output_map.keys() { + let mut stop_cmd = |actuator_cmd| { + stop_commands + .push(CheckedOutputCmdV4::new(1, 0, index as u32, 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::Heater => { + stop_cmd(message::OutputCommand::Heater(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::RotateWithDirection => { + stop_cmd(message::OutputCommand::RotateWithDirection( + OutputRotateWithDirection::new(0, true), + )); + 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; + } + } + } + } + } + Self { + identifier, + //output_command_manager: acm, + handler, + hardware, + definition: definition.clone(), + // Generating legacy attributes is cheap, just do it right when we create the device. + legacy_attributes: ServerDeviceAttributes::new(&definition.features()), + last_output_command: DashMap::new(), + stop_commands, + internal_hw_msg_sender, + } + } + + /// Get the name of the device as set in the Device Configuration File. + pub fn name(&self) -> String { + self.definition.name().to_owned() + } + + /// Disconnect from the device, if it's connected. + pub fn disconnect(&self) -> ButtplugResultFuture { + let fut = self.hardware.disconnect(); + async move { fut.await.map_err(|err| err.into()) }.boxed() + } + + /// Retreive the event stream for the device. + /// + /// This will include connections, disconnections, and notification events from subscribed + /// endpoints. + pub fn event_stream(&self) -> impl futures::Stream + Send + use<> { + let identifier = self.identifier.clone(); + let hardware_stream = convert_broadcast_receiver_to_stream(self.hardware.event_stream()) + .filter_map(move |hardware_event| { + let id = identifier.clone(); + match hardware_event { + HardwareEvent::Disconnected(_) => Some(ServerDeviceEvent::Disconnected(id)), + HardwareEvent::Notification(_address, _endpoint, _data) => { + // TODO Does this still need to be here? Does this need to be routed to the protocol it's part of? + None + } + } + }); + + let identifier = self.identifier.clone(); + let handler_mapped_stream = self.handler.event_stream().map(move |incoming_message| { + let id = identifier.clone(); + ServerDeviceEvent::Notification(id, incoming_message) + }); + hardware_stream.merge(handler_mapped_stream) + } + + pub fn needs_update(&self, _command_message: &ButtplugDeviceCommandMessageUnionV4) -> bool { + true + } + + pub fn as_device_message_info(&self, index: u32) -> DeviceMessageInfoV4 { + DeviceMessageInfoV4::new( + index, + &self.name(), + self.definition().user_config().display_name(), + 100, + &self + .definition + .features() + .iter() + .enumerate() + .map(|(i, x)| x.as_device_feature(i as u32)) + .collect::>(), + ) + } + + // In order to not have to worry about id setting at the protocol level (this + // should be taken care of in the server's device manager), we return server + // messages but Buttplug errors. + pub fn parse_message( + &self, + command_message: ButtplugDeviceCommandMessageUnionV4, + ) -> ButtplugServerResultFuture { + match command_message { + // Input messages + ButtplugDeviceCommandMessageUnionV4::InputCmd(msg) => self.handle_input_cmd(msg), + // Actuator messages + ButtplugDeviceCommandMessageUnionV4::OutputCmd(msg) => self.handle_outputcmd_v4(&msg), + ButtplugDeviceCommandMessageUnionV4::OutputVecCmd(msg) => { + let mut futs = vec![]; + let msg_id = msg.id(); + for m in msg.value_vec() { + futs.push(self.handle_outputcmd_v4(m)) + } + async move { + for f in futs { + f.await?; + } + Ok(message::OkV0::new(msg_id).into()) + } + .boxed() + } + // Other generic messages + ButtplugDeviceCommandMessageUnionV4::StopDeviceCmd(_) => self.handle_stop_device_cmd(), + } + } + + fn handle_outputcmd_v4(&self, msg: &CheckedOutputCmdV4) -> ButtplugServerResultFuture { + if let Some(last_msg) = self.last_output_command.get(&msg.feature_id()) { + if *last_msg == *msg { + trace!("No commands generated for incoming device packet, skipping and returning success."); + return future::ready(Ok(message::OkV0::default().into())).boxed(); + } + } + self + .last_output_command + .insert(msg.feature_id(), msg.clone()); + self.handle_generic_command_result(self.handler.handle_output_cmd(msg)) + } + + fn handle_hardware_commands(&self, commands: Vec) -> ButtplugServerResultFuture { + let sender = self.internal_hw_msg_sender.clone(); + async move { + let _ = sender.send(commands).await; + Ok(message::OkV0::default().into()) + } + .boxed() + } + + fn handle_generic_command_result( + &self, + command_result: Result, ButtplugDeviceError>, + ) -> ButtplugServerResultFuture { + let hardware_commands = match command_result { + Ok(commands) => commands, + Err(err) => return future::ready(Err(err.into())).boxed(), + }; + + self.handle_hardware_commands(hardware_commands) + } + + fn handle_stop_device_cmd(&self) -> ButtplugServerResultFuture { + let mut fut_vec = vec![]; + self + .stop_commands + .iter() + .for_each(|msg| fut_vec.push(self.parse_message(msg.clone()))); + async move { + for fut in fut_vec { + fut.await?; + } + Ok(message::OkV0::default().into()) + } + .boxed() + } + + fn handle_input_cmd( + &self, + message: CheckedInputCmdV4, + ) -> BoxFuture<'static, Result> { + match message.input_command() { + InputCommandType::Read => self.handle_input_read_cmd( + message.device_index(), + message.feature_index(), + message.feature_id(), + message.input_type(), + ), + InputCommandType::Subscribe => self.handle_input_subscribe_cmd( + message.device_index(), + message.feature_index(), + message.feature_id(), + message.input_type(), + ), + InputCommandType::Unsubscribe => self.handle_input_unsubscribe_cmd( + message.feature_index(), + message.feature_id(), + message.input_type(), + ), + } + } + + fn handle_input_read_cmd( + &self, + device_index: u32, + feature_index: u32, + feature_id: Uuid, + input_type: InputType, + ) -> BoxFuture<'static, Result> { + let device = self.hardware.clone(); + let handler = self.handler.clone(); + async move { + handler + .handle_input_read_cmd(device_index, device, feature_index, feature_id, input_type) + .await + .map_err(|e| e.into()) + .map(|e| e.into()) + } + .boxed() + } + + fn handle_input_subscribe_cmd( + &self, + device_index: u32, + feature_index: u32, + feature_id: Uuid, + input_type: InputType, + ) -> ButtplugServerResultFuture { + let device = self.hardware.clone(); + let handler = self.handler.clone(); + async move { + handler + .handle_input_subscribe_cmd(device_index, device, feature_index, feature_id, input_type) + .await + .map(|_| message::OkV0::new(1).into()) + .map_err(|e| e.into()) + } + .boxed() + } + + fn handle_input_unsubscribe_cmd( + &self, + feature_index: u32, + feature_id: Uuid, + input_type: InputType, + ) -> ButtplugServerResultFuture { + let device = self.hardware.clone(); + let handler = self.handler.clone(); + async move { + handler + .handle_input_unsubscribe_cmd(device, feature_index, feature_id, input_type) + .await + .map(|_| message::OkV0::new(1).into()) + .map_err(|e| e.into()) + } + .boxed() + } +} diff --git a/buttplug/src/server/device/server_device_manager.rs b/crates/buttplug_server/src/device/server_device_manager.rs similarity index 82% rename from buttplug/src/server/device/server_device_manager.rs rename to crates/buttplug_server/src/device/server_device_manager.rs index f6ce879fe..8e4b7ad69 100644 --- a/buttplug/src/server/device/server_device_manager.rs +++ b/crates/buttplug_server/src/device/server_device_manager.rs @@ -9,35 +9,28 @@ //! specific) Managers use crate::{ - core::{ - errors::{ButtplugDeviceError, ButtplugMessageError, ButtplugUnknownError}, - message::{ - self, - ButtplugClientMessageV4, - ButtplugDeviceCommandMessageUnion, - ButtplugDeviceManagerMessageUnion, - ButtplugDeviceMessage, - ButtplugMessage, - ButtplugServerMessageV4, - DeviceListV4, - DeviceMessageInfoV4, - }, + device::{ + hardware::communication::{HardwareCommunicationManager, HardwareCommunicationManagerBuilder}, + server_device_manager_event_loop::ServerDeviceManagerEventLoop, + ServerDevice, }, - server::{ - device::{ - configuration::{DeviceConfigurationManager, UserDeviceIdentifier}, - hardware::communication::{ - HardwareCommunicationManager, - HardwareCommunicationManagerBuilder, - }, - server_device_manager_event_loop::ServerDeviceManagerEventLoop, - ServerDevice, + message::{ + server_device_attributes::ServerDeviceAttributes, + spec_enums::{ + ButtplugCheckedClientMessageV4, + ButtplugDeviceCommandMessageUnionV4, + ButtplugDeviceManagerMessageUnion, }, - ButtplugServerError, - ButtplugServerResultFuture, }, + ButtplugServerError, + ButtplugServerResultFuture, +}; +use buttplug_core::{ + errors::{ButtplugDeviceError, ButtplugMessageError, ButtplugUnknownError}, + message::{self, ButtplugDeviceMessage, ButtplugMessage, ButtplugServerMessageV4, DeviceListV4}, util::{async_manager, stream::convert_broadcast_receiver_to_stream}, }; +use buttplug_server_device_config::{DeviceConfigurationManager, UserDeviceIdentifier}; use dashmap::DashMap; use futures::{ future::{self, FutureExt}, @@ -45,6 +38,7 @@ use futures::{ }; use getset::Getters; use std::{ + collections::HashMap, convert::TryFrom, sync::{ atomic::{AtomicBool, Ordering}, @@ -181,7 +175,7 @@ pub struct ServerDeviceManager { } impl ServerDeviceManager { - pub fn event_stream(&self) -> impl Stream { + pub fn event_stream(&self) -> impl Stream + use<> { // Unlike the client API, we can expect anyone using the server to pin this // themselves. convert_broadcast_receiver_to_stream(self.output_sender.subscribe()) @@ -236,39 +230,35 @@ impl ServerDeviceManager { fn parse_device_message( &self, - device_msg: ButtplugDeviceCommandMessageUnion, + device_msg: ButtplugDeviceCommandMessageUnionV4, ) -> ButtplugServerResultFuture { match self.devices.get(&device_msg.device_index()) { Some(device) => { - let fut = device.parse_message(device_msg); + //let fut = device.parse_message(device_msg); + device.parse_message(device_msg) // Create a future to run the message through the device, then handle adding the id to the result. - async move { fut.await }.boxed() + //fut.boxed() } None => ButtplugDeviceError::DeviceNotAvailable(device_msg.device_index()).into(), } } + fn generate_device_list(&self) -> DeviceListV4 { + let devices = self + .devices + .iter() + .map(|device| device.value().as_device_message_info(*device.key())) + .collect(); + DeviceListV4::new(devices) + } + fn parse_device_manager_message( &self, manager_msg: ButtplugDeviceManagerMessageUnion, ) -> ButtplugServerResultFuture { match manager_msg { ButtplugDeviceManagerMessageUnion::RequestDeviceList(msg) => { - let devices = self - .devices - .iter() - .map(|device| { - let dev = device.value(); - DeviceMessageInfoV4::new( - *device.key(), - &dev.name(), - &dev.definition().user_config().display_name(), - &None, - dev.definition().features().clone(), - ) - }) - .collect(); - let mut device_list = DeviceListV4::new(devices); + let mut device_list = self.generate_device_list(); device_list.set_id(msg.id()); future::ready(Ok(device_list.into())).boxed() } @@ -278,21 +268,29 @@ impl ServerDeviceManager { } } - pub fn parse_message(&self, msg: ButtplugClientMessageV4) -> ButtplugServerResultFuture { - if !self.running.load(Ordering::SeqCst) { + pub fn parse_message(&self, msg: ButtplugCheckedClientMessageV4) -> ButtplugServerResultFuture { + if !self.running.load(Ordering::Relaxed) { return future::ready(Err(ButtplugUnknownError::DeviceManagerNotRunning.into())).boxed(); } // If this is a device command message, just route it directly to the // device. - match ButtplugDeviceCommandMessageUnion::try_from(msg.clone()) { - Ok(device_msg) => self.parse_device_message(device_msg), - Err(_) => match ButtplugDeviceManagerMessageUnion::try_from(msg.clone()) { - Ok(manager_msg) => self.parse_device_manager_message(manager_msg), - Err(_) => ButtplugMessageError::UnexpectedMessageType(format!("{:?}", msg)).into(), - }, + if let Ok(device_msg) = ButtplugDeviceCommandMessageUnionV4::try_from(msg.clone()) { + self.parse_device_message(device_msg) + } else if let Ok(manager_msg) = ButtplugDeviceManagerMessageUnion::try_from(msg.clone()) { + self.parse_device_manager_message(manager_msg) + } else { + ButtplugMessageError::UnexpectedMessageType(format!("{msg:?}")).into() } } + pub(crate) fn feature_map(&self) -> HashMap { + self + .devices() + .iter() + .map(|x| (*x.key(), x.legacy_attributes().clone())) + .collect() + } + pub fn device_info(&self, index: u32) -> Option { self.devices.get(&index).map(|device| ServerDeviceInfo { identifier: device.value().identifier().clone(), @@ -314,7 +312,7 @@ impl ServerDeviceManager { let devices = self.devices.clone(); // Make sure that, once our owning server shuts us down, no one outside can use this manager // again. Otherwise we can have all sorts of ownership weirdness. - self.running.store(false, Ordering::SeqCst); + self.running.store(false, Ordering::Relaxed); let stop_scanning = self.stop_scanning(); let stop_devices = self.stop_all_devices(); let token = self.loop_cancellation_token.clone(); diff --git a/buttplug/src/server/device/server_device_manager_event_loop.rs b/crates/buttplug_server/src/device/server_device_manager_event_loop.rs similarity index 85% rename from buttplug/src/server/device/server_device_manager_event_loop.rs rename to crates/buttplug_server/src/device/server_device_manager_event_loop.rs index 173e1f91b..8dc4a51c3 100644 --- a/buttplug/src/server/device/server_device_manager_event_loop.rs +++ b/crates/buttplug_server/src/device/server_device_manager_event_loop.rs @@ -5,22 +5,24 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::message::{ButtplugServerMessageV4, DeviceAddedV4, DeviceRemovedV0, ScanningFinishedV0}, - server::device::{ - configuration::DeviceConfigurationManager, - hardware::communication::{HardwareCommunicationManager, HardwareCommunicationManagerEvent}, - ServerDevice, - ServerDeviceEvent, - }, +use buttplug_core::{ + message::{ButtplugServerMessageV4, DeviceListV4, ScanningFinishedV0}, util::async_manager, }; +use buttplug_server_device_config::DeviceConfigurationManager; +use tracing::info_span; + +use crate::device::{ + hardware::communication::{HardwareCommunicationManager, HardwareCommunicationManagerEvent}, + ServerDevice, + ServerDeviceEvent, + protocol::ProtocolManager +}; use dashmap::{DashMap, DashSet}; -use futures::{future, FutureExt, StreamExt}; -use std::sync::Arc; +use futures::{future, pin_mut, FutureExt, StreamExt}; +use std::sync::{atomic::AtomicBool, Arc}; use tokio::sync::{broadcast, mpsc}; use tokio_util::sync::CancellationToken; -use tracing; use tracing_futures::Instrument; use super::server_device_manager::DeviceManagerCommand; @@ -50,6 +52,10 @@ pub(super) struct ServerDeviceManagerEventLoop { connecting_devices: Arc>, /// Cancellation token for the event loop loop_cancellation_token: CancellationToken, + /// True if stop scanning message was sent, means we won't send scanning finished. + stop_scanning_received: AtomicBool, + /// Protocol map, for mapping user definitions to protocols + protocol_manager: ProtocolManager, } impl ServerDeviceManagerEventLoop { @@ -65,7 +71,7 @@ impl ServerDeviceManagerEventLoop { let (device_event_sender, device_event_receiver) = mpsc::channel(256); Self { comm_managers, - device_config_manager: device_config_manager, + device_config_manager, server_sender, device_map, device_comm_receiver, @@ -76,6 +82,8 @@ impl ServerDeviceManagerEventLoop { scanning_started: false, connecting_devices: Arc::new(DashSet::new()), loop_cancellation_token, + stop_scanning_received: AtomicBool::new(false), + protocol_manager: ProtocolManager::default(), } } @@ -92,7 +100,9 @@ impl ServerDeviceManagerEventLoop { debug!("System already scanning, ignoring new scanning request"); return; } - + self + .stop_scanning_received + .store(false, std::sync::atomic::Ordering::Relaxed); info!("No scan currently in progress, starting new scan."); self.scanning_bringup_in_progress = true; self.scanning_started = true; @@ -108,6 +118,9 @@ impl ServerDeviceManagerEventLoop { } async fn handle_stop_scanning(&mut self) { + self + .stop_scanning_received + .store(true, std::sync::atomic::Ordering::Relaxed); let fut_vec: Vec<_> = self .comm_managers .iter_mut() @@ -127,7 +140,13 @@ impl ServerDeviceManagerEventLoop { debug!("Hardware Comm Manager finished before scanning was fully started, continuing event loop."); return; } - if !self.scanning_status() && self.scanning_started { + // Only send scanning finished if we haven't requested a stop. + if !self.scanning_status() + && self.scanning_started + && !self + .stop_scanning_received + .load(std::sync::atomic::Ordering::Relaxed) + { debug!("All managers finished, emitting ScanningFinished"); self.scanning_started = false; if self @@ -176,8 +195,12 @@ impl ServerDeviceManagerEventLoop { // We used to do this in build_server_device, but we shouldn't mark devices as actually // connecting until after this happens, so we're moving it back here. let protocol_specializers = self - .device_config_manager - .protocol_specializers(&creator.specifier()); + .protocol_manager + .protocol_specializers( + &creator.specifier(), + self.device_config_manager.base_communication_specifiers(), + self.device_config_manager.user_communication_specifiers() + ); // If we have no identifiers, then there's nothing to do here. Throw an error. if protocol_specializers.is_empty() { @@ -213,7 +236,7 @@ impl ServerDeviceManagerEventLoop { address = tracing::field::display(address.clone()) ); - async_manager::spawn(async move { + let _ = async_manager::spawn(async move { match ServerDevice::build(device_config_manager, creator, protocol_specializers).await { Ok(device) => { if device_event_sender_clone @@ -233,6 +256,15 @@ impl ServerDeviceManagerEventLoop { } } + fn generate_device_list(&self) -> DeviceListV4 { + let devices = self + .device_map + .iter() + .map(|device| device.value().as_device_message_info(*device.key())) + .collect(); + DeviceListV4::new(devices) + } + async fn handle_device_event(&mut self, device_event: ServerDeviceEvent) { trace!("Got device event: {:?}", device_event); match device_event { @@ -253,7 +285,7 @@ impl ServerDeviceManagerEventLoop { // address), consider it disconnected and eject it from the map. This // should also trigger a disconnect event before our new DeviceAdded // message goes out, so timing matters here. - if let Some((_, old_device)) = self.device_map.remove(&device_index) { + match self.device_map.remove(&device_index) { Some((_, old_device)) => { info!("Device map contains key {}.", device_index); // After removing the device from the array, manually disconnect it to // make sure the event is thrown. @@ -262,9 +294,9 @@ impl ServerDeviceManagerEventLoop { // anything with it, but should at least log it. error!("Error during index collision disconnect: {:?}", err); } - } else { + } _ => { info!("Device map does not contain key {}.", device_index); - } + }} // Create event loop for forwarding device events into our selector. let event_listener = device.event_stream(); @@ -281,19 +313,15 @@ impl ServerDeviceManagerEventLoop { }); info!("Assigning index {} to {}", device_index, device.name()); - let device_added_message = DeviceAddedV4::new( - device_index, - &device.name(), - &device.definition().user_config().display_name(), - &None, - &device.definition().features().clone(), - ); - self.device_map.insert(device_index, device); + self.device_map.insert(device_index, device.clone()); + + let device_update_message: ButtplugServerMessageV4 = self.generate_device_list().into(); + // After that, we can send out to the server's event listeners to let // them know a device has been added. if self .server_sender - .send(device_added_message.into()) + .send(device_update_message.into()) .is_err() { debug!("Server not currently available, dropping Device Added event."); @@ -312,9 +340,10 @@ impl ServerDeviceManagerEventLoop { .device_map .remove(&device_index) .expect("Remove will always work."); + let device_update_message: ButtplugServerMessageV4 = self.generate_device_list().into(); if self .server_sender - .send(DeviceRemovedV0::new(device_index).into()) + .send(device_update_message) .is_err() { debug!("Server not currently available, dropping Device Removed event."); diff --git a/buttplug/src/server/mod.rs b/crates/buttplug_server/src/lib.rs similarity index 96% rename from buttplug/src/server/mod.rs rename to crates/buttplug_server/src/lib.rs index b600892ea..a82fc67e2 100644 --- a/buttplug/src/server/mod.rs +++ b/crates/buttplug_server/src/lib.rs @@ -45,21 +45,30 @@ //! - If the server object is dropped, all devices are stopped and disconnected as part //! of the [DeviceManager] teardown. +#[macro_use] +extern crate log; + +#[macro_use] +extern crate buttplug_derive; + +#[macro_use] +extern crate strum_macros; + +pub mod connector; pub mod device; +pub mod message; mod ping_timer; mod server; mod server_builder; -mod server_downgrade_wrapper; mod server_message_conversion; pub use server::ButtplugServer; pub use server_builder::ButtplugServerBuilder; -pub use server_downgrade_wrapper::ButtplugServerDowngradeWrapper; use futures::future::BoxFuture; use thiserror::Error; -use crate::core::{ +use buttplug_core::{ errors::{ButtplugDeviceError, ButtplugError}, message::ButtplugServerMessageV4, }; diff --git a/crates/buttplug_server/src/message/mod.rs b/crates/buttplug_server/src/message/mod.rs new file mode 100644 index 000000000..90afea6ec --- /dev/null +++ b/crates/buttplug_server/src/message/mod.rs @@ -0,0 +1,214 @@ +use buttplug_core::{ + errors::{ButtplugError, ButtplugMessageError}, + message::{ + ButtplugClientMessageV4, + ButtplugDeviceMessage, + ButtplugMessage, + ButtplugMessageFinalizer, + ButtplugMessageSpecVersion, + ButtplugMessageValidator, + ButtplugServerMessageV4, + InputReadingV4, + }, +}; +use server_device_attributes::ServerDeviceAttributes; + +pub mod serializer; +pub mod server_device_attributes; +mod v0; +mod v1; +mod v2; +mod v3; +mod v4; + +pub use v0::*; +pub use v1::*; +pub use v2::*; +pub use v3::*; +pub use v4::*; + +#[derive( + Debug, Clone, PartialEq, ButtplugMessage, ButtplugMessageFinalizer, ButtplugMessageValidator, +)] +pub enum ButtplugClientMessageVariant { + V0(ButtplugClientMessageV0), + V1(ButtplugClientMessageV1), + V2(ButtplugClientMessageV2), + V3(ButtplugClientMessageV3), + V4(ButtplugClientMessageV4), +} + +impl ButtplugClientMessageVariant { + pub fn version(&self) -> ButtplugMessageSpecVersion { + match self { + Self::V0(_) => ButtplugMessageSpecVersion::Version0, + Self::V1(_) => ButtplugMessageSpecVersion::Version1, + Self::V2(_) => ButtplugMessageSpecVersion::Version2, + Self::V3(_) => ButtplugMessageSpecVersion::Version3, + Self::V4(_) => ButtplugMessageSpecVersion::Version4, + } + } + + pub fn device_index(&self) -> Option { + // TODO there has to be a better way to do this. We just need to dig through our enum and see if + // our message impls ButtplugDeviceMessage. Manually doing this works but is so gross. + match self { + Self::V0(msg) => match msg { + ButtplugClientMessageV0::FleshlightLaunchFW12Cmd(a) => Some(a.device_index()), + ButtplugClientMessageV0::SingleMotorVibrateCmd(a) => Some(a.device_index()), + ButtplugClientMessageV0::VorzeA10CycloneCmd(a) => Some(a.device_index()), + _ => None, + }, + Self::V1(msg) => match msg { + ButtplugClientMessageV1::FleshlightLaunchFW12Cmd(a) => Some(a.device_index()), + ButtplugClientMessageV1::SingleMotorVibrateCmd(a) => Some(a.device_index()), + ButtplugClientMessageV1::VorzeA10CycloneCmd(a) => Some(a.device_index()), + ButtplugClientMessageV1::VibrateCmd(a) => Some(a.device_index()), + _ => None, + }, + Self::V2(msg) => match msg { + ButtplugClientMessageV2::VibrateCmd(a) => Some(a.device_index()), + ButtplugClientMessageV2::RotateCmd(a) => Some(a.device_index()), + ButtplugClientMessageV2::LinearCmd(a) => Some(a.device_index()), + ButtplugClientMessageV2::BatteryLevelCmd(a) => Some(a.device_index()), + _ => None, + }, + Self::V3(msg) => match msg { + ButtplugClientMessageV3::VibrateCmd(a) => Some(a.device_index()), + ButtplugClientMessageV3::SensorSubscribeCmd(a) => Some(a.device_index()), + ButtplugClientMessageV3::SensorUnsubscribeCmd(a) => Some(a.device_index()), + ButtplugClientMessageV3::ScalarCmd(a) => Some(a.device_index()), + ButtplugClientMessageV3::RotateCmd(a) => Some(a.device_index()), + ButtplugClientMessageV3::LinearCmd(a) => Some(a.device_index()), + ButtplugClientMessageV3::SensorReadCmd(a) => Some(a.device_index()), + _ => None, + }, + Self::V4(msg) => match msg { + ButtplugClientMessageV4::OutputCmd(a) => Some(a.device_index()), + ButtplugClientMessageV4::InputCmd(a) => Some(a.device_index()), + _ => None, + }, + } + } +} + +impl From for ButtplugClientMessageVariant { + fn from(value: ButtplugClientMessageV0) -> Self { + ButtplugClientMessageVariant::V0(value) + } +} + +impl From for ButtplugClientMessageVariant { + fn from(value: ButtplugClientMessageV1) -> Self { + ButtplugClientMessageVariant::V1(value) + } +} + +impl From for ButtplugClientMessageVariant { + fn from(value: ButtplugClientMessageV2) -> Self { + ButtplugClientMessageVariant::V2(value) + } +} + +impl From for ButtplugClientMessageVariant { + fn from(value: ButtplugClientMessageV3) -> Self { + ButtplugClientMessageVariant::V3(value) + } +} + +impl From for ButtplugClientMessageVariant { + fn from(value: ButtplugClientMessageV4) -> Self { + ButtplugClientMessageVariant::V4(value) + } +} + +#[derive( + Debug, Clone, PartialEq, ButtplugMessage, ButtplugMessageFinalizer, ButtplugMessageValidator, +)] +pub enum ButtplugServerMessageVariant { + V0(ButtplugServerMessageV0), + V1(ButtplugServerMessageV1), + V2(ButtplugServerMessageV2), + V3(ButtplugServerMessageV3), + V4(ButtplugServerMessageV4), +} + +impl ButtplugServerMessageVariant { + pub fn version(&self) -> ButtplugMessageSpecVersion { + match self { + Self::V0(_) => ButtplugMessageSpecVersion::Version0, + Self::V1(_) => ButtplugMessageSpecVersion::Version1, + Self::V2(_) => ButtplugMessageSpecVersion::Version2, + Self::V3(_) => ButtplugMessageSpecVersion::Version3, + Self::V4(_) => ButtplugMessageSpecVersion::Version4, + } + } +} + +impl From for ButtplugServerMessageVariant { + fn from(value: ButtplugServerMessageV0) -> Self { + ButtplugServerMessageVariant::V0(value) + } +} + +impl From for ButtplugServerMessageVariant { + fn from(value: ButtplugServerMessageV1) -> Self { + ButtplugServerMessageVariant::V1(value) + } +} + +impl From for ButtplugServerMessageVariant { + fn from(value: ButtplugServerMessageV2) -> Self { + ButtplugServerMessageVariant::V2(value) + } +} + +impl From for ButtplugServerMessageVariant { + fn from(value: ButtplugServerMessageV3) -> Self { + ButtplugServerMessageVariant::V3(value) + } +} + +impl From for ButtplugServerMessageVariant { + fn from(value: ButtplugServerMessageV4) -> Self { + ButtplugServerMessageVariant::V4(value) + } +} + +/// Represents all possible messages a [ButtplugServer][crate::server::ButtplugServer] can send to a +/// [ButtplugClient][crate::client::ButtplugClient] that denote an EVENT from a device. These are +/// only used in notifications, so read requests will not need to be added here, only messages that +/// will require Id of 0. +#[derive( + Debug, + Clone, + PartialEq, + Eq, + ButtplugMessage, + ButtplugMessageValidator, + ButtplugMessageFinalizer, + FromSpecificButtplugMessage, +)] +pub enum ButtplugServerDeviceMessage { + // Generic Sensor Reading Messages + SensorReading(InputReadingV4), +} + +impl From for ButtplugServerMessageV4 { + fn from(other: ButtplugServerDeviceMessage) -> Self { + match other { + ButtplugServerDeviceMessage::SensorReading(msg) => ButtplugServerMessageV4::InputReading(msg), + } + } +} + +/// TryFrom for Buttplug Device Messages that need to use a device feature definition to convert +pub(crate) trait TryFromDeviceAttributes +where + Self: Sized, +{ + fn try_from_device_attributes( + msg: T, + features: &ServerDeviceAttributes, + ) -> Result; +} diff --git a/crates/buttplug_server/src/message/serializer/mod.rs b/crates/buttplug_server/src/message/serializer/mod.rs new file mode 100644 index 000000000..f3d89d412 --- /dev/null +++ b/crates/buttplug_server/src/message/serializer/mod.rs @@ -0,0 +1,393 @@ +use buttplug_core::{ + errors::{ButtplugError, ButtplugHandshakeError, ButtplugMessageError}, + message::{ + self, + serializer::{ + json_serializer::{ + create_message_validator, + deserialize_to_message, + msg_to_protocol_json, + vec_to_protocol_json, + }, + ButtplugMessageSerializer, + ButtplugSerializedMessage, + ButtplugSerializerError, + }, + ButtplugClientMessageV4, + ButtplugMessageFinalizer, + ButtplugMessageSpecVersion, + ButtplugServerMessageCurrent, + ButtplugServerMessageV4, + }, +}; +use jsonschema::Validator; +use once_cell::sync::OnceCell; +use serde::Deserialize; + +use super::{ + ButtplugClientMessageV0, + ButtplugClientMessageV1, + ButtplugClientMessageV2, + ButtplugClientMessageV3, + ButtplugClientMessageVariant, + ButtplugServerMessageV0, + ButtplugServerMessageV1, + ButtplugServerMessageV2, + ButtplugServerMessageV3, + ButtplugServerMessageVariant, +}; + +#[derive(Deserialize, ButtplugMessageFinalizer, Clone, Debug)] +struct RequestServerInfoMessage { + #[serde(rename = "RequestServerInfo")] + rsi: RequestServerInfoVersion, +} + +#[derive(Deserialize, ButtplugMessageFinalizer, Clone, Debug)] +struct RequestServerInfoVersion { + #[serde(rename = "Id")] + _id: u32, + #[serde(rename = "ClientName")] + _client_name: String, + #[serde(default, rename = "MessageVersion")] + message_version: Option, + #[serde(default, rename = "ProtocolVersionMajor")] + api_major_version: Option, +} + +pub struct ButtplugServerJSONSerializer { + pub(super) message_version: OnceCell, + validator: Validator, +} + +impl Default for ButtplugServerJSONSerializer { + fn default() -> Self { + Self { + message_version: OnceCell::new(), + validator: create_message_validator(), + } + } +} + +impl ButtplugServerJSONSerializer { + pub fn force_message_version(&self, version: &ButtplugMessageSpecVersion) { + self + .message_version + .set(*version) + .expect("This should only ever be called once."); + } +} + +impl ButtplugMessageSerializer for ButtplugServerJSONSerializer { + type Inbound = ButtplugClientMessageVariant; + type Outbound = ButtplugServerMessageVariant; + + fn deserialize( + &self, + serialized_msg: &ButtplugSerializedMessage, + ) -> Result, ButtplugSerializerError> { + let msg = if let ButtplugSerializedMessage::Text(text_msg) = serialized_msg { + text_msg + } else { + return Err(ButtplugSerializerError::BinaryDeserializationError); + }; + + if let Some(version) = self.message_version.get() { + return Ok(match version { + ButtplugMessageSpecVersion::Version0 => { + deserialize_to_message::(Some(&self.validator), msg)? + .iter() + .cloned() + .map(|m| m.into()) + .collect() + } + ButtplugMessageSpecVersion::Version1 => { + deserialize_to_message::(Some(&self.validator), msg)? + .iter() + .cloned() + .map(|m| m.into()) + .collect() + } + ButtplugMessageSpecVersion::Version2 => { + deserialize_to_message::(Some(&self.validator), msg)? + .iter() + .cloned() + .map(|m| m.into()) + .collect() + } + ButtplugMessageSpecVersion::Version3 => { + deserialize_to_message::(Some(&self.validator), msg)? + .iter() + .cloned() + .map(|m| m.into()) + .collect() + } + ButtplugMessageSpecVersion::Version4 => { + deserialize_to_message::(Some(&self.validator), msg)? + .iter() + .cloned() + .map(|m| m.into()) + .collect() + } + }); + } + // If we don't have a message version yet, we need to parse this as a RequestServerInfo message + // to get the version. As of v4, RequestServerInfo is of a different layout than RSI v0-v3, + // therefore we need to step through versions for compatibility sake. + info!("{:?}", msg); + let msg_version = + if let Ok(msg_union) = deserialize_to_message::(None, msg) { + info!("PARSING {:?}", msg_union); + if msg_union.is_empty() { + Err(ButtplugSerializerError::MessageSpecVersionNotReceived) + } else if let Some(v) = msg_union[0].rsi.api_major_version { + ButtplugMessageSpecVersion::try_from(v as i32) + .map_err(|_| ButtplugSerializerError::MessageSpecVersionNotReceived) + } else if let Some(v) = msg_union[0].rsi.message_version { + ButtplugMessageSpecVersion::try_from(v as i32) + .map_err(|_| ButtplugSerializerError::MessageSpecVersionNotReceived) + } else { + Ok(ButtplugMessageSpecVersion::Version0) + } + } else { + info!("NOT EVEN PARSING"); + Err(ButtplugSerializerError::MessageSpecVersionNotReceived) + }?; + + info!("Setting JSON Wrapper message version to {}", msg_version); + self + .message_version + .set(msg_version) + .expect("This should only ever be called once."); + // Now that we know our version, parse the message again. + self.deserialize(serialized_msg) + } + + fn serialize(&self, msgs: &[ButtplugServerMessageVariant]) -> ButtplugSerializedMessage { + if let Some(version) = self.message_version.get() { + ButtplugSerializedMessage::Text(match version { + ButtplugMessageSpecVersion::Version0 => { + let msg_vec: Vec = msgs + .iter() + .map(|msg| match msg { + ButtplugServerMessageVariant::V0(msgv0) => msgv0.clone(), + _ => ButtplugServerMessageV0::Error(message::ErrorV0::from(ButtplugError::from( + ButtplugMessageError::MessageConversionError(format!( + "Message {msg:?} not in Spec V0! This is a server bug." + )), + ))), + }) + .collect(); + vec_to_protocol_json(&msg_vec) + } + ButtplugMessageSpecVersion::Version1 => { + let msg_vec: Vec = msgs + .iter() + .map(|msg| match msg { + ButtplugServerMessageVariant::V1(msgv1) => msgv1.clone(), + _ => ButtplugServerMessageV1::Error(message::ErrorV0::from(ButtplugError::from( + ButtplugMessageError::MessageConversionError(format!( + "Message {msg:?} not in Spec V1! This is a server bug." + )), + ))), + }) + .collect(); + vec_to_protocol_json(&msg_vec) + } + ButtplugMessageSpecVersion::Version2 => { + let msg_vec: Vec = msgs + .iter() + .map(|msg| match msg { + ButtplugServerMessageVariant::V2(msgv2) => msgv2.clone(), + _ => ButtplugServerMessageV2::Error(message::ErrorV0::from(ButtplugError::from( + ButtplugMessageError::MessageConversionError(format!( + "Message {msg:?} not in Spec V2! This is a server bug." + )), + ))), + }) + .collect(); + vec_to_protocol_json(&msg_vec) + } + ButtplugMessageSpecVersion::Version3 => { + let msg_vec: Vec = msgs + .iter() + .map(|msg| match msg { + ButtplugServerMessageVariant::V3(msgv3) => msgv3.clone(), + _ => ButtplugServerMessageV3::Error(message::ErrorV0::from(ButtplugError::from( + ButtplugMessageError::MessageConversionError(format!( + "Message {msg:?} not in Spec V3! This is a server bug." + )), + ))), + }) + .collect(); + vec_to_protocol_json(&msg_vec) + } + ButtplugMessageSpecVersion::Version4 => { + let msg_vec: Vec = msgs + .iter() + .map(|msg| match msg { + ButtplugServerMessageVariant::V4(msgv4) => msgv4.clone(), + _ => ButtplugServerMessageV4::Error(message::ErrorV0::from(ButtplugError::from( + ButtplugMessageError::MessageConversionError(format!( + "Message {msg:?} not in Spec V4! This is a server bug." + )), + ))), + }) + .collect(); + vec_to_protocol_json(&msg_vec) + } + }) + } else { + // If we don't even have enough info to know which message + // version to convert to, consider this a handshake error. + ButtplugSerializedMessage::Text(msg_to_protocol_json(ButtplugServerMessageCurrent::Error( + ButtplugError::from(ButtplugHandshakeError::RequestServerInfoExpected).into(), + ))) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_correct_message_version() { + let json = r#"[{ + "RequestServerInfo": { + "Id": 1, + "ClientName": "Test Client", + "ProtocolVersionMajor": 4, + "ProtocolVersionMinor": 0 + } + }]"#; + let serializer = ButtplugServerJSONSerializer::default(); + serializer + .deserialize(&ButtplugSerializedMessage::Text(json.to_owned())) + .expect("Infallible deserialization"); + assert_eq!( + *serializer.message_version.get().unwrap(), + ButtplugMessageSpecVersion::Version4 + ); + } + + #[test] + fn test_wrong_message_version() { + let json = r#"[{ + "RequestServerInfo": { + "Id": 1, + "ClientName": "Test Client", + "ProtocolVersionMajor": 100, + "ProtocolVersionMinor": 0 + } + }]"#; + let serializer = ButtplugServerJSONSerializer::default(); + let msg = serializer.deserialize(&ButtplugSerializedMessage::Text(json.to_owned())); + info!("{:?}", msg); + assert!(msg.is_err()); + } + + #[test] + fn test_message_array() { + let json = r#"[ + { + "RequestServerInfo": { + "Id": 1, + "ClientName": "Test Client", + "ProtocolVersionMajor": 4, + "ProtocolVersionMinor": 0 + } + }, + { + "RequestServerInfo": { + "Id": 1, + "ClientName": "Test Client", + "ProtocolVersionMajor": 4, + "ProtocolVersionMinor": 0 + } + }, + { + "RequestServerInfo": { + "Id": 1, + "ClientName": "Test Client", + "ProtocolVersionMajor": 4, + "ProtocolVersionMinor": 0 + } + } + ]"#; + let serializer = ButtplugServerJSONSerializer::default(); + let messages = serializer + .deserialize(&ButtplugSerializedMessage::Text(json.to_owned())) + .expect("Infallible deserialization"); + assert_eq!(messages.len(), 3); + } + + #[test] + fn test_streamed_message_array() { + let json = r#"[ + { + "RequestServerInfo": { + "Id": 1, + "ClientName": "Test Client", + "ProtocolVersionMajor": 4, + "ProtocolVersionMinor": 0 + } + }] + [{ + "RequestServerInfo": { + "Id": 1, + "ClientName": "Test Client", + "ProtocolVersionMajor": 4, + "ProtocolVersionMinor": 0 + } + }] + [{ + "RequestServerInfo": { + "Id": 1, + "ClientName": "Test Client", + "ProtocolVersionMajor": 4, + "ProtocolVersionMinor": 0 + } + }] + "#; + let serializer = ButtplugServerJSONSerializer::default(); + let messages = serializer + .deserialize(&ButtplugSerializedMessage::Text(json.to_owned())) + .expect("Infallible deserialization"); + assert_eq!(messages.len(), 3); + } + + #[test] + fn test_invalid_streamed_message_array() { + // Missing a } in the second message. + let json = r#"[ + "RequestServerInfo": { + "Id": 1, + "ClientName": "Test Client", + "ProtocolVersionMajor": 4, + "ProtocolVersionMinor": 0 + } + }] + [{ + "RequestServerInfo": { + "Id": 1, + "ClientName": "Test Client", + "ProtocolVersionMajor": 4, + "ProtocolVersionMinor": 0 + }] + [{ + "RequestServerInfo": { + "Id": 1, + "ClientName": "Test Client", + "ProtocolVersionMajor": 4, + "ProtocolVersionMinor": 0 + } + }] + "#; + let serializer = ButtplugServerJSONSerializer::default(); + assert!(matches!( + serializer.deserialize(&ButtplugSerializedMessage::Text(json.to_owned())), + Err(_) + )); + } +} diff --git a/crates/buttplug_server/src/message/server_device_attributes.rs b/crates/buttplug_server/src/message/server_device_attributes.rs new file mode 100644 index 000000000..535c61f9e --- /dev/null +++ b/crates/buttplug_server/src/message/server_device_attributes.rs @@ -0,0 +1,42 @@ +use super::v2::ServerDeviceMessageAttributesV2; +use super::ServerDeviceMessageAttributesV3; +use buttplug_core::errors::ButtplugError; +use buttplug_server_device_config::ServerDeviceFeature; +use getset::Getters; +use std::collections::HashMap; + +#[derive(Debug, Getters, Clone)] +pub(crate) struct ServerDeviceAttributes { + /* #[getset(get = "pub")] + attrs_v1: ClientDeviceMessageAttributesV1, + */ + #[getset(get = "pub")] + attrs_v2: ServerDeviceMessageAttributesV2, + #[getset(get = "pub")] + attrs_v3: ServerDeviceMessageAttributesV3, + #[getset(get = "pub")] + features: Vec, +} + +impl ServerDeviceAttributes { + pub fn new(features: &Vec) -> Self { + Self { + attrs_v3: ServerDeviceMessageAttributesV3::from(features.clone()), + attrs_v2: ServerDeviceMessageAttributesV2::from(features.clone()), + /* + attrs_v1: ClientDeviceMessageAttributesV1::from(features.clone()), + */ + features: features.clone(), + } + } +} + +pub(crate) trait TryFromClientMessage +where + Self: Sized, +{ + fn try_from_client_message( + msg: T, + features: &HashMap, + ) -> Result; +} diff --git a/crates/buttplug_server/src/message/v0/device_added.rs b/crates/buttplug_server/src/message/v0/device_added.rs new file mode 100644 index 000000000..73505715f --- /dev/null +++ b/crates/buttplug_server/src/message/v0/device_added.rs @@ -0,0 +1,52 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 buttplug_core::{ + errors::ButtplugMessageError, + message::{ButtplugMessage, ButtplugMessageFinalizer, ButtplugMessageValidator}, +}; +use getset::{CopyGetters, Getters}; +use serde::{Deserialize, Serialize}; + +use super::spec_enums::ButtplugDeviceMessageNameV0; + +#[derive( + Default, + ButtplugMessage, + Clone, + Debug, + PartialEq, + Eq, + Getters, + CopyGetters, + Serialize, + Deserialize, +)] +pub struct DeviceAddedV0 { + #[serde(rename = "Id")] + pub(in crate::message) id: u32, + #[serde(rename = "DeviceIndex")] + #[getset(get_copy = "pub")] + pub(in crate::message) device_index: u32, + #[serde(rename = "DeviceName")] + #[getset(get = "pub")] + pub(in crate::message) device_name: String, + #[serde(rename = "DeviceMessages")] + #[getset(get = "pub")] + pub(in crate::message) device_messages: Vec, +} + +impl ButtplugMessageValidator for DeviceAddedV0 { + fn is_valid(&self) -> Result<(), ButtplugMessageError> { + self.is_system_id(self.id) + } +} + +impl ButtplugMessageFinalizer for DeviceAddedV0 { +} + +// TODO Test repeated message type in attributes in JSON diff --git a/crates/buttplug_server/src/message/v0/device_list.rs b/crates/buttplug_server/src/message/v0/device_list.rs new file mode 100644 index 000000000..29bb88c1a --- /dev/null +++ b/crates/buttplug_server/src/message/v0/device_list.rs @@ -0,0 +1,34 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 super::device_message_info::DeviceMessageInfoV0; +use buttplug_core::{ + errors::ButtplugMessageError, + message::{ButtplugMessage, ButtplugMessageFinalizer, ButtplugMessageValidator}, +}; +use getset::Getters; +use serde::{Deserialize, Serialize}; + +#[derive( + Default, Clone, Debug, PartialEq, Eq, ButtplugMessage, Getters, Serialize, Deserialize, +)] +pub struct DeviceListV0 { + #[serde(rename = "Id")] + pub(in crate::message) id: u32, + #[serde(rename = "Devices")] + #[getset(get = "pub")] + pub(in crate::message) devices: Vec, +} + +impl ButtplugMessageValidator for DeviceListV0 { + fn is_valid(&self) -> Result<(), ButtplugMessageError> { + self.is_not_system_id(self.id) + } +} + +impl ButtplugMessageFinalizer for DeviceListV0 { +} diff --git a/crates/buttplug_server/src/message/v0/device_message_info.rs b/crates/buttplug_server/src/message/v0/device_message_info.rs new file mode 100644 index 000000000..f338ff2e0 --- /dev/null +++ b/crates/buttplug_server/src/message/v0/device_message_info.rs @@ -0,0 +1,24 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 getset::{CopyGetters, Getters}; +use serde::{Deserialize, Serialize}; + +use super::spec_enums::ButtplugDeviceMessageNameV0; + +#[derive(Clone, Debug, PartialEq, Eq, Getters, CopyGetters, Serialize, Deserialize)] +pub struct DeviceMessageInfoV0 { + #[serde(rename = "DeviceIndex")] + #[getset(get_copy = "pub")] + pub(in crate::message) device_index: u32, + #[serde(rename = "DeviceName")] + #[getset(get = "pub")] + pub(in crate::message) device_name: String, + #[serde(rename = "DeviceMessages")] + #[getset(get = "pub")] + pub(in crate::message) device_messages: Vec, +} diff --git a/buttplug/src/core/message/fleshlight_launch_fw12_cmd.rs b/crates/buttplug_server/src/message/v0/fleshlight_launch_fw12_cmd.rs similarity index 79% rename from buttplug/src/core/message/fleshlight_launch_fw12_cmd.rs rename to crates/buttplug_server/src/message/v0/fleshlight_launch_fw12_cmd.rs index 1bc2486cf..80dfc9917 100644 --- a/buttplug/src/core/message/fleshlight_launch_fw12_cmd.rs +++ b/crates/buttplug_server/src/message/v0/fleshlight_launch_fw12_cmd.rs @@ -7,24 +7,38 @@ //! Fleshlight FW v1.2 Command (Version 0 Message, Deprecated) -use super::*; +use buttplug_core::{ + errors::ButtplugMessageError, + message::{ + ButtplugDeviceMessage, + ButtplugMessage, + ButtplugMessageFinalizer, + ButtplugMessageValidator, + }, +}; use getset::CopyGetters; -#[cfg(feature = "serialize-json")] use serde::{Deserialize, Serialize}; #[derive( - Debug, ButtplugDeviceMessage, ButtplugMessageFinalizer, PartialEq, Eq, Clone, CopyGetters, + Debug, + ButtplugDeviceMessage, + ButtplugMessageFinalizer, + PartialEq, + Eq, + Clone, + CopyGetters, + Serialize, + Deserialize, )] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] pub struct FleshlightLaunchFW12CmdV0 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] + #[serde(rename = "Id")] id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] + #[serde(rename = "DeviceIndex")] device_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "Position"))] + #[serde(rename = "Position")] #[getset(get_copy = "pub")] position: u8, - #[cfg_attr(feature = "serialize-json", serde(rename = "Speed"))] + #[serde(rename = "Speed")] #[getset(get_copy = "pub")] speed: u8, } diff --git a/crates/buttplug_server/src/message/v0/mod.rs b/crates/buttplug_server/src/message/v0/mod.rs new file mode 100644 index 000000000..d95490b3a --- /dev/null +++ b/crates/buttplug_server/src/message/v0/mod.rs @@ -0,0 +1,24 @@ +mod device_added; +mod device_list; +mod device_message_info; +mod fleshlight_launch_fw12_cmd; +mod server_info; +mod single_motor_vibrate_cmd; +mod spec_enums; +mod test; +mod vorze_a10_cyclone_cmd; + +use buttplug_core::message::v0::*; +pub use device_added::DeviceAddedV0; +pub use device_list::DeviceListV0; +pub use device_message_info::DeviceMessageInfoV0; +pub use fleshlight_launch_fw12_cmd::FleshlightLaunchFW12CmdV0; +pub use server_info::ServerInfoV0; +pub use single_motor_vibrate_cmd::SingleMotorVibrateCmdV0; +pub use spec_enums::{ + ButtplugClientMessageV0, + ButtplugDeviceMessageNameV0, + ButtplugServerMessageV0, +}; +pub use test::TestV0; +pub use vorze_a10_cyclone_cmd::VorzeA10CycloneCmdV0; diff --git a/crates/buttplug_server/src/message/v0/server_info.rs b/crates/buttplug_server/src/message/v0/server_info.rs new file mode 100644 index 000000000..997928b28 --- /dev/null +++ b/crates/buttplug_server/src/message/v0/server_info.rs @@ -0,0 +1,91 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 crate::message::ServerInfoV2; +use buttplug_core::{ + errors::ButtplugMessageError, + message::{ + ButtplugMessage, + ButtplugMessageFinalizer, + ButtplugMessageSpecVersion, + ButtplugMessageValidator, + }, +}; + +use getset::{CopyGetters, Getters}; +use serde::{Deserialize, Serialize}; + +#[derive( + Debug, + ButtplugMessage, + ButtplugMessageFinalizer, + PartialEq, + Eq, + Clone, + Getters, + CopyGetters, + Serialize, + Deserialize, +)] +pub struct ServerInfoV0 { + #[serde(rename = "Id")] + id: u32, + #[serde(rename = "MajorVersion")] + #[getset(get_copy = "pub")] + major_version: u32, + #[serde(rename = "MinorVersion")] + #[getset(get_copy = "pub")] + minor_version: u32, + #[serde(rename = "BuildVersion")] + #[getset(get_copy = "pub")] + build_version: u32, + #[serde(rename = "MessageVersion")] + #[getset(get_copy = "pub")] + message_version: ButtplugMessageSpecVersion, + #[serde(rename = "MaxPingTime")] + #[getset(get_copy = "pub")] + max_ping_time: u32, + #[serde(rename = "ServerName")] + #[getset(get = "pub")] + server_name: String, +} + +impl ServerInfoV0 { + pub fn new( + server_name: &str, + message_version: ButtplugMessageSpecVersion, + max_ping_time: u32, + ) -> Self { + Self { + id: 1, + major_version: 0, + minor_version: 0, + build_version: 0, + message_version, + max_ping_time, + server_name: server_name.to_string(), + } + } +} + +impl ButtplugMessageValidator for ServerInfoV0 { + fn is_valid(&self) -> Result<(), ButtplugMessageError> { + self.is_system_id(self.id) + } +} + +impl From for ServerInfoV0 { + fn from(msg: ServerInfoV2) -> Self { + let mut out_msg = Self::new( + msg.server_name(), + msg.message_version(), + msg.max_ping_time(), + ); + out_msg.set_id(msg.id()); + out_msg + } +} diff --git a/buttplug/src/core/message/single_motor_vibrate_cmd.rs b/crates/buttplug_server/src/message/v0/single_motor_vibrate_cmd.rs similarity index 69% rename from buttplug/src/core/message/single_motor_vibrate_cmd.rs rename to crates/buttplug_server/src/message/v0/single_motor_vibrate_cmd.rs index b4e364648..db6d38938 100644 --- a/buttplug/src/core/message/single_motor_vibrate_cmd.rs +++ b/crates/buttplug_server/src/message/v0/single_motor_vibrate_cmd.rs @@ -5,19 +5,34 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use super::*; +use buttplug_core::{ + errors::ButtplugMessageError, + message::{ + ButtplugDeviceMessage, + ButtplugMessage, + ButtplugMessageFinalizer, + ButtplugMessageValidator, + }, +}; use getset::CopyGetters; -#[cfg(feature = "serialize-json")] use serde::{Deserialize, Serialize}; -#[derive(Debug, ButtplugDeviceMessage, ButtplugMessageFinalizer, PartialEq, Clone, CopyGetters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] +#[derive( + Debug, + ButtplugDeviceMessage, + ButtplugMessageFinalizer, + PartialEq, + Clone, + CopyGetters, + Serialize, + Deserialize, +)] pub struct SingleMotorVibrateCmdV0 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] + #[serde(rename = "Id")] id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] + #[serde(rename = "DeviceIndex")] device_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "Speed"))] + #[serde(rename = "Speed")] #[getset(get_copy = "pub")] speed: f64, } diff --git a/crates/buttplug_server/src/message/v0/spec_enums.rs b/crates/buttplug_server/src/message/v0/spec_enums.rs new file mode 100644 index 000000000..f4ac08f39 --- /dev/null +++ b/crates/buttplug_server/src/message/v0/spec_enums.rs @@ -0,0 +1,91 @@ +use std::cmp::Ordering; + +use super::*; +use crate::message::RequestServerInfoV1; +use buttplug_core::{ + errors::ButtplugMessageError, + message::{ButtplugMessage, ButtplugMessageFinalizer, ButtplugMessageValidator, PingV0}, +}; + +use serde::{Deserialize, Serialize}; + +/// Represents all client-to-server messages in v0 of the Buttplug Spec +#[derive( + Debug, + Clone, + PartialEq, + ButtplugMessage, + ButtplugMessageValidator, + ButtplugMessageFinalizer, + FromSpecificButtplugMessage, + Serialize, + Deserialize, +)] +pub enum ButtplugClientMessageV0 { + Ping(PingV0), + // Handshake messages + // + // We use RequestServerInfoV1 here, as the only difference between v0 and v1 was passing the spec + // version. If the spec version doesn't exist, we automatically set the spec version to 0. + RequestServerInfo(RequestServerInfoV1), + // Device enumeration messages + StartScanning(StartScanningV0), + StopScanning(StopScanningV0), + RequestDeviceList(RequestDeviceListV0), + // Generic commands + StopAllDevices(StopAllDevicesV0), + StopDeviceCmd(StopDeviceCmdV0), + // Deprecated generic commands + SingleMotorVibrateCmd(SingleMotorVibrateCmdV0), + // Deprecated device specific commands + FleshlightLaunchFW12Cmd(FleshlightLaunchFW12CmdV0), + VorzeA10CycloneCmd(VorzeA10CycloneCmdV0), +} + +/// Represents all server-to-client messages in v0 of the Buttplug Spec +#[derive( + Debug, + Clone, + PartialEq, + ButtplugMessage, + ButtplugMessageValidator, + ButtplugMessageFinalizer, + Serialize, + Deserialize, +)] +pub enum ButtplugServerMessageV0 { + // Status messages + Ok(OkV0), + Error(ErrorV0), + // Handshake messages + ServerInfo(ServerInfoV0), + // Device enumeration messages + DeviceList(DeviceListV0), + DeviceAdded(DeviceAddedV0), + DeviceRemoved(DeviceRemovedV0), + ScanningFinished(ScanningFinishedV0), +} + +#[derive(Copy, Debug, Clone, PartialEq, Eq, Hash, Display, Serialize, Deserialize)] +pub enum ButtplugDeviceMessageNameV0 { + StopDeviceCmd, + // Deprecated generic commands + SingleMotorVibrateCmd, + // Deprecated device specific commands + FleshlightLaunchFW12Cmd, + LovenseCmd, + KiirooCmd, + VorzeA10CycloneCmd, +} + +impl PartialOrd for ButtplugDeviceMessageNameV0 { + fn partial_cmp(&self, other: &ButtplugDeviceMessageNameV0) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for ButtplugDeviceMessageNameV0 { + fn cmp(&self, other: &ButtplugDeviceMessageNameV0) -> Ordering { + self.to_string().cmp(&other.to_string()) + } +} diff --git a/buttplug/src/core/message/test.rs b/crates/buttplug_server/src/message/v0/test.rs similarity index 74% rename from buttplug/src/core/message/test.rs rename to crates/buttplug_server/src/message/v0/test.rs index 8731a0063..6c60f3782 100644 --- a/buttplug/src/core/message/test.rs +++ b/crates/buttplug_server/src/message/v0/test.rs @@ -5,21 +5,31 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use super::*; +use buttplug_core::{ + errors::ButtplugMessageError, + message::{ButtplugMessage, ButtplugMessageFinalizer, ButtplugMessageValidator}, +}; use getset::Getters; -#[cfg(feature = "serialize-json")] use serde::{Deserialize, Serialize}; #[derive( - Debug, Default, ButtplugMessage, ButtplugMessageFinalizer, Clone, PartialEq, Eq, Getters, + Debug, + Default, + ButtplugMessage, + ButtplugMessageFinalizer, + Clone, + PartialEq, + Eq, + Getters, + Serialize, + Deserialize, )] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] pub struct TestV0 { /// Message Id, used for matching message pairs in remote connection instances. - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] + #[serde(rename = "Id")] id: u32, /// Test string, which will be echoed back to client when sent to server. - #[cfg_attr(feature = "serialize-json", serde(rename = "TestString"))] + #[serde(rename = "TestString")] #[getset(get = "pub")] test_string: String, } diff --git a/buttplug/src/core/message/vorze_a10_cyclone_cmd.rs b/crates/buttplug_server/src/message/v0/vorze_a10_cyclone_cmd.rs similarity index 68% rename from buttplug/src/core/message/vorze_a10_cyclone_cmd.rs rename to crates/buttplug_server/src/message/v0/vorze_a10_cyclone_cmd.rs index 61e1b7f64..e522e49cc 100644 --- a/buttplug/src/core/message/vorze_a10_cyclone_cmd.rs +++ b/crates/buttplug_server/src/message/v0/vorze_a10_cyclone_cmd.rs @@ -5,24 +5,39 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use super::*; +use buttplug_core::{ + errors::ButtplugMessageError, + message::{ + ButtplugDeviceMessage, + ButtplugMessage, + ButtplugMessageFinalizer, + ButtplugMessageValidator, + }, +}; use getset::CopyGetters; -#[cfg(feature = "serialize-json")] use serde::{Deserialize, Serialize}; #[derive( - Debug, ButtplugDeviceMessage, ButtplugMessageFinalizer, Default, PartialEq, Eq, Clone, CopyGetters, + Debug, + ButtplugDeviceMessage, + ButtplugMessageFinalizer, + Default, + PartialEq, + Eq, + Clone, + CopyGetters, + Serialize, + Deserialize, )] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] pub struct VorzeA10CycloneCmdV0 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] + #[serde(rename = "Id")] id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] + #[serde(rename = "DeviceIndex")] device_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "Speed"))] + #[serde(rename = "Speed")] #[getset(get_copy = "pub")] speed: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "Clockwise"))] + #[serde(rename = "Clockwise")] #[getset(get_copy = "pub")] clockwise: bool, } diff --git a/crates/buttplug_server/src/message/v1/client_device_message_attributes.rs b/crates/buttplug_server/src/message/v1/client_device_message_attributes.rs new file mode 100644 index 000000000..6f8a2f42d --- /dev/null +++ b/crates/buttplug_server/src/message/v1/client_device_message_attributes.rs @@ -0,0 +1,65 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 getset::{Getters, Setters}; +use serde::{Deserialize, Serialize}; + +use crate::message::{v2::ClientDeviceMessageAttributesV2, v3::ClientDeviceMessageAttributesV3}; +use buttplug_core::message::DeviceFeature; + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct NullDeviceMessageAttributesV1 {} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Getters, Setters)] +pub struct ClientDeviceMessageAttributesV1 { + // Generic commands + #[getset(get = "pub")] + #[serde(rename = "VibrateCmd")] + #[serde(skip_serializing_if = "Option::is_none")] + pub(in crate::message) vibrate_cmd: Option, + #[getset(get = "pub")] + #[serde(rename = "RotateCmd")] + #[serde(skip_serializing_if = "Option::is_none")] + pub(in crate::message) rotate_cmd: Option, + #[getset(get = "pub")] + #[serde(rename = "LinearCmd")] + #[serde(skip_serializing_if = "Option::is_none")] + pub(in crate::message) linear_cmd: Option, + + // StopDeviceCmd always exists + #[getset(get = "pub")] + pub(in crate::message) stop_device_cmd: NullDeviceMessageAttributesV1, + + // Obsolete commands are only added post-serialization + #[getset(get = "pub")] + #[serde(skip_serializing_if = "Option::is_none")] + pub(in crate::message) single_motor_vibrate_cmd: Option, + #[getset(get = "pub")] + #[serde(skip_serializing_if = "Option::is_none")] + pub(in crate::message) fleshlight_launch_fw12_cmd: Option, + #[getset(get = "pub")] + #[serde(skip_serializing_if = "Option::is_none")] + pub(in crate::message) vorze_a10_cyclone_cmd: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Getters, Setters)] +pub struct GenericDeviceMessageAttributesV1 { + #[serde(rename = "FeatureCount")] + feature_count: u32, +} + +impl GenericDeviceMessageAttributesV1 { + pub fn new(feature_count: u32) -> Self { + Self { feature_count } + } +} + +impl From> for ClientDeviceMessageAttributesV1 { + fn from(value: Vec) -> Self { + ClientDeviceMessageAttributesV2::from(ClientDeviceMessageAttributesV3::from(value)).into() + } +} diff --git a/crates/buttplug_server/src/message/v1/device_added.rs b/crates/buttplug_server/src/message/v1/device_added.rs new file mode 100644 index 000000000..6ca2362cc --- /dev/null +++ b/crates/buttplug_server/src/message/v1/device_added.rs @@ -0,0 +1,59 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 crate::message::v0::{DeviceAddedV0, DeviceMessageInfoV0}; +use buttplug_core::{ + errors::ButtplugMessageError, + message::{ButtplugMessage, ButtplugMessageFinalizer, ButtplugMessageValidator}, +}; + +use super::{device_message_info::DeviceMessageInfoV1, ClientDeviceMessageAttributesV1}; + +use getset::{CopyGetters, Getters}; + +use serde::{Deserialize, Serialize}; + +#[derive( + ButtplugMessage, Clone, Debug, PartialEq, Eq, Getters, CopyGetters, Serialize, Deserialize, +)] +pub struct DeviceAddedV1 { + #[serde(rename = "Id")] + pub(in crate::message) id: u32, + #[serde(rename = "DeviceIndex")] + #[getset(get_copy = "pub")] + pub(in crate::message) device_index: u32, + #[serde(rename = "DeviceName")] + #[getset(get = "pub")] + pub(in crate::message) device_name: String, + #[serde(rename = "DeviceMessages")] + #[getset(get = "pub")] + pub(in crate::message) device_messages: ClientDeviceMessageAttributesV1, +} + +impl From for DeviceAddedV0 { + fn from(msg: DeviceAddedV1) -> Self { + let id = msg.id(); + let dmiv1 = DeviceMessageInfoV1::from(msg); + let dmiv0 = DeviceMessageInfoV0::from(dmiv1); + + Self { + id, + device_index: dmiv0.device_index(), + device_name: dmiv0.device_name().clone(), + device_messages: dmiv0.device_messages().clone(), + } + } +} + +impl ButtplugMessageValidator for DeviceAddedV1 { + fn is_valid(&self) -> Result<(), ButtplugMessageError> { + self.is_system_id(self.id) + } +} + +impl ButtplugMessageFinalizer for DeviceAddedV1 { +} diff --git a/crates/buttplug_server/src/message/v1/device_list.rs b/crates/buttplug_server/src/message/v1/device_list.rs new file mode 100644 index 000000000..b0cca5da6 --- /dev/null +++ b/crates/buttplug_server/src/message/v1/device_list.rs @@ -0,0 +1,66 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 super::device_message_info::DeviceMessageInfoV1; +use crate::message::{ + v0::{DeviceListV0, DeviceMessageInfoV0}, + v2::DeviceListV2, +}; +use buttplug_core::{ + errors::ButtplugMessageError, + message::{ButtplugMessage, ButtplugMessageFinalizer, ButtplugMessageValidator}, +}; +use getset::Getters; +use serde::{Deserialize, Serialize}; + +#[derive( + Default, Clone, Debug, PartialEq, Eq, ButtplugMessage, Getters, Serialize, Deserialize, +)] +pub struct DeviceListV1 { + #[serde(rename = "Id")] + pub(in crate::message) id: u32, + #[serde(rename = "Devices")] + #[getset(get = "pub")] + pub(in crate::message) devices: Vec, +} + +impl From for DeviceListV0 { + fn from(msg: DeviceListV1) -> Self { + let mut devices = vec![]; + for d in msg.devices() { + let dmiv1 = d.clone(); + devices.push(DeviceMessageInfoV0::from(dmiv1)); + } + Self { + id: msg.id(), + devices, + } + } +} + +impl From for DeviceListV1 { + fn from(msg: DeviceListV2) -> Self { + let mut devices = vec![]; + for d in msg.devices() { + let dmiv2 = d.clone(); + devices.push(DeviceMessageInfoV1::from(dmiv2)); + } + Self { + id: msg.id(), + devices, + } + } +} + +impl ButtplugMessageValidator for DeviceListV1 { + fn is_valid(&self) -> Result<(), ButtplugMessageError> { + self.is_not_system_id(self.id) + } +} + +impl ButtplugMessageFinalizer for DeviceListV1 { +} diff --git a/crates/buttplug_server/src/message/v1/device_message_info.rs b/crates/buttplug_server/src/message/v1/device_message_info.rs new file mode 100644 index 000000000..c9290dde6 --- /dev/null +++ b/crates/buttplug_server/src/message/v1/device_message_info.rs @@ -0,0 +1,76 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 getset::{CopyGetters, Getters}; +use serde::{Deserialize, Serialize}; + +use crate::message::{v0::DeviceMessageInfoV0, ButtplugDeviceMessageNameV0}; + +use super::{ClientDeviceMessageAttributesV1, DeviceAddedV1}; + +#[derive(Clone, Debug, PartialEq, Eq, Getters, CopyGetters, Serialize, Deserialize)] +pub struct DeviceMessageInfoV1 { + #[serde(rename = "DeviceIndex")] + #[getset(get_copy = "pub")] + pub(in crate::message) device_index: u32, + #[serde(rename = "DeviceName")] + #[getset(get = "pub")] + pub(in crate::message) device_name: String, + #[serde(rename = "DeviceMessages")] + #[getset(get = "pub")] + pub(in crate::message) device_messages: ClientDeviceMessageAttributesV1, +} + +impl From for DeviceMessageInfoV1 { + fn from(device_added: DeviceAddedV1) -> Self { + Self { + device_index: device_added.device_index(), + device_name: device_added.device_name().clone(), + device_messages: device_added.device_messages().clone(), + } + } +} + +impl From for DeviceMessageInfoV0 { + fn from(device_message_info: DeviceMessageInfoV1) -> Self { + // Convert to array of message types. + let mut device_messages: Vec = vec![]; + + device_messages.push(ButtplugDeviceMessageNameV0::StopDeviceCmd); + if device_message_info + .device_messages() + .single_motor_vibrate_cmd() + .is_some() + { + device_messages.push(ButtplugDeviceMessageNameV0::SingleMotorVibrateCmd); + } + if device_message_info + .device_messages() + .fleshlight_launch_fw12_cmd() + .is_some() + { + device_messages.push(ButtplugDeviceMessageNameV0::FleshlightLaunchFW12Cmd); + } + if device_message_info + .device_messages() + .vorze_a10_cyclone_cmd() + .is_some() + { + device_messages.push(ButtplugDeviceMessageNameV0::VorzeA10CycloneCmd); + } + + device_messages.sort(); + + // SingleMotorVibrateCmd is added as part of the V1 conversion, so we + // can expect we'll have it here. + Self { + device_name: device_message_info.device_name().clone(), + device_index: device_message_info.device_index(), + device_messages, + } + } +} diff --git a/crates/buttplug_server/src/message/v1/linear_cmd.rs b/crates/buttplug_server/src/message/v1/linear_cmd.rs new file mode 100644 index 000000000..7e03a0c1e --- /dev/null +++ b/crates/buttplug_server/src/message/v1/linear_cmd.rs @@ -0,0 +1,86 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 buttplug_core::{ + errors::ButtplugMessageError, + message::{ + ButtplugDeviceMessage, + ButtplugMessage, + ButtplugMessageFinalizer, + ButtplugMessageValidator, + }, +}; +use getset::{CopyGetters, Getters}; +use serde::{Deserialize, Serialize}; + +/// Move device to a certain position in a certain amount of time +#[derive(Debug, PartialEq, Clone, CopyGetters, Serialize, Deserialize)] +#[getset(get_copy = "pub")] +pub struct VectorSubcommandV1 { + #[serde(rename = "Index")] + index: u32, + #[serde(rename = "Duration")] + duration: u32, + #[serde(rename = "Position")] + position: f64, +} + +impl VectorSubcommandV1 { + pub fn new(index: u32, duration: u32, position: f64) -> Self { + Self { + index, + duration, + position, + } + } +} + +#[derive( + Debug, + ButtplugDeviceMessage, + ButtplugMessageFinalizer, + PartialEq, + Clone, + Getters, + Serialize, + Deserialize, +)] +pub struct LinearCmdV1 { + #[serde(rename = "Id")] + id: u32, + #[serde(rename = "DeviceIndex")] + device_index: u32, + #[serde(rename = "Vectors")] + #[getset(get = "pub")] + vectors: Vec, +} + +impl LinearCmdV1 { + pub fn new(device_index: u32, vectors: Vec) -> Self { + Self { + id: 1, + device_index, + vectors, + } + } +} + +impl ButtplugMessageValidator for LinearCmdV1 { + fn is_valid(&self) -> Result<(), ButtplugMessageError> { + self.is_not_system_id(self.id)?; + for vec in &self.vectors { + self.is_in_command_range( + vec.position, + format!( + "VectorSubcommand position {} for index {} is invalid, should be between 0.0 and 1.0", + vec.position, vec.index + ), + )?; + } + Ok(()) + } +} diff --git a/crates/buttplug_server/src/message/v1/mod.rs b/crates/buttplug_server/src/message/v1/mod.rs new file mode 100644 index 000000000..65d6ce582 --- /dev/null +++ b/crates/buttplug_server/src/message/v1/mod.rs @@ -0,0 +1,23 @@ +mod client_device_message_attributes; +mod device_added; +mod device_list; +mod device_message_info; +mod linear_cmd; +mod request_server_info; +mod rotate_cmd; +mod spec_enums; +mod vibrate_cmd; + +pub use client_device_message_attributes::{ + ClientDeviceMessageAttributesV1, + GenericDeviceMessageAttributesV1, + NullDeviceMessageAttributesV1, +}; +pub use device_added::DeviceAddedV1; +pub use device_list::DeviceListV1; +pub use device_message_info::DeviceMessageInfoV1; +pub use linear_cmd::{LinearCmdV1, VectorSubcommandV1}; +pub use request_server_info::RequestServerInfoV1; +pub use rotate_cmd::{RotateCmdV1, RotationSubcommandV1}; +pub use spec_enums::{ButtplugClientMessageV1, ButtplugServerMessageV1}; +pub use vibrate_cmd::{VibrateCmdV1, VibrateSubcommandV1}; diff --git a/buttplug/src/core/message/request_server_info.rs b/crates/buttplug_server/src/message/v1/request_server_info.rs similarity index 75% rename from buttplug/src/core/message/request_server_info.rs rename to crates/buttplug_server/src/message/v1/request_server_info.rs index 8b7b1fbca..8770c1751 100644 --- a/buttplug/src/core/message/request_server_info.rs +++ b/crates/buttplug_server/src/message/v1/request_server_info.rs @@ -5,31 +5,46 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use super::*; +use buttplug_core::{ + errors::ButtplugMessageError, + message::{ + ButtplugMessage, + ButtplugMessageFinalizer, + ButtplugMessageSpecVersion, + ButtplugMessageValidator, + }, +}; use getset::{CopyGetters, Getters}; -#[cfg(feature = "serialize-json")] use serde::{Deserialize, Serialize}; fn return_version0() -> ButtplugMessageSpecVersion { ButtplugMessageSpecVersion::Version0 } + +// For RequestServerInfo, serde will take care of invalid message versions from json, and internal +// representations of versions require using the version enum as a type bound. Therefore we do not +// need explicit content checking for the message. #[derive( - Debug, ButtplugMessage, ButtplugMessageFinalizer, Clone, PartialEq, Eq, Getters, CopyGetters, + Debug, + ButtplugMessage, + ButtplugMessageFinalizer, + Clone, + PartialEq, + Eq, + Getters, + CopyGetters, + Serialize, + Deserialize, )] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] pub struct RequestServerInfoV1 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] + #[serde(rename = "Id")] id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "ClientName"))] + #[serde(rename = "ClientName")] #[getset(get = "pub")] client_name: String, // Default for this message is set to 0, as this field didn't exist in the // first version of the protocol. - #[cfg_attr( - feature = "serialize-json", - serde(rename = "MessageVersion"), - serde(default = "return_version0") - )] + #[serde(rename = "MessageVersion", default = "return_version0")] #[getset(get_copy = "pub")] message_version: ButtplugMessageSpecVersion, } @@ -54,7 +69,6 @@ impl ButtplugMessageValidator for RequestServerInfoV1 { mod test { use super::{ButtplugMessageSpecVersion, RequestServerInfoV1}; - #[cfg(feature = "serialize-json")] #[test] fn test_request_server_info_version1_json_conversion() { let new_json = r#" @@ -75,7 +89,6 @@ mod test { ); } - #[cfg(feature = "serialize-json")] #[test] fn test_request_server_info_version0_json_conversion() { let old_json = r#" diff --git a/crates/buttplug_server/src/message/v1/rotate_cmd.rs b/crates/buttplug_server/src/message/v1/rotate_cmd.rs new file mode 100644 index 000000000..82264bf26 --- /dev/null +++ b/crates/buttplug_server/src/message/v1/rotate_cmd.rs @@ -0,0 +1,86 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 buttplug_core::{ + errors::ButtplugMessageError, + message::{ + ButtplugDeviceMessage, + ButtplugMessage, + ButtplugMessageFinalizer, + ButtplugMessageValidator, + }, +}; +pub use getset::{CopyGetters, Getters}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Clone, CopyGetters, Serialize, Deserialize)] +#[getset(get_copy = "pub")] +pub struct RotationSubcommandV1 { + #[serde(rename = "Index")] + index: u32, + #[serde(rename = "Speed")] + speed: f64, + #[serde(rename = "Clockwise")] + clockwise: bool, +} + +impl RotationSubcommandV1 { + pub fn new(index: u32, speed: f64, clockwise: bool) -> Self { + Self { + index, + speed, + clockwise, + } + } +} + +#[derive( + Debug, + ButtplugDeviceMessage, + ButtplugMessageFinalizer, + PartialEq, + Clone, + Getters, + Serialize, + Deserialize, +)] +pub struct RotateCmdV1 { + #[serde(rename = "Id")] + id: u32, + #[serde(rename = "DeviceIndex")] + device_index: u32, + #[getset(get = "pub")] + #[serde(rename = "Rotations")] + #[getset(get = "pub")] + rotations: Vec, +} + +impl RotateCmdV1 { + pub fn new(device_index: u32, rotations: Vec) -> Self { + Self { + id: 1, + device_index, + rotations, + } + } +} + +impl ButtplugMessageValidator for RotateCmdV1 { + fn is_valid(&self) -> Result<(), ButtplugMessageError> { + self.is_not_system_id(self.id)?; + for rotation in &self.rotations { + self.is_in_command_range( + rotation.speed, + format!( + "Speed {} for RotateCmd index {} is invalid. Speed should be a value between 0.0 and 1.0", + rotation.speed, rotation.index + ), + )?; + } + Ok(()) + } +} diff --git a/crates/buttplug_server/src/message/v1/spec_enums.rs b/crates/buttplug_server/src/message/v1/spec_enums.rs new file mode 100644 index 000000000..aeece4df9 --- /dev/null +++ b/crates/buttplug_server/src/message/v1/spec_enums.rs @@ -0,0 +1,172 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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::cmp::Ordering; + +use crate::message::v0::{ + ButtplugClientMessageV0, + ButtplugServerMessageV0, + FleshlightLaunchFW12CmdV0, + ServerInfoV0, + SingleMotorVibrateCmdV0, + VorzeA10CycloneCmdV0, +}; +use buttplug_core::{ + errors::ButtplugMessageError, + message::{ + ButtplugMessage, + ButtplugMessageFinalizer, + ButtplugMessageValidator, + DeviceRemovedV0, + ErrorV0, + OkV0, + PingV0, + RequestDeviceListV0, + ScanningFinishedV0, + StartScanningV0, + StopAllDevicesV0, + StopDeviceCmdV0, + StopScanningV0, + }, +}; +use serde::{Deserialize, Serialize}; + +use super::{ + DeviceAddedV1, + DeviceListV1, + LinearCmdV1, + RequestServerInfoV1, + RotateCmdV1, + VibrateCmdV1, +}; + +/// Represents all client-to-server messages in v1 of the Buttplug Spec +#[derive( + Debug, + Clone, + PartialEq, + ButtplugMessage, + ButtplugMessageValidator, + ButtplugMessageFinalizer, + FromSpecificButtplugMessage, + Serialize, + Deserialize, +)] +pub enum ButtplugClientMessageV1 { + // Handshake and server messages + RequestServerInfo(RequestServerInfoV1), + Ping(PingV0), + // Device enumeration messages + StartScanning(StartScanningV0), + StopScanning(StopScanningV0), + RequestDeviceList(RequestDeviceListV0), + // Generic commands + StopAllDevices(StopAllDevicesV0), + VibrateCmd(VibrateCmdV1), + LinearCmd(LinearCmdV1), + RotateCmd(RotateCmdV1), + StopDeviceCmd(StopDeviceCmdV0), + // Deprecated generic commands (not removed until v2) + SingleMotorVibrateCmd(SingleMotorVibrateCmdV0), + // Deprecated device specific commands (not removed until v2) + FleshlightLaunchFW12Cmd(FleshlightLaunchFW12CmdV0), + VorzeA10CycloneCmd(VorzeA10CycloneCmdV0), +} + +// No messages were changed or deprecated before v2, so we can convert all v0 messages to v1. +impl From for ButtplugClientMessageV1 { + fn from(value: ButtplugClientMessageV0) -> Self { + match value { + ButtplugClientMessageV0::Ping(m) => ButtplugClientMessageV1::Ping(m), + ButtplugClientMessageV0::RequestServerInfo(m) => { + ButtplugClientMessageV1::RequestServerInfo(m) + } + ButtplugClientMessageV0::StartScanning(m) => ButtplugClientMessageV1::StartScanning(m), + ButtplugClientMessageV0::StopScanning(m) => ButtplugClientMessageV1::StopScanning(m), + ButtplugClientMessageV0::RequestDeviceList(m) => { + ButtplugClientMessageV1::RequestDeviceList(m) + } + ButtplugClientMessageV0::StopAllDevices(m) => ButtplugClientMessageV1::StopAllDevices(m), + ButtplugClientMessageV0::StopDeviceCmd(m) => ButtplugClientMessageV1::StopDeviceCmd(m), + ButtplugClientMessageV0::FleshlightLaunchFW12Cmd(m) => { + ButtplugClientMessageV1::FleshlightLaunchFW12Cmd(m) + } + ButtplugClientMessageV0::SingleMotorVibrateCmd(m) => { + ButtplugClientMessageV1::SingleMotorVibrateCmd(m) + } + ButtplugClientMessageV0::VorzeA10CycloneCmd(m) => { + ButtplugClientMessageV1::VorzeA10CycloneCmd(m) + } + } + } +} + +/// Represents all server-to-client messages in v2 of the Buttplug Spec +#[derive( + Debug, + Clone, + PartialEq, + ButtplugMessage, + ButtplugMessageValidator, + ButtplugMessageFinalizer, + FromSpecificButtplugMessage, + Serialize, + Deserialize, +)] +pub enum ButtplugServerMessageV1 { + // Status messages + Ok(OkV0), + Error(ErrorV0), + // Handshake messages + ServerInfo(ServerInfoV0), + // Device enumeration messages + DeviceList(DeviceListV1), + DeviceAdded(DeviceAddedV1), + DeviceRemoved(DeviceRemovedV0), + ScanningFinished(ScanningFinishedV0), +} + +impl From for ButtplugServerMessageV0 { + fn from(value: ButtplugServerMessageV1) -> Self { + match value { + ButtplugServerMessageV1::Ok(m) => ButtplugServerMessageV0::Ok(m), + ButtplugServerMessageV1::Error(m) => ButtplugServerMessageV0::Error(m), + ButtplugServerMessageV1::ServerInfo(m) => ButtplugServerMessageV0::ServerInfo(m), + ButtplugServerMessageV1::DeviceRemoved(m) => ButtplugServerMessageV0::DeviceRemoved(m), + ButtplugServerMessageV1::ScanningFinished(m) => ButtplugServerMessageV0::ScanningFinished(m), + ButtplugServerMessageV1::DeviceAdded(m) => ButtplugServerMessageV0::DeviceAdded(m.into()), + ButtplugServerMessageV1::DeviceList(m) => ButtplugServerMessageV0::DeviceList(m.into()), + } + } +} + +#[derive(Copy, Debug, Clone, PartialEq, Eq, Hash, Display, Serialize, Deserialize)] +pub enum ButtplugDeviceMessageNameV1 { + VibrateCmd, + LinearCmd, + RotateCmd, + StopDeviceCmd, + // Deprecated generic commands + SingleMotorVibrateCmd, + // Deprecated device specific commands + FleshlightLaunchFW12Cmd, + LovenseCmd, + KiirooCmd, + VorzeA10CycloneCmd, +} + +impl PartialOrd for ButtplugDeviceMessageNameV1 { + fn partial_cmp(&self, other: &ButtplugDeviceMessageNameV1) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for ButtplugDeviceMessageNameV1 { + fn cmp(&self, other: &ButtplugDeviceMessageNameV1) -> Ordering { + self.to_string().cmp(&other.to_string()) + } +} diff --git a/buttplug/src/core/message/vibrate_cmd.rs b/crates/buttplug_server/src/message/v1/vibrate_cmd.rs similarity index 63% rename from buttplug/src/core/message/vibrate_cmd.rs rename to crates/buttplug_server/src/message/v1/vibrate_cmd.rs index ddea90871..6ef855880 100644 --- a/buttplug/src/core/message/vibrate_cmd.rs +++ b/crates/buttplug_server/src/message/v1/vibrate_cmd.rs @@ -5,18 +5,24 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use super::*; +use buttplug_core::{ + errors::ButtplugMessageError, + message::{ + ButtplugDeviceMessage, + ButtplugMessage, + ButtplugMessageFinalizer, + ButtplugMessageValidator, + }, +}; use getset::{CopyGetters, Getters}; -#[cfg(feature = "serialize-json")] use serde::{Deserialize, Serialize}; -#[derive(Debug, Default, PartialEq, Clone, CopyGetters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] +#[derive(Debug, Default, PartialEq, Clone, CopyGetters, Serialize, Deserialize)] #[getset(get_copy = "pub")] pub struct VibrateSubcommandV1 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Index"))] + #[serde(rename = "Index")] index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "Speed"))] + #[serde(rename = "Speed")] speed: f64, } @@ -27,15 +33,22 @@ impl VibrateSubcommandV1 { } #[derive( - Debug, Default, ButtplugDeviceMessage, ButtplugMessageFinalizer, PartialEq, Clone, Getters, + Debug, + Default, + ButtplugDeviceMessage, + ButtplugMessageFinalizer, + PartialEq, + Clone, + Getters, + Serialize, + Deserialize, )] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] pub struct VibrateCmdV1 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] + #[serde(rename = "Id")] id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] + #[serde(rename = "DeviceIndex")] device_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "Speeds"))] + #[serde(rename = "Speeds")] #[getset(get = "pub")] speeds: Vec, } diff --git a/crates/buttplug_server/src/message/v2/battery_level_cmd.rs b/crates/buttplug_server/src/message/v2/battery_level_cmd.rs new file mode 100644 index 000000000..8892db322 --- /dev/null +++ b/crates/buttplug_server/src/message/v2/battery_level_cmd.rs @@ -0,0 +1,98 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 crate::message::{ + checked_input_cmd::CheckedInputCmdV4, + ServerDeviceAttributes, + TryFromDeviceAttributes, +}; +use buttplug_core::{ + errors::{ButtplugDeviceError, ButtplugError, ButtplugMessageError}, + message::{ + ButtplugDeviceMessage, + ButtplugMessage, + ButtplugMessageFinalizer, + ButtplugMessageValidator, + InputCommandType, + InputType, + }, +}; +use serde::{Deserialize, Serialize}; + +/// Battery level request +#[derive( + Debug, + ButtplugDeviceMessage, + ButtplugMessageFinalizer, + PartialEq, + Eq, + Clone, + Serialize, + Deserialize, +)] +pub struct BatteryLevelCmdV2 { + #[serde(rename = "Id")] + id: u32, + #[serde(rename = "DeviceIndex")] + device_index: u32, +} + +impl BatteryLevelCmdV2 { + pub fn new(device_index: u32) -> Self { + Self { + id: 1, + device_index, + } + } +} + +impl ButtplugMessageValidator for BatteryLevelCmdV2 { + fn is_valid(&self) -> Result<(), ButtplugMessageError> { + self.is_not_system_id(self.id) + } +} + +impl TryFromDeviceAttributes for CheckedInputCmdV4 { + fn try_from_device_attributes( + msg: BatteryLevelCmdV2, + features: &ServerDeviceAttributes, + ) -> Result { + let battery_feature = features + .attrs_v2() + .battery_level_cmd() + .as_ref() + .ok_or(ButtplugError::from( + ButtplugDeviceError::DeviceConfigurationError( + "Device configuration does not have Battery sensor available.".to_owned(), + ), + ))? + .feature(); + + let feature_index = features + .features() + .iter() + .enumerate() + .find(|(_, p)| { + if let Some(sensor_map) = p.input() { + if sensor_map.contains_key(&InputType::Battery) { + return true; + } + } + false + }) + .expect("Already found matching battery feature, can unwrap this.") + .0; + + Ok(CheckedInputCmdV4::new( + msg.device_index(), + feature_index as u32, + InputType::Battery, + InputCommandType::Read, + battery_feature.id(), + )) + } +} diff --git a/buttplug/src/core/message/battery_level_reading.rs b/crates/buttplug_server/src/message/v2/battery_level_reading.rs similarity index 68% rename from buttplug/src/core/message/battery_level_reading.rs rename to crates/buttplug_server/src/message/v2/battery_level_reading.rs index e23bda46c..3035a2a96 100644 --- a/buttplug/src/core/message/battery_level_reading.rs +++ b/crates/buttplug_server/src/message/v2/battery_level_reading.rs @@ -5,20 +5,35 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use super::*; +use buttplug_core::{ + errors::ButtplugMessageError, + message::{ + ButtplugDeviceMessage, + ButtplugMessage, + ButtplugMessageFinalizer, + ButtplugMessageValidator, + }, +}; use getset::CopyGetters; -#[cfg(feature = "serialize-json")] use serde::{Deserialize, Serialize}; /// Battery level response -#[derive(Debug, ButtplugDeviceMessage, ButtplugMessageFinalizer, PartialEq, Clone, CopyGetters)] -#[cfg_attr(feature = "serialize-json", derive(Serialize, Deserialize))] +#[derive( + Debug, + ButtplugDeviceMessage, + ButtplugMessageFinalizer, + PartialEq, + Clone, + CopyGetters, + Serialize, + Deserialize, +)] pub struct BatteryLevelReadingV2 { - #[cfg_attr(feature = "serialize-json", serde(rename = "Id"))] + #[serde(rename = "Id")] id: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "DeviceIndex"))] + #[serde(rename = "DeviceIndex")] device_index: u32, - #[cfg_attr(feature = "serialize-json", serde(rename = "BatteryLevel"))] + #[serde(rename = "BatteryLevel")] #[getset(get_copy = "pub")] battery_level: f64, } diff --git a/crates/buttplug_server/src/message/v2/client_device_message_attributes.rs b/crates/buttplug_server/src/message/v2/client_device_message_attributes.rs new file mode 100644 index 000000000..fe3ca571f --- /dev/null +++ b/crates/buttplug_server/src/message/v2/client_device_message_attributes.rs @@ -0,0 +1,109 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 crate::message::{ + v1::{ + ClientDeviceMessageAttributesV1, + GenericDeviceMessageAttributesV1, + NullDeviceMessageAttributesV1, + }, + v3::ClientDeviceMessageAttributesV3, +}; +use buttplug_core::message::DeviceFeature; +use getset::{CopyGetters, Getters, Setters}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Getters, Setters)] +pub struct ClientDeviceMessageAttributesV2 { + // Generic commands + #[getset(get = "pub")] + #[serde(rename = "VibrateCmd")] + #[serde(skip_serializing_if = "Option::is_none")] + pub(in crate::message) vibrate_cmd: Option, + #[getset(get = "pub")] + #[serde(rename = "RotateCmd")] + #[serde(skip_serializing_if = "Option::is_none")] + pub(in crate::message) rotate_cmd: Option, + #[getset(get = "pub")] + #[serde(rename = "LinearCmd")] + #[serde(skip_serializing_if = "Option::is_none")] + pub(in crate::message) linear_cmd: Option, + #[getset(get = "pub")] + #[serde(rename = "BatteryLevelCmd")] + #[serde(skip_serializing_if = "Option::is_none")] + pub(in crate::message) battery_level_cmd: Option, + + // RSSILevel is added post-serialization (only for bluetooth devices) + #[getset(get = "pub")] + #[serde(rename = "RSSILevelCmd")] + #[serde(skip_serializing_if = "Option::is_none")] + pub(in crate::message) rssi_level_cmd: Option, + + // StopDeviceCmd always exists + #[getset(get = "pub")] + #[serde(rename = "StopDeviceCmd")] + pub(in crate::message) stop_device_cmd: NullDeviceMessageAttributesV1, + + // Needed to load from config for fallback, but unused here. + #[getset(get = "pub")] + #[serde(rename = "FleshlightLaunchFW12Cmd")] + #[serde(skip)] + pub(in crate::message) fleshlight_launch_fw12_cmd: Option, + #[getset(get = "pub")] + #[serde(rename = "VorzeA10CycloneCmd")] + #[serde(skip)] + pub(in crate::message) vorze_a10_cyclone_cmd: Option, +} + +impl From for ClientDeviceMessageAttributesV1 { + fn from(other: ClientDeviceMessageAttributesV2) -> Self { + Self { + vibrate_cmd: other + .vibrate_cmd() + .as_ref() + .map(|x| GenericDeviceMessageAttributesV1::from(x.clone())), + rotate_cmd: other + .rotate_cmd() + .as_ref() + .map(|x| GenericDeviceMessageAttributesV1::from(x.clone())), + linear_cmd: other + .linear_cmd() + .as_ref() + .map(|x| GenericDeviceMessageAttributesV1::from(x.clone())), + stop_device_cmd: other.stop_device_cmd().clone(), + fleshlight_launch_fw12_cmd: other.fleshlight_launch_fw12_cmd().clone(), + vorze_a10_cyclone_cmd: other.vorze_a10_cyclone_cmd().clone(), + single_motor_vibrate_cmd: if other.vibrate_cmd().is_some() { + Some(NullDeviceMessageAttributesV1::default()) + } else { + None + }, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Getters, CopyGetters, Setters)] +pub struct GenericDeviceMessageAttributesV2 { + #[getset(get_copy = "pub")] + #[serde(rename = "FeatureCount")] + pub(in crate::message) feature_count: u32, + #[getset(get = "pub")] + #[serde(rename = "StepCount")] + pub(in crate::message) step_count: Vec, +} + +impl From for GenericDeviceMessageAttributesV1 { + fn from(attributes: GenericDeviceMessageAttributesV2) -> Self { + Self::new(attributes.feature_count()) + } +} + +impl From> for ClientDeviceMessageAttributesV2 { + fn from(value: Vec) -> Self { + ClientDeviceMessageAttributesV3::from(value).into() + } +} diff --git a/crates/buttplug_server/src/message/v2/device_added.rs b/crates/buttplug_server/src/message/v2/device_added.rs new file mode 100644 index 000000000..41bc776e8 --- /dev/null +++ b/crates/buttplug_server/src/message/v2/device_added.rs @@ -0,0 +1,58 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 super::{device_message_info::DeviceMessageInfoV2, ClientDeviceMessageAttributesV2}; +use crate::message::v1::{DeviceAddedV1, DeviceMessageInfoV1}; +use buttplug_core::{ + errors::ButtplugMessageError, + message::{ButtplugMessage, ButtplugMessageFinalizer, ButtplugMessageValidator}, +}; + +use getset::{CopyGetters, Getters}; + +use serde::{Deserialize, Serialize}; + +#[derive( + ButtplugMessage, Clone, Debug, PartialEq, Eq, Getters, CopyGetters, Serialize, Deserialize, +)] +pub struct DeviceAddedV2 { + #[serde(rename = "Id")] + pub(in crate::message) id: u32, + #[serde(rename = "DeviceIndex")] + #[getset(get_copy = "pub")] + pub(in crate::message) device_index: u32, + #[serde(rename = "DeviceName")] + #[getset(get = "pub")] + pub(in crate::message) device_name: String, + #[serde(rename = "DeviceMessages")] + #[getset(get = "pub")] + pub(in crate::message) device_messages: ClientDeviceMessageAttributesV2, +} + +impl From for DeviceAddedV1 { + fn from(msg: DeviceAddedV2) -> Self { + let id = msg.id(); + let dmiv2 = DeviceMessageInfoV2::from(msg); + let dmiv1 = DeviceMessageInfoV1::from(dmiv2); + + Self { + id, + device_index: dmiv1.device_index(), + device_name: dmiv1.device_name().clone(), + device_messages: dmiv1.device_messages().clone(), + } + } +} + +impl ButtplugMessageValidator for DeviceAddedV2 { + fn is_valid(&self) -> Result<(), ButtplugMessageError> { + self.is_system_id(self.id) + } +} + +impl ButtplugMessageFinalizer for DeviceAddedV2 { +} diff --git a/crates/buttplug_server/src/message/v2/device_list.rs b/crates/buttplug_server/src/message/v2/device_list.rs new file mode 100644 index 000000000..5f5d0d3fd --- /dev/null +++ b/crates/buttplug_server/src/message/v2/device_list.rs @@ -0,0 +1,34 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 super::device_message_info::DeviceMessageInfoV2; +use buttplug_core::{ + errors::ButtplugMessageError, + message::{ButtplugMessage, ButtplugMessageFinalizer, ButtplugMessageValidator}, +}; +use getset::Getters; +use serde::{Deserialize, Serialize}; + +#[derive( + Default, Clone, Debug, PartialEq, Eq, ButtplugMessage, Getters, Serialize, Deserialize, +)] +pub struct DeviceListV2 { + #[serde(rename = "Id")] + pub(in crate::message) id: u32, + #[serde(rename = "Devices")] + #[getset(get = "pub")] + pub(in crate::message) devices: Vec, +} + +impl ButtplugMessageValidator for DeviceListV2 { + fn is_valid(&self) -> Result<(), ButtplugMessageError> { + self.is_not_system_id(self.id) + } +} + +impl ButtplugMessageFinalizer for DeviceListV2 { +} diff --git a/crates/buttplug_server/src/message/v2/device_message_info.rs b/crates/buttplug_server/src/message/v2/device_message_info.rs new file mode 100644 index 000000000..b1661b537 --- /dev/null +++ b/crates/buttplug_server/src/message/v2/device_message_info.rs @@ -0,0 +1,49 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 crate::message::v1::{ClientDeviceMessageAttributesV1, DeviceMessageInfoV1}; + +use super::*; +use getset::{CopyGetters, Getters}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Getters, CopyGetters, Serialize, Deserialize)] +pub struct DeviceMessageInfoV2 { + #[serde(rename = "DeviceIndex")] + #[getset(get_copy = "pub")] + pub(in crate::message) device_index: u32, + #[serde(rename = "DeviceName")] + #[getset(get = "pub")] + pub(in crate::message) device_name: String, + #[serde(rename = "DeviceMessages")] + #[getset(get = "pub")] + pub(in crate::message) device_messages: ClientDeviceMessageAttributesV2, +} + +impl From for DeviceMessageInfoV1 { + fn from(device_message_info: DeviceMessageInfoV2) -> Self { + // No structural difference, it's all content changes + Self { + device_index: device_message_info.device_index(), + device_name: device_message_info.device_name().clone(), + device_messages: ClientDeviceMessageAttributesV1::from( + device_message_info.device_messages().clone(), + ), + } + } +} + +impl From for DeviceMessageInfoV2 { + fn from(device_added: DeviceAddedV2) -> Self { + // No structural difference, it's all content changes + Self { + device_index: device_added.device_index(), + device_name: device_added.device_name().clone(), + device_messages: device_added.device_messages().clone(), + } + } +} diff --git a/crates/buttplug_server/src/message/v2/mod.rs b/crates/buttplug_server/src/message/v2/mod.rs new file mode 100644 index 000000000..93899cd31 --- /dev/null +++ b/crates/buttplug_server/src/message/v2/mod.rs @@ -0,0 +1,27 @@ +mod battery_level_cmd; +mod battery_level_reading; +mod client_device_message_attributes; +mod device_added; +mod device_list; +mod device_message_info; +mod server_device_message_attributes; +mod server_info; +mod spec_enums; + +pub use { + battery_level_cmd::BatteryLevelCmdV2, + battery_level_reading::BatteryLevelReadingV2, + client_device_message_attributes::{ + ClientDeviceMessageAttributesV2, + GenericDeviceMessageAttributesV2, + }, + device_added::DeviceAddedV2, + device_list::DeviceListV2, + device_message_info::DeviceMessageInfoV2, + server_device_message_attributes::{ + ServerDeviceMessageAttributesV2, + ServerGenericDeviceMessageAttributesV2, + }, + server_info::ServerInfoV2, + spec_enums::{ButtplugClientMessageV2, ButtplugDeviceMessageNameV2, ButtplugServerMessageV2}, +}; diff --git a/crates/buttplug_server/src/message/v2/server_device_message_attributes.rs b/crates/buttplug_server/src/message/v2/server_device_message_attributes.rs new file mode 100644 index 000000000..3b74976f9 --- /dev/null +++ b/crates/buttplug_server/src/message/v2/server_device_message_attributes.rs @@ -0,0 +1,167 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 crate::message::{ + v1::NullDeviceMessageAttributesV1, + ServerDeviceMessageAttributesV3, + ServerGenericDeviceMessageAttributesV3, +}; +use buttplug_core::message::{InputType, OutputType}; + +use buttplug_server_device_config::ServerDeviceFeature; + +use getset::{CopyGetters, Getters, Setters}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Getters, Setters)] +pub struct ServerDeviceMessageAttributesV2 { + // Generic commands + #[getset(get = "pub")] + #[serde(rename = "VibrateCmd")] + #[serde(skip_serializing_if = "Option::is_none")] + pub(in crate::message) vibrate_cmd: Option, + #[getset(get = "pub")] + #[serde(rename = "RotateCmd")] + #[serde(skip_serializing_if = "Option::is_none")] + pub(in crate::message) rotate_cmd: Option, + #[getset(get = "pub")] + #[serde(rename = "LinearCmd")] + #[serde(skip_serializing_if = "Option::is_none")] + pub(in crate::message) linear_cmd: Option, + #[getset(get = "pub")] + #[serde(rename = "BatteryLevelCmd")] + #[serde(skip_serializing_if = "Option::is_none")] + pub(in crate::message) battery_level_cmd: Option, + + // RSSILevel is added post-serialization (only for bluetooth devices) + #[getset(get = "pub")] + #[serde(rename = "RSSILevelCmd")] + #[serde(skip_serializing_if = "Option::is_none")] + pub(in crate::message) rssi_level_cmd: Option, + + // StopDeviceCmd always exists + #[getset(get = "pub")] + #[serde(rename = "StopDeviceCmd")] + pub(in crate::message) stop_device_cmd: NullDeviceMessageAttributesV1, + + // Needed to load from config for fallback, but unused here. + #[getset(get = "pub")] + #[serde(rename = "FleshlightLaunchFW12Cmd")] + #[serde(skip)] + pub(in crate::message) fleshlight_launch_fw12_cmd: Option, + #[getset(get = "pub")] + #[serde(rename = "VorzeA10CycloneCmd")] + #[serde(skip)] + pub(in crate::message) vorze_a10_cyclone_cmd: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Getters, CopyGetters, Setters)] +pub struct ServerGenericDeviceMessageAttributesV2 { + #[getset(get_copy = "pub")] + #[serde(rename = "FeatureCount")] + pub(in crate::message) feature_count: u32, + #[getset(get = "pub")] + #[serde(rename = "StepCount")] + pub(in crate::message) step_count: Vec, + #[getset(get = "pub")] + #[serde(skip)] + pub(in crate::message) features: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, Getters, Setters)] +pub struct ServerSensorDeviceMessageAttributesV2 { + #[getset(get = "pub")] + #[serde(skip)] + feature: ServerDeviceFeature, +} + +impl ServerSensorDeviceMessageAttributesV2 { + pub fn new(feature: &ServerDeviceFeature) -> Self { + Self { + feature: feature.clone(), + } + } +} + +impl From> for ServerDeviceMessageAttributesV2 { + fn from(value: Vec) -> Self { + ServerDeviceMessageAttributesV3::from(value).into() + } +} + +pub fn vibrate_cmd_from_scalar_cmd( + attributes_vec: &[ServerGenericDeviceMessageAttributesV3], +) -> ServerGenericDeviceMessageAttributesV2 { + let mut feature_count = 0u32; + let mut step_count = vec![]; + let mut features = vec![]; + for attr in attributes_vec { + if *attr.actuator_type() == OutputType::Vibrate { + feature_count += 1; + step_count.push(*attr.step_count()); + features.push(attr.feature().clone()); + } + } + ServerGenericDeviceMessageAttributesV2 { + feature_count, + step_count, + features, + } +} + +impl From for ServerDeviceMessageAttributesV2 { + fn from(other: ServerDeviceMessageAttributesV3) -> Self { + Self { + vibrate_cmd: other + .scalar_cmd() + .as_ref() + .map(|x| vibrate_cmd_from_scalar_cmd(x)) + .filter(|x| x.feature_count() != 0), + rotate_cmd: other + .rotate_cmd() + .as_ref() + .map(|x| ServerGenericDeviceMessageAttributesV2::from(x.clone())), + linear_cmd: other + .linear_cmd() + .as_ref() + .map(|x| ServerGenericDeviceMessageAttributesV2::from(x.clone())), + battery_level_cmd: { + if let Some(sensor_info) = other.sensor_read_cmd() { + sensor_info + .iter() + .find(|x| *x.sensor_type() == InputType::Battery) + .map(|attr| ServerSensorDeviceMessageAttributesV2::new(attr.feature())) + } else { + None + } + }, + rssi_level_cmd: { + if let Some(sensor_info) = other.sensor_read_cmd() { + sensor_info + .iter() + .find(|x| *x.sensor_type() == InputType::Rssi) + .map(|attr| ServerSensorDeviceMessageAttributesV2::new(attr.feature())) + } else { + None + } + }, + stop_device_cmd: other.stop_device_cmd().clone(), + fleshlight_launch_fw12_cmd: other.fleshlight_launch_fw12_cmd().clone(), + vorze_a10_cyclone_cmd: other.vorze_a10_cyclone_cmd().clone(), + } + } +} + +impl From> for ServerGenericDeviceMessageAttributesV2 { + fn from(attributes_vec: Vec) -> Self { + Self { + feature_count: attributes_vec.len() as u32, + step_count: attributes_vec.iter().map(|x| *x.step_count()).collect(), + features: attributes_vec.iter().map(|x| x.feature().clone()).collect(), + } + } +} diff --git a/crates/buttplug_server/src/message/v2/server_info.rs b/crates/buttplug_server/src/message/v2/server_info.rs new file mode 100644 index 000000000..ecd252ea9 --- /dev/null +++ b/crates/buttplug_server/src/message/v2/server_info.rs @@ -0,0 +1,77 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 buttplug_core::{ + errors::ButtplugMessageError, + message::{ + ButtplugMessage, + ButtplugMessageFinalizer, + ButtplugMessageSpecVersion, + ButtplugMessageValidator, + ServerInfoV4, + }, +}; +use getset::{CopyGetters, Getters}; +use serde::{Deserialize, Serialize}; + +#[derive( + Debug, + ButtplugMessage, + ButtplugMessageFinalizer, + PartialEq, + Eq, + Clone, + Getters, + CopyGetters, + Serialize, + Deserialize, +)] +pub struct ServerInfoV2 { + #[serde(rename = "Id")] + id: u32, + #[serde(rename = "MessageVersion")] + #[getset(get_copy = "pub")] + message_version: ButtplugMessageSpecVersion, + #[serde(rename = "MaxPingTime")] + #[getset(get_copy = "pub")] + max_ping_time: u32, + #[serde(rename = "ServerName")] + #[getset(get = "pub")] + server_name: String, +} + +impl ServerInfoV2 { + pub fn new( + server_name: &str, + message_version: ButtplugMessageSpecVersion, + max_ping_time: u32, + ) -> Self { + Self { + id: 1, + message_version, + max_ping_time, + server_name: server_name.to_string(), + } + } +} + +impl ButtplugMessageValidator for ServerInfoV2 { + fn is_valid(&self) -> Result<(), ButtplugMessageError> { + self.is_not_system_id(self.id) + } +} + +impl From for ServerInfoV2 { + fn from(value: ServerInfoV4) -> Self { + Self { + id: value.id(), + server_name: value.server_name().clone(), + message_version: value.protocol_version_major(), + max_ping_time: value.max_ping_time(), + } + } +} diff --git a/crates/buttplug_server/src/message/v2/spec_enums.rs b/crates/buttplug_server/src/message/v2/spec_enums.rs new file mode 100644 index 000000000..cc3d03675 --- /dev/null +++ b/crates/buttplug_server/src/message/v2/spec_enums.rs @@ -0,0 +1,163 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 crate::message::v1::{ + ButtplugClientMessageV1, + ButtplugServerMessageV1, + LinearCmdV1, + RequestServerInfoV1, + RotateCmdV1, + VibrateCmdV1, +}; +use buttplug_core::{ + errors::{ButtplugError, ButtplugMessageError}, + message::{ + ButtplugMessage, + ButtplugMessageFinalizer, + ButtplugMessageValidator, + DeviceRemovedV0, + ErrorV0, + OkV0, + PingV0, + RequestDeviceListV0, + ScanningFinishedV0, + StartScanningV0, + StopAllDevicesV0, + StopDeviceCmdV0, + StopScanningV0, + }, +}; +use serde::{Deserialize, Serialize}; + +use super::{BatteryLevelCmdV2, BatteryLevelReadingV2, DeviceAddedV2, DeviceListV2, ServerInfoV2}; + +/// Represents all client-to-server messages in v2 of the Buttplug Spec +#[derive( + Debug, + Clone, + PartialEq, + ButtplugMessage, + ButtplugMessageValidator, + ButtplugMessageFinalizer, + FromSpecificButtplugMessage, + Serialize, + Deserialize, +)] +pub enum ButtplugClientMessageV2 { + // Handshake messages + RequestServerInfo(RequestServerInfoV1), + Ping(PingV0), + // Device enumeration messages + StartScanning(StartScanningV0), + StopScanning(StopScanningV0), + RequestDeviceList(RequestDeviceListV0), + // Generic commands + StopAllDevices(StopAllDevicesV0), + VibrateCmd(VibrateCmdV1), + LinearCmd(LinearCmdV1), + RotateCmd(RotateCmdV1), + StopDeviceCmd(StopDeviceCmdV0), + // Sensor commands + BatteryLevelCmd(BatteryLevelCmdV2), +} + +// For v1 to v2, several messages were deprecated. Throw errors when trying to convert those. +impl TryFrom for ButtplugClientMessageV2 { + type Error = ButtplugMessageError; + + fn try_from(value: ButtplugClientMessageV1) -> Result { + match value { + ButtplugClientMessageV1::Ping(m) => Ok(ButtplugClientMessageV2::Ping(m.clone())), + ButtplugClientMessageV1::RequestServerInfo(m) => { + Ok(ButtplugClientMessageV2::RequestServerInfo(m.clone())) + } + ButtplugClientMessageV1::StartScanning(m) => { + Ok(ButtplugClientMessageV2::StartScanning(m.clone())) + } + ButtplugClientMessageV1::StopScanning(m) => { + Ok(ButtplugClientMessageV2::StopScanning(m.clone())) + } + ButtplugClientMessageV1::RequestDeviceList(m) => { + Ok(ButtplugClientMessageV2::RequestDeviceList(m.clone())) + } + ButtplugClientMessageV1::StopAllDevices(m) => { + Ok(ButtplugClientMessageV2::StopAllDevices(m.clone())) + } + ButtplugClientMessageV1::StopDeviceCmd(m) => { + Ok(ButtplugClientMessageV2::StopDeviceCmd(m.clone())) + } + ButtplugClientMessageV1::VibrateCmd(m) => Ok(ButtplugClientMessageV2::VibrateCmd(m.clone())), + ButtplugClientMessageV1::LinearCmd(m) => Ok(ButtplugClientMessageV2::LinearCmd(m.clone())), + ButtplugClientMessageV1::RotateCmd(m) => Ok(ButtplugClientMessageV2::RotateCmd(m.clone())), + ButtplugClientMessageV1::FleshlightLaunchFW12Cmd(_) => { + // Direct access to FleshlightLaunchFW12Cmd could cause some devices to break via rapid + // changes of position/speed. Yes, some Kiiroo devices really *are* that fragile. + Err(ButtplugMessageError::MessageConversionError("FleshlightLaunchFW12Cmd is not implemented. Please update the client software to use a newer command".to_owned())) + } + _ => Err(ButtplugMessageError::MessageConversionError(format!( + "Cannot convert message {value:?} to current message spec while lacking state." + ))), + } + } +} + +/// Represents all server-to-client messages in v2 of the Buttplug Spec +#[derive( + Debug, + Clone, + PartialEq, + ButtplugMessage, + ButtplugMessageValidator, + ButtplugMessageFinalizer, + FromSpecificButtplugMessage, + Serialize, + Deserialize, +)] +pub enum ButtplugServerMessageV2 { + // Status messages + Ok(OkV0), + Error(ErrorV0), + // Handshake messages + ServerInfo(ServerInfoV2), + // Device enumeration messages + DeviceList(DeviceListV2), + DeviceAdded(DeviceAddedV2), + DeviceRemoved(DeviceRemovedV0), + ScanningFinished(ScanningFinishedV0), + // Sensor commands + BatteryLevelReading(BatteryLevelReadingV2), +} + +impl From for ButtplugServerMessageV1 { + fn from(value: ButtplugServerMessageV2) -> Self { + match value { + ButtplugServerMessageV2::Ok(m) => ButtplugServerMessageV1::Ok(m), + ButtplugServerMessageV2::Error(m) => ButtplugServerMessageV1::Error(m), + ButtplugServerMessageV2::ServerInfo(m) => ButtplugServerMessageV1::ServerInfo(m.into()), + ButtplugServerMessageV2::DeviceRemoved(m) => ButtplugServerMessageV1::DeviceRemoved(m), + ButtplugServerMessageV2::ScanningFinished(m) => ButtplugServerMessageV1::ScanningFinished(m), + ButtplugServerMessageV2::DeviceAdded(m) => ButtplugServerMessageV1::DeviceAdded(m.into()), + ButtplugServerMessageV2::DeviceList(m) => ButtplugServerMessageV1::DeviceList(m.into()), + ButtplugServerMessageV2::BatteryLevelReading(_) => { + ButtplugServerMessageV1::Error(ErrorV0::from(ButtplugError::from( + ButtplugMessageError::MessageConversionError( + "BatteryLevelReading cannot be converted to Buttplug Message Spec V1".to_owned(), + ), + ))) + } + } + } +} + +#[derive(Copy, Debug, Clone, PartialEq, Eq, Hash, Display, Serialize, Deserialize)] +pub enum ButtplugDeviceMessageNameV2 { + LinearCmd, + RotateCmd, + StopDeviceCmd, + VibrateCmd, + BatteryLevelCmd, +} 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 new file mode 100644 index 000000000..3e8d95d16 --- /dev/null +++ b/crates/buttplug_server/src/message/v3/client_device_message_attributes.rs @@ -0,0 +1,368 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 crate::message::{ + v1::NullDeviceMessageAttributesV1, + v2::{ClientDeviceMessageAttributesV2, GenericDeviceMessageAttributesV2}, +}; +use buttplug_core::message::{DeviceFeature, InputCommandType, InputType, OutputType}; +use getset::{Getters, MutGetters, Setters}; +use serde::{ser::SerializeSeq, Deserialize, Serialize, Serializer}; +use std::ops::RangeInclusive; + +// This will look almost exactly like ServerDeviceMessageAttributes. However, it will only contain +// information we want the client to know, i.e. step counts versus specific step ranges. This is +// what will be sent to the client as part of DeviceAdded/DeviceList messages. It should not be used +// for outside configuration/serialization, rather it should be a subset of that information. +// +// 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. +#[derive( + Clone, Debug, Default, PartialEq, Getters, MutGetters, Setters, Serialize, Deserialize, +)] +pub struct ClientDeviceMessageAttributesV3 { + // Generic commands + #[getset(get = "pub", get_mut = "pub(super)")] + #[serde(rename = "ScalarCmd")] + #[serde(skip_serializing_if = "Option::is_none")] + pub(in crate::message) scalar_cmd: Option>, + #[getset(get = "pub", get_mut = "pub(super)")] + #[serde(rename = "RotateCmd")] + #[serde(skip_serializing_if = "Option::is_none")] + pub(in crate::message) rotate_cmd: Option>, + #[getset(get = "pub", get_mut = "pub(super)")] + #[serde(rename = "LinearCmd")] + #[serde(skip_serializing_if = "Option::is_none")] + pub(in crate::message) linear_cmd: Option>, + + // Sensor Messages + #[getset(get = "pub")] + #[serde(rename = "SensorReadCmd")] + #[serde(skip_serializing_if = "Option::is_none")] + pub(in crate::message) sensor_read_cmd: Option>, + #[getset(get = "pub")] + #[serde(rename = "SensorSubscribeCmd")] + #[serde(skip_serializing_if = "Option::is_none")] + pub(in crate::message) sensor_subscribe_cmd: Option>, + + // StopDeviceCmd always exists + #[getset(get = "pub")] + #[serde(rename = "StopDeviceCmd")] + #[serde(skip_deserializing)] + pub(in crate::message) stop_device_cmd: NullDeviceMessageAttributesV1, + + // Needed to load from config for fallback, but unused here. + #[getset(get = "pub")] + #[serde(rename = "FleshlightLaunchFW12Cmd")] + #[serde(skip_serializing)] + pub(in crate::message) fleshlight_launch_fw12_cmd: Option, + #[getset(get = "pub")] + #[serde(rename = "VorzeA10CycloneCmd")] + #[serde(skip_serializing)] + pub(in crate::message) vorze_a10_cyclone_cmd: Option, +} + +pub fn vibrate_cmd_from_scalar_cmd( + attributes_vec: &[ClientGenericDeviceMessageAttributesV3], +) -> GenericDeviceMessageAttributesV2 { + let mut feature_count = 0u32; + let mut step_count = vec![]; + for attr in attributes_vec { + if *attr.actuator_type() == OutputType::Vibrate { + feature_count += 1; + step_count.push(*attr.step_count()); + } + } + GenericDeviceMessageAttributesV2 { + feature_count, + step_count, + } +} + +impl From for ClientDeviceMessageAttributesV2 { + fn from(other: ClientDeviceMessageAttributesV3) -> Self { + Self { + vibrate_cmd: other + .scalar_cmd() + .as_ref() + .map(|x| vibrate_cmd_from_scalar_cmd(x)) + .filter(|x| x.feature_count() != 0), + rotate_cmd: other + .rotate_cmd() + .as_ref() + .map(|x| GenericDeviceMessageAttributesV2::from(x.clone())), + linear_cmd: other + .linear_cmd() + .as_ref() + .map(|x| GenericDeviceMessageAttributesV2::from(x.clone())), + battery_level_cmd: { + if let Some(sensor_info) = other.sensor_read_cmd() { + if sensor_info + .iter() + .any(|x| *x.sensor_type() == InputType::Battery) + { + Some(NullDeviceMessageAttributesV1::default()) + } else { + None + } + } else { + None + } + }, + rssi_level_cmd: { + if let Some(sensor_info) = other.sensor_read_cmd() { + if sensor_info + .iter() + .any(|x| *x.sensor_type() == InputType::Rssi) + { + Some(NullDeviceMessageAttributesV1::default()) + } else { + None + } + } else { + None + } + }, + stop_device_cmd: other.stop_device_cmd().clone(), + fleshlight_launch_fw12_cmd: other.fleshlight_launch_fw12_cmd().clone(), + vorze_a10_cyclone_cmd: other.vorze_a10_cyclone_cmd().clone(), + } + } +} + +impl ClientDeviceMessageAttributesV3 { + pub fn finalize(&mut self) { + if let Some(scalar_attrs) = &mut self.scalar_cmd { + for (i, attr) in scalar_attrs.iter_mut().enumerate() { + attr.index = i as u32; + } + } + if let Some(sensor_read_attrs) = &mut self.sensor_read_cmd { + for (i, attr) in sensor_read_attrs.iter_mut().enumerate() { + attr.index = i as u32; + } + } + if let Some(sensor_subscribe_attrs) = &mut self.sensor_subscribe_cmd { + for (i, attr) in sensor_subscribe_attrs.iter_mut().enumerate() { + attr.index = i as u32; + } + } + } +} + +fn unspecified_feature() -> String { + "N/A".to_string() +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Getters, Setters)] +pub struct ClientGenericDeviceMessageAttributesV3 { + #[getset(get = "pub")] + #[serde(rename = "FeatureDescriptor")] + #[serde(default = "unspecified_feature")] + pub(in crate::message) feature_descriptor: String, + #[getset(get = "pub")] + #[serde(rename = "ActuatorType")] + pub(in crate::message) actuator_type: OutputType, + #[serde(rename = "StepCount")] + #[getset(get = "pub")] + pub(in crate::message) step_count: u32, + // TODO This needs to actually be part of the device info relayed to the client in spec v4. + #[getset(get = "pub")] + #[serde(skip, default)] + pub(in crate::message) index: u32, +} + +impl From> for GenericDeviceMessageAttributesV2 { + fn from(attributes_vec: Vec) -> Self { + Self { + feature_count: attributes_vec.len() as u32, + step_count: attributes_vec.iter().map(|x| *x.step_count()).collect(), + } + } +} + +impl ClientGenericDeviceMessageAttributesV3 { + pub fn new(feature_descriptor: &str, step_count: u32, actuator_type: OutputType) -> Self { + Self { + feature_descriptor: feature_descriptor.to_owned(), + actuator_type, + step_count, + index: 0, + } + } +} + +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()])?; + } + seq.end() +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Getters, Setters)] +pub struct SensorDeviceMessageAttributesV3 { + #[getset(get = "pub")] + #[serde(rename = "FeatureDescriptor")] + pub(in crate::message) feature_descriptor: String, + #[getset(get = "pub")] + #[serde(rename = "SensorType")] + pub(in crate::message) sensor_type: InputType, + #[getset(get = "pub")] + #[serde(rename = "SensorRange", serialize_with = "range_sequence_serialize")] + pub(in crate::message) sensor_range: Vec>, + // TODO This needs to actually be part of the device info relayed to the client in spec v4. + #[getset(get = "pub")] + #[serde(skip, default)] + pub(in crate::message) index: u32, + // Matching device feature for this attribute. Do not serialize or deserialize this, it's not part + // of this version of the protocol, only use it for comparison when doing message conversion. + #[getset(get = "pub")] + #[serde(skip)] + pub(in crate::message) feature: DeviceFeature, +} + +// This is an almost exact copy of the conversion we do for ServerDeviceFeature -> +// ServerDeviceMessageAttributesV3, but there's enough differences in the members that it's just +// easiest to mostly repeat here. +impl From> for ClientDeviceMessageAttributesV3 { + fn from(features: Vec) -> Self { + let scalar_attrs: Vec = features + .iter() + .flat_map(|feature| { + let mut actuator_vec = vec![]; + if let Some(output_map) = feature.output() { + for (actuator_type, actuator) in output_map { + if ![ + OutputType::PositionWithDuration, + OutputType::RotateWithDirection, + ] + .contains(actuator_type) + { + let actuator_type = *actuator_type; + let attrs = ClientGenericDeviceMessageAttributesV3 { + feature_descriptor: feature.description().to_owned(), + actuator_type, + step_count: actuator.step_count(), + index: 0, + }; + actuator_vec.push(attrs) + } + } + } + actuator_vec + }) + .collect(); + + // We have to calculate rotation attributes seperately, since they're a combination of + // feature type and message in >= v4. + let rotate_attrs: Vec = features + .iter() + .flat_map(|feature| { + let mut actuator_vec = vec![]; + if let Some(output_map) = feature.output() { + for (actuator_type, actuator) in output_map { + if *actuator_type == OutputType::RotateWithDirection { + let actuator_type = OutputType::Rotate; + let attrs = ClientGenericDeviceMessageAttributesV3 { + feature_descriptor: feature.description().to_owned(), + actuator_type, + step_count: actuator.step_count(), + index: 0, + }; + actuator_vec.push(attrs) + } + } + } + actuator_vec + }) + .collect(); + + let linear_attrs: Vec = features + .iter() + .flat_map(|feature| { + let mut actuator_vec = vec![]; + if let Some(output_map) = feature.output() { + for (actuator_type, actuator) in output_map { + if *actuator_type == OutputType::PositionWithDuration { + let actuator_type = OutputType::Position; + let attrs = ClientGenericDeviceMessageAttributesV3 { + feature_descriptor: feature.description().to_owned(), + actuator_type, + step_count: actuator.step_count(), + index: 0, + }; + actuator_vec.push(attrs) + } + } + } + actuator_vec + }) + .collect(); + + let sensor_filter = { + let attrs: Vec = features + .iter() + .map(|feature| { + let mut sensor_vec = vec![]; + if let Some(sensor_map) = feature.input() { + for (sensor_type, sensor) in sensor_map { + // Only convert Battery backwards. Other sensors weren't really built for v3 and we + // never recommended using them or implemented much for them. + if *sensor_type == InputType::Battery + && sensor.input_commands().contains(&InputCommandType::Read) + { + sensor_vec.push(SensorDeviceMessageAttributesV3 { + feature_descriptor: feature.description().to_owned(), + sensor_type: *sensor_type, + sensor_range: sensor.value_range().clone(), + feature: feature.clone(), + index: 0, + }); + } + } + } + sensor_vec + }) + .flatten() + .collect(); + if !attrs.is_empty() { + Some(attrs) + } else { + None + } + }; + + Self { + scalar_cmd: if scalar_attrs.is_empty() { + None + } else { + Some(scalar_attrs) + }, + rotate_cmd: if rotate_attrs.is_empty() { + None + } else { + Some(rotate_attrs) + }, + linear_cmd: if linear_attrs.is_empty() { + None + } else { + Some(linear_attrs) + }, + sensor_read_cmd: sensor_filter, + sensor_subscribe_cmd: None, + ..Default::default() + } + } +} diff --git a/crates/buttplug_server/src/message/v3/device_added.rs b/crates/buttplug_server/src/message/v3/device_added.rs new file mode 100644 index 000000000..688975294 --- /dev/null +++ b/crates/buttplug_server/src/message/v3/device_added.rs @@ -0,0 +1,127 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 crate::message::{ + v0::DeviceMessageInfoV0, + v1::DeviceMessageInfoV1, + v2::{DeviceAddedV2, DeviceMessageInfoV2}, +}; +use buttplug_core::{ + errors::ButtplugMessageError, + message::{ButtplugMessage, ButtplugMessageFinalizer, ButtplugMessageValidator, DeviceFeature, DeviceMessageInfoV4}, +}; + +use getset::{CopyGetters, Getters}; + +use serde::{Deserialize, Serialize}; + +use super::{ClientDeviceMessageAttributesV3, DeviceMessageInfoV3}; + +/// Notification that a device has been found and connected to the server. +#[derive( + ButtplugMessage, PartialEq, Clone, Debug, Getters, CopyGetters, Serialize, Deserialize, +)] +pub struct DeviceAddedV3 { + #[serde(rename = "Id")] + id: u32, + // DeviceAdded is not considered a device message because it only notifies of existence and is not + // a command (and goes from server to client), therefore we have to define the getter ourselves. + #[serde(rename = "DeviceIndex")] + #[getset(get_copy = "pub")] + device_index: u32, + #[serde(rename = "DeviceName")] + #[getset(get = "pub")] + device_name: String, + #[serde(rename = "DeviceDisplayName", skip_serializing_if = "Option::is_none")] + #[getset(get = "pub")] + device_display_name: Option, + #[serde(rename = "DeviceMessageTimingGap")] + #[getset(get_copy = "pub")] + device_message_timing_gap: u32, + #[serde(rename = "DeviceMessages")] + #[getset(get = "pub")] + device_messages: ClientDeviceMessageAttributesV3, +} + +impl DeviceAddedV3 { + pub fn new( + device_index: u32, + device_name: &str, + device_display_name: &Option, + device_message_timing_gap: u32, + device_messages: &ClientDeviceMessageAttributesV3, + ) -> Self { + let mut obj = Self { + id: 0, + device_index, + device_name: device_name.to_string(), + device_display_name: device_display_name.clone(), + device_message_timing_gap, + device_messages: device_messages.clone(), + }; + obj.finalize(); + obj + } +} + +impl ButtplugMessageValidator for DeviceAddedV3 { + fn is_valid(&self) -> Result<(), ButtplugMessageError> { + self.is_system_id(self.id) + } +} + +impl ButtplugMessageFinalizer for DeviceAddedV3 { + fn finalize(&mut self) { + self.device_messages.finalize(); + } +} + +impl From for DeviceMessageInfoV0 { + fn from(device_added: DeviceAddedV3) -> Self { + let dmi = DeviceMessageInfoV3::from(device_added); + let dmi_v2: DeviceMessageInfoV2 = dmi.into(); + let dmi_v1: DeviceMessageInfoV1 = dmi_v2.into(); + dmi_v1.into() + } +} + +impl From for DeviceAddedV2 { + fn from(msg: DeviceAddedV3) -> Self { + let id = msg.id(); + let dmi = DeviceMessageInfoV3::from(msg); + let dmiv1 = DeviceMessageInfoV2::from(dmi); + + Self { + id, + device_index: dmiv1.device_index(), + device_name: dmiv1.device_name().clone(), + device_messages: dmiv1.device_messages().clone(), + } + } +} + +impl From for DeviceMessageInfoV2 { + fn from(device_added: DeviceAddedV3) -> Self { + let dmi = DeviceMessageInfoV3::from(device_added); + DeviceMessageInfoV2::from(dmi) + } +} + +impl From for DeviceAddedV3 { + fn from(value: DeviceMessageInfoV4) -> Self { + let feature_vec: Vec = value.device_features().values().cloned().collect(); + let mut da3 = DeviceAddedV3::new( + value.device_index(), + value.device_name(), + value.device_display_name(), + value.device_message_timing_gap(), + &feature_vec.into() + ); + da3.set_id(0); + da3 + } +} diff --git a/crates/buttplug_server/src/message/v3/device_list.rs b/crates/buttplug_server/src/message/v3/device_list.rs new file mode 100644 index 000000000..835a4fc19 --- /dev/null +++ b/crates/buttplug_server/src/message/v3/device_list.rs @@ -0,0 +1,67 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 crate::message::v2::{DeviceListV2, DeviceMessageInfoV2}; +use buttplug_core::{ + errors::ButtplugMessageError, + message::{ButtplugMessage, ButtplugMessageFinalizer, ButtplugMessageValidator, DeviceListV4}, +}; +use getset::Getters; +use serde::{Deserialize, Serialize}; + +use super::DeviceMessageInfoV3; + +/// List of all devices currently connected to the server. +#[derive(Default, Clone, Debug, PartialEq, ButtplugMessage, Getters, Serialize, Deserialize)] +pub struct DeviceListV3 { + #[serde(rename = "Id")] + id: u32, + #[serde(rename = "Devices")] + #[getset(get = "pub")] + devices: Vec, +} + +impl DeviceListV3 { + pub fn new(devices: Vec) -> Self { + Self { id: 1, devices } + } +} + +impl ButtplugMessageValidator for DeviceListV3 { + fn is_valid(&self) -> Result<(), ButtplugMessageError> { + self.is_not_system_id(self.id) + } +} + +impl ButtplugMessageFinalizer for DeviceListV3 { + fn finalize(&mut self) { + for device in &mut self.devices { + device.device_messages_mut().finalize(); + } + } +} + +impl From for DeviceListV2 { + fn from(msg: DeviceListV3) -> Self { + let mut devices = vec![]; + for d in msg.devices() { + devices.push(DeviceMessageInfoV2::from(d.clone())); + } + Self { + id: msg.id(), + devices, + } + } +} + +impl From for DeviceListV3 { + fn from(value: DeviceListV4) -> Self { + let mut dl3 = DeviceListV3::new(value.devices().iter().map(|x| x.1.clone().into()).collect()); + dl3.set_id(value.id()); + dl3 + } +} diff --git a/crates/buttplug_server/src/message/v3/device_message_info.rs b/crates/buttplug_server/src/message/v3/device_message_info.rs new file mode 100644 index 000000000..8ed262134 --- /dev/null +++ b/crates/buttplug_server/src/message/v3/device_message_info.rs @@ -0,0 +1,87 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 crate::message::v2::DeviceMessageInfoV2; +use buttplug_core::message::{DeviceFeature, DeviceMessageInfoV4}; + +use super::*; +use getset::{CopyGetters, Getters, MutGetters}; +use serde::{Deserialize, Serialize}; + +/// Substructure of device messages, used for attribute information (name, messages supported, etc...) +#[derive(Clone, Debug, PartialEq, MutGetters, Getters, CopyGetters, Serialize, Deserialize)] +pub struct DeviceMessageInfoV3 { + #[serde(rename = "DeviceIndex")] + #[getset(get_copy = "pub")] + device_index: u32, + #[serde(rename = "DeviceName")] + #[getset(get = "pub")] + device_name: String, + #[serde(rename = "DeviceDisplayName", skip_serializing_if = "Option::is_none")] + #[getset(get = "pub")] + device_display_name: Option, + #[serde(rename = "DeviceMessageTimingGap")] + #[getset(get = "pub")] + device_message_timing_gap: u32, + #[serde(rename = "DeviceMessages")] + #[getset(get = "pub", get_mut = "pub(super)")] + device_messages: ClientDeviceMessageAttributesV3, +} + +impl DeviceMessageInfoV3 { + pub fn new( + device_index: u32, + device_name: &str, + device_display_name: &Option, + device_message_timing_gap: u32, + device_messages: ClientDeviceMessageAttributesV3, + ) -> Self { + Self { + device_index, + device_name: device_name.to_owned(), + device_display_name: device_display_name.clone(), + device_message_timing_gap, + device_messages, + } + } +} + +impl From for DeviceMessageInfoV3 { + fn from(device_added: DeviceAddedV3) -> Self { + Self { + device_index: device_added.device_index(), + device_name: device_added.device_name().clone(), + device_display_name: device_added.device_display_name().clone(), + device_message_timing_gap: device_added.device_message_timing_gap(), + device_messages: device_added.device_messages().clone(), + } + } +} + +impl From for DeviceMessageInfoV2 { + fn from(device_message_info: DeviceMessageInfoV3) -> Self { + // No structural difference, it's all content changes + Self { + device_index: device_message_info.device_index(), + device_name: device_message_info.device_name().clone(), + device_messages: device_message_info.device_messages().clone().into(), + } + } +} + +impl From for DeviceMessageInfoV3 { + fn from(value: DeviceMessageInfoV4) -> Self { + let feature_vec: Vec = value.device_features().values().cloned().collect(); + DeviceMessageInfoV3::new( + value.device_index(), + value.device_name(), + value.device_display_name(), + value.device_message_timing_gap(), + feature_vec.into(), + ) + } +} diff --git a/crates/buttplug_server/src/message/v3/mod.rs b/crates/buttplug_server/src/message/v3/mod.rs new file mode 100644 index 000000000..3b20db899 --- /dev/null +++ b/crates/buttplug_server/src/message/v3/mod.rs @@ -0,0 +1,35 @@ +mod client_device_message_attributes; +mod device_added; +mod device_list; +mod device_message_info; +mod scalar_cmd; +mod sensor_read_cmd; +mod sensor_reading; +mod sensor_subscribe_cmd; +mod sensor_unsubscribe_cmd; +mod server_device_message_attributes; +mod spec_enums; + +pub use client_device_message_attributes::{ + ClientDeviceMessageAttributesV3, + ClientGenericDeviceMessageAttributesV3, + SensorDeviceMessageAttributesV3, +}; +pub use device_added::DeviceAddedV3; +pub use device_list::DeviceListV3; +pub use device_message_info::DeviceMessageInfoV3; +pub use scalar_cmd::{ScalarCmdV3, ScalarSubcommandV3}; +pub use sensor_read_cmd::SensorReadCmdV3; +pub use sensor_reading::SensorReadingV3; +pub use sensor_subscribe_cmd::SensorSubscribeCmdV3; +pub use sensor_unsubscribe_cmd::SensorUnsubscribeCmdV3; +pub use server_device_message_attributes::{ + ServerDeviceMessageAttributesV3, + ServerGenericDeviceMessageAttributesV3, + ServerSensorDeviceMessageAttributesV3, +}; +pub use spec_enums::{ + ButtplugClientMessageV3, + ButtplugDeviceMessageNameV3, + ButtplugServerMessageV3, +}; diff --git a/crates/buttplug_server/src/message/v3/scalar_cmd.rs b/crates/buttplug_server/src/message/v3/scalar_cmd.rs new file mode 100644 index 000000000..fff770872 --- /dev/null +++ b/crates/buttplug_server/src/message/v3/scalar_cmd.rs @@ -0,0 +1,88 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 buttplug_core::{ + errors::ButtplugMessageError, + message::{ + ButtplugDeviceMessage, + ButtplugMessage, + ButtplugMessageFinalizer, + ButtplugMessageValidator, + OutputType, + }, +}; +use getset::{CopyGetters, Getters}; +use serde::{Deserialize, Serialize}; + +/// Generic command for setting a level (single magnitude value) of a device feature. +#[derive(Debug, PartialEq, Clone, CopyGetters, Serialize, Deserialize)] +#[getset(get_copy = "pub")] +pub struct ScalarSubcommandV3 { + #[serde(rename = "Index")] + index: u32, + #[serde(rename = "Scalar")] + scalar: f64, + #[serde(rename = "ActuatorType")] + actuator_type: OutputType, +} + +impl ScalarSubcommandV3 { + pub fn new(index: u32, scalar: f64, actuator_type: OutputType) -> Self { + Self { + index, + scalar, + actuator_type, + } + } +} + +#[derive( + Debug, + Default, + ButtplugDeviceMessage, + ButtplugMessageFinalizer, + PartialEq, + Clone, + Getters, + Serialize, + Deserialize, +)] +pub struct ScalarCmdV3 { + #[serde(rename = "Id")] + id: u32, + #[serde(rename = "DeviceIndex")] + device_index: u32, + #[serde(rename = "Scalars")] + #[getset(get = "pub")] + scalars: Vec, +} + +impl ScalarCmdV3 { + pub fn new(device_index: u32, scalars: Vec) -> Self { + Self { + id: 1, + device_index, + scalars, + } + } +} + +impl ButtplugMessageValidator for ScalarCmdV3 { + fn is_valid(&self) -> Result<(), ButtplugMessageError> { + self.is_not_system_id(self.id)?; + for level in &self.scalars { + self.is_in_command_range( + level.scalar, + format!( + "Level {} for ScalarCmd index {} is invalid. Level should be a value between 0.0 and 1.0", + level.scalar, level.index + ), + )?; + } + Ok(()) + } +} diff --git a/crates/buttplug_server/src/message/v3/sensor_read_cmd.rs b/crates/buttplug_server/src/message/v3/sensor_read_cmd.rs new file mode 100644 index 000000000..89a43563a --- /dev/null +++ b/crates/buttplug_server/src/message/v3/sensor_read_cmd.rs @@ -0,0 +1,103 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 crate::message::{ + checked_input_cmd::CheckedInputCmdV4, + ServerDeviceAttributes, + TryFromDeviceAttributes, +}; +use buttplug_core::{ + errors::{ButtplugDeviceError, ButtplugError, ButtplugMessageError}, + message::{ + ButtplugDeviceMessage, + ButtplugMessage, + ButtplugMessageFinalizer, + ButtplugMessageValidator, + InputCommandType, + InputType, + }, +}; +use getset::{CopyGetters, Getters}; +use serde::{Deserialize, Serialize}; + +#[derive( + Debug, + ButtplugDeviceMessage, + ButtplugMessageFinalizer, + PartialEq, + Eq, + Clone, + Getters, + CopyGetters, + Serialize, + Deserialize, +)] +pub struct SensorReadCmdV3 { + #[serde(rename = "Id")] + id: u32, + #[serde(rename = "DeviceIndex")] + device_index: u32, + #[getset(get = "pub")] + #[serde(rename = "SensorIndex")] + sensor_index: u32, + #[getset(get = "pub")] + #[serde(rename = "SensorType")] + sensor_type: InputType, +} + +impl SensorReadCmdV3 { + pub fn new(device_index: u32, sensor_index: u32, sensor_type: InputType) -> Self { + Self { + id: 1, + device_index, + sensor_index, + sensor_type, + } + } +} + +impl ButtplugMessageValidator for SensorReadCmdV3 { + fn is_valid(&self) -> Result<(), ButtplugMessageError> { + self.is_not_system_id(self.id) + // TODO Should expected_length always be > 0? + } +} + +impl TryFromDeviceAttributes for CheckedInputCmdV4 { + fn try_from_device_attributes( + msg: SensorReadCmdV3, + features: &ServerDeviceAttributes, + ) -> Result { + // Reject any SensorRead that's not a battery, we never supported sensors otherwise in v3. + if msg.sensor_type != InputType::Battery { + Err(ButtplugError::from( + ButtplugDeviceError::MessageNotSupported("SensorReadCmdV3".to_owned()), + )) + } else if let Some((feature_index, feature)) = + features.features().iter().enumerate().find(|(_, p)| { + if let Some(sensor_map) = p.input() { + if sensor_map.contains_key(&InputType::Battery) { + return true; + } + } + false + }) + { + Ok(CheckedInputCmdV4::new( + msg.device_index(), + feature_index as u32, + InputType::Battery, + InputCommandType::Read, + feature.id(), + )) + } else { + Err(ButtplugError::from( + ButtplugDeviceError::MessageNotSupported("SensorReadCmdV3".to_owned()), + )) + } + } +} diff --git a/crates/buttplug_server/src/message/v3/sensor_reading.rs b/crates/buttplug_server/src/message/v3/sensor_reading.rs new file mode 100644 index 000000000..9dba0e2a8 --- /dev/null +++ b/crates/buttplug_server/src/message/v3/sensor_reading.rs @@ -0,0 +1,59 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 buttplug_core::message::{ + ButtplugDeviceMessage, + ButtplugMessage, + ButtplugMessageFinalizer, + ButtplugMessageValidator, + InputType, +}; +use getset::{CopyGetters, Getters}; +use serde::{Deserialize, Serialize}; + +// This message can have an Id of 0, as it can be emitted as part of a +// subscription and won't have a matching task Id in that case. +#[derive( + Debug, + ButtplugDeviceMessage, + ButtplugMessageValidator, + ButtplugMessageFinalizer, + Clone, + Getters, + CopyGetters, + PartialEq, + Eq, + Serialize, + Deserialize, +)] +pub struct SensorReadingV3 { + #[serde(rename = "Id")] + id: u32, + #[serde(rename = "DeviceIndex")] + device_index: u32, + #[serde(rename = "SensorIndex")] + #[getset[get_copy="pub"]] + sensor_index: u32, + #[serde(rename = "SensorType")] + #[getset[get_copy="pub"]] + sensor_type: InputType, + #[serde(rename = "Data")] + #[getset[get="pub"]] + data: Vec, +} + +impl SensorReadingV3 { + pub fn new(device_index: u32, sensor_index: u32, sensor_type: InputType, data: Vec) -> Self { + Self { + id: 0, + device_index, + sensor_index, + sensor_type, + data, + } + } +} diff --git a/crates/buttplug_server/src/message/v3/sensor_subscribe_cmd.rs b/crates/buttplug_server/src/message/v3/sensor_subscribe_cmd.rs new file mode 100644 index 000000000..ed2abb99a --- /dev/null +++ b/crates/buttplug_server/src/message/v3/sensor_subscribe_cmd.rs @@ -0,0 +1,60 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 buttplug_core::{ + errors::ButtplugMessageError, + message::{ + ButtplugDeviceMessage, + ButtplugMessage, + ButtplugMessageFinalizer, + ButtplugMessageValidator, + InputType, + }, +}; +use getset::Getters; +use serde::{Deserialize, Serialize}; + +#[derive( + Debug, + ButtplugDeviceMessage, + ButtplugMessageFinalizer, + PartialEq, + Eq, + Clone, + Getters, + Serialize, + Deserialize, +)] +pub struct SensorSubscribeCmdV3 { + #[serde(rename = "Id")] + id: u32, + #[serde(rename = "DeviceIndex")] + device_index: u32, + #[getset(get = "pub")] + #[serde(rename = "SensorIndex")] + sensor_index: u32, + #[getset(get = "pub")] + #[serde(rename = "SensorType")] + sensor_type: InputType, +} + +impl SensorSubscribeCmdV3 { + pub fn new(device_index: u32, sensor_index: u32, sensor_type: InputType) -> Self { + Self { + id: 1, + device_index, + sensor_index, + sensor_type, + } + } +} + +impl ButtplugMessageValidator for SensorSubscribeCmdV3 { + fn is_valid(&self) -> Result<(), ButtplugMessageError> { + self.is_not_system_id(self.id) + } +} diff --git a/crates/buttplug_server/src/message/v3/sensor_unsubscribe_cmd.rs b/crates/buttplug_server/src/message/v3/sensor_unsubscribe_cmd.rs new file mode 100644 index 000000000..897451fc9 --- /dev/null +++ b/crates/buttplug_server/src/message/v3/sensor_unsubscribe_cmd.rs @@ -0,0 +1,60 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 buttplug_core::{ + errors::ButtplugMessageError, + message::{ + ButtplugDeviceMessage, + ButtplugMessage, + ButtplugMessageFinalizer, + ButtplugMessageValidator, + InputType, + }, +}; +use getset::Getters; +use serde::{Deserialize, Serialize}; + +#[derive( + Debug, + ButtplugDeviceMessage, + ButtplugMessageFinalizer, + PartialEq, + Eq, + Clone, + Getters, + Serialize, + Deserialize, +)] +pub struct SensorUnsubscribeCmdV3 { + #[serde(rename = "Id")] + id: u32, + #[serde(rename = "DeviceIndex")] + device_index: u32, + #[serde(rename = "SensorIndex")] + #[getset(get = "pub")] + sensor_index: u32, + #[serde(rename = "SensorType")] + #[getset(get = "pub")] + sensor_type: InputType, +} + +impl SensorUnsubscribeCmdV3 { + pub fn new(device_index: u32, sensor_index: u32, sensor_type: InputType) -> Self { + Self { + id: 1, + device_index, + sensor_index, + sensor_type, + } + } +} + +impl ButtplugMessageValidator for SensorUnsubscribeCmdV3 { + fn is_valid(&self) -> Result<(), ButtplugMessageError> { + self.is_not_system_id(self.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 new file mode 100644 index 000000000..acdb2df06 --- /dev/null +++ b/crates/buttplug_server/src/message/v3/server_device_message_attributes.rs @@ -0,0 +1,191 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 crate::message::v1::NullDeviceMessageAttributesV1; +use buttplug_core::message::{InputType, OutputType}; +use buttplug_server_device_config::ServerDeviceFeature; + +use getset::{Getters, MutGetters, Setters}; +use std::ops::RangeInclusive; + +#[derive(Clone, Debug, Default, PartialEq, Eq, Getters, MutGetters, Setters)] +#[getset(get = "pub")] +pub struct ServerDeviceMessageAttributesV3 { + // Generic commands + pub(in crate::message) scalar_cmd: Option>, + pub(in crate::message) rotate_cmd: Option>, + pub(in crate::message) linear_cmd: Option>, + + // Sensor Messages + pub(in crate::message) sensor_read_cmd: Option>, + pub(in crate::message) sensor_subscribe_cmd: Option>, + + // StopDeviceCmd always exists + pub(in crate::message) stop_device_cmd: NullDeviceMessageAttributesV1, + + // Needed to load from config for fallback, but unused here. + pub(in crate::message) fleshlight_launch_fw12_cmd: Option, + pub(in crate::message) vorze_a10_cyclone_cmd: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Getters, Setters)] +#[getset(get = "pub")] +pub struct ServerGenericDeviceMessageAttributesV3 { + pub(in crate::message) feature_descriptor: String, + pub(in crate::message) actuator_type: OutputType, + pub(in crate::message) step_count: u32, + pub(in crate::message) index: u32, + pub(in crate::message) feature: ServerDeviceFeature, +} + +#[derive(Clone, Debug, PartialEq, Eq, Getters, Setters)] +#[getset(get = "pub")] +pub struct ServerSensorDeviceMessageAttributesV3 { + pub(in crate::message) feature_descriptor: String, + pub(in crate::message) sensor_type: InputType, + pub(in crate::message) sensor_range: Vec>, + pub(in crate::message) index: u32, + pub(in crate::message) feature: ServerDeviceFeature, +} + +impl From> for ServerDeviceMessageAttributesV3 { + fn from(features: Vec) -> Self { + let scalar_attrs: Vec = features + .iter() + .flat_map(|feature| { + let mut actuator_vec = vec![]; + if let Some(output_map) = feature.output() { + for (actuator_type, actuator) in output_map { + if ![ + OutputType::PositionWithDuration, + OutputType::RotateWithDirection, + ] + .contains(actuator_type) + { + let actuator_type = *actuator_type; + let step_limit = actuator.step_limit(); + let step_count = step_limit.end() - step_limit.start(); + let attrs = ServerGenericDeviceMessageAttributesV3 { + feature_descriptor: feature.description().to_owned(), + actuator_type, + step_count, + feature: feature.clone(), + index: 0, + }; + actuator_vec.push(attrs) + } + } + } + actuator_vec + }) + .collect(); + + // We have to calculate rotation attributes seperately, since they're a combination of + // feature type and message in >= v4. + let rotate_attrs: Vec = features + .iter() + .flat_map(|feature| { + let mut actuator_vec = vec![]; + if let Some(output_map) = feature.output() { + for (actuator_type, actuator) in output_map { + if *actuator_type == OutputType::RotateWithDirection { + let actuator_type = OutputType::Rotate; + let step_limit = actuator.step_limit(); + let step_count = step_limit.end() - step_limit.start(); + let attrs = ServerGenericDeviceMessageAttributesV3 { + feature_descriptor: feature.description().to_owned(), + actuator_type, + step_count, + feature: feature.clone(), + index: 0, + }; + actuator_vec.push(attrs) + } + } + } + actuator_vec + }) + .collect(); + + let linear_attrs: Vec = features + .iter() + .flat_map(|feature| { + let mut actuator_vec = vec![]; + if let Some(output_map) = feature.output() { + for (actuator_type, actuator) in output_map { + if *actuator_type == OutputType::PositionWithDuration { + let actuator_type = OutputType::Position; + let step_limit = actuator.step_limit(); + let step_count = step_limit.end() - step_limit.start(); + let attrs = ServerGenericDeviceMessageAttributesV3 { + feature_descriptor: feature.description().to_owned(), + actuator_type, + step_count, + feature: feature.clone(), + index: 0, + }; + actuator_vec.push(attrs) + } + } + } + actuator_vec + }) + .collect(); + + let sensor_filter = { + let attrs: Vec = features + .iter() + .map(|feature| { + let mut sensor_vec = vec![]; + if let Some(sensor_map) = feature.input() { + for (sensor_type, sensor) in sensor_map { + // Only convert Battery backwards. Other sensors weren't really built for v3 and we + // never recommended using them or implemented much for them. + if *sensor_type == InputType::Battery { + sensor_vec.push(ServerSensorDeviceMessageAttributesV3 { + feature_descriptor: feature.description().to_owned(), + sensor_type: *sensor_type, + sensor_range: sensor.value_range().clone(), + feature: feature.clone(), + index: 0, + }); + } + } + } + sensor_vec + }) + .flatten() + .collect(); + if !attrs.is_empty() { + Some(attrs) + } else { + None + } + }; + + Self { + scalar_cmd: if scalar_attrs.is_empty() { + None + } else { + Some(scalar_attrs) + }, + rotate_cmd: if rotate_attrs.is_empty() { + None + } else { + Some(rotate_attrs) + }, + linear_cmd: if linear_attrs.is_empty() { + None + } else { + Some(linear_attrs) + }, + sensor_read_cmd: sensor_filter, + sensor_subscribe_cmd: None, + ..Default::default() + } + } +} diff --git a/crates/buttplug_server/src/message/v3/spec_enums.rs b/crates/buttplug_server/src/message/v3/spec_enums.rs new file mode 100644 index 000000000..44940a883 --- /dev/null +++ b/crates/buttplug_server/src/message/v3/spec_enums.rs @@ -0,0 +1,202 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 crate::message::{ + v1::{LinearCmdV1, RequestServerInfoV1, RotateCmdV1, VibrateCmdV1}, + v2::{ButtplugClientMessageV2, ButtplugServerMessageV2, ServerInfoV2}, +}; +use buttplug_core::{ + errors::{ButtplugError, ButtplugMessageError}, + message::{ + ButtplugMessage, + ButtplugMessageFinalizer, + ButtplugMessageValidator, + ButtplugServerMessageV4, + DeviceRemovedV0, + ErrorV0, + OkV0, + PingV0, + RequestDeviceListV0, + ScanningFinishedV0, + StartScanningV0, + StopAllDevicesV0, + StopDeviceCmdV0, + StopScanningV0, + }, +}; +use serde::{Deserialize, Serialize}; + +use super::{ + DeviceAddedV3, + DeviceListV3, + ScalarCmdV3, + SensorReadCmdV3, + SensorReadingV3, + SensorSubscribeCmdV3, + SensorUnsubscribeCmdV3, +}; + +/// Represents all client-to-server messages in v3 of the Buttplug Spec +#[derive( + Debug, + Clone, + PartialEq, + ButtplugMessage, + ButtplugMessageValidator, + ButtplugMessageFinalizer, + FromSpecificButtplugMessage, + Serialize, + Deserialize, +)] +pub enum ButtplugClientMessageV3 { + // Handshake messages + RequestServerInfo(RequestServerInfoV1), + Ping(PingV0), + // Device enumeration messages + StartScanning(StartScanningV0), + StopScanning(StopScanningV0), + RequestDeviceList(RequestDeviceListV0), + // Generic commands + StopAllDevices(StopAllDevicesV0), + VibrateCmd(VibrateCmdV1), + LinearCmd(LinearCmdV1), + RotateCmd(RotateCmdV1), + StopDeviceCmd(StopDeviceCmdV0), + ScalarCmd(ScalarCmdV3), + // Sensor commands + SensorReadCmd(SensorReadCmdV3), + SensorSubscribeCmd(SensorSubscribeCmdV3), + SensorUnsubscribeCmd(SensorUnsubscribeCmdV3), +} + +// For v2 to v3, all deprecations should be treated as conversions, but will require current +// connected device state, meaning they'll need to be implemented where they can also access the +// device manager. +impl TryFrom for ButtplugClientMessageV3 { + type Error = ButtplugMessageError; + + fn try_from(value: ButtplugClientMessageV2) -> Result { + match value { + ButtplugClientMessageV2::Ping(m) => Ok(ButtplugClientMessageV3::Ping(m.clone())), + ButtplugClientMessageV2::RequestServerInfo(m) => { + Ok(ButtplugClientMessageV3::RequestServerInfo(m.clone())) + } + ButtplugClientMessageV2::StartScanning(m) => { + Ok(ButtplugClientMessageV3::StartScanning(m.clone())) + } + ButtplugClientMessageV2::StopScanning(m) => { + Ok(ButtplugClientMessageV3::StopScanning(m.clone())) + } + ButtplugClientMessageV2::RequestDeviceList(m) => { + Ok(ButtplugClientMessageV3::RequestDeviceList(m.clone())) + } + ButtplugClientMessageV2::StopAllDevices(m) => { + Ok(ButtplugClientMessageV3::StopAllDevices(m.clone())) + } + ButtplugClientMessageV2::StopDeviceCmd(m) => { + Ok(ButtplugClientMessageV3::StopDeviceCmd(m.clone())) + } + // Vibrate was supposed to be phased out in v3 but was left in the allowable message set. + // Oops. + ButtplugClientMessageV2::VibrateCmd(m) => Ok(ButtplugClientMessageV3::VibrateCmd(m)), + ButtplugClientMessageV2::LinearCmd(m) => Ok(ButtplugClientMessageV3::LinearCmd(m)), + ButtplugClientMessageV2::RotateCmd(m) => Ok(ButtplugClientMessageV3::RotateCmd(m)), + _ => Err(ButtplugMessageError::MessageConversionError(format!( + "Cannot convert message {value:?} to V3 message spec while lacking state." + ))), + } + } +} + +/// Represents all server-to-client messages in v3 of the Buttplug Spec +#[derive( + Debug, + Clone, + PartialEq, + ButtplugMessage, + ButtplugMessageValidator, + FromSpecificButtplugMessage, + Serialize, + Deserialize, +)] +pub enum ButtplugServerMessageV3 { + // Status messages + Ok(OkV0), + Error(ErrorV0), + // Handshake messages + ServerInfo(ServerInfoV2), + // Device enumeration messages + DeviceList(DeviceListV3), + DeviceAdded(DeviceAddedV3), + DeviceRemoved(DeviceRemovedV0), + ScanningFinished(ScanningFinishedV0), + // Sensor commands + SensorReading(SensorReadingV3), +} + +impl ButtplugMessageFinalizer for ButtplugServerMessageV3 { + fn finalize(&mut self) { + match self { + ButtplugServerMessageV3::DeviceAdded(da) => da.finalize(), + ButtplugServerMessageV3::DeviceList(dl) => dl.finalize(), + _ => (), + } + } +} + +impl From for ButtplugServerMessageV2 { + fn from(value: ButtplugServerMessageV3) -> Self { + match value { + ButtplugServerMessageV3::Ok(m) => ButtplugServerMessageV2::Ok(m), + ButtplugServerMessageV3::Error(m) => ButtplugServerMessageV2::Error(m), + ButtplugServerMessageV3::ServerInfo(m) => ButtplugServerMessageV2::ServerInfo(m), + ButtplugServerMessageV3::DeviceRemoved(m) => ButtplugServerMessageV2::DeviceRemoved(m), + ButtplugServerMessageV3::ScanningFinished(m) => ButtplugServerMessageV2::ScanningFinished(m), + ButtplugServerMessageV3::DeviceAdded(m) => ButtplugServerMessageV2::DeviceAdded(m.into()), + ButtplugServerMessageV3::DeviceList(m) => ButtplugServerMessageV2::DeviceList(m.into()), + ButtplugServerMessageV3::SensorReading(_) => ButtplugServerMessageV2::Error(ErrorV0::from( + ButtplugError::from(ButtplugMessageError::MessageConversionError( + "SensorReading cannot be converted to Buttplug Message Spec V2".to_owned(), + )), + )), + } + } +} + +impl TryFrom for ButtplugServerMessageV3 { + type Error = ButtplugMessageError; + + fn try_from( + value: ButtplugServerMessageV4, + ) -> Result>::Error> { + match value { + // Direct conversions + ButtplugServerMessageV4::Ok(m) => Ok(ButtplugServerMessageV3::Ok(m)), + ButtplugServerMessageV4::Error(m) => Ok(ButtplugServerMessageV3::Error(m)), + ButtplugServerMessageV4::ServerInfo(m) => Ok(ButtplugServerMessageV3::ServerInfo(m.into())), + ButtplugServerMessageV4::ScanningFinished(m) => { + Ok(ButtplugServerMessageV3::ScanningFinished(m)) + } + ButtplugServerMessageV4::DeviceList(m) => Ok(ButtplugServerMessageV3::DeviceList(m.into())), + // All other messages (SensorReading) requires device manager context. + _ => Err(ButtplugMessageError::MessageConversionError(format!( + "Cannot convert message {value:?} to current message spec while lacking state." + ))), + } + } +} + +#[derive(Copy, Debug, Clone, PartialEq, Eq, Hash, Display, Serialize, Deserialize)] +pub enum ButtplugDeviceMessageNameV3 { + LinearCmd, + RotateCmd, + StopDeviceCmd, + ScalarCmd, + SensorReadCmd, + SensorSubscribeCmd, + SensorUnsubscribeCmd, +} diff --git a/crates/buttplug_server/src/message/v4/checked_input_cmd.rs b/crates/buttplug_server/src/message/v4/checked_input_cmd.rs new file mode 100644 index 000000000..b62a44754 --- /dev/null +++ b/crates/buttplug_server/src/message/v4/checked_input_cmd.rs @@ -0,0 +1,103 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 crate::message::TryFromDeviceAttributes; +use buttplug_core::{ + errors::{ButtplugDeviceError, ButtplugError, ButtplugMessageError}, + message::{ + ButtplugDeviceMessage, + ButtplugMessage, + ButtplugMessageFinalizer, + ButtplugMessageValidator, + InputCmdV4, + InputCommandType, + InputType, + }, +}; +use getset::CopyGetters; +use uuid::Uuid; + +#[derive( + Debug, ButtplugDeviceMessage, ButtplugMessageFinalizer, PartialEq, Eq, Clone, CopyGetters, +)] +#[getset(get_copy = "pub")] +pub struct CheckedInputCmdV4 { + id: u32, + device_index: u32, + feature_index: u32, + input_type: InputType, + input_command: InputCommandType, + feature_id: Uuid, +} + +impl CheckedInputCmdV4 { + pub fn new( + device_index: u32, + feature_index: u32, + input_type: InputType, + input_command: InputCommandType, + feature_id: Uuid, + ) -> Self { + Self { + id: 1, + device_index, + feature_index, + input_type, + input_command, + feature_id, + } + } +} + +impl ButtplugMessageValidator for CheckedInputCmdV4 { + fn is_valid(&self) -> Result<(), ButtplugMessageError> { + self.is_not_system_id(self.id) + // TODO Should expected_length always be > 0? + } +} + +impl TryFromDeviceAttributes for CheckedInputCmdV4 { + fn try_from_device_attributes( + msg: InputCmdV4, + features: &crate::message::ServerDeviceAttributes, + ) -> Result { + if let Some(feature) = features.features().get(msg.feature_index() as usize) { + if let Some(sensor_map) = feature.input() { + if let Some(sensor) = sensor_map.get(&msg.input_type()) { + if sensor.input_commands().contains(&msg.input_command()) { + Ok(CheckedInputCmdV4::new( + msg.device_index(), + msg.feature_index(), + msg.input_type(), + msg.input_command(), + feature.id(), + )) + } else { + Err(ButtplugError::from( + ButtplugDeviceError::DeviceNoSensorError("InputCmd".to_string()), + )) + } + } else { + Err(ButtplugError::from( + ButtplugDeviceError::DeviceNoSensorError("InputCmd".to_string()), + )) + } + } else { + Err(ButtplugError::from( + ButtplugDeviceError::DeviceNoSensorError("InputCmd".to_string()), + )) + } + } else { + Err(ButtplugError::from( + ButtplugDeviceError::DeviceFeatureIndexError( + features.features().len() as u32, + msg.feature_index(), + ), + )) + } + } +} diff --git a/crates/buttplug_server/src/message/v4/checked_output_cmd.rs b/crates/buttplug_server/src/message/v4/checked_output_cmd.rs new file mode 100644 index 000000000..56afe5fb0 --- /dev/null +++ b/crates/buttplug_server/src/message/v4/checked_output_cmd.rs @@ -0,0 +1,150 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 crate::message::{ServerDeviceAttributes, TryFromDeviceAttributes}; +use buttplug_core::{ + errors::{ButtplugDeviceError, ButtplugError, ButtplugMessageError}, + message::{ + ButtplugDeviceMessage, + ButtplugMessage, + ButtplugMessageFinalizer, + ButtplugMessageValidator, + OutputCmdV4, + OutputCommand, + }, +}; + +use getset::{CopyGetters, Getters}; +use uuid::Uuid; + +use super::spec_enums::ButtplugDeviceMessageNameV4; + +#[derive( + Debug, ButtplugDeviceMessage, ButtplugMessageFinalizer, Clone, Getters, CopyGetters, Eq, +)] +#[getset(get_copy = "pub")] +pub struct CheckedOutputCmdV4 { + id: u32, + device_index: u32, + feature_index: u32, + feature_id: Uuid, + output_command: OutputCommand, +} + +impl PartialEq for CheckedOutputCmdV4 { + fn eq(&self, other: &Self) -> bool { + // Compare everything but the message id + self.device_index() == other.device_index() + && self.feature_index() == other.feature_index() + && self.feature_id() == other.feature_id() + && self.output_command() == other.output_command() + } +} + +/* + +impl From for ActuatorCmdV4 { + fn from(value: CheckedActuatorCmdV4) -> Self { + ActuatorCmdV4::new( + value.device_index(), + value.feature_index(), + value.actuator_type(), + value.output_command() + ) + } +} + */ + +impl CheckedOutputCmdV4 { + pub fn new( + id: u32, + device_index: u32, + feature_index: u32, + feature_id: Uuid, + output_command: OutputCommand, + ) -> Self { + Self { + id, + device_index, + feature_index, + feature_id, + output_command: output_command, + } + } +} + +impl ButtplugMessageValidator for CheckedOutputCmdV4 { + fn is_valid(&self) -> Result<(), ButtplugMessageError> { + self.is_not_system_id(self.id)?; + Ok(()) + } +} + +impl TryFromDeviceAttributes for CheckedOutputCmdV4 { + fn try_from_device_attributes( + cmd: OutputCmdV4, + attrs: &ServerDeviceAttributes, + ) -> Result { + let features = attrs.features(); + + // Since we have the feature info already, check limit and unpack into step range when creating. + // + // If this message isn't the result of an upgrade from another older message, we won't have set + // our feature id yet. + let (feature, _) = if let Some(feature) = features.get(cmd.feature_index() as usize) { + (feature, feature.id()) + } else { + return Err(ButtplugError::from( + ButtplugDeviceError::DeviceFeatureIndexError(features.len() as u32, cmd.feature_index()), + )); + }; + + // Check to make sure the feature has an actuator that handles the data we've been passed + if let Some(output_map) = feature.output() { + if let Some(actuator) = output_map.get(&cmd.command().as_output_type()) { + let value = cmd.command().value(); + let step_count = actuator.step_count(); + if value > step_count { + Err(ButtplugError::from( + ButtplugDeviceError::DeviceStepRangeError(step_count, value), + )) + } else { + // Only set adjusted value if we haven't gotten zero, otherwise assume stop. + let new_value = if step_count != 0 && value != 0 { + actuator.step_limit().start() + value + } else { + 0 + }; + 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(), + ), + )) + } + } else { + Err(ButtplugError::from( + ButtplugDeviceError::MessageNotSupported( + ButtplugDeviceMessageNameV4::OutputCmd.to_string(), + ), + )) + } + } +} 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 new file mode 100644 index 000000000..843439e2a --- /dev/null +++ b/crates/buttplug_server/src/message/v4/checked_output_vec_cmd.rs @@ -0,0 +1,414 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 crate::message::{ + v0::SingleMotorVibrateCmdV0, + v1::VibrateCmdV1, + v3::ScalarCmdV3, + ButtplugDeviceMessageNameV3, + LinearCmdV1, + RotateCmdV1, + ServerDeviceAttributes, + TryFromDeviceAttributes, +}; +use buttplug_core::{ + errors::{ButtplugDeviceError, ButtplugError, ButtplugMessageError}, + message::{ + ButtplugDeviceMessage, + ButtplugMessage, + ButtplugMessageFinalizer, + ButtplugMessageValidator, + OutputCommand, + OutputPositionWithDuration, + OutputRotateWithDirection, + OutputType, + OutputValue, + }, +}; +use getset::{CopyGetters, Getters}; + +use super::checked_output_cmd::CheckedOutputCmdV4; + +#[derive( + Debug, + Default, + ButtplugDeviceMessage, + ButtplugMessageFinalizer, + PartialEq, + Clone, + Getters, + CopyGetters, +)] +pub struct CheckedOutputVecCmdV4 { + #[getset(get_copy = "pub")] + id: u32, + #[getset(get_copy = "pub")] + device_index: u32, + #[getset(get = "pub")] + value_vec: Vec, +} + +impl CheckedOutputVecCmdV4 { + pub fn new(id: u32, device_index: u32, mut value_vec: Vec) -> Self { + // Several tests and parts of the system assumed we always sorted by feature index. This is not + // necessarily true of incoming messages, but we also never explicitly specified the execution + // order of subcommands within a message, so we'll just sort here for now to make tests pass, + // and implement unordered checking after v4 ships. + value_vec.sort_by_key(|k| k.feature_index()); + Self { + id, + device_index, + value_vec, + } + } +} + +impl ButtplugMessageValidator for CheckedOutputVecCmdV4 { + fn is_valid(&self) -> Result<(), ButtplugMessageError> { + self.is_not_system_id(self.id)?; + Ok(()) + } +} + +impl TryFromDeviceAttributes for CheckedOutputVecCmdV4 { + // For VibrateCmd, just take everything out of V2's VibrateCmd and make a command. + fn try_from_device_attributes( + msg: SingleMotorVibrateCmdV0, + features: &ServerDeviceAttributes, + ) -> Result { + let mut vibrate_features = features + .features() + .iter() + .enumerate() + .filter(|(_, feature)| { + if let Some(output_map) = feature.output() { + output_map.contains_key(&buttplug_core::message::OutputType::Vibrate) + } else { + false + } + }) + .peekable(); + + // Check to make sure we have any vibrate attributes at all. + if vibrate_features.peek().is_none() { + return Err( + ButtplugDeviceError::DeviceFeatureMismatch("Device has no Vibrate features".to_owned()) + .into(), + ); + } + + let mut cmds = vec![]; + 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() + .get(&OutputType::Vibrate) + .unwrap(); + // This doesn't need to run through a security check because we have to construct it to be + // inherently secure anyways. + cmds.push(CheckedOutputCmdV4::new( + msg.id(), + msg.device_index(), + index as u32, + feature.id(), + OutputCommand::Vibrate(OutputValue::new( + (msg.speed() * ((*actuator.step_limit().end() - *actuator.step_limit().start()) as f64) + + *actuator.step_limit().start() as f64) + .ceil() as u32, + )), + )) + } + Ok(CheckedOutputVecCmdV4::new( + msg.id(), + msg.device_index(), + cmds, + )) + } +} + +impl TryFromDeviceAttributes for CheckedOutputVecCmdV4 { + // VibrateCmd only exists up through Message Spec v2. We can assume that, if we're receiving it, + // we can just use the V2 spec client device attributes for it. If this was sent on a V1 protocol, + // it'll still have all the same features. + // + // Due to specs v1/2 using feature counts instead of per-feature objects, we calculate our indexes + // based on the feature counts in our current device definitions, as that's how we generate them + // on the way out. + fn try_from_device_attributes( + msg: VibrateCmdV1, + features: &ServerDeviceAttributes, + ) -> Result { + let vibrate_attributes = + features + .attrs_v2() + .vibrate_cmd() + .as_ref() + .ok_or(ButtplugError::from( + ButtplugDeviceError::DeviceFeatureCountMismatch(0, msg.speeds().len() as u32), + ))?; + + let mut cmds: Vec = vec![]; + for vibrate_cmd in msg.speeds() { + if vibrate_cmd.index() > vibrate_attributes.features().len() as u32 { + return Err(ButtplugError::from( + ButtplugDeviceError::DeviceFeatureCountMismatch( + vibrate_cmd.index(), + msg.speeds().len() as u32, + ), + )); + } + let feature = &vibrate_attributes.features()[vibrate_cmd.index() as usize]; + let idx = features + .features() + .iter() + .enumerate() + .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(), + ))? + .get(&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(), + idx as u32, + feature.id(), + OutputCommand::Vibrate(OutputValue::new( + (vibrate_cmd.speed() + * ((*actuator.step_limit().end() - *actuator.step_limit().start()) as f64) + + *actuator.step_limit().start() as f64) + .ceil() as u32, + )), + )) + } + Ok(CheckedOutputVecCmdV4::new( + msg.id(), + msg.device_index(), + cmds, + )) + } +} + +impl TryFromDeviceAttributes for CheckedOutputVecCmdV4 { + // ScalarCmd only came in with V3, so we can just use the V3 device attributes. + fn try_from_device_attributes( + msg: ScalarCmdV3, + attrs: &ServerDeviceAttributes, + ) -> Result { + let mut cmds: Vec = vec![]; + if msg.scalars().is_empty() { + return Err(ButtplugError::from( + ButtplugDeviceError::ProtocolRequirementError( + "ScalarCmd with no subcommands is not allowed.".to_owned(), + ), + )); + } + for cmd in msg.scalars() { + let scalar_attrs = attrs + .attrs_v3() + .scalar_cmd() + .as_ref() + .ok_or(ButtplugError::from( + ButtplugDeviceError::MessageNotSupported( + ButtplugDeviceMessageNameV3::ScalarCmd.to_string(), + ), + ))?; + let feature = scalar_attrs + .get(cmd.index() as usize) + .ok_or(ButtplugError::from( + ButtplugDeviceError::DeviceFeatureIndexError(scalar_attrs.len() as u32, cmd.index()), + ))?; + let idx = attrs + .features() + .iter() + .enumerate() + .find(|(_, f)| f.id() == feature.feature().id()) + .expect("Already proved existence") + .0 as u32; + let actuator = feature + .feature() + .output() + .as_ref() + .ok_or(ButtplugError::from( + ButtplugDeviceError::DeviceNoActuatorError("ScalarCmdV3".to_owned()), + ))? + .get(&cmd.actuator_type()) + .ok_or(ButtplugError::from( + ButtplugDeviceError::DeviceNoActuatorError("ScalarCmdV3".to_owned()), + ))?; + + // This needs to take the user configured step limit into account, otherwise we'll hand back + // the wrong placement and it won't be noticed. + if cmd.scalar() > 0.000001 { + cmds.push(CheckedOutputCmdV4::new( + msg.id(), + msg.device_index(), + idx, + feature.feature.id(), + OutputCommand::from_output_type( + cmd.actuator_type(), + (cmd.scalar() + * ((*actuator.step_limit().end() - *actuator.step_limit().start()) as f64) + + *actuator.step_limit().start() as f64) + .ceil() as u32, + ) + .unwrap(), + )); + } else { + cmds.push(CheckedOutputCmdV4::new( + msg.id(), + msg.device_index(), + idx, + feature.feature.id(), + OutputCommand::from_output_type(cmd.actuator_type(), 0).unwrap(), + )); + } + } + + Ok(CheckedOutputVecCmdV4::new( + msg.id(), + msg.device_index(), + cmds, + )) + } +} + +impl TryFromDeviceAttributes for CheckedOutputVecCmdV4 { + fn try_from_device_attributes( + msg: LinearCmdV1, + features: &ServerDeviceAttributes, + ) -> Result { + let features = features + .attrs_v3() + .linear_cmd() + .as_ref() + .ok_or(ButtplugError::from( + ButtplugDeviceError::DeviceFeatureMismatch( + "Device has no PositionWithDuration features".to_owned(), + ), + ))?; + + let mut cmds = vec![]; + for x in msg.vectors() { + let f = features + .get(x.index() as usize) + .ok_or(ButtplugDeviceError::DeviceFeatureIndexError( + features.len() as u32, + 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(), + ), + ))? + .get(&buttplug_core::message::OutputType::PositionWithDuration) + .ok_or(ButtplugError::from( + ButtplugDeviceError::DeviceFeatureMismatch( + "Device got LinearCmd command but has no actuators on Linear feature.".to_owned(), + ), + ))?; + cmds.push(CheckedOutputCmdV4::new( + msg.device_index(), + x.index(), + 0, + f.id(), + OutputCommand::PositionWithDuration(OutputPositionWithDuration::new( + (x.position() * ((*actuator.step_limit().end() - *actuator.step_limit().start()) as f64) + + *actuator.step_limit().start() as f64) + .ceil() as u32, + x.duration().try_into().map_err(|_| { + ButtplugError::from(ButtplugMessageError::InvalidMessageContents( + "Duration should be under 2^31. You are not waiting 24 days to run this command." + .to_owned(), + )) + })?, + )), + )); + } + Ok(CheckedOutputVecCmdV4::new( + msg.id(), + msg.device_index(), + cmds, + )) + } +} + +impl TryFromDeviceAttributes for CheckedOutputVecCmdV4 { + // RotateCmd exists up through Message Spec v3. We can assume that, if we're receiving it, we can + // just use the V3 spec client device attributes for it. If this was sent on a V1/V2 protocol, + // it'll still have all the same features. + fn try_from_device_attributes( + msg: RotateCmdV1, + attrs: &ServerDeviceAttributes, + ) -> Result { + let mut cmds: Vec = vec![]; + for cmd in msg.rotations() { + let rotate_attrs = attrs + .attrs_v3() + .rotate_cmd() + .as_ref() + .ok_or(ButtplugError::from( + ButtplugDeviceError::MessageNotSupported( + ButtplugDeviceMessageNameV3::RotateCmd.to_string(), + ), + ))?; + let feature = rotate_attrs + .get(cmd.index() as usize) + .ok_or(ButtplugError::from( + ButtplugDeviceError::DeviceFeatureIndexError(rotate_attrs.len() as u32, cmd.index()), + ))?; + let idx = attrs + .features() + .iter() + .enumerate() + .find(|(_, f)| f.id() == feature.feature().id()) + .expect("Already proved existence") + .0 as u32; + let actuator = feature + .feature() + .output() + .as_ref() + .ok_or(ButtplugError::from( + ButtplugDeviceError::DeviceNoActuatorError("RotateCmdV1".to_owned()), + ))? + .get(&buttplug_core::message::OutputType::RotateWithDirection) + .ok_or(ButtplugError::from( + ButtplugDeviceError::DeviceNoActuatorError("RotateCmdV1".to_owned()), + ))?; + cmds.push(CheckedOutputCmdV4::new( + msg.id(), + msg.device_index(), + idx, + feature.feature.id(), + OutputCommand::RotateWithDirection(OutputRotateWithDirection::new( + (cmd.speed() * ((*actuator.step_limit().end() - *actuator.step_limit().start()) as f64) + + *actuator.step_limit().start() as f64) + .ceil() as u32, + cmd.clockwise(), + )), + )); + } + Ok(CheckedOutputVecCmdV4::new( + msg.id(), + msg.device_index(), + cmds, + )) + } +} diff --git a/crates/buttplug_server/src/message/v4/mod.rs b/crates/buttplug_server/src/message/v4/mod.rs new file mode 100644 index 000000000..07aec7617 --- /dev/null +++ b/crates/buttplug_server/src/message/v4/mod.rs @@ -0,0 +1,4 @@ +pub mod checked_input_cmd; +pub mod checked_output_cmd; +pub mod checked_output_vec_cmd; +pub mod spec_enums; diff --git a/crates/buttplug_server/src/message/v4/spec_enums.rs b/crates/buttplug_server/src/message/v4/spec_enums.rs new file mode 100644 index 000000000..0cf760898 --- /dev/null +++ b/crates/buttplug_server/src/message/v4/spec_enums.rs @@ -0,0 +1,390 @@ +use std::{collections::HashMap, fmt::Debug}; + +use crate::message::{ + server_device_attributes::TryFromClientMessage, + v0::ButtplugClientMessageV0, + v1::ButtplugClientMessageV1, + v2::ButtplugClientMessageV2, + v3::ButtplugClientMessageV3, + ButtplugClientMessageVariant, + RequestServerInfoV1, + ServerDeviceAttributes, + TryFromDeviceAttributes, +}; +use buttplug_core::{ + errors::{ButtplugDeviceError, ButtplugError, ButtplugMessageError}, + message::{ + ButtplugClientMessageV4, + ButtplugDeviceMessage, + ButtplugMessage, + ButtplugMessageFinalizer, + ButtplugMessageValidator, + PingV0, + RequestDeviceListV0, + RequestServerInfoV4, + StartScanningV0, + StopAllDevicesV0, + StopDeviceCmdV0, + StopScanningV0, + }, +}; + +use super::{ + checked_input_cmd::CheckedInputCmdV4, + checked_output_cmd::CheckedOutputCmdV4, + checked_output_vec_cmd::CheckedOutputVecCmdV4, +}; + +/// An CheckedClientMessage has had its contents verified and should need no further error/validity +/// checking. Processing may still return errors, but should be due to system state, not message +/// contents. +/// +/// There should only be one version of CheckedClientMessage in the library, matching the latest +/// version of the message spec. For any messages that don't require error checking, their regular +/// struct can be used as an enum parameter. Any messages requiring error checking or validation +/// will have an alternate Checked[x] form that they will need to be cast as. +#[derive( + Debug, + Clone, + PartialEq, + ButtplugMessage, + ButtplugMessageValidator, + ButtplugMessageFinalizer, + FromSpecificButtplugMessage, +)] +pub enum ButtplugCheckedClientMessageV4 { + // Handshake messages + RequestServerInfo(RequestServerInfoV4), + Ping(PingV0), + // Device enumeration messages + StartScanning(StartScanningV0), + StopScanning(StopScanningV0), + RequestDeviceList(RequestDeviceListV0), + // Generic commands + StopDeviceCmd(StopDeviceCmdV0), + StopAllDevices(StopAllDevicesV0), + OutputCmd(CheckedOutputCmdV4), + // Sensor commands + InputCmd(CheckedInputCmdV4), + // Internal conversions for v1-v3 messages with subcommands + OutputVecCmd(CheckedOutputVecCmdV4), +} + +impl TryFromClientMessage for ButtplugCheckedClientMessageV4 { + fn try_from_client_message( + value: ButtplugClientMessageV4, + feature_map: &HashMap, + ) -> Result { + match value { + // Messages that don't need checking + ButtplugClientMessageV4::RequestServerInfo(m) => { + Ok(ButtplugCheckedClientMessageV4::RequestServerInfo(m)) + } + ButtplugClientMessageV4::Ping(m) => Ok(ButtplugCheckedClientMessageV4::Ping(m)), + ButtplugClientMessageV4::StartScanning(m) => { + Ok(ButtplugCheckedClientMessageV4::StartScanning(m)) + } + ButtplugClientMessageV4::StopScanning(m) => { + Ok(ButtplugCheckedClientMessageV4::StopScanning(m)) + } + ButtplugClientMessageV4::RequestDeviceList(m) => { + Ok(ButtplugCheckedClientMessageV4::RequestDeviceList(m)) + } + ButtplugClientMessageV4::StopAllDevices(m) => { + Ok(ButtplugCheckedClientMessageV4::StopAllDevices(m)) + } + + // Messages that need device index checking + ButtplugClientMessageV4::StopDeviceCmd(m) => { + if feature_map.get(&m.device_index()).is_some() { + Ok(ButtplugCheckedClientMessageV4::StopDeviceCmd(m)) + } else { + Err(ButtplugError::from( + ButtplugDeviceError::DeviceNotAvailable(m.device_index()), + )) + } + } + + // Message that need device index and feature checking + ButtplugClientMessageV4::OutputCmd(m) => { + if let Some(features) = feature_map.get(&m.device_index()) { + Ok(ButtplugCheckedClientMessageV4::OutputCmd( + CheckedOutputCmdV4::try_from_device_attributes(m, features)?, + )) + } else { + Err(ButtplugError::from( + ButtplugDeviceError::DeviceNotAvailable(m.device_index()), + )) + } + } + ButtplugClientMessageV4::InputCmd(m) => { + if let Some(features) = feature_map.get(&m.device_index()) { + Ok(ButtplugCheckedClientMessageV4::InputCmd( + CheckedInputCmdV4::try_from_device_attributes(m, features)?, + )) + } else { + Err(ButtplugError::from( + ButtplugDeviceError::DeviceNotAvailable(m.device_index()), + )) + } + } + } + } +} + +impl From for RequestServerInfoV4 { + fn from(value: RequestServerInfoV1) -> Self { + let mut msg = RequestServerInfoV4::new(value.client_name(), value.message_version(), 0); + msg.set_id(value.id()); + msg + } +} + +// For v3 to v4, all deprecations should be treated as conversions, but will require current +// connected device state, meaning they'll need to be implemented where they can also access the +// device manager. +impl TryFrom for ButtplugCheckedClientMessageV4 { + type Error = ButtplugMessageError; + + fn try_from(value: ButtplugClientMessageV3) -> Result { + match value { + ButtplugClientMessageV3::Ping(m) => Ok(ButtplugCheckedClientMessageV4::Ping(m.clone())), + ButtplugClientMessageV3::RequestServerInfo(m) => Ok( + ButtplugCheckedClientMessageV4::RequestServerInfo(RequestServerInfoV4::from(m)), + ), + ButtplugClientMessageV3::StartScanning(m) => { + Ok(ButtplugCheckedClientMessageV4::StartScanning(m.clone())) + } + ButtplugClientMessageV3::StopScanning(m) => { + Ok(ButtplugCheckedClientMessageV4::StopScanning(m.clone())) + } + ButtplugClientMessageV3::RequestDeviceList(m) => { + Ok(ButtplugCheckedClientMessageV4::RequestDeviceList(m.clone())) + } + ButtplugClientMessageV3::StopAllDevices(m) => { + Ok(ButtplugCheckedClientMessageV4::StopAllDevices(m.clone())) + } + ButtplugClientMessageV3::StopDeviceCmd(m) => { + Ok(ButtplugCheckedClientMessageV4::StopDeviceCmd(m.clone())) + } + _ => Err(ButtplugMessageError::MessageConversionError(format!( + "Cannot convert message {value:?} to V4 message spec while lacking state." + ))), + } + } +} + +impl TryFromClientMessage for ButtplugCheckedClientMessageV4 { + fn try_from_client_message( + msg: ButtplugClientMessageVariant, + features: &HashMap, + ) -> Result { + let id = msg.id(); + let mut converted_msg = match msg { + ButtplugClientMessageVariant::V0(m) => Self::try_from_client_message(m, features), + ButtplugClientMessageVariant::V1(m) => Self::try_from_client_message(m, features), + ButtplugClientMessageVariant::V2(m) => Self::try_from_client_message(m, features), + ButtplugClientMessageVariant::V3(m) => Self::try_from_client_message(m, features), + ButtplugClientMessageVariant::V4(m) => Self::try_from_client_message(m, features), + }?; + // Always make sure the ID is set after conversion + converted_msg.set_id(id); + Ok(converted_msg) + } +} + +impl TryFromClientMessage for ButtplugCheckedClientMessageV4 { + fn try_from_client_message( + msg: ButtplugClientMessageV0, + features: &HashMap, + ) -> Result { + // All v0 messages can be converted to v1 messages. + Self::try_from_client_message(ButtplugClientMessageV1::from(msg), features) + } +} + +fn check_device_index_and_convert( + msg: T, + features: &HashMap, +) -> Result +where + T: ButtplugDeviceMessage + Debug, + U: TryFromDeviceAttributes + Debug, +{ + // Vorze and RotateCmd are equivalent, so this is an ok conversion. + if let Some(attrs) = features.get(&msg.device_index()) { + Ok(U::try_from_device_attributes(msg.clone(), attrs)?) + } else { + Err(ButtplugError::from( + ButtplugDeviceError::DeviceNotAvailable(msg.device_index()), + )) + } +} + +impl TryFromClientMessage for ButtplugCheckedClientMessageV4 { + fn try_from_client_message( + msg: ButtplugClientMessageV1, + features: &HashMap, + ) -> Result { + // Instead of converting to v2 message attributes then to v4 device features, we move directly + // from v0 command messages to v4 device features here. There's no reason to do the middle step. + match msg { + ButtplugClientMessageV1::VorzeA10CycloneCmd(_) => { + // Vorze and RotateCmd are equivalent, so this is an ok conversion. + Err(ButtplugError::ButtplugMessageError(ButtplugMessageError::MessageConversionError("VorzeA10CycloneCmd is considered unused, and no longer supported. If you are seeing this message and need VorzeA10CycloneCmd, file an issue in the Buttplug repo.".to_owned()))) + } + ButtplugClientMessageV1::SingleMotorVibrateCmd(m) => { + Ok(check_device_index_and_convert::<_, CheckedOutputVecCmdV4>(m, features)?.into()) + } + _ => Self::try_from_client_message(ButtplugClientMessageV2::try_from(msg)?, features), + } + } +} + +impl TryFromClientMessage for ButtplugCheckedClientMessageV4 { + fn try_from_client_message( + msg: ButtplugClientMessageV2, + features: &HashMap, + ) -> Result { + match msg { + // Convert v2 specific queries to v3 generic sensor queries + ButtplugClientMessageV2::BatteryLevelCmd(m) => { + Ok(check_device_index_and_convert::<_, CheckedInputCmdV4>(m, features)?.into()) + } + // Convert VibrateCmd to a ScalarCmd command + ButtplugClientMessageV2::VibrateCmd(m) => { + Ok(check_device_index_and_convert::<_, CheckedOutputVecCmdV4>(m, features)?.into()) + } + _ => Self::try_from_client_message(ButtplugClientMessageV3::try_from(msg)?, features), + } + } +} + +impl TryFromClientMessage for ButtplugCheckedClientMessageV4 { + fn try_from_client_message( + msg: ButtplugClientMessageV3, + features: &HashMap, + ) -> Result { + match msg { + // Convert v1/v2 message attribute commands into device feature commands + ButtplugClientMessageV3::VibrateCmd(m) => { + Ok(check_device_index_and_convert::<_, CheckedOutputVecCmdV4>(m, features)?.into()) + } + ButtplugClientMessageV3::ScalarCmd(m) => { + Ok(check_device_index_and_convert::<_, CheckedOutputVecCmdV4>(m, features)?.into()) + } + ButtplugClientMessageV3::RotateCmd(m) => { + Ok(check_device_index_and_convert::<_, CheckedOutputVecCmdV4>(m, features)?.into()) + } + ButtplugClientMessageV3::LinearCmd(m) => { + Ok(check_device_index_and_convert::<_, CheckedOutputVecCmdV4>(m, features)?.into()) + } + ButtplugClientMessageV3::SensorReadCmd(m) => { + Ok(check_device_index_and_convert::<_, CheckedInputCmdV4>(m, features)?.into()) + } + ButtplugClientMessageV3::SensorSubscribeCmd(_) => { + // Always reject v3 sub/unsub. It was never implemented or indexed correctly. + Err(ButtplugError::from( + ButtplugDeviceError::MessageNotSupported("SensorSubscribeCmdV3".to_owned()), + )) + } + ButtplugClientMessageV3::SensorUnsubscribeCmd(_) => { + // Always reject v3 sub/unsub. It was never implemented or indexed correctly. + Err(ButtplugError::from( + ButtplugDeviceError::MessageNotSupported("SensorUnsubscribeCmdV3".to_owned()), + )) + } + _ => { + ButtplugCheckedClientMessageV4::try_from(msg).map_err(|e: ButtplugMessageError| e.into()) + } + } + } +} + +/// Represents messages that should go to the +/// [DeviceManager][crate::server::device_manager::DeviceManager] of a +/// [ButtplugServer](crate::server::ButtplugServer) +#[derive( + Debug, + Clone, + PartialEq, + Eq, + ButtplugMessage, + ButtplugMessageValidator, + ButtplugMessageFinalizer, + FromSpecificButtplugMessage, +)] +pub(crate) enum ButtplugDeviceManagerMessageUnion { + RequestDeviceList(RequestDeviceListV0), + StopAllDevices(StopAllDevicesV0), + StartScanning(StartScanningV0), + StopScanning(StopScanningV0), +} + +impl TryFrom for ButtplugDeviceManagerMessageUnion { + type Error = (); + + fn try_from(value: ButtplugCheckedClientMessageV4) -> Result { + match value { + ButtplugCheckedClientMessageV4::RequestDeviceList(m) => { + Ok(ButtplugDeviceManagerMessageUnion::RequestDeviceList(m)) + } + ButtplugCheckedClientMessageV4::StopAllDevices(m) => { + Ok(ButtplugDeviceManagerMessageUnion::StopAllDevices(m)) + } + ButtplugCheckedClientMessageV4::StartScanning(m) => { + Ok(ButtplugDeviceManagerMessageUnion::StartScanning(m)) + } + ButtplugCheckedClientMessageV4::StopScanning(m) => { + Ok(ButtplugDeviceManagerMessageUnion::StopScanning(m)) + } + _ => Err(()), + } + } +} + +/// Represents all possible device command message types. +#[derive( + Debug, + Clone, + PartialEq, + ButtplugDeviceMessage, + ButtplugMessageValidator, + ButtplugMessageFinalizer, + FromSpecificButtplugMessage, +)] +pub enum ButtplugDeviceCommandMessageUnionV4 { + StopDeviceCmd(StopDeviceCmdV0), + OutputCmd(CheckedOutputCmdV4), + OutputVecCmd(CheckedOutputVecCmdV4), + InputCmd(CheckedInputCmdV4), +} + +impl TryFrom for ButtplugDeviceCommandMessageUnionV4 { + type Error = (); + + fn try_from(value: ButtplugCheckedClientMessageV4) -> Result { + match value { + ButtplugCheckedClientMessageV4::StopDeviceCmd(m) => { + Ok(ButtplugDeviceCommandMessageUnionV4::StopDeviceCmd(m)) + } + ButtplugCheckedClientMessageV4::OutputCmd(m) => { + Ok(ButtplugDeviceCommandMessageUnionV4::OutputCmd(m)) + } + ButtplugCheckedClientMessageV4::OutputVecCmd(m) => { + Ok(ButtplugDeviceCommandMessageUnionV4::OutputVecCmd(m)) + } + ButtplugCheckedClientMessageV4::InputCmd(m) => { + Ok(ButtplugDeviceCommandMessageUnionV4::InputCmd(m)) + } + _ => Err(()), + } + } +} + +#[derive(Copy, Debug, Clone, PartialEq, Eq, Hash, Display)] +pub enum ButtplugDeviceMessageNameV4 { + StopDeviceCmd, + InputCmd, + OutputCmd, +} diff --git a/buttplug/src/server/ping_timer.rs b/crates/buttplug_server/src/ping_timer.rs similarity index 82% rename from buttplug/src/server/ping_timer.rs rename to crates/buttplug_server/src/ping_timer.rs index 4090c21fb..36d972e0a 100644 --- a/buttplug/src/server/ping_timer.rs +++ b/crates/buttplug_server/src/ping_timer.rs @@ -5,8 +5,8 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::util::{async_manager, sleep}; -use futures::{Future, FutureExt}; +use buttplug_core::util::{async_manager, sleep}; +use futures::Future; use std::{ sync::{ atomic::{AtomicBool, Ordering}, @@ -14,7 +14,10 @@ use std::{ }, time::Duration, }; -use tokio::sync::{mpsc, Notify}; +use tokio::{ + select, + sync::{mpsc, Notify}, +}; pub enum PingMessage { Ping, @@ -33,17 +36,17 @@ async fn ping_timer( let mut pinged = false; loop { select! { - _ = sleep(Duration::from_millis(max_ping_time.into())).fuse() => { + _ = sleep(Duration::from_millis(max_ping_time.into())) => { if started { if !pinged { notifier.notify_waiters(); - pinged_out_status.store(true, Ordering::SeqCst); + pinged_out_status.store(true, Ordering::Relaxed); return; } pinged = false; } } - msg = ping_msg_receiver.recv().fuse() => { + msg = ping_msg_receiver.recv() => { if msg.is_none() { return; } @@ -90,7 +93,7 @@ impl PingTimer { ping_timeout_notifier.clone(), pinged_out.clone(), ); - async_manager::spawn(async move { fut.await }); + async_manager::spawn(fut); } Self { max_ping_time, @@ -100,14 +103,14 @@ impl PingTimer { } } - pub fn ping_timeout_waiter(&self) -> impl Future { + pub fn ping_timeout_waiter(&self) -> impl Future + use<> { let notify = self.ping_timeout_notifier.clone(); async move { notify.notified().await; } } - fn send_ping_msg(&self, msg: PingMessage) -> impl Future { + fn send_ping_msg(&self, msg: PingMessage) -> impl Future + use<> { let ping_msg_sender = self.ping_msg_sender.clone(); let max_ping_time = self.max_ping_time; async move { @@ -120,21 +123,21 @@ impl PingTimer { } } - pub fn start_ping_timer(&self) -> impl Future { + pub fn start_ping_timer(&self) -> impl Future + use<> { // If we're starting the timer, clear our status. - self.pinged_out.store(false, Ordering::SeqCst); + self.pinged_out.store(false, Ordering::Relaxed); self.send_ping_msg(PingMessage::StartTimer) } - pub fn stop_ping_timer(&self) -> impl Future { + pub fn stop_ping_timer(&self) -> impl Future + use<> { self.send_ping_msg(PingMessage::StopTimer) } - pub fn update_ping_time(&self) -> impl Future { + pub fn update_ping_time(&self) -> impl Future + use<> { self.send_ping_msg(PingMessage::Ping) } pub fn pinged_out(&self) -> bool { - self.pinged_out.load(Ordering::SeqCst) + self.pinged_out.load(Ordering::Relaxed) } } diff --git a/buttplug/src/server/server.rs b/crates/buttplug_server/src/server.rs similarity index 50% rename from buttplug/src/server/server.rs rename to crates/buttplug_server/src/server.rs index 78b067a43..497ab7bb8 100644 --- a/buttplug/src/server/server.rs +++ b/crates/buttplug_server/src/server.rs @@ -5,21 +5,35 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use super::{device::ServerDeviceManager, ping_timer::PingTimer, ButtplugServerResultFuture}; -use crate::{ - core::{ - errors::*, - message::{ - self, - ButtplugClientMessageV4, - ButtplugDeviceCommandMessageUnion, +use crate::server_message_conversion::ButtplugServerDeviceEventMessageConverter; + +use super::{ + device::ServerDeviceManager, + message::{ + server_device_attributes::TryFromClientMessage, + spec_enums::{ + ButtplugCheckedClientMessageV4, + ButtplugDeviceCommandMessageUnionV4, ButtplugDeviceManagerMessageUnion, - ButtplugMessage, - ButtplugServerMessageV4, - StopAllDevicesV0, - StopScanningV0, - BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION, }, + ButtplugClientMessageVariant, + ButtplugServerMessageVariant, + }, + ping_timer::PingTimer, + server_message_conversion::ButtplugServerMessageConverter, + ButtplugServerResultFuture, +}; +use buttplug_core::{ + errors::*, + message::{ + self, + ButtplugMessage, + ButtplugMessageSpecVersion, + ButtplugServerMessageV4, + ErrorV0, + StopAllDevicesV0, + StopScanningV0, + BUTTPLUG_CURRENT_API_MAJOR_VERSION, }, util::stream::convert_broadcast_receiver_to_stream, }; @@ -27,6 +41,7 @@ use futures::{ future::{self, BoxFuture, FutureExt}, Stream, }; +use once_cell::sync::OnceCell; use std::{ fmt, sync::{ @@ -34,8 +49,9 @@ use std::{ Arc, }, }; -use tokio::sync::{broadcast, RwLock}; +use tokio::sync::broadcast; use tokio_stream::StreamExt; +use tracing::info_span; use tracing_futures::Instrument; /// The server side of the Buttplug protocol. Frontend for connection to device management and @@ -45,7 +61,7 @@ pub struct ButtplugServer { /// confirmation in UI dialogs) server_name: String, /// The maximum ping time, in milliseconds, for the server. If the server does not receive a - /// [Ping](crate::core::messages::Ping) message in this amount of time after the handshake has + /// [Ping](buttplug_core::messages::Ping) message in this amount of time after the handshake has /// succeeded, the server will automatically disconnect. If this is not called, the ping timer /// will not be activated. /// @@ -62,7 +78,9 @@ pub struct ButtplugServer { /// [ButtplugServer::event_stream()] method. output_sender: broadcast::Sender, /// Name of the connected client, assuming there is one. - client_name: Arc>>, + client_name: Arc>, + /// Current spec version for the connected client + spec_version: Arc>, } impl std::fmt::Debug for ButtplugServer { @@ -91,22 +109,46 @@ impl ButtplugServer { device_manager, connected, output_sender, - client_name: Arc::new(RwLock::new(None)), + client_name: Arc::new(OnceCell::new()), + spec_version: Arc::new(OnceCell::new()), } } pub fn client_name(&self) -> Option { - self - .client_name - .try_read() - .expect("We should never conflict on name access") - .clone() + self.client_name.get().cloned() } /// Retreive an async stream of ButtplugServerMessages. This is how the server sends out /// non-query-related updates to the system, including information on devices being added/removed, /// client disconnection, etc... - pub fn event_stream(&self) -> impl Stream { + pub fn event_stream(&self) -> impl Stream + use<> { + let spec_version = self.spec_version.clone(); + let converter = ButtplugServerMessageConverter::new(None); + let device_indexes: Vec = self.device_manager.devices().iter().map(|x| *x.key()).collect(); + let device_event_converter = ButtplugServerDeviceEventMessageConverter::new(device_indexes); + self.server_version_event_stream().map(move |m| { + if let ButtplugServerMessageV4::DeviceList(list) = m { + device_event_converter.convert_device_list(spec_version + .get() + .unwrap_or(&ButtplugMessageSpecVersion::Version4), &list) + } else { + // If we get an event and don't have a spec version yet, just throw out the latest. + converter + .convert_outgoing( + &m, + spec_version + .get() + .unwrap_or(&ButtplugMessageSpecVersion::Version4), + ) + .unwrap() + } + }) + } + + /// Retreive an async stream of ButtplugServerMessages, always at the latest available message + /// spec. This is how the server sends out non-query-related updates to the system, including + /// information on devices being added/removed, client disconnection, etc... + pub fn server_version_event_stream(&self) -> impl Stream + use<> { // Unlike the client API, we can expect anyone using the server to pin this // themselves. let server_receiver = convert_broadcast_receiver_to_stream(self.output_sender.subscribe()); @@ -121,29 +163,24 @@ impl ButtplugServer { /// If true, client is currently connected to the server. pub fn connected(&self) -> bool { - self.connected.load(Ordering::SeqCst) + self.connected.load(Ordering::Relaxed) } /// Disconnects the server from a client, if it is connected. pub fn disconnect(&self) -> BoxFuture> { debug!("Buttplug Server {} disconnect requested", self.server_name); let ping_timer = self.ping_timer.clone(); - // HACK Injecting messages here is weird since we're never quite sure what version they should - // be. Can we turn this into actual method calls? - let stop_scanning_fut = self.parse_message(ButtplugClientMessageV4::StopScanning( - StopScanningV0::default(), - )); - let stop_fut = self.parse_message(ButtplugClientMessageV4::StopAllDevices( + // As long as StopScanning/StopAllDevices aren't changed across message specs, we can inject + // them using parse_checked_message and bypass version checking. + let stop_scanning_fut = self.parse_checked_message( + ButtplugCheckedClientMessageV4::StopScanning(StopScanningV0::default()), + ); + let stop_fut = self.parse_checked_message(ButtplugCheckedClientMessageV4::StopAllDevices( StopAllDevicesV0::default(), )); let connected = self.connected.clone(); - let mut name = self - .client_name - .try_write() - .expect("We should never conflict on name access"); - *name = None; async move { - connected.store(false, Ordering::SeqCst); + connected.store(false, Ordering::Relaxed); ping_timer.stop_ping_timer().await; // Ignore returns here, we just want to stop. info!("Server disconnected, stopping device scanning if it was started..."); @@ -161,9 +198,94 @@ impl ButtplugServer { async move { device_manager.shutdown().await }.boxed() } + /// Sends a [ButtplugClientMessage] to be parsed by the server (for handshake or ping), or passed + /// into the server's [DeviceManager] for communication with devices. pub fn parse_message( &self, - msg: ButtplugClientMessageV4, + msg: ButtplugClientMessageVariant, + ) -> BoxFuture<'static, Result> { + let features = self.device_manager().feature_map(); + let msg_id = msg.id(); + debug!("Server received: {:?}", msg); + match msg { + ButtplugClientMessageVariant::V4(msg) => { + let internal_msg = + match ButtplugCheckedClientMessageV4::try_from_client_message(msg, &features) { + Ok(m) => m, + Err(e) => { + let mut err_msg = ErrorV0::from(e); + err_msg.set_id(msg_id); + return future::ready(Err(ButtplugServerMessageVariant::from( + ButtplugServerMessageV4::from(err_msg), + ))) + .boxed(); + } + }; + let fut = self.parse_checked_message(internal_msg); + async move { + Ok( + fut + .await + .map_err(|e| ButtplugServerMessageVariant::from(ButtplugServerMessageV4::from(e)))? + .into(), + ) + } + .boxed() + } + msg => { + let v = msg.version(); + let converter = ButtplugServerMessageConverter::new(Some(msg.clone())); + let spec_version = *self.spec_version.get_or_init(|| { + info!( + "Setting Buttplug Server Message Spec Downgrade version to {}", + v + ); + v + }); + match ButtplugCheckedClientMessageV4::try_from_client_message(msg, &features) { + Ok(converted_msg) => { + debug!("Converted message: {:?}", converted_msg); + let fut = self.parse_checked_message(converted_msg); + async move { + let result = fut.await.map_err(|e| { + converter + .convert_outgoing(&e.into(), &spec_version) + .unwrap() + })?; + let out_msg = converter + .convert_outgoing(&result, &spec_version) + .map_err(|e| { + converter + .convert_outgoing( + &ButtplugServerMessageV4::from(ErrorV0::from(e)), + &spec_version, + ) + .unwrap() + }); + debug!("Server returning: {:?}", out_msg); + out_msg + } + .boxed() + } + Err(e) => { + let mut err_msg = ErrorV0::from(e); + err_msg.set_id(msg_id); + + future::ready(Err( + converter + .convert_outgoing(&ButtplugServerMessageV4::from(err_msg), &spec_version) + .unwrap(), + )) + .boxed() + } + } + } + } + } + + pub fn parse_checked_message( + &self, + msg: ButtplugCheckedClientMessageV4, ) -> BoxFuture<'static, Result> { trace!( "Buttplug Server {} received message to client parse: {:?}", @@ -179,7 +301,7 @@ impl ButtplugServer { Some(message::ErrorV0::from(ButtplugError::from( ButtplugPingError::PingedOut, ))) - } else if !matches!(msg, ButtplugClientMessageV4::RequestServerInfo(_)) { + } else if !matches!(msg, ButtplugCheckedClientMessageV4::RequestServerInfo(_)) { Some(message::ErrorV0::from(ButtplugError::from( ButtplugHandshakeError::RequestServerInfoExpected, ))) @@ -198,14 +320,16 @@ impl ButtplugServer { // tagging the result with the message id in the future we put out as the // return value from this method. let out_fut = if ButtplugDeviceManagerMessageUnion::try_from(msg.clone()).is_ok() - || ButtplugDeviceCommandMessageUnion::try_from(msg.clone()).is_ok() + || ButtplugDeviceCommandMessageUnionV4::try_from(msg.clone()).is_ok() { self.device_manager.parse_message(msg.clone()) } else { match msg { - ButtplugClientMessageV4::RequestServerInfo(rsi_msg) => self.perform_handshake(rsi_msg), - ButtplugClientMessageV4::Ping(p) => self.handle_ping(p), - _ => ButtplugMessageError::UnexpectedMessageType(format!("{:?}", msg)).into(), + ButtplugCheckedClientMessageV4::RequestServerInfo(rsi_msg) => { + self.perform_handshake(rsi_msg) + } + ButtplugCheckedClientMessageV4::Ping(p) => self.handle_ping(p), + _ => ButtplugMessageError::UnexpectedMessageType(format!("{msg:?}")).into(), } }; // Simple way to set the ID on the way out. Just rewrap @@ -215,6 +339,7 @@ impl ButtplugServer { .await .map(|mut ok_msg| { ok_msg.set_id(id); + trace!("Server returning message: {:?}", ok_msg); ok_msg }) .map_err(|err| { @@ -227,42 +352,53 @@ impl ButtplugServer { .boxed() } - /// Performs the [RequestServerInfo]([ServerInfo](crate::core::message::RequestServerInfo) / - /// [ServerInfo](crate::core::message::ServerInfo) handshake, as specified in the [Buttplug + /// Performs the [RequestServerInfo]([ServerInfo](buttplug_core::message::RequestServerInfo) / + /// [ServerInfo](buttplug_core::message::ServerInfo) handshake, as specified in the [Buttplug /// Protocol Spec](https://buttplug-spec.docs.buttplug.io). This is the first thing that must /// happens upon connection to the server, in order to make sure the server can speak the same /// protocol version as the client. - fn perform_handshake(&self, msg: message::RequestServerInfoV1) -> ButtplugServerResultFuture { + fn perform_handshake(&self, msg: message::RequestServerInfoV4) -> ButtplugServerResultFuture { if self.connected() { return ButtplugHandshakeError::HandshakeAlreadyHappened.into(); } + if !self.connected() && self.client_name.get().is_some() { + return ButtplugHandshakeError::ReconnectDenied.into(); + } info!( - "Performing server handshake check with client {} at message version {}.", + "Performing server handshake check with client {} at message version {}.{}", msg.client_name(), - msg.message_version() + msg.protocol_version_major(), + msg.protocol_version_minor() ); - #[cfg(not(feature = "allow-unstable-v4-connections"))] - if BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION < msg.message_version() { + if BUTTPLUG_CURRENT_API_MAJOR_VERSION < msg.protocol_version_major() { return ButtplugHandshakeError::MessageSpecVersionMismatch( - BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION, - msg.message_version(), + BUTTPLUG_CURRENT_API_MAJOR_VERSION, + msg.protocol_version_major(), ) .into(); } + // Only start the ping timer after we've received the handshake. let ping_timer = self.ping_timer.clone(); + + // Due to programming/spec errors in prior versions of the protocol, anything before v4 expected + // that it would be back a matching api version of the server. The correct response is to send back whatever the + let output_version = if (msg.protocol_version_major() as u32) < 4 { + msg.protocol_version_major() + } else { + BUTTPLUG_CURRENT_API_MAJOR_VERSION + }; let out_msg = - message::ServerInfoV2::new(&self.server_name, msg.message_version(), self.max_ping_time); + message::ServerInfoV4::new(&self.server_name, output_version, 0, self.max_ping_time); let connected = self.connected.clone(); - let mut name = self + self .client_name - .try_write() + .set(msg.client_name().to_owned()) .expect("We should never conflict on name access"); - *name = Some(msg.client_name().clone()); async move { ping_timer.start_ping_timer().await; - connected.store(true, Ordering::SeqCst); + connected.store(true, Ordering::Relaxed); debug!("Server handshake check successful."); Result::Ok(out_msg.into()) } @@ -285,19 +421,17 @@ impl ButtplugServer { #[cfg(test)] mod test { - use crate::{ - core::message::{self, BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION}, - server::ButtplugServerBuilder, - }; + use crate::ButtplugServerBuilder; + use buttplug_core::message::{self, BUTTPLUG_CURRENT_API_MAJOR_VERSION}; #[tokio::test] - async fn test_server_reuse() { + async fn test_server_deny_reuse() { let server = ButtplugServerBuilder::default().finish().unwrap(); let msg = - message::RequestServerInfoV1::new("Test Client", BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION); - let mut reply = server.parse_message(msg.clone().into()).await; + message::RequestServerInfoV4::new("Test Client", BUTTPLUG_CURRENT_API_MAJOR_VERSION, 0); + let mut reply = server.parse_checked_message(msg.clone().into()).await; assert!(reply.is_ok(), "Should get back ok: {:?}", reply); - reply = server.parse_message(msg.clone().into()).await; + reply = server.parse_checked_message(msg.clone().into()).await; assert!( reply.is_err(), "Should get back err on double handshake: {:?}", @@ -305,10 +439,10 @@ mod test { ); assert!(server.disconnect().await.is_ok(), "Should disconnect ok"); - reply = server.parse_message(msg.clone().into()).await; + reply = server.parse_checked_message(msg.clone().into()).await; assert!( - reply.is_ok(), - "Should get back ok on handshake after disconnect: {:?}", + reply.is_err(), + "Should get back err on handshake after disconnect: {:?}", reply ); } diff --git a/buttplug/src/server/server_builder.rs b/crates/buttplug_server/src/server_builder.rs similarity index 87% rename from buttplug/src/server/server_builder.rs rename to crates/buttplug_server/src/server_builder.rs index d5800fae3..43e6e7987 100644 --- a/buttplug/src/server/server_builder.rs +++ b/crates/buttplug_server/src/server_builder.rs @@ -6,22 +6,17 @@ // for full license information. use super::{ - device::{ - configuration::DeviceConfigurationManagerBuilder, - ServerDeviceManager, - ServerDeviceManagerBuilder, - }, + device::{ServerDeviceManager, ServerDeviceManagerBuilder}, ping_timer::PingTimer, server::ButtplugServer, ButtplugServerError, }; -use crate::{ - core::{ - errors::*, - message::{self, ButtplugServerMessageV4}, - }, +use buttplug_core::{ + errors::*, + message::{self, ButtplugServerMessageV4}, util::async_manager, }; +use buttplug_server_device_config::DeviceConfigurationManagerBuilder; use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, @@ -72,7 +67,7 @@ impl ButtplugServerBuilder { Self { name: "Buttplug Server".to_owned(), max_ping_time: None, - device_manager: device_manager, + device_manager, } } @@ -84,7 +79,7 @@ impl ButtplugServerBuilder { } /// Set the maximum ping time, in milliseconds, for the server. If the server does not receive a - /// [Ping](crate::core::messages::Ping) message in this amount of time after the handshake has + /// [Ping](buttplug_core::messages::Ping) message in this amount of time after the handshake has /// succeeded, the server will automatically disconnect. If this is not called, the ping timer /// will not be activated. /// @@ -99,7 +94,6 @@ impl ButtplugServerBuilder { pub fn finish(&self) -> Result { // Create the server debug!("Creating server '{}'", self.name); - info!("Buttplug Server Operating System Info: {}", os_info::get()); // Set up our channels to different parts of the system. let (output_sender, _) = broadcast::channel(256); @@ -121,7 +115,7 @@ impl ButtplugServerBuilder { // This will only exit if we've pinged out. ping_timeout_notifier.await; error!("Ping out signal received, stopping server"); - connected_clone.store(false, Ordering::SeqCst); + connected_clone.store(false, Ordering::Relaxed); async_manager::spawn(async move { if let Err(e) = device_manager_clone.stop_all_devices().await { error!("Could not stop devices on ping timeout: {:?}", e); @@ -129,9 +123,9 @@ impl ButtplugServerBuilder { }); // TODO Should the event sender return a result instead of an error message? if output_sender_clone - .send(ButtplugServerMessageV4::Error( - message::ErrorV0::from(ButtplugError::from(ButtplugPingError::PingedOut)).into(), - )) + .send(ButtplugServerMessageV4::Error(message::ErrorV0::from( + ButtplugError::from(ButtplugPingError::PingedOut), + ))) .is_err() { error!("Server disappeared, cannot update about ping out."); diff --git a/crates/buttplug_server/src/server_message_conversion.rs b/crates/buttplug_server/src/server_message_conversion.rs new file mode 100644 index 000000000..66da8551c --- /dev/null +++ b/crates/buttplug_server/src/server_message_conversion.rs @@ -0,0 +1,213 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 Nonpolynomial Labs LLC. All rights reserved. +// +// Licensed under the BSD 3-Clause license. See LICENSE file in the project root +// for full license information. + +//! Buttplug Message Spec Conversion +//! +//! This module contains code to convert any message from an older spec version up to the current +//! message spec, and then convert any response from the current message spec back down the sending +//! spec. This is handled within the server, as the server is the only portion of Buttplug that +//! needs to handle up/downgrading (the client should never have to care and should only ever talk +//! one version of the spec, preferably the latest). Having this done within the server also allows +//! us to access required state for converting between messages that requires knowledge of ephemeral +//! device structures (i.e. converting from v4 device features to <= v3 message attributes for +//! messages like DeviceAdded). + +use buttplug_core::{ + errors::{ButtplugError, ButtplugMessageError}, + message::{ + ButtplugDeviceMessage, ButtplugMessage, ButtplugMessageSpecVersion, ButtplugServerMessageV4, DeviceListV4, DeviceMessageInfoV4, DeviceRemovedV0, InputTypeData + }, +}; + +use dashmap::DashSet; + +use crate::message::{DeviceAddedV0, DeviceAddedV1, DeviceAddedV2, DeviceAddedV3}; + +use super::message::{ + BatteryLevelReadingV2, + ButtplugClientMessageV2, + ButtplugClientMessageV3, + ButtplugClientMessageVariant, + ButtplugServerMessageV0, + ButtplugServerMessageV1, + ButtplugServerMessageV2, + ButtplugServerMessageV3, + ButtplugServerMessageVariant, + SensorReadingV3, +}; + +pub struct ButtplugServerDeviceEventMessageConverter { + device_indexes: DashSet +} + +impl ButtplugServerDeviceEventMessageConverter { + pub fn new(indexes: Vec) -> Self { + let device_indexes = DashSet::new(); + indexes.iter().for_each(|x| { device_indexes.insert(*x); }); + Self { + device_indexes + } + } + + // Due to the way we generate device events, we expect every new DeviceList to only have one + // change currently. + pub fn convert_device_list(&self, version: &ButtplugMessageSpecVersion, list: &DeviceListV4) -> ButtplugServerMessageVariant { + let new_indexes: Vec = list.devices().iter().map(|x| *x.0).collect(); + if new_indexes.len() > self.device_indexes.len() { + // Device Added + let connected_devices: Vec<&DeviceMessageInfoV4> = list.devices().values().filter(|x| !self.device_indexes.contains(&x.device_index())).collect(); + self.device_indexes.insert(connected_devices[0].device_index()); + if *version == ButtplugMessageSpecVersion::Version4 { + return ButtplugServerMessageVariant::V4(list.clone().into()); + } + let da3 = DeviceAddedV3::from(connected_devices[0].clone()); + if *version == ButtplugMessageSpecVersion::Version3 { + return ButtplugServerMessageVariant::V3(da3.into()); + } + let da2 = DeviceAddedV2::from(da3); + if *version == ButtplugMessageSpecVersion::Version2 { + return ButtplugServerMessageVariant::V2(da2.into()); + } + let da1 = DeviceAddedV1::from(da2); + if *version == ButtplugMessageSpecVersion::Version1 { + return ButtplugServerMessageVariant::V1(da1.into()); + } + let da0 = DeviceAddedV0::from(da1); + return ButtplugServerMessageVariant::V0(ButtplugServerMessageV0::DeviceAdded(da0)); + } else { + // Device Removed + let disconnected_indexes: Vec = self.device_indexes.iter().filter(|x| !new_indexes.contains(x)).map(|x| *x).collect(); + self.device_indexes.remove(&disconnected_indexes[0]); + match version { + ButtplugMessageSpecVersion::Version0 => { + return ButtplugServerMessageVariant::V0(ButtplugServerMessageV0::DeviceRemoved(DeviceRemovedV0::new(disconnected_indexes[0]))) + } + ButtplugMessageSpecVersion::Version1 => { + return ButtplugServerMessageVariant::V1(DeviceRemovedV0::new(disconnected_indexes[0]).into()) + } + ButtplugMessageSpecVersion::Version2 => { + return ButtplugServerMessageVariant::V2(DeviceRemovedV0::new(disconnected_indexes[0]).into()) + } + ButtplugMessageSpecVersion::Version3 => { + return ButtplugServerMessageVariant::V3(DeviceRemovedV0::new(disconnected_indexes[0]).into()) + } + ButtplugMessageSpecVersion::Version4 => return ButtplugServerMessageVariant::V4(list.clone().into()), + } + } + // There is no == here because the only way DeviceList would be returned is via a + // RequestDeviceList call. Events will only ever be additions or deletions. + } +} + +pub struct ButtplugServerMessageConverter { + original_message: Option, +} + +impl ButtplugServerMessageConverter { + pub fn new(msg: Option) -> Self { + Self { + original_message: msg, + } + } + + // + // Outgoing Conversion + // + + pub fn convert_outgoing( + &self, + msg: &ButtplugServerMessageV4, + version: &ButtplugMessageSpecVersion, + ) -> Result { + let mut outgoing_msg = match version { + ButtplugMessageSpecVersion::Version0 => { + ButtplugServerMessageVariant::V0(self.convert_servermessagev4_to_servermessagev0(msg)?) + } + ButtplugMessageSpecVersion::Version1 => { + ButtplugServerMessageVariant::V1(self.convert_servermessagev4_to_servermessagev1(msg)?) + } + ButtplugMessageSpecVersion::Version2 => { + ButtplugServerMessageVariant::V2(self.convert_servermessagev4_to_servermessagev2(msg)?) + } + ButtplugMessageSpecVersion::Version3 => { + ButtplugServerMessageVariant::V3(self.convert_servermessagev4_to_servermessagev3(msg)?) + } + ButtplugMessageSpecVersion::Version4 => ButtplugServerMessageVariant::V4(msg.clone()), + }; + // Always make sure the ID is set after conversion + outgoing_msg.set_id(msg.id()); + Ok(outgoing_msg) + } + + fn convert_servermessagev4_to_servermessagev3( + &self, + msg: &ButtplugServerMessageV4, + ) -> Result { + match msg { + ButtplugServerMessageV4::InputReading(m) => { + let original_msg = self.original_message.as_ref().unwrap(); + if let ButtplugClientMessageVariant::V3(ButtplugClientMessageV3::SensorReadCmd(msg)) = + &original_msg + { + // We only ever implemented battery in v3, so only accept that. + if let InputTypeData::Battery(value) = m.data() { + let msg_out = SensorReadingV3::new( + msg.device_index(), + *msg.sensor_index(), + *msg.sensor_type(), + vec!(value.data() as i32) + ); + Ok(msg_out.into()) + } else { + Err(ButtplugMessageError::UnexpectedMessageType("SensorReading".to_owned()).into()) + } + } else { + Err(ButtplugMessageError::UnexpectedMessageType("SensorReading".to_owned()).into()) + } + } + _ => Ok(msg.clone().try_into()?), + } + } + + fn convert_servermessagev4_to_servermessagev2( + &self, + msg: &ButtplugServerMessageV4, + ) -> Result { + let msg_v3 = self.convert_servermessagev4_to_servermessagev3(msg)?; + match msg_v3 { + ButtplugServerMessageV3::SensorReading(m) => { + let original_msg = self.original_message.as_ref().unwrap(); + // Sensor Reading didn't exist in v2, we only had Battery or RSSI. Therefore we need to + // context of the original message to make sure this conversion happens correctly. + if let ButtplugClientMessageVariant::V2(ButtplugClientMessageV2::BatteryLevelCmd(msg)) = + &original_msg + { + Ok(BatteryLevelReadingV2::new(msg.device_index(), m.data()[0] as f64 / 100f64).into()) + } else { + Err(ButtplugMessageError::UnexpectedMessageType("SensorReading".to_owned()).into()) + } + } + _ => Ok(msg_v3.into()), + } + } + + fn convert_servermessagev4_to_servermessagev1( + &self, + msg: &ButtplugServerMessageV4, + ) -> Result { + Ok(self.convert_servermessagev4_to_servermessagev2(msg)?.into()) + } + + fn convert_servermessagev4_to_servermessagev0( + &self, + msg: &ButtplugServerMessageV4, + ) -> Result { + Ok(self.convert_servermessagev4_to_servermessagev1(msg)?.into()) + } + + // Outgoing Conversion Utility Methods +} diff --git a/crates/buttplug_server_device_config/Cargo.toml b/crates/buttplug_server_device_config/Cargo.toml new file mode 100644 index 000000000..79fb8d61f --- /dev/null +++ b/crates/buttplug_server_device_config/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "buttplug_server_device_config" +version = "10.0.0" +authors = ["Nonpolynomial Labs, LLC "] +description = "Buttplug Intimate Hardware Control Library - Server Device Config Library" +license = "BSD-3-Clause" +homepage = "http://buttplug.io" +repository = "https://github.com/buttplugio/buttplug.git" +readme = "./README.md" +keywords = ["usb", "serial", "hardware", "bluetooth", "teledildonics"] +edition = "2024" +exclude = ["examples/**"] + +[lib] +name = "buttplug_server_device_config" +path = "src/lib.rs" +test = true +doctest = true +doc = true + +[dependencies] +buttplug_derive = "0.8.1" +buttplug_core = { path = "../buttplug_core" } +# buttplug_derive = { path = "../buttplug_derive" } +futures = "0.3.31" +futures-util = "0.3.31" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" +serde_repr = "0.1.20" +thiserror = "2.0.12" +displaydoc = "0.2.5" +dashmap = { version = "6.1.0", features = ["serde"] } +log = "0.4.27" +getset = "0.1.6" +jsonschema = { version = "0.30.0", default-features = false } +uuid = { version = "1.17.0", features = ["serde", "v4"] } +strum_macros = "0.27.1" +strum = "0.27.1" + +[build-dependencies] +serde_yaml = "0.9.34" +serde_json = "1.0.140" +serde = { version = "1.0.219", features = ["derive"] } +buttplug_core = { path = "../buttplug_core" } diff --git a/buttplug/buttplug-device-config/build-config/_headers b/crates/buttplug_server_device_config/build-config/_headers similarity index 100% rename from buttplug/buttplug-device-config/build-config/_headers rename to crates/buttplug_server_device_config/build-config/_headers diff --git a/buttplug/buttplug-device-config/build-config/_redirects b/crates/buttplug_server_device_config/build-config/_redirects similarity index 100% rename from buttplug/buttplug-device-config/build-config/_redirects rename to crates/buttplug_server_device_config/build-config/_redirects diff --git a/crates/buttplug_server_device_config/build-config/buttplug-device-config-v4.json b/crates/buttplug_server_device_config/build-config/buttplug-device-config-v4.json new file mode 100644 index 000000000..536624046 --- /dev/null +++ b/crates/buttplug_server_device_config/build-config/buttplug-device-config-v4.json @@ -0,0 +1,20659 @@ +{ + "version": { + "major": 4, + "minor": 59 + }, + "protocols": { + "activejoy": { + "communication": [ + { + "btle": { + "names": [ + "SS-TD-YDTD-001" + ], + "services": { + "0000f0b0-0000-1000-8000-00805f9b34fb": { + "rx": "0000f0b2-0000-1000-8000-00805f9b34fb", + "tx": "0000f0b1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "1fec4773-16a2-4bec-8910-1fcd9a85edaf", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "62e7b76d-ab99-42ca-89ea-865a6072451e", + "name": "IntoYou Remote Egg Vibrator" + } + }, + "adrienlastic": { + "communication": [ + { + "btle": { + "advertised-services": [ + "00001320-0000-1000-8000-00805f9b34fb" + ], + "names": [ + "Placeholder to avoid conflict with bad attempt to clone a Lovense Lush" + ], + "services": { + "6e400001-b5a3-f393-e0a9-e50e24dcca9e": { + "tx": "6e400002-b5a3-f393-e0a9-e50e24dcca9e" + } + } + } + } + ], + "configurations": [ + { + "id": "92c43355-c16f-471a-9c5d-ea30186b75a8", + "identifier": [ + "LVS-S001" + ], + "name": "Adrien Lastic Palpitation" + }, + { + "id": "ef491238-d560-46e4-84ed-72c902632bb2", + "identifier": [ + "LVS-S002" + ], + "name": "Adrien Lastic Revelation" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "714132f1-7ddd-420e-bf9f-6927fce0c9c3", + "output": { + "Vibrate": { + "step-range": [ + 0, + 16 + ] + } + } + } + ], + "id": "d5c4c815-9226-430d-8b40-915c0e208483", + "name": "Adrien Lastic Device" + } + }, + "amorelie-joy": { + "communication": [ + { + "btle": { + "names": [ + "4D01", + "4D02", + "4D03", + "4D04", + "4D05", + "4D06", + "4D07", + "4D08", + "4D09" + ], + "services": { + "0000ffe0-0000-1000-8000-00805f9b34fb": { + "rx": "0000ffe2-0000-1000-8000-00805f9b34fb", + "tx": "0000ffe3-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "b5681266-9f56-4a6f-9985-be33301af6af", + "identifier": [ + "4D02" + ], + "name": "Amorelie Joy Move" + }, + { + "id": "891e1acb-84ec-41e5-8782-2392a1343a34", + "identifier": [ + "4D05" + ], + "name": "Amorelie Joy Cha-Cha" + }, + { + "id": "fdc21c92-80d8-4cfa-a4e2-a79fef020e1c", + "identifier": [ + "4D06" + ], + "name": "Amorelie Joy Boogie" + }, + { + "id": "7a98633a-8b7e-4065-8e10-12b17588f504", + "identifier": [ + "4D01" + ], + "name": "Amorelie Joy Shimmer" + }, + { + "id": "bd784815-49d7-4379-98d0-34aa1d9c0097", + "identifier": [ + "4D03" + ], + "name": "Amorelie Joy Grow" + }, + { + "id": "6124dfc8-b0f4-4db0-b85a-b8d0da53b6a8", + "identifier": [ + "4D04" + ], + "name": "Amorelie Joy Shuffle" + }, + { + "id": "7e7776a5-98a8-42ef-a9e9-b4aeaf5adbaa", + "identifier": [ + "4D07" + ], + "name": "Amorelie Joy Salsa" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "9be34b27-431e-47d0-871b-fea3c116d32d", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "df7c19cc-8e49-4c55-98d1-0b060424260f", + "name": "Amorelie Joy Device" + } + }, + "aneros": { + "communication": [ + { + "btle": { + "names": [ + "Massage Demo" + ], + "services": { + "0000ff00-0000-1000-8000-00805f9b34fb": { + "tx": "0000ff01-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "description": "Perineum Vibrator", + "feature-type": "Vibrate", + "id": "a980bc1a-5554-4293-a75f-6d17bf25ebee", + "output": { + "Vibrate": { + "step-range": [ + 0, + 127 + ] + } + } + }, + { + "description": "Internal Vibrator", + "feature-type": "Vibrate", + "id": "811d7d6e-6a75-4925-943a-a06042223e3a", + "output": { + "Vibrate": { + "step-range": [ + 0, + 127 + ] + } + } + } + ], + "id": "f023f0f4-6629-469e-84c4-171ed4939f3d", + "name": "Aneros Vivi" + } + }, + "ankni": { + "communication": [ + { + "btle": { + "names": [ + "DSJM" + ], + "services": { + "0000180a-0000-1000-8000-00805f9b34fb": { + "generic0": "00002a50-0000-1000-8000-00805f9b34fb" + }, + "0000fe00-0000-1000-8000-00805f9b34fb": { + "tx": "0000fe01-0000-1000-8000-00805f9b34fb" + }, + "0000fffe-0000-1000-8000-00805f9b34fb": { + "tx": "0000fe02-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "2ba5d52d-0f40-4f1f-8738-955f9f7715f3", + "output": { + "Vibrate": { + "step-range": [ + 0, + 3 + ] + } + } + } + ], + "id": "9a26d86b-afd3-4413-ad72-faddf14b7f03", + "name": "Roselex Device" + } + }, + "bananasome": { + "communication": [ + { + "btle": { + "names": [ + "火箭X7" + ], + "services": { + "0000ae00-0000-1000-8000-00805f9b34fb": { + "tx": "0000ae01-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Oscillate", + "id": "63fa90c4-1ab9-4841-bfa3-45113f2c1d18", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "3e738dbf-3ff1-495a-a5bf-6d57776d80e8", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "c2a5f510-44fc-4c79-a9e2-ebf4862c45cb", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "83c998f8-1a18-48af-aa52-2f310252eb54", + "name": "Bananasome Rocket X7" + } + }, + "cachito": { + "communication": [ + { + "btle": { + "names": [ + "CCTSK", + "CCTXueGao" + ], + "services": { + "0000fff0-0000-1000-8000-00805f9b34fb": { + "tx": "0000fff2-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "8c4ee478-8dbb-41e6-b41c-a5664eec1532", + "identifier": [ + "CCTSK" + ], + "name": "Cachito Lure Tao" + }, + { + "id": "57b25f6e-03d6-44ef-b378-0ef9e69170d4", + "identifier": [ + "CCTXueGao" + ], + "name": "Cachito Ice Cream" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "6e5ce97a-2eae-4807-a857-0e74a9f0d095", + "output": { + "Vibrate": { + "step-range": [ + 0, + 5 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "2ec18700-3fac-4f3b-91c1-ead90bf853d0", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "0ce7063c-f118-44ea-80ed-66f3edb90a57", + "name": "Cachito Device" + } + }, + "cowgirl": { + "communication": [ + { + "btle": { + "names": [ + "THE COWGIRL", + "THE UNICORN" + ], + "services": { + "0000fe00-0000-1000-8000-00805f9b34fb": { + "tx": "0000fe01-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "188130d5-6ea1-473f-a9f4-a176929221ff", + "identifier": [ + "THE COWGIRL" + ], + "name": "The Cowgirl" + }, + { + "id": "675d61d0-b30f-4f60-abf7-6d5f67a5b56c", + "identifier": [ + "THE UNICORN" + ], + "name": "The Unicorn" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "11c01b64-e6cc-4b19-9a4d-eaf03a317b03", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Rotate", + "id": "9f3e0837-26e5-4ab1-bb2c-67be33ca920d", + "output": { + "Rotate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "5cdfacc3-7a69-415c-aefc-1d889fc5e824", + "name": "The Cowgirl Device" + } + }, + "cowgirl-cone": { + "communication": [ + { + "btle": { + "names": [ + "CG-CONE" + ], + "services": { + "0000ffe0-0000-1000-8000-00805f9b34fb": { + "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "72ec0578-c6dc-4835-a72d-3388816f9611", + "identifier": [ + "CG-CONE" + ], + "name": "The Cowgirl Cone" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "d9247325-2173-4ac7-95c3-6730f0d37964", + "output": { + "Vibrate": { + "step-range": [ + 0, + 128 + ] + } + } + } + ], + "id": "2dc2667a-2305-4dd4-a0a0-9c1dbcf119ea", + "name": "The Cowgirl Cone" + } + }, + "cueme": { + "communication": [ + { + "btle": { + "names": [ + "FUNCODE_*" + ], + "services": { + "0000fff0-0000-1000-8000-00805f9b34fb": { + "tx": "0000fff1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "ff44bb15-c9ae-4751-b993-8f325129cbb2", + "identifier": [ + "1" + ], + "name": "Cueme Mens" + }, + { + "id": "dcb3e162-5271-4737-b2e3-88534daafe05", + "identifier": [ + "2" + ], + "name": "Cueme Bra" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "b4554560-c0ad-42ac-82a8-4a8042fc6ab9", + "output": { + "Vibrate": { + "step-range": [ + 0, + 15 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "d666a28d-3701-499f-b0b9-7f6ccf722159", + "output": { + "Vibrate": { + "step-range": [ + 0, + 15 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "d2789e16-6771-4046-b5de-500def289894", + "output": { + "Vibrate": { + "step-range": [ + 0, + 15 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "c01700e6-1b57-41aa-831b-b3f7a54dbefe", + "output": { + "Vibrate": { + "step-range": [ + 0, + 15 + ] + } + } + } + ], + "id": "29364127-d158-411f-9e28-e8f33a5ca4a6", + "identifier": [ + "3" + ], + "name": "Cueme Womans" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "812c9f59-e9a9-42d9-8c30-1dc91feea5ac", + "output": { + "Vibrate": { + "step-range": [ + 0, + 15 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "bbd5955a-5c2e-494e-911d-c64708763bea", + "output": { + "Vibrate": { + "step-range": [ + 0, + 15 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "9c152f4a-8441-47f4-9b02-d0f64a468517", + "output": { + "Vibrate": { + "step-range": [ + 0, + 15 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "f19d9974-0631-4413-a544-7bf02c039743", + "output": { + "Vibrate": { + "step-range": [ + 0, + 15 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "ec23bb7f-34df-4480-8eba-3f95dc0d1e0a", + "output": { + "Vibrate": { + "step-range": [ + 0, + 15 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "24c910ea-7cfb-486c-8e86-451e8b3bc22f", + "output": { + "Vibrate": { + "step-range": [ + 0, + 15 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "b8659ec6-6b50-4d74-8a92-2c127856a7ff", + "output": { + "Vibrate": { + "step-range": [ + 0, + 15 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "96b18136-9780-4771-b5e6-f090927fbe14", + "output": { + "Vibrate": { + "step-range": [ + 0, + 15 + ] + } + } + } + ], + "id": "aeecfe99-106d-4f25-a9b6-4a809971ebfb", + "name": "Cueme Device" + } + }, + "cupido": { + "communication": [ + { + "btle": { + "names": [ + "MY2607-BLE-V1.0" + ], + "services": { + "0000f0b0-0000-1000-8000-00805f9b34fb": { + "rx": "0000f0b2-0000-1000-8000-00805f9b34fb", + "tx": "0000f0b1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "7f645006-1074-415f-8b06-43aa473573c0", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "8ef3fe28-6903-4418-9dd8-5323788ca961", + "name": "Cupido Device" + } + }, + "deepsire": { + "communication": [ + { + "btle": { + "names": [ + "IMP 3" + ], + "services": { + "0000ffe0-0000-1000-8000-00805f9b34fb": { + "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "ee9f0605-415e-4b07-8deb-c7252eff7053", + "identifier": [ + "IMP 3" + ], + "name": "Kuirkish Imp 3" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "08e0cd3e-65eb-42a4-8b15-990eb2e4c855", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "dd188bc6-784e-4799-b80c-3f568f8794cc", + "name": "DeepSire Device" + } + }, + "feelingso": { + "communication": [ + { + "btle": { + "names": [ + "Flair Feel" + ], + "services": { + "42410001-0000-0101-0000-736278637a72": { + "rx": "42410003-0000-0101-0000-736278637a72", + "tx": "42410002-0000-0101-0000-736278637a72" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "ad577b65-e74b-44c3-868b-86e3bfd53dbe", + "output": { + "Vibrate": { + "step-range": [ + 0, + 19 + ] + } + } + }, + { + "feature-type": "Oscillate", + "id": "5a2bd962-a9ab-4bd6-af7b-ae1fd6b39d79", + "output": { + "Oscillate": { + "step-range": [ + 0, + 19 + ] + } + } + } + ], + "id": "2f2d3b3d-e832-40e4-ad74-705c0f02997d", + "name": "FeelingSo Flair Feel" + } + }, + "fleshy-thrust": { + "communication": [ + { + "btle": { + "names": [ + "BT05" + ], + "services": { + "0000ffe0-0000-1000-8000-00805f9b34fb": { + "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "PositionWithDuration", + "id": "a8185061-6d41-4eea-bc24-1ff1c5c405b9", + "output": { + "PositionWithDuration": { + "step-range": [ + 0, + 180 + ] + } + } + } + ], + "id": "f273ebd5-a698-4c35-9c46-0625fa442960", + "name": "Fleshy Thrust Sync" + } + }, + "foreo": { + "communication": [ + { + "btle": { + "names": [ + "FOFO", + "LUNA fofo", + "LUNA FOFO", + "LUNA PLAY SMART", + "LUNA PLAYSMART2", + "LUNA PLAY SMART2", + "LUNA play smart2", + "LUNA play smart 2", + "LUNA 3", + "LUNA3", + "LUNA3PLUS", + "LUNA3 PLUS", + "LUNA 3 PLUS", + "LUNA 3 plus", + "LUNA 3 MEN", + "LUNA3MEN", + "LUNA MINI3", + "LUNA MINI 3", + "LUNA mini 3", + "LUNA4PLUS", + "LUNA4", + "LUNA 4", + "LUNA4PLUS", + "LUNA4 PLUS", + "LUNA 4 plus", + "LUNA4MEN", + "LUNA 4 MEN", + "LUNA 4 FOR MEN", + "LUNA MINI4", + "LUNA MINI 4", + "LUNA mini 4", + "LUNA 4 mini", + "UFO", + "UFO mini", + "UFO MINI", + "UFO MIN", + "UFO2", + "UFO 2", + "UFOMINI2", + "UFO mini 2", + "UFO3", + "UFO3mini", + "UFO3go", + "UFO3led", + "BEAR", + "BEAR_MINI", + "BEAR MINI", + "BEAR mini", + "BEAR2", + "BEAR 2", + "BEAR2go", + "BEAR2body", + "BEAR2eyes", + "KIWI", + "KIWI derma" + ], + "services": { + "0000fff0-0000-1000-8000-00805f9b34fb": { + "tx": "0000fff1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "98f14be3-8938-403a-8f90-d4bf5d15409f", + "identifier": [ + "FOFO", + "LUNA fofo", + "LUNA FOFO", + "LUNA PLAY SMART" + ], + "name": "Foreo LUNA fofo" + }, + { + "id": "ee014806-a78a-4d83-9c22-25941f13c26e", + "identifier": [ + "LUNA PLAYSMART2", + "LUNA PLAY SMART2", + "LUNA play smart2", + "LUNA play smart 2" + ], + "name": "Foreo LUNA play smart 2" + }, + { + "id": "c711b125-092c-4ece-bb98-83050b3fdf52", + "identifier": [ + "LUNA 3", + "LUNA3" + ], + "name": "Foreo LUNA 3" + }, + { + "id": "da0802b8-f60c-4261-83f7-6c703e587fa2", + "identifier": [ + "LUNA3PLUS", + "LUNA3 PLUS", + "LUNA 3 PLUS", + "LUNA 3 plus" + ], + "name": "Foreo LUNA 3 plus" + }, + { + "id": "de02db79-eba2-48dc-b539-5364aaae4bd2", + "identifier": [ + "LUNA 3 MEN", + "LUNA3MEN" + ], + "name": "Foreo LUNA 3 men" + }, + { + "id": "2ec4a921-d834-4da0-b710-a9d10fba4942", + "identifier": [ + "LUNA MINI3", + "LUNA MINI 3", + "LUNA mini 3" + ], + "name": "Foreo LUNA 3 mini" + }, + { + "id": "695d3e66-e545-43ae-a8fa-8a8883e32439", + "identifier": [ + "LUNA4", + "LUNA 4" + ], + "name": "Foreo LUNA 4" + }, + { + "id": "34503c35-05ef-44f4-875e-e46c9c81a71f", + "identifier": [ + "LUNA4PLUS", + "LUNA4 PLUS", + "LUNA 4 plus" + ], + "name": "Foreo LUNA 4 plus" + }, + { + "id": "e519d03d-35e4-4e06-84da-a183a516d2bf", + "identifier": [ + "LUNA4MEN", + "LUNA 4 MEN", + "LUNA 4 FOR MEN" + ], + "name": "Foreo LUNA 4 men" + }, + { + "id": "52c53ab8-513a-4cb8-abb5-622086c7b6b0", + "identifier": [ + "LUNA MINI4", + "LUNA MINI 4", + "LUNA mini 4", + "LUNA 4 mini" + ], + "name": "Foreo LUNA 4 mini" + }, + { + "id": "67c567c0-1ea2-4093-80bf-a109f6831621", + "identifier": [ + "UFO" + ], + "name": "Foreo UFO" + }, + { + "id": "305f6099-c0a7-4eb0-bf0f-7499ef152d8c", + "identifier": [ + "UFO mini", + "UFO MINI", + "UFO MIN" + ], + "name": "Foreo UFO mini" + }, + { + "id": "5e5700df-c1b1-448a-822f-1808e453641f", + "identifier": [ + "UFO2", + "UFO 2" + ], + "name": "Foreo UFO 2" + }, + { + "id": "3256b258-13cd-4df9-abdb-d8e547c396d5", + "identifier": [ + "UFO3" + ], + "name": "Foreo UFO 3" + }, + { + "id": "1ca37f05-520d-4696-86b1-d0edcf9fa803", + "identifier": [ + "UFO3go" + ], + "name": "Foreo UFO 3 go" + }, + { + "id": "77d89601-216c-42ee-9908-c0afd777c9a6", + "identifier": [ + "UFO3eyes" + ], + "name": "Foreo UFO 3 led" + }, + { + "id": "58f9677c-440f-43c9-9ab6-7f938edd3f4a", + "identifier": [ + "UFO3mini" + ], + "name": "Foreo UFO 3 mini" + }, + { + "id": "d555e823-52aa-4f02-8d8e-788c3dbe3a5e", + "identifier": [ + "UFOMINI2", + "UFO mini 2" + ], + "name": "Foreo UFO mini 2" + }, + { + "id": "a050edb2-71b2-494a-b3db-4f0d9ac20310", + "identifier": [ + "BEAR" + ], + "name": "Foreo BEAR" + }, + { + "id": "1231d10c-eee6-4061-8eb2-ffdec6f1523a", + "identifier": [ + "BEAR_MINI", + "BEAR MINI", + "BEAR mini" + ], + "name": "Foreo BEAR mini" + }, + { + "id": "c57d9ca7-f3e6-4f48-b65c-fec9a648b699", + "identifier": [ + "BEAR2", + "BEAR 2" + ], + "name": "Foreo BEAR 2" + }, + { + "id": "35a0a090-3085-4f83-b9d2-eb26d0c21ea9", + "identifier": [ + "BEAR2go" + ], + "name": "Foreo BEAR 2 go" + }, + { + "id": "c66dd16e-13e0-4446-809f-a1567fe746c7", + "identifier": [ + "BEAR2eyes" + ], + "name": "Foreo BEAR 2 eyes" + }, + { + "id": "a837cdd0-6513-4962-85be-d4859e1a7c98", + "identifier": [ + "BEAR2body" + ], + "name": "Foreo BEAR 2 body" + }, + { + "id": "d14e7fd0-1da8-44dc-8028-39a5655185fa", + "identifier": [ + "KIWI" + ], + "name": "Foreo KIWI" + }, + { + "id": "ee07bc74-21af-455d-a26a-fab22f188f97", + "identifier": [ + "KIWI derma" + ], + "name": "Foreo KIWI derma" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "0749f306-bd4c-48d7-9c2a-1309817a4dcc", + "output": { + "Vibrate": { + "step-range": [ + 0, + 10 + ] + } + } + } + ], + "id": "92d98050-7a3f-45b2-9df1-41e8cda28033", + "name": "Foreo Device" + } + }, + "fox": { + "communication": [ + { + "btle": { + "names": [ + "FOX", + "FOX M70 Pro", + "FoxM70Pro", + "FOX M70-2" + ], + "services": { + "0000ae00-0000-1000-8000-00805f9b34fb": { + "tx": "0000ae01-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "e43828a2-7dc6-4af1-b450-73c50441849f", + "output": { + "Vibrate": { + "step-range": [ + 0, + 3 + ] + } + } + } + ], + "id": "4138dc32-5276-47e8-89d4-fddc6ca42c1d", + "name": "Fox Device" + } + }, + "fredorch": { + "communication": [ + { + "btle": { + "names": [ + "YXlinksSPP" + ], + "services": { + "0000ffb0-0000-1000-8000-00805f9b34fb": { + "rx": "0000ffb2-0000-1000-8000-00805f9b34fb", + "tx": "0000ffb1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "PositionWithDuration", + "id": "d3985f07-f95a-4f72-859e-8b0ac76f251f", + "output": { + "PositionWithDuration": { + "step-range": [ + 0, + 150 + ] + } + } + } + ], + "id": "cbd6a5b5-50c0-4fb5-93e3-408fd027ff4d", + "name": "Fredorch Device" + } + }, + "fredorch-rotary": { + "communication": [ + { + "btle": { + "names": [ + "M1_*" + ], + "services": { + "0000ae10-0000-1000-8000-00805f9b34fb": { + "rx": "0000ae02-0000-1000-8000-00805f9b34fb", + "tx": "0000ae01-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "description": "Fucking Machine Oscillation Speed", + "feature-type": "Oscillate", + "id": "0ec02168-f724-481a-a927-6ea6df4c89b5", + "output": { + "Oscillate": { + "step-range": [ + 0, + 20 + ] + } + } + } + ], + "id": "86b9ab9e-8507-4abf-b6af-8ecd01a94476", + "name": "Fredorch Rotary Device" + } + }, + "galaku": { + "communication": [ + { + "btle": { + "names": [ + "GX85", + "GX07", + "GX17", + "GX21", + "GX22", + "GX16", + "GX29", + "GX23", + "GX25", + "GX26", + "GK03", + "GX39", + "G321", + "G304", + "G336", + "G331", + "G326", + "G335", + "G341", + "G355", + "G349", + "G407", + "G204", + "G171", + "G12D", + "G123", + "G23A", + "G336", + "G23A", + "A073", + "GLMT", + "G901", + "G912", + "G901", + "G20B", + "K112", + "G202", + "K118", + "K107", + "G203", + "TXHL", + "TXMM", + "TXKL", + "K108", + "K109", + "KWL2", + "TFHL", + "TFMM", + "TFKL", + "K120", + "K12A", + "K12C", + "LL18", + "CYX2", + "RC31", + "MD19", + "QD48", + "BGSF", + "BGQS", + "AX05", + "G317", + "G312", + "G302", + "G320", + "G314", + "G228", + "G315", + "G307", + "K311", + "G339", + "G354", + "G12B", + "G29C", + "G29D", + "GKML", + "G348", + "G913", + "G213", + "TFF1", + "G310", + "K113", + "G228", + "G310", + "TFF1", + "D358", + "G322", + "D402", + "G40A", + "G403", + "G43A", + "K12B", + "QCVW", + "QCSW", + "QCPW", + "SN80", + "TFG1", + "GK27", + "GX27", + "GK25", + "AC695X_1(BLE)", + "GX33", + "WSXK" + ], + "services": { + "00001000-0000-1000-8000-00805f9b34fb": { + "rxblebattery": "00001002-0000-1000-8000-00805f9b34fb", + "tx": "00001001-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "53a117ec-0e2d-43ce-a77b-0ed4fbf82d07", + "identifier": [ + "V415" + ], + "name": "Galaku Nebula" + }, + { + "id": "6c62e478-d684-4c3a-9d74-0860be907a8e", + "identifier": [ + "GX85" + ], + "name": "Galaku Shana" + }, + { + "id": "ccda61b7-8517-4d31-8ef6-a730b1a0ab9a", + "identifier": [ + "GX07" + ], + "name": "Galaku Miya" + }, + { + "id": "0f24a925-bad8-48ec-9a35-887f78bc967d", + "identifier": [ + "GX17" + ], + "name": "Galaku Capsule lipstick" + }, + { + "id": "9d8d0d14-1507-48ee-8b99-1b5cc6f4a67e", + "identifier": [ + "GX21" + ], + "name": "Galaku Vitality Cat" + }, + { + "id": "22e21fb8-c399-490f-9680-5abe44c46bc9", + "identifier": [ + "GX22" + ], + "name": "Galaku Phantom X" + }, + { + "id": "c829fb46-4cf5-4034-bdea-2032e00a34c3", + "identifier": [ + "GX16" + ], + "name": "Galaku Vitality Strawberry" + }, + { + "id": "aaa2d14e-2b93-46e5-87a0-c622f6f9c82b", + "identifier": [ + "GX29" + ], + "name": "Galaku Little Magic Box" + }, + { + "id": "859c82eb-9163-426c-90c4-4b567ff34e95", + "identifier": [ + "GX23" + ], + "name": "Galaku Little Whale" + }, + { + "id": "fffd1a38-2ac8-470a-bffb-70360a4099ba", + "identifier": [ + "GX25" + ], + "name": "Galaku Happy Vibrator" + }, + { + "id": "a2e6b3c3-8101-4ff7-8113-4d5c9641f557", + "identifier": [ + "GX26" + ], + "name": "Galaku Xiaobao Beans" + }, + { + "id": "28e47ecf-6a79-48c0-acd1-82ee75955836", + "identifier": [ + "GK03" + ], + "name": "Galaku Capsule Vibrator" + }, + { + "id": "af836ee8-9c73-4759-80f4-d305a14e51c1", + "identifier": [ + "GX39" + ], + "name": "Galaku Ice cone miniAV stick" + }, + { + "id": "9b6a27bd-75d6-42c7-9a71-7f95807eb9c4", + "identifier": [ + "G321" + ], + "name": "Galaku mini ice cream cone" + }, + { + "id": "a1042c91-cfa0-41b8-9afa-637599c076ac", + "identifier": [ + "G304" + ], + "name": "Galaku Shia's Collar" + }, + { + "id": "bae928b3-7ff5-45d1-b251-882812d5ef88", + "identifier": [ + "G336" + ], + "name": "Galaku The Second Generation of Vitality Bird" + }, + { + "id": "074ef604-51bf-4f0a-97ee-16508c582968", + "identifier": [ + "G331" + ], + "name": "Galaku Octopus glans massager" + }, + { + "id": "ca21391e-6aa2-4480-a1a5-c138318bf44c", + "identifier": [ + "G326" + ], + "name": "Galaku Alice" + }, + { + "id": "d1a0cd58-1aa2-447c-bd7e-da471fdee5d8", + "identifier": [ + "G335" + ], + "name": "Galaku Unicorn Butt Plug" + }, + { + "id": "398c32ab-6498-4358-a25f-8553916719fd", + "identifier": [ + "G341" + ], + "name": "Galaku Ace" + }, + { + "id": "05dc7803-1513-48d9-9c2f-2719e8b71905", + "identifier": [ + "G355" + ], + "name": "Galaku Little cute turtle" + }, + { + "id": "5e8a289b-9f5f-4865-9f92-d7bd06c68950", + "identifier": [ + "G349" + ], + "name": "Galaku Little Bullet" + }, + { + "id": "9cc769ed-e911-491b-b8ad-1a78ed8675fe", + "identifier": [ + "G407" + ], + "name": "Galaku Joy Vibrator" + }, + { + "id": "e213ecfd-d0f9-44e1-9c17-d3d78f7c6216", + "identifier": [ + "G204" + ], + "name": "Galaku Bowling" + }, + { + "id": "299b1c71-e7fc-426b-8d6f-0375685de6a8", + "identifier": [ + "G171" + ], + "name": "Galaku Mixin Controller" + }, + { + "id": "aa6c0314-58bc-4b83-b9d7-5988151b0c53", + "identifier": [ + "G12D" + ], + "name": "Galaku Hua Chao Brush" + }, + { + "id": "ed5b32b5-79fa-4d74-8d44-3afc3e71fc38", + "identifier": [ + "G123" + ], + "name": "Galaku 花sai" + }, + { + "id": "9811b596-7c23-4f18-b0b6-895680d273b0", + "identifier": [ + "G23A" + ], + "name": "Galaku Dream Vibration" + }, + { + "id": "36d612d2-806c-49f5-85b6-0f291342ea34", + "identifier": [ + "G336" + ], + "name": "Galaku The Second Generation of Vitality Bird" + }, + { + "id": "83521db1-be7a-4ca6-be82-fe218dac73db", + "identifier": [ + "G23A" + ], + "name": "Galaku Dream Vibration" + }, + { + "id": "d34943d6-709c-4972-97c8-ffa75c7ff005", + "identifier": [ + "A073" + ], + "name": "Galaku Joy Vibrator" + }, + { + "id": "587af267-9322-4ac6-afe6-8dcd4217ced4", + "identifier": [ + "GLMT" + ], + "name": "Galaku Rogue Rabbit" + }, + { + "id": "3ee263a4-1aa6-4b6c-8d09-b82d24df4017", + "identifier": [ + "G901" + ], + "name": "Galaku Suck the vibrator" + }, + { + "id": "25528b16-8cfa-45d5-b8bc-cd238f2a0416", + "identifier": [ + "G912" + ], + "name": "Galaku Donut" + }, + { + "id": "9593572e-e19d-4863-86ba-3e0542ad54fb", + "identifier": [ + "G901" + ], + "name": "Galaku Suck the vibrator" + }, + { + "id": "1d5f2345-034e-4d41-93a7-3d0ef80933e0", + "identifier": [ + "G20B" + ], + "name": "Galaku Ballet Vibrator" + }, + { + "id": "52071636-ceb7-4f79-afb1-5d8af4dbf5a2", + "identifier": [ + "K112" + ], + "name": "Galaku Donut" + }, + { + "id": "d9abd771-c3bc-449a-8c4a-06938231111d", + "identifier": [ + "G202" + ], + "name": "Galaku Flirting Pen" + }, + { + "id": "bbb54012-bee5-451a-aea3-98f28ca695a9", + "identifier": [ + "K118" + ], + "name": "Galaku Ball vibrator" + }, + { + "id": "104e8fcf-db34-4006-9a27-183ca2b8aaf5", + "identifier": [ + "K107" + ], + "name": "Galaku Cyberpunk Airplane Cup" + }, + { + "id": "48e98efa-7c01-4a8e-a0b5-f721799d78e0", + "identifier": [ + "G203" + ], + "name": "Galaku Vitality Cute Pet" + }, + { + "id": "76ad7e0f-fcbf-4c21-b4f9-c2affe73355a", + "identifier": [ + "TXHL" + ], + "name": "Galaku Little gourd vibrating egg" + }, + { + "id": "2c2a664d-851d-4686-b432-1e2eef36b713", + "identifier": [ + "TXMM" + ], + "name": "Galaku little kitten" + }, + { + "id": "7ab1f6e5-ed53-463c-8379-40db8fa580b4", + "identifier": [ + "TXKL" + ], + "name": "Galaku Little Dinosaur" + }, + { + "id": "43e3d3d0-0c9f-46c0-b44b-4d2739a43522", + "identifier": [ + "K108" + ], + "name": "Galaku Bell sucking" + }, + { + "id": "7ce8bdb5-eebc-44e8-9369-b8a9633a0365", + "identifier": [ + "K109" + ], + "name": "Galaku Ring vibration" + }, + { + "id": "9106168e-1758-424e-8713-7266b96cbf6d", + "identifier": [ + "KWL2" + ], + "name": "Galaku Erection Booster" + }, + { + "id": "b56b1b77-0174-47f6-8429-06f83a7c2382", + "identifier": [ + "TFHL" + ], + "name": "Galaku Gyoyo-G (meaning Yue-little gourd)" + }, + { + "id": "c90795b9-355b-4cc3-b493-e63c92c4efe5", + "identifier": [ + "TFMM" + ], + "name": "Galaku Gyoyo (meaning joy)" + }, + { + "id": "f73faf1a-dc8d-47a6-ba00-435aec9fbfb1", + "identifier": [ + "TFKL" + ], + "name": "Galaku Gyoyo (meaning joy)" + }, + { + "id": "911b8708-8cc6-406b-8fca-f31dbecb8cbc", + "identifier": [ + "K120" + ], + "name": "Galaku Pinky stick" + }, + { + "id": "03a0ede5-fb62-4f5c-a3e9-c821a9afbfbf", + "identifier": [ + "K12A" + ], + "name": "Galaku Little Turtle Stick" + }, + { + "id": "d924b656-3e8e-4742-ab5e-cba345aa6c9b", + "identifier": [ + "K12C" + ], + "name": "Galaku Xiao Xian Wan" + }, + { + "id": "761d7fc2-ba70-4093-8bf7-f3e3ee1d639e", + "identifier": [ + "LL18" + ], + "name": "Galaku Mitang" + }, + { + "id": "bdd69b72-0c3d-4c14-b923-accd305e9ccc", + "identifier": [ + "CYX2" + ], + "name": "Secret Lover Simon" + }, + { + "id": "e17ab832-ca1b-430a-b03a-c053c268407e", + "identifier": [ + "RC31" + ], + "name": "Secret Lover Betty" + }, + { + "id": "546731c9-21c5-4bca-bb85-9fec1c3c627e", + "identifier": [ + "MD19" + ], + "name": "Secret Lover Kevin" + }, + { + "id": "f4cdbb77-f674-49df-9b76-75d460179c69", + "identifier": [ + "QD48" + ], + "name": "Adorime Wearable Egg Vibrator" + }, + { + "id": "c81608ec-36cc-4ca1-9a17-72e4e3ce020f", + "identifier": [ + "BGSF" + ], + "name": "Adorime Male Masturbator" + }, + { + "id": "0c62c632-fbf1-43f1-b3ab-bff41ea88c3a", + "identifier": [ + "BGQS" + ], + "name": "Adorime Penis Vibrator" + }, + { + "id": "bce17a9d-1cfa-47e7-a6fd-6471896bb1d3", + "identifier": [ + "AX05" + ], + "name": "Adorime Anal Vibrator" + }, + { + "features": [ + { + "description": "Oscillate", + "feature-type": "Oscillate", + "id": "f427019a-a136-45a0-a866-dac460d8770c", + "output": { + "Oscillate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "0fa679ef-eb23-4b10-a456-dd1f99ed7dee", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "19ac04ae-9d77-4b3b-a706-5df8252569a7", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "58de185f-a52c-42e0-b06f-bb7a293a9d40", + "identifier": [ + "G317" + ], + "name": "Galaku Zaku Aircraft Cup" + }, + { + "features": [ + { + "description": "Oscillate", + "feature-type": "Oscillate", + "id": "9a04b080-4956-499c-894d-d7538322160e", + "output": { + "Oscillate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "a8a8f9c0-f406-4b80-8c8e-3ff1bf9bff72", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "769865df-58b9-4d0f-8697-4ee78304a10c", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "8c3f6848-0c63-4a56-8f28-ffba313240e3", + "identifier": [ + "G312" + ], + "name": "Galaku Mecha-Original Owner's Aircraft Cup" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "c09c7502-7e42-49be-8620-44bf0dda08af", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "ccf2e0e7-4ade-4a9b-8b49-405653f72c7c", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "22792e4e-bf84-42d4-a1ec-cbffddd3d777", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "1f53344c-173d-4a00-abb4-623969d7b174", + "identifier": [ + "G302" + ], + "name": "Galaku Little Devil" + }, + { + "features": [ + { + "description": "Oscillate", + "feature-type": "Oscillate", + "id": "c86290fd-1271-45d3-98bf-bcd168a1948a", + "output": { + "Oscillate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "70de4e79-4db7-45ee-a7c1-490cdf23bb33", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "a6fb0d1b-9160-40ca-81a7-905776aeff83", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "e8c6ef4f-b574-4fa3-8887-df3415368621", + "identifier": [ + "G320" + ], + "name": "Galaku Athena" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "75943039-8932-4a1c-af26-d1f075e78c01", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "05804a02-980d-4380-b407-a30f56477f8e", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "a104dc8a-7759-4dd9-8113-d3b450b24658", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "a8f4769e-945e-4f32-b2fb-1d15c6be62c6", + "identifier": [ + "G314" + ], + "name": "Galaku Vitality Octopus II" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "7751e53b-a722-49e5-9534-5a5798de081c", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "68d399dd-a3c9-4423-b244-d231c7e0a131", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "398eb416-b3d7-4f23-90ec-2f9fb05487f7", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "ead84aad-7180-415d-8740-3a8c84be3fc9", + "identifier": [ + "G228" + ], + "name": "Galaku Little Dolphin" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "02fda4c8-b86c-4131-8d9f-447534785404", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "a21f8a77-22ce-47a3-b220-028f87d3a50d", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "e85a8553-4f3c-49ba-ae88-929d0052e04d", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "9ca11ed6-aa8a-4506-a7f8-78f515075340", + "identifier": [ + "G315" + ], + "name": "Galaku Unicorn" + }, + { + "features": [ + { + "description": "Oscillate", + "feature-type": "Oscillate", + "id": "3525faff-24d5-4b84-9b4d-b6e92f51f2f4", + "output": { + "Oscillate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "c1150106-9f41-4a80-b30b-6015e1a7e80a", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "57638eed-03e4-4279-8fc1-cc03a2d9066c", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "113cb4d3-f8a9-45b5-bf66-3e93e5209e4d", + "identifier": [ + "G307" + ], + "name": "Galaku Queen Bee Gun" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "c52a581b-0838-4431-bd39-179628da18d4", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "ba7de25e-d0fd-4431-afc5-e8b72431b025", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "309ff7a2-aa2f-44e4-ace9-c1d485bf47ae", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "13e7fd6e-2dec-400e-80e5-908a088572fc", + "identifier": [ + "K311" + ], + "name": "Galaku Freya" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "75e8f6e5-a69b-48d4-937b-c202961b464f", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "3854e366-6eb9-4947-bc90-e246146bec11", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "be8475dd-8928-447d-9e94-1e0543056b29", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "5d47e890-6093-4eae-b7e8-e637dc82a2ea", + "identifier": [ + "G339" + ], + "name": "Galaku Rhino Prostate Massager" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "dc4348f2-7788-4b63-96f8-80ed74e4f9c2", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "e79abb39-74ab-46cc-9363-41637a43c885", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "23e5cc47-944a-427c-be33-8611fffc70c8", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "1d9030a8-bfd2-4e49-8e8d-683c7776ae83", + "identifier": [ + "G354" + ], + "name": "Galaku Double-A Aircraft Cup" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "e86333ca-254b-4c40-b448-eeb0e397e2f6", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "f531ad54-4f1f-4fe6-91dd-bba265307fb5", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "f989b7c6-ad5d-49fa-b103-2a21ff2213d5", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "7565ed2f-36c6-4210-830b-c916c4f8132b", + "identifier": [ + "G12B" + ], + "name": "Galaku Flower Season" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "d8b78598-520b-4d28-9340-1a51d918f31a", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "ddc439b2-dc60-46bd-b6dc-4ce2b92783c0", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "34bf9651-bbd6-475f-a2ea-536b04c5db62", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "8a41b478-7239-4412-b251-66dcb62f0e98", + "identifier": [ + "G29C" + ], + "name": "Galaku Little Rubik's Cube" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "8dccfd7a-397e-450c-8911-31d2258506f5", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "6031712c-95a0-457f-93b6-e24b8ab7d335", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "7e0681c6-7206-41d0-97d2-f3e01d6c8de4", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "fae6c568-0e7f-446f-9523-81964f51728c", + "identifier": [ + "G29D" + ], + "name": "Galaku Small powder cake" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "48936afe-dfda-4a35-bd45-1da66bdc020f", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "f17eba7d-aab9-43d9-a621-4e5b3addd682", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "67430820-ef54-4821-8d43-37b7ebc6702f", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "722fc3e9-8349-4659-b71b-9c77d437f695", + "identifier": [ + "GKML" + ], + "name": "Galaku Milly" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "8afa26c6-e525-4afc-84f7-a9602d82ddf9", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "ed5039d6-24ea-4adb-becd-ab549aff67ce", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "8b8b2df2-1f06-4649-b575-ae0abef990dc", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "d546987d-311b-4db1-80d6-b8df1a06b275", + "identifier": [ + "G348" + ], + "name": "Galaku Rhinoceros Back Court" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "dff9df20-91d3-478f-b5dd-409db449d9ff", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "f23839bb-69c4-4570-9eb0-ea387a1fa87f", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "10d3c65c-e6b1-4802-b71f-5843bb6ae4bd", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "536ea0fc-ef97-40a1-be31-56f9cabd489e", + "identifier": [ + "G913" + ], + "name": "Galaku Unicorn II" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "5e4c85dc-27df-45fa-a7cc-f2870596b7ed", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "cb5581ba-2f77-49e3-bf0a-856639e045e1", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "f8057621-5690-43fe-8cf9-aa2b1d4ceb07", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "ee326d2c-8241-40b7-9ccd-3662a5901197", + "identifier": [ + "G213" + ], + "name": "Galaku Phantom" + }, + { + "features": [ + { + "description": "Oscillate", + "feature-type": "Oscillate", + "id": "5027b245-170a-47ca-b9b6-d93c48532d56", + "output": { + "Oscillate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "376aee27-8c1b-4d26-a5e3-9b92be56036d", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "42b39996-60ac-4ee7-9880-1bc8d73b543a", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "e1516f9a-9f56-4859-832d-6b637c6880e5", + "identifier": [ + "TFF1" + ], + "name": "Galaku F1 Aircraft Cup" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "7d6f9b0d-2296-42d6-a989-63366e943fff", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "ed69fd16-6951-4176-96b5-e267cb4213e4", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "76599534-d259-4420-acf8-f172421b684e", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "a849f281-4415-4b0d-a2e2-5b93e8d36833", + "identifier": [ + "G310" + ], + "name": "Galaku Scepter AV Stick" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "5debcf2d-4e98-4b5f-88b0-45f4bcd3aaf1", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "787e3d35-0ea2-407e-8b4b-ecb0680ddfa3", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "c6d8ebc8-bba3-4aaa-b616-3758a6a84b06", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "568f5426-4d6d-4fed-b915-c4ead0dc2b70", + "identifier": [ + "K113" + ], + "name": "Galaku Unicorn II" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "484bcea7-f227-49f3-83f8-ab825c46e0f4", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "4d68f7a8-2fd1-40f3-8d5f-b932b0fb5d8f", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "f93f3c1d-8046-40f2-a4d3-4c5315c809e6", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "b1a680ee-43ea-44a1-95f0-b287d9b87d07", + "identifier": [ + "G228" + ], + "name": "Galaku Little Dolphin" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "525a328a-1fe1-4f54-be62-1aade3f4dcab", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "0f5a8b59-1ba2-4e0f-9de4-272ee2fae908", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "246cddf5-f04a-45e2-ba07-1f5354d15fdd", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "59723525-29b0-4cfe-b327-c4337e94cce7", + "identifier": [ + "G310" + ], + "name": "Galaku Scepter AV Stick" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "e19f5460-6145-48b9-9151-c16765130341", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "f44a3499-e077-41c5-93ba-56a840c8485b", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "79874bf3-3055-4d5a-a6aa-ea183f434324", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "f989e3e1-6df9-4ab1-a2a6-04aded3fe9a3", + "identifier": [ + "TFF1" + ], + "name": "Galaku F1 Aircraft Cup" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "98b72986-86e9-44dc-a48c-e4b64d5941c0", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "907f514f-4cfa-4210-88c8-2ae602cade4b", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "338f4e14-793b-4cb7-b26e-0ff47f2e72cc", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "3ff9c409-8790-4b06-af84-a0ddf103bf23", + "identifier": [ + "D358" + ], + "name": "Galaku Classic vibration-absorbing AV state" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "d61c7b5a-b021-43bf-a246-9b7dc193cf98", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "64ecb833-2b8a-46c6-afac-28aa36d05580", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "87973aa3-f77e-47b1-92dc-1a6b32bba5d5", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "d3c966a9-9341-44b5-a54d-842402010dc5", + "identifier": [ + "G322" + ], + "name": "Galaku Unicorn" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "daedd54d-0d62-434f-8408-d3d9f69cd151", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "7ebb5f9d-e447-4b67-8b3a-997b46a5f2be", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "b872a7d6-df4c-4d50-8e7b-57cc7102b151", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "9ccc2c45-2762-4005-9de1-f636b44d0e0e", + "identifier": [ + "D402" + ], + "name": "Galaku New series of vibrators" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "1954d249-a830-4c2f-9a54-73962b0a7f62", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "b0a5e213-8e34-4868-9f93-477d707b555a", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "f5555828-157d-44af-a6f3-61c184adc78b", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "8202daae-1d8f-468e-b772-31f6032e92ff", + "identifier": [ + "G40A" + ], + "name": "Galaku New series of vibrators" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "1db2e6ef-89a9-44a6-b4fe-858c583181cc", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "af1c0858-6f69-49bd-81e0-2b5634cba141", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "0acf4462-c96b-4dec-b283-d56fdeae3e09", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "5910e68c-1ed0-4dd1-b9b9-74bb2332d3b7", + "identifier": [ + "G403" + ], + "name": "Galaku New series of vibrators" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "9204650b-9e73-4423-9de1-94e87cf8cf7b", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "3e533985-211f-4c4e-996e-6ee5999a8f7b", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "01388799-5cdf-4127-824b-a51ae1c38e60", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "49ce5f25-f210-43cf-a20e-bb0879b89c63", + "identifier": [ + "G43A" + ], + "name": "Galaku New series of vibrators" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "50c856df-a8d2-4840-bc3d-17f7bc2144e8", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "cc865a89-7a1f-4d9c-ac03-8822ec1ab715", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "9ecdcaa7-b228-4f67-b04b-a1ff3642ebe2", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "ec43f998-0089-4bef-8a8d-d3ce49747fff", + "identifier": [ + "K12B" + ], + "name": "Galaku Little Turtle Stick" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "cf8ed969-86d5-4597-850f-35c60cfc40e8", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "13dd1aad-9102-46c9-b126-5293b5da88ad", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "421f8bf8-6732-405a-b563-139e858bc4fb", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "c61bdc8f-230b-4cc8-9474-c145ecba7682", + "identifier": [ + "QCVW" + ], + "name": "Kisstoy Lost (Vibrating)" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "02b1d882-d47e-4dc2-8062-91e9b6defdd4", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "1e4691ca-fda3-40da-bad9-b2f7393d5554", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "0b41e97c-17f9-475d-8a30-d8ed1f52cb67", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "287d283c-d1f6-4dd4-9b53-fc01adafed30", + "identifier": [ + "QCSW" + ], + "name": "Kisstoy Lost (Sucking)" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "2d070dbf-a2ad-4072-b7ee-a13b278fe4a4", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "cddbd1f6-227d-48e3-a1bc-74332b153a24", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "ad753ac1-6c20-495a-bb0d-409b251fbe26", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "746b8d6f-41ba-433f-b225-b3bf98c7aec9", + "identifier": [ + "QCPW" + ], + "name": "Kisstoy Lost (Insertable)" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "42efb235-b450-44a6-97fd-a98b3d9750ad", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "76a8c59e-2001-4334-bacd-f436f6858e85", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "47b24f11-bb92-4173-9123-80a330c76041", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "361c0746-f630-41d3-a0bf-f993e2259217", + "identifier": [ + "SN80" + ], + "name": "Adorime G-spot Rabbit Dildo Vibrator" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "2b5fdcd4-3b35-4939-b086-950a827141e1", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Suction Pump", + "feature-type": "Constrict", + "id": "59498f0e-ad39-4701-9197-a5c7428b0acc", + "output": { + "Constrict": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "591ca427-79d4-4d6a-bf00-8596cd9cb493", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "0d60d3a5-bad0-4df7-99ad-4bf4ee442c5d", + "identifier": [ + "TFG1" + ], + "name": "Galaku Aurora Aircraft Cup" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "ff51f8a4-4ac0-434c-b656-d94e0b2eec53", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "e0b9f2c7-68d9-4c7b-9327-6e0802973a44", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "687bbb0e-b5a6-47d8-bca3-3395c510d996", + "identifier": [ + "GK27", + "GX27" + ], + "name": "Galaku Cannon-GT" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "d8411669-9823-4755-afe4-969f7a4200cd", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "afb9c389-4624-4871-bfed-c19eccbcd3e3", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "4169f6af-723c-437c-be39-d90508c95e0a", + "identifier": [ + "GK25" + ], + "name": "Galaku Phantom PLUS" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "8626a95c-2ebd-43b4-a592-27282c6cc275", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "b680b236-52f4-4d8e-907e-78e71a0d23e9", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "637fec12-7e76-4107-ba18-931046975976", + "identifier": [ + "AC695X_1(BLE)" + ], + "name": "Galaku Vision" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "90351a28-a5c0-4b77-bd61-d5e667588cf1", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "ab7abe60-7733-4391-a61d-765655275261", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "34c495ac-a36f-4d8c-9823-191895926d49", + "identifier": [ + "GX33" + ], + "name": "Galaku Dimension No. 1" + }, + { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "80d6340d-70bd-40ba-87bd-014f034a3186", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "1ef7a2d2-1725-4fd9-9e70-d8e0674ac17f", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "938f9e14-3d1d-4778-821a-a1c17bb42936", + "identifier": [ + "WSXK" + ], + "name": "Galaku Starry Sky CUP" + } + ], + "defaults": { + "features": [ + { + "description": "Vibrate", + "feature-type": "Vibrate", + "id": "f650b5a9-7413-4ac9-b25e-863180daa04c", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "d9c34cf9-5645-4e04-bf92-51e5df708417", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "c1766383-def6-4bd0-b6ce-1e8f993fa6ae", + "name": "Galaku Device" + } + }, + "galaku-pump": { + "communication": [ + { + "btle": { + "names": [ + "V415" + ], + "services": { + "00001000-0000-1000-8000-00805f9b34fb": { + "tx": "00001001-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "7689175c-af6e-4529-a2ae-c4f41f1db595", + "identifier": [ + "V415" + ], + "name": "Galaku Nebula" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Oscillate", + "id": "60946646-0160-425f-85ca-9210d35d61fd", + "output": { + "Oscillate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "97f24406-d413-43ed-b830-b76c3f912fad", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "2e954d01-4f42-4acd-9be8-9fdfa0172998", + "name": "Galaku Device" + } + }, + "hgod": { + "communication": [ + { + "btle": { + "names": [ + "AMN NEO" + ], + "services": { + "0000ffe3-0000-1000-8000-00805f9b34fb": { + "rx": "0000ffe2-0000-1000-8000-00805f9b34fb", + "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "cd638669-9f47-400f-8dcf-80583e7e563a", + "output": { + "Vibrate": { + "step-range": [ + 0, + 10 + ] + } + } + } + ], + "id": "d786a1cc-7a7c-4b8b-996c-1d2fce573ca2", + "name": "Hgod Device" + } + }, + "hismith": { + "communication": [ + { + "btle": { + "names": [ + "HISMITH", + "Wildolo", + "\u0007HISMITH" + ], + "services": { + "0000ff90-0000-1000-8000-00805f9b34fb": { + "rxblemodel": "0000ff96-0000-1000-8000-00805f9b34fb" + }, + "0000ffe5-0000-1000-8000-00805f9b34fb": { + "tx": "0000ffe9-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "169414bc-55d6-4ada-a9ec-eae862e80e09", + "identifier": [ + "1001" + ], + "name": "Hismith Sex Machine" + }, + { + "id": "33a59054-9a87-4ecb-9893-3b5101b6431b", + "identifier": [ + "1002" + ], + "name": "Hismith Pro Traveler" + }, + { + "id": "119197ff-5750-40bf-9770-024e75cbe20c", + "identifier": [ + "1003" + ], + "name": "Hismith Capsule" + }, + { + "features": [ + { + "description": "Stroker Oscillation Speed", + "feature-type": "Oscillate", + "id": "1663c651-cab6-444d-bbd7-39baf190d6ab", + "output": { + "Oscillate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "b6a5ed20-e10a-4370-aa9e-0cd85bf1c6f7", + "output": { + "Vibrate": { + "step-range": [ + 0, + 1 + ] + } + } + } + ], + "id": "188ee17a-d776-4f9b-baaa-903b9fea276f", + "identifier": [ + "2001" + ], + "name": "Hismith Thrusting Cup" + }, + { + "features": [ + { + "description": "Stroker Oscillation Speed", + "feature-type": "Oscillate", + "id": "8621627f-4561-4272-9d95-231d9b8d3440", + "output": { + "Oscillate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "5815777e-11e1-4998-b9a6-68e09656f18c", + "output": { + "Vibrate": { + "step-range": [ + 0, + 1 + ] + } + } + } + ], + "id": "fb1d1aa1-5a88-4a39-af74-bc127d670ab1", + "identifier": [ + "1006" + ], + "name": "Hismith G011" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "5ac186f5-ada6-4ec2-a65a-910b8b2292cc", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "ef153cf6-130d-43a1-82f1-4a16e457e8ea", + "identifier": [ + "3001" + ], + "name": "Wildolo Device" + } + ], + "defaults": { + "features": [ + { + "description": "Fucking Machine Oscillation Speed", + "feature-type": "Oscillate", + "id": "24291feb-53a7-49ee-898a-8c42f534508f", + "output": { + "Oscillate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "a8689335-db27-4a23-8724-6973168bb474", + "name": "Hismith device" + } + }, + "hismith-mini": { + "communication": [ + { + "btle": { + "names": [ + "Auxfun-Box", + "Sinloli", + "Sinloli-Sherry", + "Eropair *", + "HISMITH S1", + "HISMITH S2", + "HISMITH S3", + "Sinloli Cosima", + "Sinloli-Ethel", + "Sinloli Aston" + ], + "services": { + "0000ff90-0000-1000-8000-00805f9b34fb": { + "rxblemodel": "0000ff96-0000-1000-8000-00805f9b34fb" + }, + "0000ffe5-0000-1000-8000-00805f9b34fb": { + "tx": "0000ffe9-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "6227affb-9e0e-49cb-a77b-7913d40f83ce", + "identifier": [ + "4001" + ], + "name": "Auxfun Sex Machine" + }, + { + "id": "de78cf6a-30c2-40ce-ac8a-a060735c65ac", + "identifier": [ + "1005", + "1102" + ], + "name": "Hismith Sex Machine" + }, + { + "id": "fa840f6f-6815-4fed-b238-4260ac21b90f", + "identifier": [ + "1004" + ], + "name": "Hismith Mini Sex Machine" + }, + { + "id": "330de697-9702-4bc7-89d6-3faf603f0238", + "identifier": [ + "1101" + ], + "name": "Hismith Servo Sex Machine" + }, + { + "id": "18f342d3-a927-44ac-9605-cf16ec8aad74", + "identifier": [ + "1402" + ], + "name": "Hismith Ukulele" + }, + { + "id": "5b98725d-56b3-499b-830d-50dc004c27c5", + "identifier": [ + "1501" + ], + "name": "Hismith PleasureDrive" + }, + { + "features": [ + { + "description": "Air Pump", + "feature-type": "Constrict", + "id": "1c45bd7c-ca54-483b-9994-f6d4c18cd59f", + "output": { + "Constrict": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrator", + "feature-type": "Vibrate", + "id": "23c0c1f0-af15-492d-8405-3ce3f24d13a3", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "81341b4e-144b-4427-b5e9-5024b12441c7", + "identifier": [ + "2201" + ], + "name": "Sinloli Automatic Sex Doll" + }, + { + "features": [ + { + "description": "Internal Vibrator", + "feature-type": "Vibrate", + "id": "85ca7d86-a508-4d9e-9ee5-0223a4b68805", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "External Vibrator", + "feature-type": "Vibrate", + "id": "950bc937-6be1-4f6c-8d18-36cbd4d25bee", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "e59964ad-0c44-4301-9148-f8837e197d35", + "identifier": [ + "3101" + ], + "name": "Eropair Rabbit Vibrator" + }, + { + "features": [ + { + "description": "Thruster", + "feature-type": "Oscillate", + "id": "6255e8b0-f188-4a8b-9325-4c70af3b20be", + "output": { + "Oscillate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrator", + "feature-type": "Vibrate", + "id": "e0eb75eb-a14b-4947-97de-0bd36517dabd", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "c1762d51-d2f7-4a03-bb8e-30cde5942831", + "identifier": [ + "3102" + ], + "name": "Eropair Thrusting Vibrating Dildo" + }, + { + "features": [ + { + "description": "Air Pump", + "feature-type": "Constrict", + "id": "39ed62dd-77c2-4488-ba09-33792a65b013", + "output": { + "Constrict": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrator", + "feature-type": "Vibrate", + "id": "d36a28fd-0042-4c5c-a36c-e0a72173e0ab", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "8ffeec80-9b8f-4cb5-a70d-6b6d8170a688", + "identifier": [ + "2101" + ], + "name": "Eropair Cup" + }, + { + "features": [ + { + "description": "Stroker Oscillation Speed", + "feature-type": "Oscillate", + "id": "928b7b2b-9e4e-47bc-8196-e304174e78fa", + "output": { + "Oscillate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Air Pump", + "feature-type": "Constrict", + "id": "e9b6dc68-e89a-4f7b-a74f-8a25b31346ee", + "output": { + "Constrict": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "9eb5977d-38be-4e77-8a26-1d69e8286689", + "identifier": [ + "2204" + ], + "name": "Sinloli Cosima" + }, + { + "features": [ + { + "description": "Stroker Oscillation Speed", + "feature-type": "Oscillate", + "id": "030bcd37-38f1-415f-b59e-d0013497fadf", + "output": { + "Oscillate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Vibrator", + "feature-type": "Vibrate", + "id": "19ca1ed9-94ee-46f8-9b70-0e79a013db9d", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "a14d8479-e4b9-463f-af23-e78bd0c5d2c7", + "identifier": [ + "2202" + ], + "name": "Sinloli Ethel" + }, + { + "id": "d9ced3ed-cc74-4731-baeb-7bbf7fda288e", + "identifier": [ + "2205" + ], + "name": "Sinloli Aston" + } + ], + "defaults": { + "features": [ + { + "description": "Fucking Machine Oscillation Speed", + "feature-type": "Oscillate", + "id": "cd95dc09-627b-489e-841a-39cd5f06bf6d", + "output": { + "Oscillate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "195a4797-7b3a-4ecf-bffb-810f9b870a8b", + "name": "Hismith Mini device" + } + }, + "htk_bm": { + "communication": [ + { + "btle": { + "names": [ + "HTK-BLE-BM001" + ], + "services": { + "00001802-0000-1000-8000-00805f9b34fb": { + "tx": "00002a06-0000-1000-8000-00805f9b34fb" + }, + "0000180f-0000-1000-8000-00805f9b34fb": { + "rxblebattery": "00002a19-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "3b33611d-bbba-498e-969d-526106c7e785", + "output": { + "Vibrate": { + "step-range": [ + 0, + 1 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "d41e037a-b6ab-4016-a07c-f9eb7e414efb", + "output": { + "Vibrate": { + "step-range": [ + 0, + 1 + ] + } + } + } + ], + "id": "3589254d-f271-4059-b2c3-3a5776d1eb02", + "name": "HTK Breast Massager" + } + }, + "itoys": { + "communication": [ + { + "btle": { + "names": [ + "26-021-B", + "SML-2310-SZ-B", + "ASF-001-BT-R" + ], + "services": { + "0000ffa0-0000-1000-8000-00805f9b34fb": { + "tx": "0000ffa1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "2eafb465-e72a-4acd-9344-b9e13fc1f2ed", + "identifier": [ + "26-021-B" + ], + "name": "iToys Seagull" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "07601b03-2dc3-4996-aaa7-d23b5aa793f5", + "output": { + "Vibrate": { + "step-range": [ + 0, + 3 + ] + } + } + }, + { + "feature-type": "Oscillate", + "id": "6d3f5346-4947-41b1-847e-39cd2f485a0a", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "f0458e98-a317-4b1c-af82-bb3f163aeff3", + "identifier": [ + "SML-2310-SZ-B" + ], + "name": "iToys Twinkling Stars" + }, + { + "features": [ + { + "feature-type": "Oscillate", + "id": "c742d608-2110-4377-aaea-7173d7f1dc83", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "0108797c-1cea-486d-9ed5-3b4412fb6593", + "identifier": [ + "ASF-001-BT-R" + ], + "name": "Defyeah Horizontal Sex Machine ASFO16" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "5f1a3edb-6015-404a-865a-c3ee2d568ed4", + "output": { + "Vibrate": { + "step-range": [ + 0, + 3 + ] + } + } + } + ], + "id": "5c58b967-b75f-4f5d-99ef-f581b2579918", + "name": "iToys Device" + } + }, + "jejoue": { + "communication": [ + { + "btle": { + "names": [ + "Je Joue" + ], + "services": { + "0000fff0-0000-1000-8000-00805f9b34fb": { + "tx": "0000fff1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "a723e382-c32d-4170-b909-50e9ecb9d17f", + "output": { + "Vibrate": { + "step-range": [ + 0, + 5 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "79434539-5c1d-459a-abbe-833f0a7403be", + "output": { + "Vibrate": { + "step-range": [ + 0, + 5 + ] + } + } + } + ], + "id": "3ad4a393-215b-4cc7-9d77-9541b3b1dab1", + "name": "Je Joue Device" + } + }, + "joyhub": { + "communication": [ + { + "btle": { + "names": [ + "J-Petalwish2", + "J-VortexTongue", + "J-Velocity", + "JOYHUB-ROSELLA2", + "J-VibSiren", + "J-ElixirEgg", + "J-RetroGuard", + "J-TrueForm", + "J-TrueForm3", + "J-Rhythmic2", + "J-Rhythmic3", + "J-Mysticolor", + "J-VividWings", + "J-Rainbow", + "J-BlackBull", + "J-Peacock", + "J-Mariner", + "J-Mace", + "J-MarsLion", + "J-Tarian", + "J-Pul", + "J-Euphoric", + "J-Euphoric3", + "J-Torrian", + "J-Rayen", + "J-ROSELLA3", + "J-Mackay", + "J-Rowdy3", + "J-Eclipse", + "J-DukeDazzle2", + "J-Scarlett", + "J-Tarik", + "J-UricaGuard2", + "J-Viva", + "J-Ryden", + "J-Mars", + "J-MarsLion2", + "J-Myrna", + "J-Vase2", + "J-Martino", + "J-Enam", + "J-Viv", + "J-Vivara", + "J-Explorer2", + "J-Derik" + ], + "services": { + "0000ffa0-0000-1000-8000-00805f9b34fb": { + "tx": "0000ffa1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "5b78e797-3ff6-4ca8-be15-28a1f3983dca", + "identifier": [ + "JOYHUB-ROSELLA2" + ], + "name": "JoyHub Rosella 2" + }, + { + "id": "bc35f659-b67b-4df5-afdd-46053c2a5366", + "identifier": [ + "J-Velocity" + ], + "name": "JoyHub Velocity" + }, + { + "id": "6cbbce9e-6154-4260-8d2c-69cc52edd2ee", + "identifier": [ + "J-ElixirEgg" + ], + "name": "JoyHub ElixirEgg" + }, + { + "id": "481344d5-9edd-48c4-8867-d0d639648d09", + "identifier": [ + "J-RetroGuard" + ], + "name": "JoyHub Retro Guard" + }, + { + "id": "5a3c541a-2924-44cc-a92d-d48b58cf0159", + "identifier": [ + "J-TrueForm3" + ], + "name": "JoyHub TrueForm 3" + }, + { + "id": "6368a677-6c33-4765-8baf-1cd0cd4bb06e", + "identifier": [ + "J-TrueForm" + ], + "name": "JoyHub TrueForm" + }, + { + "id": "46533dc6-6f1b-4b17-9f31-06b076f417d6", + "identifier": [ + "J-Rhythmic2" + ], + "name": "JoyHub Rhythmic 2" + }, + { + "id": "1a5dd035-8107-4db3-924d-503113b1c600", + "identifier": [ + "J-Rhythmic3" + ], + "name": "JoyHub Rhythmic 3" + }, + { + "id": "907042dc-2681-46a0-9a49-3b8564faa41a", + "identifier": [ + "J-Rainbow" + ], + "name": "JoyHub Rainbow" + }, + { + "id": "b92595de-f564-4298-a444-9c8bd1a2c7f9", + "identifier": [ + "J-BlackBull" + ], + "name": "JoyHub Black Bull" + }, + { + "id": "1b560be9-462d-4e08-adb5-2a38690e6ab2", + "identifier": [ + "J-Peacock" + ], + "name": "JoyHub Peacock" + }, + { + "id": "b67fe066-44ff-41be-983d-0ed3e4a7b3ee", + "identifier": [ + "J-Mace" + ], + "name": "JoyHub Mace" + }, + { + "id": "609b9d5a-45c2-4f6d-a396-34f21e932c12", + "identifier": [ + "J-Tarian" + ], + "name": "JoyHub Tarian" + }, + { + "id": "c2aea3e0-551b-4e7f-90e6-819878ad6aec", + "identifier": [ + "J-Euphoric" + ], + "name": "JoyHub Euphoric" + }, + { + "id": "4b936259-c2d8-4459-9824-5992c0c22430", + "identifier": [ + "J-Euphoric3" + ], + "name": "JoyHub Euphoric3" + }, + { + "id": "a0a65312-dc6a-4e7b-a5cb-b1b8499df070", + "identifier": [ + "J-Torrian" + ], + "name": "JoyHub Torrian" + }, + { + "id": "08956682-7cf2-4a01-85d7-7132f8b0690e", + "identifier": [ + "J-Rayen" + ], + "name": "JoyHub Rayen" + }, + { + "id": "add6c7a5-7a3f-4d3d-abac-da7f9b498ef2", + "identifier": [ + "J-Mackay" + ], + "name": "JoyHub Mackay" + }, + { + "id": "f175684d-3bc2-4c8a-a36b-b68275602179", + "identifier": [ + "J-Rowdy3" + ], + "name": "JoyHub Rowdy 3" + }, + { + "id": "26bab7e2-0a38-4790-bdf0-8d9e1927106a", + "identifier": [ + "J-Eclipse" + ], + "name": "JoyHub Eclipse" + }, + { + "id": "d7176dba-ce2b-4395-bf26-1b8ab653d8b5", + "identifier": [ + "J-Scarlett" + ], + "name": "JoyHub Scarlett" + }, + { + "id": "f6b8c5db-eca9-4041-9e07-48521ed3a55f", + "identifier": [ + "J-Tarik" + ], + "name": "JoyHub Tarik" + }, + { + "id": "7252d5cc-5f1c-49ca-b2c8-49d7502c1f6b", + "identifier": [ + "J-Derik" + ], + "name": "JoyHub Urica Guard" + }, + { + "id": "a2f973ff-e6cd-4b70-a711-2b24f2d03b6d", + "identifier": [ + "J-UricaGuard2" + ], + "name": "JoyHub Urica Guard 2" + }, + { + "id": "6d3ee1c9-0452-4a01-8f73-75d196179e5c", + "identifier": [ + "J-Viva" + ], + "name": "JoyHub Viva" + }, + { + "id": "25ef0abd-31ed-497f-8fc0-ea374f600ee7", + "identifier": [ + "J-Ryden" + ], + "name": "JoyHub Ryden" + }, + { + "id": "eda4e5c4-4a91-4260-a14b-570926e346f6", + "identifier": [ + "J-Peachy" + ], + "name": "JoyHub Peachy" + }, + { + "id": "b1aa4a71-1346-43c4-9de3-7ecc642607d5", + "identifier": [ + "J-Enam" + ], + "name": "JoyHub Enam" + }, + { + "id": "5a4c2185-67a9-4e15-862b-3c913aaecaad", + "identifier": [ + "J-Viv" + ], + "name": "JoyHub Viv" + }, + { + "id": "307496a5-a0e0-498e-b635-c6bc346cab1c", + "identifier": [ + "J-Vivara" + ], + "name": "JoyHub Vivara" + }, + { + "id": "f499492b-571c-4766-830c-c751706e280d", + "identifier": [ + "J-Explorer2" + ], + "name": "JoyHub Explorer 2" + }, + { + "features": [ + { + "feature-type": "Oscillate", + "id": "0d5685ae-95ea-4d2d-849e-b75b7354bc35", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "e092343a-c826-4bc8-a579-e179b50cf65e", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "904ef5c8-7030-4c2f-9c12-d69154ab10c3", + "identifier": [ + "J-Petalwish2" + ], + "name": "JoyHub Petalwish 2" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "95313411-9fb3-4df9-b672-c7279ca7d243", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Air Pump", + "feature-type": "Constrict", + "id": "d2f66bd3-96c4-4377-b1f5-45a2f3d99c9e", + "output": { + "Constrict": { + "step-range": [ + 0, + 3 + ] + } + } + }, + { + "feature-type": "Rotate", + "id": "042a4817-348c-4595-9fbc-463ffa903041", + "output": { + "Rotate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "c85fd4cf-5bc1-4300-9cb8-a4db4fa8b85f", + "identifier": [ + "J-VortexTongue" + ], + "name": "JoyHub Vortex Tongue" + }, + { + "features": [ + { + "description": "External vibrator", + "feature-type": "Vibrate", + "id": "d03ea16f-3126-469d-bf85-843a7c6e2cf6", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Oscillate", + "id": "115ec3d5-df22-474a-aa5a-32236fcb517e", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Internal vibrator", + "feature-type": "Vibrate", + "id": "cd3828ee-8fe0-4214-acce-9fc4aac9ea46", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "380428d0-73a4-4437-bf48-fb6b26663d1d", + "identifier": [ + "J-VibSiren" + ], + "name": "JoyHub VibSiren" + }, + { + "features": [ + { + "feature-type": "Rotate", + "id": "a7a34c6b-5d77-4a38-9708-780ba97cd34f", + "output": { + "Rotate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Air Pump", + "feature-type": "Constrict", + "id": "7891e1b3-82c3-4e83-936c-2a156f2ba826", + "output": { + "Constrict": { + "step-range": [ + 0, + 7 + ] + } + } + } + ], + "id": "1ca6396e-bee2-42c8-901c-82e975998085", + "identifier": [ + "J-Mysticolor" + ], + "name": "JoyHub Mysticolor" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "686761a8-fcc9-4a41-9725-045d5cb0dae9", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Oscillate", + "id": "21c831d4-0956-4b9b-a90e-31a545a89708", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "576095da-d4a5-4f19-9b14-6244cbfe8096", + "identifier": [ + "J-VividWings" + ], + "name": "JoyHub Vivid Wings" + }, + { + "features": [ + { + "feature-type": "Rotate", + "id": "439bea28-4c09-4b81-8dd5-dce2ec31781e", + "output": { + "Rotate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Air Pump", + "feature-type": "Constrict", + "id": "9f386242-41a2-4c86-9167-db6c58840cc7", + "output": { + "Constrict": { + "step-range": [ + 0, + 2 + ] + } + } + } + ], + "id": "67ed28b9-c0fe-4155-b7b8-3829ec12a485", + "identifier": [ + "J-Mariner" + ], + "name": "JoyHub Mariner" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "e43f723f-412d-4c75-8123-2483113a06a8", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Air Pump", + "feature-type": "Constrict", + "id": "54e3da8e-7f97-46c7-8a1e-9fa549b877c2", + "output": { + "Constrict": { + "step-range": [ + 0, + 5 + ] + } + } + } + ], + "id": "3f3b7c49-94b2-49b6-ba67-3e5539e204b9", + "identifier": [ + "J-MarsLion" + ], + "name": "JoyHub MarsLion" + }, + { + "features": [ + { + "feature-type": "Oscillate", + "id": "a9b7d261-2877-4214-a539-8ce30e038386", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "db3efe9b-839c-495e-8c2e-b800b3125b36", + "identifier": [ + "J-Pul" + ], + "name": "JoyHub Pul" + }, + { + "features": [ + { + "description": "Air Pump", + "feature-type": "Constrict", + "id": "0d3b3010-d438-4899-b1c2-d81bff0c6714", + "output": { + "Constrict": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "ca36d3a7-c305-45e3-b8f7-3106b36b233a", + "identifier": [ + "J-ROSELLA3" + ], + "name": "JoyHub Rose Love" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "9fde0544-3307-4a4f-8abf-88ffb1dc3caf", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Oscillate", + "id": "e0ca1697-1e42-4822-925c-691561916bee", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "877a8e55-9f08-4bea-826c-20371ba57577", + "identifier": [ + "J-DukeDazzle2" + ], + "name": "JoyHub Edasich" + }, + { + "features": [ + { + "feature-type": "Oscillate", + "id": "a4a079b4-6cf2-47fc-bfef-0f2921c243db", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "b4235543-7287-4698-a1e7-9d78c53d4c0a", + "identifier": [ + "J-Mars" + ], + "name": "JoyHub Mars" + }, + { + "features": [ + { + "feature-type": "Oscillate", + "id": "b306148c-c1d9-4281-bae9-fe1ccd876399", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "76d1ddf5-e46b-4912-bea1-a748ce28a18e", + "identifier": [ + "J-Martino" + ], + "name": "JoyHub Martino" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "b6ffc3b3-9e8a-46cd-82f2-97df7237be83", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Constrict", + "id": "ead93a87-9ad6-448f-a26a-cce980db265e", + "output": { + "Constrict": { + "step-range": [ + 0, + 5 + ] + } + } + } + ], + "id": "e693fbe3-f697-446e-8fa2-87e99e9e8cb6", + "identifier": [ + "J-MarsLion2" + ], + "name": "JoyHub Mars Lion 2" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "393dfa94-e3c8-4962-a053-c39e0447e420", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Constrict", + "id": "b6e89b8c-207d-4588-9fff-f71d42e1a1a5", + "output": { + "Constrict": { + "step-range": [ + 0, + 9 + ] + } + } + } + ], + "id": "e6502f8e-73c3-4b1f-9080-4428d6670045", + "identifier": [ + "J-Myrna" + ], + "name": "JoyHub Myrna" + }, + { + "features": [ + { + "description": "Biting lips", + "feature-type": "Vibrate", + "id": "7e13af66-c20f-42b3-ba85-764a2cdeaca0", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Sideways flicker", + "feature-type": "Vibrate", + "id": "f80dc564-7d53-4c6b-991e-ec18051a3207", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "cd4e9b09-367e-4ac1-8571-4f0ff4ca8996", + "identifier": [ + "J-Vase2" + ], + "name": "JoyHub Vase 2" + } + ], + "defaults": { + "features": [ + { + "feature-settings": { + "alt-protocol-index": 1 + }, + "feature-type": "Vibrate", + "id": "fc2f0fc2-fb75-4eee-b92b-20eaf7cc9a1e", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "53cf03db-266d-46c1-964e-0ef505a64200", + "name": "JoyHub Device" + } + }, + "joyhub-v2": { + "communication": [ + { + "btle": { + "names": [ + "J-Pearlconch", + "J-PearlconchL", + "J-PetiteRose", + "J-MoonHorn", + "J-VibTrefoil", + "J-Panther", + "J-Mecha", + "J-Lagoon", + "J-Firedragon", + "J-Dina", + "J-Vbarbie3f", + "J-CHERLY2c", + "J-Pathfinder2", + "J-Pathfinder", + "J-VibRipple", + "J-Verax", + "J-Verax2", + "J-Euphoric2", + "J-ROSEBUD", + "J-Morningbuds2", + "J-Rhythmic4", + "J-Virtuoso2", + "J-Dyllis", + "J-Flamewing", + "J-VelvetRabbit", + "J-VividPulse", + "J-VioletVine", + "J-VibSiren2", + "J-Veemy", + "J-Fabledragon", + "J-Faunus", + "J-VortexTongue2", + "J-Torin", + "J-VBarbiep", + "J-Vbarbie", + "J-Viball", + "J-Vase", + "J-Vortex2s", + "J-Royaleye", + "J-VBarbie2t", + "J-Pau", + "J-Petalwish3", + "J-Marshal", + "J-Piet2", + "J-Vince", + "J-Dallin", + "J-Mace2", + "J-Verax4", + "J-Palmyra", + "J-Maiden", + "J-Viele3", + "J-Xylia", + "J-Troi", + "J-Tanmouth", + "J-Marcela", + "J-Vita", + "J-LACH", + "J-Markel", + "J-Pipes", + "J-Vigo" + ], + "services": { + "0000ffa0-0000-1000-8000-00805f9b34fb": { + "tx": "0000ffa1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "features": [ + { + "feature-type": "Rotate", + "id": "ae8e847a-fbe2-4650-8c7e-372399981bac", + "output": { + "Rotate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "eb9b02b6-7902-4f4e-8a3d-ae9b6a77595d", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "7f324fea-ce2c-4e72-bfc2-b2227251a2c7", + "identifier": [ + "J-Pearlconch" + ], + "name": "JoyHub Pearlconch" + }, + { + "features": [ + { + "feature-type": "Rotate", + "id": "e5102a93-330d-48b2-a901-79b2b1c6990c", + "output": { + "Rotate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "002b77e4-cef3-4718-98e3-0644cf0461d7", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "9a5b2555-5d9f-4364-8e5b-0e0c2eed9849", + "identifier": [ + "J-Pearlconch" + ], + "name": "JoyHub Pearlconch" + }, + { + "features": [ + { + "feature-type": "Rotate", + "id": "a696f55c-376d-4304-aaa4-c25013c4e20f", + "output": { + "Rotate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "597375f8-9698-4c08-8d45-9d732b84b06e", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "d91c5f72-7a5e-4a38-999a-3118a49ff6d4", + "identifier": [ + "J-PearlconchL" + ], + "name": "JoyHub Pearlconch L" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "00a0dfd6-93a3-40e9-a72f-8c182bb76b67", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Rotate", + "id": "67e1286e-5572-4c3a-bf11-15f1161f3697", + "output": { + "Rotate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "d2aa1980-7943-4c39-b66d-a2f0ba495ce5", + "identifier": [ + "J-Piet2" + ], + "name": "JoyHub Piet 2" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "3d236d1d-51b3-4412-bba4-6fc959e5fddf", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Rotate", + "id": "9307744e-0fcb-4a8a-a5cc-537b4d57c326", + "output": { + "Rotate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "84323f4e-f5f0-48be-9504-cb2798702780", + "identifier": [ + "J-Panther" + ], + "name": "JoyHub Panther" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "bb3a1f82-2b94-40b7-993b-375c77a92a4f", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Rotate", + "id": "4b5e922d-f920-43eb-b6f9-2772a4c62496", + "output": { + "Rotate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "a8b1f6cd-6b86-488a-a21a-5715669134cc", + "identifier": [ + "J-PetiteRose" + ], + "name": "JoyHub Petite Rose" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "12048627-fb6c-48af-8fd1-2ab5f40c59df", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Suction", + "feature-type": "Constrict", + "id": "8b6ce43b-6b60-4497-9c5b-d2b48de13c13", + "output": { + "Constrict": { + "step-range": [ + 0, + 9 + ] + } + } + } + ], + "id": "46fe6203-6b1c-40c5-ba96-91748b35cdd7", + "identifier": [ + "J-MoonHorn" + ], + "name": "JoyHub Moon Horn" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "23b843f6-801e-48cb-b741-ecfb249ad6a0", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Suction", + "feature-type": "Constrict", + "id": "d67b7e66-080e-4d2c-bbb8-d6e38392961b", + "output": { + "Constrict": { + "step-range": [ + 0, + 7 + ] + } + } + } + ], + "id": "764cd060-fd7d-454b-a0bc-10183bb34238", + "identifier": [ + "J-Mecha" + ], + "name": "JoyHub Mecha" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "4095e42c-1979-42c1-895f-033c3a348a3f", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Suction", + "feature-type": "Constrict", + "id": "c663c71c-befb-4ed1-bb81-d344ee61f3c0", + "output": { + "Constrict": { + "step-range": [ + 0, + 5 + ] + } + } + } + ], + "id": "74ba519b-e31f-4708-8430-6bf0cdea42ac", + "identifier": [ + "J-Lagoon" + ], + "name": "JoyHub Lagoon" + }, + { + "features": [ + { + "description": "External vibrator", + "feature-type": "Vibrate", + "id": "8c5ab96c-da9e-419b-ae89-a775ee65fc6d", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Internal vibrator", + "feature-type": "Vibrate", + "id": "18af5f39-ea31-43d6-af1e-1b0073576294", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "f3b581da-64cd-4643-97d9-0d97683c26f3", + "identifier": [ + "J-VibTrefoil" + ], + "name": "JoyHub VibTrefoil" + }, + { + "features": [ + { + "feature-type": "Oscillate", + "id": "5bdbe9f5-8075-4afe-8df0-6a960030feeb", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "49429631-a654-4a44-bffe-58c0c2d5289a", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "1a1e5e28-5892-4f51-b236-9af6e190cb29", + "identifier": [ + "J-Firedragon" + ], + "name": "JoyHub Firedragon" + }, + { + "features": [ + { + "feature-type": "Oscillate", + "id": "32860a3d-7370-41ce-9183-046b4fb78f15", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Internal vibrator", + "feature-type": "Vibrate", + "id": "c88be4c1-7aed-45b5-af68-1f6345d30acb", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "External vibrator", + "feature-type": "Vibrate", + "id": "bebeab4e-9bbd-4064-adb2-d704958c63b0", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "bd517815-efb5-427d-88a1-edaff6b0ceba", + "identifier": [ + "J-Dina" + ], + "name": "JoyHub Deena" + }, + { + "features": [ + { + "description": "External vibrator", + "feature-type": "Vibrate", + "id": "08410e6a-b6f6-4bea-a570-9535407b946b", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Internal vibrator", + "feature-type": "Vibrate", + "id": "5a5dc25a-0859-4491-a092-814c71b33b67", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Oscillate", + "id": "52cc6b42-a1f1-4b8b-ab81-cde582ce1aa9", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "ed4f639b-e041-4258-ad8d-4f9ef5f850a7", + "identifier": [ + "J-Vbarbie3f" + ], + "name": "JoyHub Cherly" + }, + { + "features": [ + { + "description": "Internal vibrator", + "feature-type": "Vibrate", + "id": "3b9cebe0-369d-4086-8a6c-c2d1fe0499a5", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Internal Whip", + "feature-type": "Vibrate", + "id": "de793e03-1879-40e3-aa8a-5b76a832a56d", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "External vibrator", + "feature-type": "Vibrate", + "id": "ddec3601-be51-490c-a20a-df9a01def1a5", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "0b29424b-d609-4049-b206-831c00bd53c1", + "identifier": [ + "J-CHERLY2c" + ], + "name": "JoyHub Cherly 2c" + }, + { + "features": [ + { + "feature-type": "Oscillate", + "id": "2dcf4211-6e27-413a-aa7a-bd9085edb9fe", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "0bde094e-f3d9-48d1-b076-56412838d1c9", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "5b6ebea4-e363-463d-9922-99add3a7c656", + "identifier": [ + "J-Pathfinder2" + ], + "name": "JoyHub Pathfinder 2" + }, + { + "features": [ + { + "feature-type": "Oscillate", + "id": "b4564c01-12d0-44f9-b3cf-de53068d4692", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "881dc72c-b2a1-4b0e-9cf7-a351d7b27fe9", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "828d5f2d-9381-4363-bb7e-ffa4964a0970", + "identifier": [ + "J-Pathfinder" + ], + "name": "JoyHub Pathfinder" + }, + { + "features": [ + { + "description": "External vibrator", + "feature-type": "Vibrate", + "id": "788cb23d-d3c2-4a84-8114-1ee7df4fe367", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Internal vibrator", + "feature-type": "Vibrate", + "id": "f70b48a2-75ab-44ca-98d3-3f11a2440698", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "9f1be5fa-70c9-4853-bc11-1685304a0d86", + "identifier": [ + "J-VibRipple" + ], + "name": "JoyHub Angela" + }, + { + "features": [ + { + "description": "Internal Whip", + "feature-type": "Vibrate", + "id": "36586dac-a0e5-45ce-a5d5-ff2ec6961e83", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Internal vibrator", + "feature-type": "Vibrate", + "id": "76c2ca34-393d-407c-9ae8-954fcc6c13d1", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "07ce35bd-9fc9-4224-8809-13245fe1d3f0", + "identifier": [ + "J-Verax" + ], + "name": "JoyHub Verax" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "be955fe4-d3af-4a0a-a4f9-0c2b3c3cddf7", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Rotate", + "id": "763324b6-3056-497a-bd07-99c69780358a", + "output": { + "Rotate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "258d4904-2feb-4b68-b7fc-7dd4df687a9e", + "identifier": [ + "J-Verax2" + ], + "name": "JoyHub Verax 2" + }, + { + "features": [ + { + "feature-type": "Oscillate", + "id": "7a437340-eb86-450a-8db3-4c594a638d63", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "42504b4b-cd77-49c0-abb0-f2ddba7cda72", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "f09e8dde-475d-488e-bf21-60bf80f8d2ac", + "identifier": [ + "J-Euphoric2" + ], + "name": "JoyHub Euphoric 2" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "d4c00919-5cd0-434c-9164-62da64967ec8", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Flicker", + "feature-type": "Rotate", + "id": "727d8c05-7896-4812-9996-36decea2dd49", + "output": { + "Rotate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Suction", + "feature-type": "Constrict", + "id": "c9f73966-4777-4512-91c2-30349a0bd270", + "output": { + "Constrict": { + "step-range": [ + 0, + 5 + ] + } + } + } + ], + "id": "40a2d620-719e-4d0f-abfc-ec3fa2fe9f92", + "identifier": [ + "J-ROSEBUD" + ], + "name": "JoyHub RoseBUD" + }, + { + "features": [ + { + "feature-type": "Rotate", + "id": "3ecaa10d-338b-4119-bd21-77d662cc1fd1", + "output": { + "Rotate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "f33780a7-56a9-4e8a-b05b-6f92ca0c1366", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "10030c6e-d04d-4613-8feb-41748e638684", + "identifier": [ + "J-Morningbuds2" + ], + "name": "JoyHub Morningbuds" + }, + { + "features": [ + { + "feature-type": "Oscillate", + "id": "77ff9786-c024-4755-af20-0b86a5165269", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "05de8ce7-24c5-4cb4-8162-5d57f9b46d26", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "da2596bc-b8c9-4a47-b671-20095ac1bcdb", + "identifier": [ + "J-Rhythmic4" + ], + "name": "JoyHub Rhythmic 4" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "3391b4b5-a2f5-4bcd-9274-76e8586a4af6", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Rotate", + "id": "e06a6c43-a6ed-4e13-a49e-6375b8aab136", + "output": { + "Rotate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Suction", + "feature-type": "Constrict", + "id": "10ca15ff-70e6-4ec4-a258-d7ac8119c47a", + "output": { + "Constrict": { + "step-range": [ + 0, + 3 + ] + } + } + } + ], + "id": "b73b29bf-5202-4c45-b292-b9a3d538bbb6", + "identifier": [ + "J-Virtuoso2" + ], + "name": "JoyHub Virtuoso 2" + }, + { + "features": [ + { + "feature-type": "Oscillate", + "id": "aa769623-c0cb-41d2-bbfa-eb15348422f7", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "e783132a-c6e1-4445-83e2-6ab985c2af66", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "a8278c49-58c3-416e-9ae1-072dcfe0f694", + "identifier": [ + "J-Dyllis" + ], + "name": "JoyHub Dyllis" + }, + { + "features": [ + { + "feature-type": "Oscillate", + "id": "0c1cd9b2-a466-4807-a8be-5b2158a7b04d", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "da7ca1ac-4c38-4cc6-aa88-737ff2d4be27", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "1f6a2310-f773-40aa-8a93-bd83f7d78119", + "identifier": [ + "J-Flamewing" + ], + "name": "JoyHub PhoenixGP" + }, + { + "features": [ + { + "feature-type": "Oscillate", + "id": "f20ff8eb-afc6-45c4-be6b-0b071141b1bc", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "52eb1885-853a-45f8-85a2-b43a18b79d89", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "ee76aeea-337d-44b8-9631-2bd8c8f2acda", + "identifier": [ + "J-Fabledragon" + ], + "name": "JoyHub Fable Dragon" + }, + { + "features": [ + { + "feature-type": "Oscillate", + "id": "06b57eb1-50f8-4393-908d-05628120bd14", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "5a4433de-c45c-46b6-9911-b17948daae74", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "8c4d26b6-f091-4e34-bf13-c6bc303712b5", + "identifier": [ + "J-Faunus" + ], + "name": "JoyHub Faunus" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "03b40869-05c1-4d17-9ebf-9566f7f2e9c9", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "9231af9e-98db-464a-931a-fe80bad3fcaf", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "6eae28db-c885-454f-98d4-2e5683bb05d9", + "identifier": [ + "J-VelvetRabbit" + ], + "name": "JoyHub Velvet Rabbit" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "66e6dd1e-6717-4f47-8868-de317e09b42a", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Oscillate", + "id": "7e8fc7f6-39c5-469c-b479-dcf85e8deeef", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "90caf141-3bee-4024-8d5e-cc854da852d0", + "identifier": [ + "J-VividPulse" + ], + "name": "JoyHub Vivid Pulse" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "d45e5cf6-fe20-4eb3-9c48-0c8ed6a4aad6", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "fc78a0c8-262e-4b24-920e-8e91f38417c0", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "4b0128a4-b849-4f60-a0b4-16ebe8500cfe", + "identifier": [ + "J-VioletVine" + ], + "name": "JoyHub Violet Vine" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "904e3dfa-d69c-4e0e-9d50-9f119ff959f2", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "ffc701ee-ec1b-42d1-8c99-9a755d595438", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Oscillate", + "id": "7fafb528-74f3-49df-af78-dc2b64e4bed1", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "e2eeccb0-2601-43d1-b1cc-b10234e0004d", + "identifier": [ + "J-VibSiren2" + ], + "name": "JoyHub VibSiren 2" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "53ef1d9b-4020-408d-8126-1d484448bccc", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "88fbe85b-a98a-4965-9f47-c69812fbc66f", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "873595ac-acdd-41b2-b162-74ca9776f0f8", + "identifier": [ + "J-Veemy" + ], + "name": "JoyHub Veemy" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "9ac37f94-8129-4c09-83d2-bd2b0d4aae53", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Oscillate", + "id": "fce9a8eb-f227-41f1-bb75-f6dc64573fc5", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "ccecf0fc-e657-432a-8a68-ada09d396934", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "e3646777-6550-4984-91bb-3cd738744494", + "identifier": [ + "J-Viball" + ], + "name": "JoyHub Viball" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "0d80c22d-a8c4-4f7a-8ec0-0f912653b8a4", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "21fff2c0-5ccf-459c-9eea-02f95b3174a8", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Oscillate", + "id": "c534acf2-bc28-4384-aa79-f70537b23ab8", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "24d26313-74a9-4515-945f-0f31edb3650a", + "identifier": [ + "J-Vase" + ], + "name": "JoyHub Vase" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "a0383ad8-05ae-4dae-be06-b384744499f3", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "cddef660-59b2-4f4b-b9ec-16439cd7c12e", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "14c6efec-d40c-4f21-8459-67a11c079c2d", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "dbe616e2-478e-4e87-8f7b-4c86835502fe", + "identifier": [ + "J-Vortex2s" + ], + "name": "JoyHub Vortex 2s" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "e72404a7-9f94-4074-bf3c-40ba5e2a4fbf", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Rotate", + "id": "25ceb7c6-0dfd-415e-aa74-b1f4ac49d031", + "output": { + "Rotate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Air Pump", + "feature-type": "Constrict", + "id": "4bda889f-f1b5-4293-8bd8-f05e30ac188c", + "output": { + "Constrict": { + "step-range": [ + 0, + 3 + ] + } + } + } + ], + "id": "83956181-5ebd-4251-bc92-4b10f9bec1f4", + "identifier": [ + "J-VortexTongue2" + ], + "name": "JoyHub Lips" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "051de0d3-5d2f-4a04-8f4c-a9a6747b2cd1", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "ac0377fa-a7c2-4d5b-bbcc-402d378a1343", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "985e3726-cc4d-4059-972d-654af41a5947", + "identifier": [ + "J-Torin" + ], + "name": "JoyHub Torin" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "38c3e4ae-0de5-4e17-9d7a-2e639c293aeb", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "95db76e1-abc0-4774-a588-9092615291e7", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "1347963d-6bad-41c5-bf3a-314980e3316b", + "identifier": [ + "J-VBarbiep" + ], + "name": "JoyHub VBarbie p" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "058349cf-49ea-453d-8fbd-0b13e880c301", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "0cbd4cd8-3a5d-4528-b49a-05f199828155", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "73a6f6a2-1fb0-45b0-b379-89eac6aefae5", + "identifier": [ + "J-Vbarbie" + ], + "name": "JoyHub VBarbie" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "6ee6fa8a-a6a3-4131-8ea9-c35909999167", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "06a656af-181b-4fa3-94e2-4aa0115cfbc9", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "6f05cc4a-adb1-402d-a392-daa120223257", + "identifier": [ + "J-Royaleye" + ], + "name": "JoyHub Royaleye" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "d314083c-0588-46ae-aecb-9695305c3439", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "e8afb080-dd64-418a-a07a-197bc6779a9e", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Oscillate", + "id": "9c9a7901-540d-44b1-ba38-0c8e794e1d9b", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "2e417090-ec06-4039-8e60-bf497cec3257", + "identifier": [ + "J-VBarbie2t" + ], + "name": "JoyHub Norma" + }, + { + "features": [ + { + "feature-type": "Oscillate", + "id": "63355e3e-edef-4317-a679-89b85ced0f4a", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "a159d6eb-2e95-4d4b-b74d-537cc77cf7b1", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "d693dc6b-3b7a-4ff0-8990-1a10f884ddc4", + "identifier": [ + "J-Pau" + ], + "name": "JoyHub Pau" + }, + { + "features": [ + { + "feature-type": "Oscillate", + "id": "fe2531e3-3815-4110-9022-06f7f4aa44aa", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "5930bf48-ec9a-4914-b110-47d7e13ddbaf", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "cb6f0926-32bd-4b48-8676-4cd6df9123a4", + "identifier": [ + "J-Petalwish3" + ], + "name": "JoyHub Petalwish 3" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "29a272ab-f6b6-4a90-ad84-7c21846d7164", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Air Pump", + "feature-type": "Constrict", + "id": "485b9a41-05d4-440a-a3a4-a3b2bf1ee693", + "output": { + "Constrict": { + "step-range": [ + 0, + 9 + ] + } + } + } + ], + "id": "a4d28447-2535-415b-aaab-ebe3ee2e92ba", + "identifier": [ + "J-Marshal" + ], + "name": "JoyHub Marshal" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "b8bf1392-8a84-4647-a833-be03de144b0a", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "e983d64e-411e-486f-8695-76b4e57b3bd1", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "6dd6c377-c35d-4300-a892-4aace5589ec5", + "identifier": [ + "J-Vince" + ], + "name": "JoyHub Vince" + }, + { + "features": [ + { + "feature-type": "Oscillate", + "id": "8412021b-0962-4469-b45e-0a59f3272ad0", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "bbc10f1c-171a-4f14-b6e4-520dda5df19f", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "b559b1ec-d336-45bb-b6e6-cc22344eefd7", + "identifier": [ + "J-Dallin" + ], + "name": "JoyHub Dallin" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "f79abcb3-666d-4ba4-b6d3-9cff722b8a1f", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Air Pump", + "feature-type": "Constrict", + "id": "92fb7f24-e7a2-4bdd-8c93-27610ba1f45d", + "output": { + "Constrict": { + "step-range": [ + 0, + 9 + ] + } + } + } + ], + "id": "d418dd65-6f41-4af4-a04d-4b343ec778ab", + "identifier": [ + "J-Mace2" + ], + "name": "JoyHub Maynor" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "9ee6b8e0-a694-4c22-8a82-3fc01f60f99c", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "514ec2f4-2a2b-4c1e-9eb3-eed3b67c2951", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "905657e5-fda1-4f0b-9043-a7b3d760e7da", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "2a5abb95-efac-45e0-9f56-9fb9f1c9f274", + "identifier": [ + "J-Verax4" + ], + "name": "JoyHub Verax 4" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "d7fed551-18b0-4da8-a8b0-596e93fc3e0b", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Oscillate", + "id": "33414af0-d5bc-461c-821f-54c43d85423b", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "8fe7695d-60aa-4af5-92c2-364e8eebf076", + "identifier": [ + "J-Palmyra" + ], + "name": "JoyHub Palmyra" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "8148b859-0acd-4749-a8f3-57ca82d4a156", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Oscillate", + "id": "b1e1444f-e6d7-4045-8565-adff4f25eb87", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "bdc796d7-d029-4732-9d8d-037e421f19e8", + "identifier": [ + "J-Xylia" + ], + "name": "JoyHub Xylia" + }, + { + "features": [ + { + "feature-type": "Rotate", + "id": "90bf6a90-e1cb-4600-ad00-d4f29bfc4adb", + "output": { + "Rotate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Constrict", + "id": "0663888b-60c0-491d-aa66-7ec4c2c57b08", + "output": { + "Constrict": { + "step-range": [ + 0, + 5 + ] + } + } + } + ], + "id": "c5bd6fb4-b36f-4b3c-865c-943eab645f5e", + "identifier": [ + "J-Maiden" + ], + "name": "JoyHub Maiden" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "518d1ed4-3b91-4f56-bd29-b7af30598ef1", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Rotate", + "id": "f575f285-a104-4d0d-b5f7-414ea6d67433", + "output": { + "Rotate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "5a23e800-0b33-435b-9139-023533b92880", + "identifier": [ + "J-Viele3" + ], + "name": "JoyHub Viele 3" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "f48cb279-cbe7-4857-8178-632bd0d1081c", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "3041d01a-fb7c-48c3-a302-e71d37f5a12e", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "01ba3988-0a1c-4afc-b6c7-1c19a2b15ac4", + "identifier": [ + "J-Troi" + ], + "name": "JoyHub Troi" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "d2f033a7-0805-40e0-acc2-51d4bb635095", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "a44ab42a-fb71-4120-b7a9-705181549ecb", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "192325d9-a343-4b9b-bd77-6d9b665a6988", + "identifier": [ + "J-Tanmouth" + ], + "name": "JoyHub Tanmouth" + }, + { + "features": [ + { + "feature-type": "Oscillate", + "id": "aab23df2-2530-488b-8d1a-3bc6429409ae", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "cfe637a9-7024-4aa0-9b97-55815f082332", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "1df39ccb-a6d2-41d5-906e-14a42bbd96ed", + "identifier": [ + "J-Marcela" + ], + "name": "JoyHub Marcela" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "e3308e8e-c0ba-4cf8-a3b3-26cbbea3bea5", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Rotate", + "id": "95ebe9f7-ad90-4627-bfcc-4ee1f1fdfdba", + "output": { + "Rotate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Oscillate", + "id": "ad45f3ec-513d-423e-a60f-57765c5a07b0", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "1a066cb3-b758-48d2-9296-4dec65115e9a", + "identifier": [ + "J-Vita" + ], + "name": "JoyHub Vita" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "33aa95b4-e36d-4af8-9de7-cc6447afd03d", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Constrict", + "id": "5ee461b4-770f-4686-bd6c-c13f12ab0f54", + "output": { + "Constrict": { + "step-range": [ + 0, + 5 + ] + } + } + } + ], + "id": "e309c90f-c63a-4883-af14-4a69e899cf12", + "identifier": [ + "J-LACH" + ], + "name": "JoyHub Lach" + }, + { + "features": [ + { + "feature-type": "Oscillate", + "id": "90cfdc1e-9bc5-49f9-8993-058f85e5e082", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Suction", + "feature-type": "Constrict", + "id": "2cb024d3-33be-4369-bb0c-4c61cc39c62e", + "output": { + "Constrict": { + "step-range": [ + 0, + 9 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "22e539e8-4bf0-49e9-883c-112a2d51ea60", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "d818b1e1-4270-4e38-8b07-d723c0a97e31", + "identifier": [ + "J-Markel" + ], + "name": "JoyHub Markel" + }, + { + "features": [ + { + "feature-type": "Rotate", + "id": "558425ee-cf28-48bf-b08f-12568cd3b3ee", + "output": { + "Rotate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "8c8f8f70-e814-4a0e-aa5c-b06b53a9ab80", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "641296fe-8ccc-4a63-8487-790dd419321e", + "identifier": [ + "J-Pipes" + ], + "name": "JoyHub Pipes" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "89a3e300-3640-4a11-99e4-6585dce725a4", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "a23b9a72-7b22-42ec-ab7d-7936d7141689", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Oscillate", + "id": "1e6c3008-5efc-4dd1-bee5-95e7e0b016ad", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "398a3a62-3bba-433e-80ad-50d129b695db", + "identifier": [ + "J-Vigo" + ], + "name": "JoyHub Vigo" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "076c95a5-a869-401b-bd5f-c51ef681c488", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "e126925b-4cd6-414c-84fb-dc62464e07bb", + "name": "JoyHub Device" + } + }, + "joyhub-v3": { + "communication": [ + { + "btle": { + "names": [ + "J-Ringstar", + "J-RapidTwist2" + ], + "services": { + "0000ffa0-0000-1000-8000-00805f9b34fb": { + "tx": "0000ffa1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "40241a70-ecbd-4c08-8acf-8ee70e7b5d55", + "identifier": [ + "J-Ringstar" + ], + "name": "JoyHub Starfish" + }, + { + "id": "4611fa22-18b8-46fe-bece-070e24e1b9e8", + "identifier": [ + "J-RapidTwist2" + ], + "name": "JoyHub Resi Ring 2" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "3adea9b9-8a81-4358-8774-17b621f33907", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "acd3b85a-c842-458d-8ff8-eeaaf9be1562", + "name": "JoyHub Device" + } + }, + "joyhub-v4": { + "communication": [ + { + "btle": { + "names": [ + "J-RoseLin", + "J-Viele" + ], + "services": { + "0000ffa0-0000-1000-8000-00805f9b34fb": { + "tx": "0000ffa1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "cea67021-dff3-4012-88c0-321706408a55", + "identifier": [ + "J-RoseLin" + ], + "name": "JoyHub RoseLin" + }, + { + "features": [ + { + "description": "Internal Simulator", + "feature-type": "Rotate", + "id": "c731fe0b-3216-428a-9cc5-8e8f2fa21275", + "output": { + "Rotate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Internal Whip", + "feature-type": "Vibrate", + "id": "5462e403-9c83-429f-9dd5-db099f18e4e8", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Internal Vibrator", + "feature-type": "Vibrate", + "id": "f4407e47-4094-41c6-95b8-41f7c20e0f04", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "7c5a1ffd-3228-4513-a180-115c94983eac", + "identifier": [ + "J-Viele" + ], + "name": "JoyHub Viele" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "95e495dc-7b4f-43fd-91ee-b7842f047f59", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Rotate", + "id": "0f6f75c5-66e8-4293-9ee0-50af9ecfc1b0", + "output": { + "Rotate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Suction", + "feature-type": "Constrict", + "id": "487bb0bd-af93-40ff-a92c-6e18772e707f", + "output": { + "Constrict": { + "step-range": [ + 0, + 4 + ] + } + } + } + ], + "id": "12907be0-52b2-4df1-a4d1-29c246d72f2f", + "name": "JoyHub Device" + } + }, + "joyhub-v5": { + "communication": [ + { + "btle": { + "names": [ + "J-Virtuoso", + "J-Pathfinder3" + ], + "services": { + "0000ffa0-0000-1000-8000-00805f9b34fb": { + "tx": "0000ffa1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "fa5a696c-780f-4763-9af2-a619cbae330c", + "identifier": [ + "J-Virtuoso" + ], + "name": "JoyHub Virtuoso" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "b91f2775-f628-43c4-bd04-a8844f74d4e1", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Oscillate", + "id": "3e00301a-c942-4b8d-8f49-fe2af7ecf0b6", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "6e782468-f084-442a-936f-27d7abd5f840", + "identifier": [ + "J-Pathfinder3" + ], + "name": "JoyHub Pathfinder 3" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Rotate", + "id": "2c03096f-8fd6-4c80-84ba-d07936f76928", + "output": { + "Rotate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Suction", + "feature-type": "Constrict", + "id": "e9e32817-2cc1-4365-baa6-054fb7f6aa74", + "output": { + "Constrict": { + "step-range": [ + 0, + 1 + ] + } + } + } + ], + "id": "abc5309a-008d-41fd-b4db-5fd54614c582", + "name": "JoyHub Device" + } + }, + "joyhub-v6": { + "communication": [ + { + "btle": { + "names": [ + "J-Melody" + ], + "services": { + "0000ffa0-0000-1000-8000-00805f9b34fb": { + "tx": "0000ffa1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "2c33b13e-9d00-4823-bc5b-fda18dbd3691", + "identifier": [ + "J-Melody" + ], + "name": "JoyHub Melody" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "9fbf30f4-3f0d-4377-a232-55132d023d11", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Suction", + "feature-type": "Constrict", + "id": "a38653c9-c245-4c98-86c9-3c0da68d646c", + "output": { + "Constrict": { + "step-range": [ + 0, + 9 + ] + } + } + } + ], + "id": "f89fcd7a-2411-4241-ae81-f4488e926d16", + "name": "JoyHub Device" + } + }, + "kgoal-boost": { + "communication": [ + { + "btle": { + "names": [ + "Boost" + ], + "services": { + "0000180f-0000-1000-8000-00805f9b34fb": { + "rxblebattery": "00002a19-0000-1000-8000-00805f9b34fb" + }, + "8e7c6065-7656-17ad-1b41-b53d1a548e0d": { + "rxpressure": "10c2be2d-d2d5-b7a8-5f42-e2468c9ebbf5" + } + } + } + } + ], + "defaults": { + "features": [ + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "59d2de82-3acf-4316-982f-c2b570afd297", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "1835b668-d778-4552-b75a-95053e06cd5c", + "name": "KGoal Boost" + } + }, + "kiiroo-prowand": { + "communication": [ + { + "btle": { + "names": [ + "ProWand", + "Luxus" + ], + "services": { + "00001400-0000-1000-8000-00805f9b34fb": { + "tx": "00001401-0000-1000-8000-00805f9b34fb" + }, + "0000180f-0000-1000-8000-00805f9b34fb": { + "rxblebattery": "00002a19-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "a857e95b-3d5a-4034-92d2-7105c4febb8e", + "identifier": [ + "Luxus" + ], + "name": "Luxus" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "2e585349-127b-4536-85b7-9d5b90e44df4", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "ad812cb2-e04a-4656-9103-a80766601455", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "d1675d72-6d25-4cc4-99dc-a42e4e4fee97", + "name": "Kiiroo ProWand" + } + }, + "kiiroo-spot": { + "communication": [ + { + "btle": { + "names": [ + "SPOT W1" + ], + "services": { + "00001400-0000-1000-8000-00805f9b34fb": { + "tx": "00001401-0000-1000-8000-00805f9b34fb" + }, + "0000180f-0000-1000-8000-00805f9b34fb": { + "rxblebattery": "00002a19-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "a047482e-01d1-477a-bf67-71c1ee667f94", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "5171bb1b-b234-4a56-96ae-d592d3065d00", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "850e3d26-54df-4eb3-879e-e6f6aa93d335", + "name": "Kiiroo Spot" + } + }, + "kiiroo-v1": { + "communication": [ + { + "btle": { + "names": [ + "ONYX", + "PEARL" + ], + "services": { + "49535343-fe7d-4ae5-8fa9-9fafd205e455": { + "command": "49535343-aca3-481c-91ec-d85e28a60318", + "rx": "49535343-1e4d-4bd9-ba61-23c647249616", + "tx": "49535343-8841-43f4-a8d4-ecbe34729bb3" + } + } + } + } + ], + "configurations": [ + { + "features": [ + { + "feature-type": "Vibrate", + "id": "31eee57b-a1d8-49de-ac72-0dba46885a28", + "output": { + "Vibrate": { + "step-range": [ + 0, + 4 + ] + } + } + } + ], + "id": "aa35c397-8827-44c8-bc9f-a9acc234fba5", + "identifier": [ + "PEARL" + ], + "name": "Kiiroo Pearl" + }, + { + "features": [ + { + "feature-type": "PositionWithDuration", + "id": "2fe100ee-4665-4132-b4c6-d70a4037d6ac", + "output": { + "PositionWithDuration": { + "step-range": [ + 0, + 4 + ] + } + } + } + ], + "id": "f01513ef-a0c9-412d-ae70-b965b65379a8", + "identifier": [ + "ONYX" + ], + "name": "Kiiroo Onyx" + } + ], + "defaults": { + "features": [], + "id": "dec656b7-b312-4626-9811-fe2d51ed1242", + "name": "Kiiroo V1 Device" + } + }, + "kiiroo-v2": { + "communication": [ + { + "btle": { + "names": [ + "Launch", + "Onyx2" + ], + "services": { + "88f80580-0000-01e6-aace-0002a5d5c51b": { + "firmware": "88f80583-0000-01e6-aace-0002a5d5c51b", + "rx": "88f80582-0000-01e6-aace-0002a5d5c51b", + "tx": "88f80581-0000-01e6-aace-0002a5d5c51b" + }, + "f60402a6-0293-4bdb-9f20-6758133f7090": { + "firmware": "c7b7a04b-2cc4-40ff-8b10-5d531d1161db", + "rx": "d44d0393-0731-43b3-a373-8fc70b1f3323", + "tx": "02962ac9-e86f-4094-989d-231d69995fc2" + } + } + } + } + ], + "configurations": [ + { + "id": "f54eacbc-d84d-4c58-9410-9fbff25f14e8", + "identifier": [ + "Launch" + ], + "name": "Fleshlight Launch" + }, + { + "id": "5f3e8a6a-3a47-43a0-aed6-689101509481", + "identifier": [ + "Onyx2" + ], + "name": "Kiiroo Onyx 2" + } + ], + "defaults": { + "features": [ + { + "feature-type": "PositionWithDuration", + "id": "49b06ca8-dd4d-4306-91c6-931143dee212", + "output": { + "PositionWithDuration": { + "step-range": [ + 0, + 99 + ] + } + } + } + ], + "id": "1de4322c-86c4-40b1-8e1b-1f51c30392c0", + "name": "Kiiroo v2 Device" + } + }, + "kiiroo-v2-vibrator": { + "communication": [ + { + "btle": { + "names": [ + "Pearl2", + "Fuse", + "Virtual Blowbot", + "Titan", + "Virtual Rabbit" + ], + "services": { + "88f82580-0000-01e6-aace-0002a5d5c51b": { + "rxaccel": "88f82584-0000-01e6-aace-0002a5d5c51b", + "rxtouch": "88f82582-0000-01e6-aace-0002a5d5c51b", + "tx": "88f82581-0000-01e6-aace-0002a5d5c51b" + } + } + } + } + ], + "configurations": [ + { + "features": [ + { + "feature-type": "Vibrate", + "id": "e0374b68-eb67-4ecd-b566-8ca8bb74ce68", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "7581a2c2-0d94-45b4-b427-4a52b0ae3dea", + "identifier": [ + "Pearl2" + ], + "name": "Kiiroo Pearl 2" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "49587cee-c54e-41ab-9d70-0687ba4e6fec", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "a44beeed-4997-4e52-badc-7e1321338fbc", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "31e26147-c9af-45f0-8ee1-edd6c9f9e22e", + "identifier": [ + "Fuse" + ], + "name": "OhMiBod Fuse" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "de373981-ea04-4afb-8e58-15e392c7cbdf", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "db2f18c1-0a5f-40b2-b825-ac5a6932334e", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "0dbe6911-f95f-4abb-9550-5041a21f2ede", + "identifier": [ + "Virtual Rabbit" + ], + "name": "PornHub Virtual Rabbit" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "35c2cebd-e539-42f6-be6a-15398bb60a22", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "f6ac9d49-3d48-4709-83ac-2ae0eb5ec74b", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "d78facf3-706c-44ec-98e8-c4e7baba5966", + "identifier": [ + "Virtual Blowbot" + ], + "name": "PornHub Virtual Blowbot" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "5c535532-d02d-4acf-9482-fb17a5bc02ad", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "7a5a79b2-ff14-4ee6-ad91-d40649ca9d98", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "9fc946db-8889-403b-b7e1-ce86614b8176", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "b588d818-be20-4f01-b3ef-5383f6b60684", + "identifier": [ + "Titan" + ], + "name": "Kiiroo Titan" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "9a7b7a0b-6601-48d6-adfe-0b39a6f152a8", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "b1c6be0a-efc9-4327-8103-5315ebf3ac95", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "33fd2145-87d1-48fd-aaa9-0188b218d444", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "7dd84343-dfa3-4436-88b8-d3b3cca14064", + "name": "Kiiroo V2 Vibrator Device" + } + }, + "kiiroo-v21": { + "communication": [ + { + "btle": { + "names": [ + "Titan1.1", + "Cliona", + "Pearl2.1", + "Pearl2+", + "Pearl 2+", + "Pearl3", + "Pearl 3", + "OhMiBod 4.0", + "OhMiBod LUMEN", + "OhMiBod NEX2", + "OhMiBod NEX3", + "OhMiBod ESCA", + "OhMiBod Foxy", + "OhMiBod Chill Panty Vibe", + "OhMiBod Sphinx", + "Pulse Interactive", + "Fuse1.1" + ], + "services": { + "00001900-0000-1000-8000-00805f9b34fb": { + "rx": "00001903-0000-1000-8000-00805f9b34fb", + "tx": "00001902-0000-1000-8000-00805f9b34fb", + "whitelist": "00001901-0000-1000-8000-00805f9b34fb" + }, + "a0d70001-4c16-4ba7-977a-d394920e13a3": { + "rx": "a0d70003-4c16-4ba7-977a-d394920e13a3", + "tx": "a0d70002-4c16-4ba7-977a-d394920e13a3" + } + } + } + } + ], + "configurations": [ + { + "features": [ + { + "feature-type": "Vibrate", + "id": "ba4166e4-fba3-4eb9-90a2-5b281bb02f1e", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "61cf5ea0-f9d0-48f0-a337-f905fb89c2c3", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "1e922dde-c4f7-4ca9-96dd-d565135a184f", + "identifier": [ + "Pearl2.1" + ], + "name": "Kiiroo Pearl 2.1" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "222c4e24-d5ee-48c3-bc9d-d3f86d666c2c", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "232eab7f-e237-4683-a07f-e05e04b46360", + "identifier": [ + "Cliona" + ], + "name": "Kiiroo Cliona" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "75940e97-626d-4016-87eb-2777c29aaec6", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "8d19c7db-4547-4a8d-b4e4-c8bd2379bcd0", + "identifier": [ + "OhMiBod 4.0", + "OhMiBod ESCA" + ], + "name": "OhMiBod Esca 2" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "a5a42b68-553c-4ba4-b68d-322c49d405bc", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "PositionWithDuration", + "id": "b77ed4d9-9350-4868-8cb3-a6c48112f8b2", + "output": { + "PositionWithDuration": { + "step-range": [ + 0, + 99 + ] + } + } + } + ], + "id": "410c22ed-e0f8-4911-8e56-7f23b4e71bcc", + "identifier": [ + "Titan1.1" + ], + "name": "Kiiroo Titan 1.1" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "7d824538-bc5c-47d9-8d4d-8a503bf35284", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "69ae3f47-bb0f-4761-a641-3fc68c7de630", + "identifier": [ + "OhMiBod LUMEN" + ], + "name": "OhMiBod Lumen" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "ba1e86b4-9c6e-42d8-bff5-ac28628b3092", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "73fb1747-2056-403b-a6fb-56c521886a93", + "identifier": [ + "OhMiBod NEX2" + ], + "name": "OhMiBod NEX|2" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "9172bb5c-bbdc-4b56-a315-cb6b08bcb278", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "00784de1-fb46-4c86-973e-dd12f01e9827", + "identifier": [ + "OhMiBod NEX3" + ], + "name": "OhMiBod NEX|3" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "b369b6d0-5d5d-40cd-bf7f-3cb7641e1ce7", + "output": { + "Vibrate": { + "step-range": [ + 0, + 6 + ] + } + } + } + ], + "id": "e44fdd29-b3a0-4d37-b9af-e732f7934a13", + "identifier": [ + "Pulse Interactive" + ], + "name": "Hot Octopuss Pulse Solo Interactive" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "0e0820e3-aeec-4df2-ae2a-b4bf82b9a823", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "d6675d9e-9ddb-41dc-a0e4-0b0d54fd29cb", + "identifier": [ + "Fuse1.1" + ], + "name": "OhMiBod Fuse 1.1" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "187e471d-3815-4dab-85bc-e81969f26d40", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "bdcf6cd9-cc98-46c3-97eb-78b70b2a00a4", + "identifier": [ + "OhMiBod Foxy" + ], + "name": "OhMiBod Foxy" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "75ed3cd9-8d21-4567-9816-71f7925dcce4", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "50d6c107-7ddf-4adc-9de6-f9fd1e08cdcf", + "identifier": [ + "OhMiBod Chill Panty Vibe" + ], + "name": "OhMiBod Chill" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "6a78e124-8314-40ec-bcc4-45f10341eaf7", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "15a13fb0-d287-4262-bf7a-26ae019d997b", + "identifier": [ + "OhMiBod Sphinx" + ], + "name": "OhMiBod Sphinx" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "69d4719c-2342-4d80-a8bc-70f5008b1628", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "5ef95603-09d0-4d44-9714-a7100b319371", + "identifier": [ + "Pearl2+", + "Pearl 2+" + ], + "name": "Kiiroo Pearl 2+" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "b3b2cea4-5987-413f-b611-aa068c76c04c", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "8fb6578e-bbbc-42d7-9c2e-7c813bd89f29", + "identifier": [ + "Pearl3", + "Pearl 3" + ], + "name": "Kiiroo Pearl 3" + } + ], + "defaults": { + "features": [], + "id": "189a4912-3c5b-4a0d-ab8b-d44ab6c97f0b", + "name": "Kiiroo V2.1 Device" + } + }, + "kiiroo-v21-initialized": { + "communication": [ + { + "btle": { + "names": [ + "Rey", + "We-Vibe Rocketman", + "Realm1.1", + "Onyx2.1", + "Onyx+", + "KEON", + "Keon R2" + ], + "services": { + "00001900-0000-1000-8000-00805f9b34fb": { + "rx": "00001903-0000-1000-8000-00805f9b34fb", + "tx": "00001902-0000-1000-8000-00805f9b34fb", + "whitelist": "00001901-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "features": [ + { + "feature-type": "PositionWithDuration", + "id": "8cd94334-adde-4d9b-aad9-c2de93adb2c0", + "output": { + "PositionWithDuration": { + "step-range": [ + 0, + 99 + ] + } + } + } + ], + "id": "eac00879-448c-46ed-aaa5-efe86226fb48", + "identifier": [ + "Onyx2.1" + ], + "name": "Kiiroo Onyx 2.1" + }, + { + "features": [ + { + "feature-type": "PositionWithDuration", + "id": "c66d882d-f752-45b4-806e-166d3e160eb8", + "output": { + "PositionWithDuration": { + "step-range": [ + 0, + 99 + ] + } + } + } + ], + "id": "40dafef9-ef94-4b03-8b8a-e9d7e9fef317", + "identifier": [ + "Onyx+" + ], + "name": "Kiiroo Onyx+" + }, + { + "features": [ + { + "feature-type": "PositionWithDuration", + "id": "da002a11-610a-4e13-94c5-4c45d51814f2", + "output": { + "PositionWithDuration": { + "step-range": [ + 0, + 99 + ] + } + } + } + ], + "id": "f3675b2e-d7b8-463b-8b91-30a5ebef24f4", + "identifier": [ + "KEON", + "Keon R2" + ], + "name": "Kiiroo Keon" + }, + { + "features": [ + { + "feature-type": "PositionWithDuration", + "id": "8c896f82-2e17-46f9-9db2-531cc7e42236", + "output": { + "PositionWithDuration": { + "step-range": [ + 0, + 99 + ] + } + } + } + ], + "id": "d2fde950-8e0a-4231-8ebc-5c39dcf3349f", + "identifier": [ + "Rey", + "We-Vibe Rocketman", + "Realm1.1" + ], + "name": "Kiiroo Onyx+ Realm Edition" + } + ], + "defaults": { + "features": [], + "id": "bd9c7fa4-214b-4871-8373-c5266ace0b90", + "name": "Kiiroo V2.1 Initialized Device" + } + }, + "kizuna": { + "communication": [ + { + "serial": { + "baud-rate": 19200, + "data-bits": 8, + "parity": "N", + "port": "default", + "stop-bits": 1 + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Rotate", + "id": "7077cb50-d3d5-4357-8b5f-42517ffc83b8", + "output": { + "Rotate": { + "step-range": [ + 0, + 9 + ] + } + } + } + ], + "id": "654be6a2-bfe6-4358-bd0a-0d8f2cd9d105", + "name": "Kizuna Smart" + } + }, + "lelo-f1s": { + "communication": [ + { + "btle": { + "names": [ + "F1s" + ], + "services": { + "0000fff0-0000-1000-8000-00805f9b34fb": { + "rx": "00000aa4-0000-1000-8000-00805f9b34fb", + "tx": "0000fff1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "006eb802-d890-4a0f-a566-288d86ec1caf", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "787c4a90-e78c-489a-a0eb-f66b3c70d6d2", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "83c52d23-0532-4b57-8a0b-c8132a5c52bd", + "name": "Lelo F1s" + } + }, + "lelo-f1sv2": { + "communication": [ + { + "btle": { + "names": [ + "F1SV2A", + "F1SV2X", + "F1SV3" + ], + "services": { + "0000fff0-0000-1000-8000-00805f9b34fb": { + "generic0": "00000a11-0000-1000-8000-00805f9b34fb", + "rx": "00000a04-0000-1000-8000-00805f9b34fb", + "tx": "0000fff1-0000-1000-8000-00805f9b34fb", + "txvibrate": "0000fff2-0000-1000-8000-00805f9b34fb", + "whitelist": "00000a10-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "64505ced-309b-4a32-93a8-13ee55e2da2c", + "identifier": [ + "F1SV2A", + "F1SV2X" + ], + "name": "Lelo F1s V2" + }, + { + "id": "36adf7ce-98bf-4fad-b916-b44d20a5d9e1", + "identifier": [ + "F1SV3" + ], + "name": "Lelo F1s V3" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "90bd67a5-4601-4c49-97bb-0845ab7011ba", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "05fc758b-a3fe-4156-b3ae-9cdcb9ae95c6", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "108d5cfe-2155-477f-b1b6-c48da6c4b7d8", + "name": "Lelo F1s V2" + } + }, + "lelo-harmony": { + "communication": [ + { + "btle": { + "names": [ + "IdaWave", + "Ida Wave", + "TianiHarmony", + "Tiani Harmony", + "TOR3", + "Hugo2", + "DoubleSonic", + "GIGI3", + "LIV3" + ], + "services": { + "0000fff0-0000-1000-8000-00805f9b34fb": { + "command": "0000fff1-0000-1000-8000-00805f9b34fb", + "tx": "0000fff2-0000-1000-8000-00805f9b34fb", + "whitelist": "00000a11-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "features": [ + { + "feature-type": "Vibrate", + "id": "c887327d-e635-4086-83dc-2f21286f485c", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Rotate", + "id": "5bd48a1d-992e-4c69-ae74-ed94505eec58", + "output": { + "Rotate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "a9de3981-7e0d-4b07-b8a9-10031bb6ddae", + "identifier": [ + "IdaWave", + "Ida Wave" + ], + "name": "Lelo Ida Wave" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "d0c39af5-62b4-4bfe-a0bb-71f5c2e86c99", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "e0104054-fba7-4ba2-b51f-0f3d95aee1ba", + "identifier": [ + "TOR3" + ], + "name": "Lelo Tor 3" + }, + { + "id": "7d302aee-23cd-4681-b9fc-1275250e8a03", + "identifier": [ + "Hugo2" + ], + "name": "Lelo Hugo 2" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "8a9d2c49-1486-4515-a0a4-320c9c903ccc", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Rotate", + "id": "6fdbe4ae-f0fc-44e0-b0a4-cbb56dee61d8", + "output": { + "Rotate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "c6bf86e6-1054-4c14-a3bb-d415edf81834", + "identifier": [ + "DoubleSonic" + ], + "name": "Lelo Enigma Double Sonic" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "ea1ca70a-b3e9-41ba-8863-3f74156fef87", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "e722ba98-5c2d-4f77-a56d-ac72b213ed53", + "identifier": [ + "GIGI3" + ], + "name": "Lelo Gigi 3" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "1599b3d9-055d-4c9b-a1fe-7cef1fac4c9e", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "0daa8498-172c-47bc-b6c4-57414589509b", + "identifier": [ + "LIV3" + ], + "name": "Lelo Liv 3" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "0cf2b478-2235-4f83-897c-d8bbebb822e8", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "0c89262b-0fcd-48c9-9492-a79758da781f", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "3bde5251-e810-418a-9ebf-8c3a50684d9a", + "name": "Lelo Tiani Harmony" + } + }, + "leten": { + "communication": [ + { + "btle": { + "names": [ + "T528-LT", + "F537-LT", + "F520B-LT", + "F520A-LT" + ], + "services": { + "0000ffe0-0000-1000-8000-00805f9b34fb": { + "rx": "0000ffe1-0000-1000-8000-00805f9b34fb" + }, + "0000fff0-0000-1000-8000-00805f9b34fb": { + "tx": "0000fff1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "f9df3044-6d90-4767-97a9-05d15e2f97ec", + "output": { + "Vibrate": { + "step-range": [ + 0, + 25 + ] + } + } + } + ], + "id": "8c613401-3bc2-434b-8ffe-881879b1e287", + "name": "Leten Device" + } + }, + "libo-elle": { + "communication": [ + { + "btle": { + "names": [ + "PiPiJing", + "Shuidi" + ], + "services": { + "00006000-0000-1000-8000-00805f9b34fb": { + "tx": "00006001-0000-1000-8000-00805f9b34fb", + "txmode": "00006002-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "af187899-8704-42f1-994e-694616576149", + "identifier": [ + "PiPiJing" + ], + "name": "LiBo Elle" + }, + { + "id": "98f5289c-98b4-4410-bed2-4d3050a4761e", + "identifier": [ + "Shuidi" + ], + "name": "Libo Elle 2" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "1b336a6e-6f35-458f-837e-a0147f67c7f5", + "output": { + "Vibrate": { + "step-range": [ + 0, + 3 + ] + } + } + } + ], + "id": "fe54deb6-5c13-4f69-a804-1af5fce5de96", + "name": "Libo Elle Device" + } + }, + "libo-karen": { + "communication": [ + { + "btle": { + "names": [ + "SuoYinQiu" + ], + "services": { + "00006000-0000-1000-8000-00805f9b34fb": { + "tx": "00006001-0000-1000-8000-00805f9b34fb", + "txmode": "00006002-0000-1000-8000-00805f9b34fb" + }, + "00006050-0000-1000-8000-00805f9b34fb": { + "rxpressure": "00006051-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [], + "id": "2d9f29c7-7d0d-4319-967c-9f7b89eb7b1d", + "name": "Libo Karen" + } + }, + "libo-shark": { + "communication": [ + { + "btle": { + "names": [ + "ShaYu" + ], + "services": { + "00006000-0000-1000-8000-00805f9b34fb": { + "tx": "00006001-0000-1000-8000-00805f9b34fb", + "txmode": "00006002-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "52d614a1-4f43-4946-a7bd-9d413791e642", + "output": { + "Vibrate": { + "step-range": [ + 0, + 3 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "7cebc2d6-3b11-4117-aec4-ced57a738a13", + "output": { + "Vibrate": { + "step-range": [ + 0, + 3 + ] + } + } + } + ], + "id": "44915af5-e3b9-4766-ae2e-b2df758689fd", + "name": "Libo Shark" + } + }, + "libo-vibes": { + "communication": [ + { + "btle": { + "names": [ + "XiaoLu", + "LuXiaoHan", + "BaiHu", + "Gugudai", + "Yuyi", + "LuWuShuang", + "LiBo", + "QingTing", + "Huohu", + "Yuyi", + "Haima" + ], + "services": { + "00006000-0000-1000-8000-00805f9b34fb": { + "tx": "00006001-0000-1000-8000-00805f9b34fb", + "txmode": "00006002-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "9c9b46bd-ab5e-4ec2-a9db-c80571074cfb", + "identifier": [ + "XiaoLu" + ], + "name": "Libo Lottie" + }, + { + "id": "80deea27-6833-4bdc-9d24-02615c3197d9", + "identifier": [ + "LuXiaoHan" + ], + "name": "Libo LuLu" + }, + { + "id": "982d708e-788b-4962-b9bb-c253f49becf8", + "identifier": [ + "Yuyi" + ], + "name": "Libo Lina" + }, + { + "id": "d761eb50-9051-44ce-82ed-d301aa532cc3", + "identifier": [ + "LuWuShuang" + ], + "name": "Libo Adel" + }, + { + "id": "f9e758fe-3327-435b-94e3-eda7445d49e1", + "identifier": [ + "LiBo" + ], + "name": "Libo Lily" + }, + { + "id": "93ce6ac4-2f24-4a8e-ab81-7a046403eb0c", + "identifier": [ + "QingTing" + ], + "name": "Libo Lucy" + }, + { + "id": "f0234003-d8d3-4858-837b-8051109e6770", + "identifier": [ + "Huohu" + ], + "name": "Libo Lara" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "39eca274-5634-4433-9be5-2c688fb9b65c", + "output": { + "Vibrate": { + "step-range": [ + 0, + 99 + ] + } + } + } + ], + "id": "c63739df-3b00-4602-8d3d-8f1080ec499c", + "identifier": [ + "Yuyi" + ], + "name": "Libo Feather" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "4239e32b-b3ad-49e2-a96e-1fb7298b1889", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "5f43a406-9567-43fc-b3b8-5383b5200bfd", + "output": { + "Vibrate": { + "step-range": [ + 0, + 3 + ] + } + } + } + ], + "id": "2de690ff-ad02-4272-a2c7-845c3ea8b28c", + "identifier": [ + "BaiHu" + ], + "name": "Libo LaLa" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "6fc0149e-d041-4987-a66e-dbf36739331f", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "80b80fb2-b458-4661-a1e2-a8f27651d390", + "output": { + "Vibrate": { + "step-range": [ + 0, + 3 + ] + } + } + } + ], + "id": "8e342d89-66d4-4943-ae42-015cb268444b", + "identifier": [ + "Gugudai" + ], + "name": "Libo Carlos" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "54c02210-8494-40c6-a04c-e0a302aa735e", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "a2fb0a58-895b-49f5-bc88-b0a38bc64e68", + "output": { + "Vibrate": { + "step-range": [ + 0, + 3 + ] + } + } + } + ], + "id": "6d2f4df7-18a1-4568-81be-0e8e545e82a1", + "identifier": [ + "Haima" + ], + "name": "Libo Selina" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "db5d9b0a-8498-4f5a-b53b-111a9940367d", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "8ba2bd4c-962b-45ff-87e1-3812084c7c1c", + "name": "Libo Vibes Device" + } + }, + "lioness": { + "communication": [ + { + "btle": { + "names": [ + "Lioness", + "Lioness2" + ], + "services": { + "d973f2e5-b19e-11e2-9e96-0800200c9a66": { + "rx": "d973f2e6-b19e-11e2-9e96-0800200c9a66" + }, + "d973f2ed-b19e-11e2-9e96-0800200c9a66": { + "tx": "d973f2f4-b19e-11e2-9e96-0800200c9a66" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "30051e05-190c-43e9-a35d-480a7615622d", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "a35b0291-002b-4382-9eaf-6ebd9d04b668", + "name": "Lioness" + } + }, + "loob": { + "communication": [ + { + "btle": { + "names": [ + "LOOB" + ], + "services": { + "b75c49d2-04a3-4071-a0b5-35853eb08307": { + "tx": "ba5c49d2-04a3-4071-a0b5-35853eb08307" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "PositionWithDuration", + "id": "7078c41e-0cd3-4264-8f54-c331ac4c81f9", + "output": { + "PositionWithDuration": { + "step-range": [ + 0, + 1000 + ] + } + } + } + ], + "id": "26c0103c-9b39-4dbb-ad33-5cbdff03c178", + "name": "Joyroid Loob" + } + }, + "lovedistance": { + "communication": [ + { + "btle": { + "names": [ + "REACH G", + "REACH", + "MAG", + "SPAN", + "RANGE", + "ORBIT", + "JOIN G", + "LINK", + "GRASP", + "RECEIVE" + ], + "services": { + "0000ff00-0000-1000-8000-00805f9b34fb": { + "rx": "0000ff02-0000-1000-8000-00805f9b34fb", + "tx": "0000ff01-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "7b190a71-6667-4b63-9929-42dc3a22d113", + "identifier": [ + "REACH G" + ], + "name": "Love Distance Reach G" + }, + { + "id": "ad11cd1c-7450-4a0e-b7cf-4ff94e53b685", + "identifier": [ + "REACH" + ], + "name": "Love Distance Reach" + }, + { + "id": "bae30100-1dfa-4bd9-a2b3-e9415bebd1cb", + "identifier": [ + "MAG" + ], + "name": "Love Distance Mag" + }, + { + "id": "84d00425-1a74-4fef-ad06-a5cdf22450d4", + "identifier": [ + "SPAN" + ], + "name": "Love Distance Span" + }, + { + "id": "9cd3854e-03d7-4a32-b189-a97990ef45be", + "identifier": [ + "RANGE" + ], + "name": "Love Distance Range" + }, + { + "id": "04c77f83-87bc-4547-87cc-d2c45c203313", + "identifier": [ + "ORBIT" + ], + "name": "Love Distance Range" + }, + { + "id": "21f4d6ea-9c83-4d3e-a095-f5761e6c63ed", + "identifier": [ + "JOIN G" + ], + "name": "Love Distance Join G" + }, + { + "id": "7dfc44e0-0a77-4725-be94-55ae7fab2601", + "identifier": [ + "LINK" + ], + "name": "Love Distance Link" + }, + { + "id": "57d24ed8-fc9d-4dad-87b0-d978d3ebe8cd", + "identifier": [ + "GRASP" + ], + "name": "Love Distance Grasp" + }, + { + "id": "d104ec28-cd82-4fdb-bb9b-96ffc3b639ed", + "identifier": [ + "RECEIVE" + ], + "name": "Love Distance Receive" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "3eae1a60-e996-4726-858b-2128a1ae376a", + "output": { + "Vibrate": { + "step-range": [ + 0, + 121 + ] + } + } + } + ], + "id": "1cd71bad-3cfc-41ee-a6b8-8651bf658489", + "name": "Love Distance Device" + } + }, + "lovehoney-desire": { + "communication": [ + { + "btle": { + "names": [ + "PROSTATE VIBE", + "KNICKER VIBE", + "LOVE EGG" + ], + "services": { + "0000ff00-0000-1000-8000-00805f9b34fb": { + "tx": "0000ff01-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "d7aa359d-a9f0-40b1-8e20-b55e8ef809c0", + "identifier": [ + "PROSTATE VIBE" + ], + "name": "Lovehoney Desire Prostate Vibrator" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "5e192f37-2beb-4e21-b182-ff113642f465", + "output": { + "Vibrate": { + "step-range": [ + 0, + 127 + ] + } + } + } + ], + "id": "439c5fe2-3e8d-4917-bcd7-8f24824d854b", + "identifier": [ + "KNICKER VIBE" + ], + "name": "Lovehoney Desire Knicker Vibrator" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "980c9d39-e0bc-45d9-8d41-3e95af348d6c", + "output": { + "Vibrate": { + "step-range": [ + 0, + 127 + ] + } + } + } + ], + "id": "00d4e759-900d-4c37-b6a3-ce446bb8f590", + "identifier": [ + "LOVE EGG" + ], + "name": "Lovehoney Desire Love Egg" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "716bdae7-2075-4e8a-a2cb-d37b6fc35a5b", + "output": { + "Vibrate": { + "step-range": [ + 0, + 127 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "ce0315b0-9918-4769-af8e-6ec6258d0e1a", + "output": { + "Vibrate": { + "step-range": [ + 0, + 127 + ] + } + } + } + ], + "id": "fabcaab7-a38b-4c24-bf36-2ca4905a1e49", + "name": "Lovehoney Device" + } + }, + "lovense": { + "communication": [ + { + "btle": { + "advertised-services": [ + "6e400001-b5a3-f393-e0a9-e50e24dcca9e", + "50300001-0024-4bd4-bbd5-a6920e4c5653", + "57300001-0023-4bd4-bbd5-a6920e4c5653", + "5a300001-0024-4bd4-bbd5-a6920e4c5653", + "50300001-0023-4bd4-bbd5-a6920e4c5653", + "53300001-0023-4bd4-bbd5-a6920e4c5653", + "5a300001-0023-4bd4-bbd5-a6920e4c5653", + "4f300001-0023-4bd4-bbd5-a6920e4c5653", + "42300001-0023-4bd4-bbd5-a6920e4c5653", + "43300001-0023-4bd4-bbd5-a6920e4c5653", + "4c300001-0023-4bd4-bbd5-a6920e4c5653", + "4c410001-0023-4bd4-bbd5-a6920e4c5653", + "56300001-0023-4bd4-bbd5-a6920e4c5653", + "58300001-0023-4bd4-bbd5-a6920e4c5653", + "52300001-0023-4bd4-bbd5-a6920e4c5653", + "46300001-0023-4bd4-bbd5-a6920e4c5653", + "50300011-0023-4bd4-bbd5-a6920e4c5653", + "4a300001-0023-4bd4-bbd5-a6920e4c5653", + "45440001-0023-4bd4-bbd5-a6920e4c5653", + "45420001-0023-4bd4-bbd5-a6920e4c5653", + "54300001-0023-4bd4-bbd5-a6920e4c5653", + "45490001-0023-4bd4-bbd5-a6920e4c5653", + "4e300001-0023-4bd4-bbd5-a6920e4c5653", + "45410001-0023-4bd4-bbd5-a6920e4c5653", + "51300001-0023-4bd4-bbd5-a6920e4c5653", + "45460001-0023-4bd4-bbd5-a6920e4c5653", + "454c0001-0023-4bd4-bbd5-a6920e4c5653", + "55300001-0023-4bd4-bbd5-a6920e4c5653", + "53440001-0023-4bd4-bbd5-a6920e4c5653", + "48300001-0023-4bd4-bbd5-a6920e4c5653", + "46530001-0023-4bd4-bbd5-a6920e4c5653", + "42410001-0023-4bd4-bbd5-a6920e4c5653", + "43410001-0023-4bd4-bbd5-a6920e4c5653", + "4f430001-0023-4bd4-bbd5-a6920e4c5653", + "455a0001-0023-4bd4-bbd5-a6920e4c5653" + ], + "manufacturer-data": [ + { + "company": 620, + "data": [ + 255, + 33 + ] + } + ], + "names": [ + "LVS-*", + "LOVE-*" + ], + "services": { + "0000fff0-0000-1000-8000-00805f9b34fb": { + "rx": "0000fff1-0000-1000-8000-00805f9b34fb", + "tx": "0000fff2-0000-1000-8000-00805f9b34fb" + }, + "42300001-0023-4bd4-bbd5-a6920e4c5653": { + "rx": "42300003-0023-4bd4-bbd5-a6920e4c5653", + "tx": "42300002-0023-4bd4-bbd5-a6920e4c5653" + }, + "42410001-0023-4bd4-bbd5-a6920e4c5653": { + "rx": "42410003-0023-4bd4-bbd5-a6920e4c5653", + "tx": "42410002-0023-4bd4-bbd5-a6920e4c5653" + }, + "43300001-0023-4bd4-bbd5-a6920e4c5653": { + "rx": "43300003-0023-4bd4-bbd5-a6920e4c5653", + "tx": "43300002-0023-4bd4-bbd5-a6920e4c5653" + }, + "43410001-0023-4bd4-bbd5-a6920e4c5653": { + "rx": "43410003-0023-4bd4-bbd5-a6920e4c5653", + "tx": "43410002-0023-4bd4-bbd5-a6920e4c5653" + }, + "45410001-0023-4bd4-bbd5-a6920e4c5653": { + "rx": "45410003-0023-4bd4-bbd5-a6920e4c5653", + "tx": "45410002-0023-4bd4-bbd5-a6920e4c5653" + }, + "45420001-0023-4bd4-bbd5-a6920e4c5653": { + "rx": "45420003-0023-4bd4-bbd5-a6920e4c5653", + "tx": "45420002-0023-4bd4-bbd5-a6920e4c5653" + }, + "45440001-0023-4bd4-bbd5-a6920e4c5653": { + "rx": "45440003-0023-4bd4-bbd5-a6920e4c5653", + "tx": "45440002-0023-4bd4-bbd5-a6920e4c5653" + }, + "45460001-0023-4bd4-bbd5-a6920e4c5653": { + "rx": "45460003-0023-4bd4-bbd5-a6920e4c5653", + "tx": "45460002-0023-4bd4-bbd5-a6920e4c5653" + }, + "45490001-0023-4bd4-bbd5-a6920e4c5653": { + "rx": "45490003-0023-4bd4-bbd5-a6920e4c5653", + "tx": "45490002-0023-4bd4-bbd5-a6920e4c5653" + }, + "454c0001-0023-4bd4-bbd5-a6920e4c5653": { + "rx": "454c0003-0023-4bd4-bbd5-a6920e4c5653", + "tx": "454c0002-0023-4bd4-bbd5-a6920e4c5653" + }, + "455a0001-0023-4bd4-bbd5-a6920e4c5653": { + "rx": "455a0003-0023-4bd4-bbd5-a6920e4c5653", + "tx": "455a0002-0023-4bd4-bbd5-a6920e4c5653" + }, + "46300001-0023-4bd4-bbd5-a6920e4c5653": { + "rx": "46300003-0023-4bd4-bbd5-a6920e4c5653", + "tx": "46300002-0023-4bd4-bbd5-a6920e4c5653" + }, + "46530001-0023-4bd4-bbd5-a6920e4c5653": { + "rx": "46530003-0023-4bd4-bbd5-a6920e4c5653", + "tx": "46530002-0023-4bd4-bbd5-a6920e4c5653" + }, + "48300001-0023-4bd4-bbd5-a6920e4c5653": { + "rx": "48300003-0023-4bd4-bbd5-a6920e4c5653", + "tx": "48300002-0023-4bd4-bbd5-a6920e4c5653" + }, + "4a300001-0023-4bd4-bbd5-a6920e4c5653": { + "rx": "4a300003-0023-4bd4-bbd5-a6920e4c5653", + "tx": "4a300002-0023-4bd4-bbd5-a6920e4c5653" + }, + "4c300001-0023-4bd4-bbd5-a6920e4c5653": { + "rx": "4c300003-0023-4bd4-bbd5-a6920e4c5653", + "tx": "4c300002-0023-4bd4-bbd5-a6920e4c5653" + }, + "4c410001-0023-4bd4-bbd5-a6920e4c5653": { + "rx": "4c410003-0023-4bd4-bbd5-a6920e4c5653", + "tx": "4c410002-0023-4bd4-bbd5-a6920e4c5653" + }, + "4e300001-0023-4bd4-bbd5-a6920e4c5653": { + "rx": "4e300003-0023-4bd4-bbd5-a6920e4c5653", + "tx": "4e300002-0023-4bd4-bbd5-a6920e4c5653" + }, + "4f300001-0023-4bd4-bbd5-a6920e4c5653": { + "rx": "4f300003-0023-4bd4-bbd5-a6920e4c5653", + "tx": "4f300002-0023-4bd4-bbd5-a6920e4c5653" + }, + "4f430001-0023-4bd4-bbd5-a6920e4c5653": { + "rx": "4f430003-0023-4bd4-bbd5-a6920e4c5653", + "tx": "4f430002-0023-4bd4-bbd5-a6920e4c5653" + }, + "50300001-0023-4bd4-bbd5-a6920e4c5653": { + "rx": "50300003-0023-4bd4-bbd5-a6920e4c5653", + "tx": "50300002-0023-4bd4-bbd5-a6920e4c5653" + }, + "50300001-0024-4bd4-bbd5-a6920e4c5653": { + "rx": "50300003-0024-4bd4-bbd5-a6920e4c5653", + "tx": "50300002-0024-4bd4-bbd5-a6920e4c5653" + }, + "50300011-0023-4bd4-bbd5-a6920e4c5653": { + "rx": "50300013-0023-4bd4-bbd5-a6920e4c5653", + "tx": "50300012-0023-4bd4-bbd5-a6920e4c5653" + }, + "51300001-0023-4bd4-bbd5-a6920e4c5653": { + "rx": "51300003-0023-4bd4-bbd5-a6920e4c5653", + "tx": "51300002-0023-4bd4-bbd5-a6920e4c5653" + }, + "52300001-0023-4bd4-bbd5-a6920e4c5653": { + "rx": "52300003-0023-4bd4-bbd5-a6920e4c5653", + "tx": "52300002-0023-4bd4-bbd5-a6920e4c5653" + }, + "53300001-0023-4bd4-bbd5-a6920e4c5653": { + "rx": "53300003-0023-4bd4-bbd5-a6920e4c5653", + "tx": "53300002-0023-4bd4-bbd5-a6920e4c5653" + }, + "53440001-0023-4bd4-bbd5-a6920e4c5653": { + "rx": "53440003-0023-4bd4-bbd5-a6920e4c5653", + "tx": "53440002-0023-4bd4-bbd5-a6920e4c5653" + }, + "54300001-0023-4bd4-bbd5-a6920e4c5653": { + "rx": "54300003-0023-4bd4-bbd5-a6920e4c5653", + "tx": "54300002-0023-4bd4-bbd5-a6920e4c5653" + }, + "55300001-0023-4bd4-bbd5-a6920e4c5653": { + "rx": "55300003-0023-4bd4-bbd5-a6920e4c5653", + "tx": "55300002-0023-4bd4-bbd5-a6920e4c5653" + }, + "56300001-0023-4bd4-bbd5-a6920e4c5653": { + "rx": "56300003-0023-4bd4-bbd5-a6920e4c5653", + "tx": "56300002-0023-4bd4-bbd5-a6920e4c5653" + }, + "57300001-0023-4bd4-bbd5-a6920e4c5653": { + "rx": "57300003-0023-4bd4-bbd5-a6920e4c5653", + "tx": "57300002-0023-4bd4-bbd5-a6920e4c5653" + }, + "58300001-0023-4bd4-bbd5-a6920e4c5653": { + "rx": "58300003-0023-4bd4-bbd5-a6920e4c5653", + "tx": "58300002-0023-4bd4-bbd5-a6920e4c5653" + }, + "5a300001-0023-4bd4-bbd5-a6920e4c5653": { + "rx": "5a300003-0023-4bd4-bbd5-a6920e4c5653", + "tx": "5a300002-0023-4bd4-bbd5-a6920e4c5653" + }, + "5a300001-0024-4bd4-bbd5-a6920e4c5653": { + "rx": "5a300003-0024-4bd4-bbd5-a6920e4c5653", + "tx": "5a300002-0024-4bd4-bbd5-a6920e4c5653" + }, + "6e400001-b5a3-f393-e0a9-e50e24dcca9e": { + "rx": "6e400003-b5a3-f393-e0a9-e50e24dcca9e", + "tx": "6e400002-b5a3-f393-e0a9-e50e24dcca9e" + } + } + } + } + ], + "configurations": [ + { + "features": [ + { + "description": "Vibrator", + "feature-type": "Vibrate", + "id": "d9c9b4a7-008e-4182-b28c-0984af970c32", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "Air Pump", + "feature-type": "Constrict", + "id": "fed393a9-3ac6-4924-859d-5cb4ae059cea", + "output": { + "Constrict": { + "step-range": [ + 0, + 3 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "b4be6835-5b91-4540-bc7b-0c3d8dcb89fd", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "99024e29-c0ed-4c26-aede-e0db0679eae5", + "identifier": [ + "B" + ], + "name": "Lovense Max" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "cb286b22-998b-4420-82f3-84e8d39db6b5", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "c8b72e1d-d7d4-4417-8cbc-e6c0f435889a", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "66b31efb-3bd9-4e3a-9972-88c66e9fca28", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "2e309985-6bbf-4b75-866f-76d845b3ce42", + "identifier": [ + "P" + ], + "name": "Lovense Edge" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "2c5da93b-36a0-4209-ac8c-cead63b838c6", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "feature-type": "RotateWithDirection", + "id": "515e07e2-a6e6-4ac0-a4b0-512504311260", + "output": { + "RotateWithDirection": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "820d8fb1-c6ec-434d-b7c4-835bdf36552a", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "463a18b9-42a5-4f7b-8156-0e61346fdb8a", + "identifier": [ + "A", + "C" + ], + "name": "Lovense Nora" + }, + { + "id": "7053fde9-0902-4aab-926d-fc51869f6ccc", + "identifier": [ + "L" + ], + "name": "Lovense Ambi" + }, + { + "id": "670560f0-981e-42cb-b83d-c911dd9826e2", + "identifier": [ + "S" + ], + "name": "Lovense Lush" + }, + { + "id": "37642e1c-a416-44d3-bada-76b6d9e245c9", + "identifier": [ + "Z" + ], + "name": "Lovense Hush" + }, + { + "id": "e788f8d5-037a-4ce4-a13f-6b2e8ec31fb6", + "identifier": [ + "W" + ], + "name": "Lovense Domi" + }, + { + "id": "45bf66e7-01e0-48ad-ad1c-2b48d1279da1", + "identifier": [ + "O" + ], + "name": "Lovense Osci" + }, + { + "id": "45e2fc5c-79e8-4228-beba-a97a14d84e7d", + "identifier": [ + "V" + ], + "name": "Lovense Mission" + }, + { + "id": "a8f36834-d8eb-48d5-9bad-237e67f6fd5b", + "identifier": [ + "CA" + ], + "name": "Lovense Mission 2" + }, + { + "id": "481b101b-ff4d-4045-84fe-da2b9bba93e2", + "identifier": [ + "X" + ], + "name": "Lovense Ferri" + }, + { + "id": "df95c01b-88d3-49b3-b360-69777b341795", + "identifier": [ + "R" + ], + "name": "Lovense Diamo" + }, + { + "id": "30830f67-4550-4133-88a9-b5eccd83083b", + "identifier": [ + "ToyS" + ], + "name": "Loveai Dolp" + }, + { + "features": [ + { + "description": "Fucking Machine Oscillation Speed", + "feature-type": "Oscillate", + "id": "f9506652-c4ac-43b1-b184-cd8016b64623", + "output": { + "Oscillate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "7c382c60-0ee2-4315-b8cf-cfd3ab4c9ccd", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "8667f7b6-7baa-4e46-9d76-947fb707f0f3", + "identifier": [ + "F" + ], + "name": "Lovense Sex Machine" + }, + { + "features": [ + { + "description": "Fucking Machine Oscillation Speed", + "feature-type": "Oscillate", + "id": "aaf55cab-8ebd-42b3-9bbb-74a57efdf014", + "output": { + "Oscillate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "68defbd8-af87-4f04-97da-edfa8fb576f9", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "48d5c76b-8c0e-4152-9f3b-5ba92ebf30fe", + "identifier": [ + "FS" + ], + "name": "Lovense Mini Sex Machine" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "930b9aee-0ba5-4268-95ca-2a5691d31239", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "62b2b22c-c028-4aa4-a85c-a7fe8c5f9dcb", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "60868f44-3d56-44ed-bcc4-00041a7b5997", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "0bddb3da-2c8d-4af8-9e80-1e0038878f27", + "identifier": [ + "J" + ], + "name": "Lovense Dolce" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "4cf78058-44c7-4513-913a-37558a84b91e", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "f4ada339-8bb2-4b02-b907-69a3257bce3b", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "3933bfcb-6daf-4c33-b834-877cb29ce77d", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "a8b175a8-3447-4938-b1df-7215464b56e6", + "identifier": [ + "OC" + ], + "name": "Lovense Osci 3" + }, + { + "id": "6071cc3a-a8e7-4142-bc80-08fe122452d8", + "identifier": [ + "ED" + ], + "name": "Lovense Gush" + }, + { + "id": "51de38d3-114f-453e-a440-3958918af423", + "identifier": [ + "EZ" + ], + "name": "Lovense Gush 2" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "39b063fa-958b-4d1a-bbd1-8480e105dd88", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "b40accca-7c73-4bff-9819-45f806a194a8", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "8fa6dc63-430e-42cb-9345-42d37f0c2629", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "a6a0c988-3e04-4fa3-89e2-4f4d2f242ffd", + "identifier": [ + "EB" + ], + "name": "Lovense Hyphy" + }, + { + "id": "bdab9bf5-25f8-4140-bf4d-3f0edf1883d2", + "identifier": [ + "T" + ], + "name": "Lovense Calor" + }, + { + "id": "c90a2d78-5b08-40ad-a2c9-ac7eacb43b3d", + "identifier": [ + "EI" + ], + "name": "Lovense Flexer (Firmware update needed)" + }, + { + "features": [ + { + "description": "Internal Vibe", + "feature-type": "Vibrate", + "id": "9b2dcb58-6c2c-46ef-abe4-81631d1a5f66", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "External Vibe", + "feature-type": "Vibrate", + "id": "d8b571fd-614e-4d33-8595-b9fbc81b96bd", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "Finger motion", + "feature-type": "Rotate", + "id": "eb6a2d21-93e0-4a08-9674-36fa2d299651", + "output": { + "Rotate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "6548133f-118f-419d-8900-660fde26b42f", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "8f93dd90-1788-4d2c-8b8f-9a339be12c0e", + "identifier": [ + "EI-FW3" + ], + "name": "Lovense Flexer" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "de8d83b6-76b4-4851-b53d-616d3527040c", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "2ea51cd8-b173-408c-bfef-f6508c5b9087", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "710384a5-a7dd-43f1-b55c-147256dc636a", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "9c72451e-1df7-410a-b4b6-e133f3bd9219", + "identifier": [ + "N" + ], + "name": "Lovense Gemini" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "93fa269e-ba3b-4c09-85d0-43385b49ee79", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "feature-type": "Oscillate", + "id": "475bde3a-4aae-4e84-87be-4df3a634da26", + "output": { + "Oscillate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "104da492-67f1-46fc-b412-b98871ebb518", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "b57dfb65-260d-49b2-bff0-659e38947186", + "identifier": [ + "EA" + ], + "name": "Lovense Gravity" + }, + { + "id": "abe8f908-3d93-4ba3-8bb1-3623fcd04202", + "identifier": [ + "Q" + ], + "name": "Lovense Tenera" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "0627be5e-8553-4f20-b4cf-15f5e1896e5f", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "feature-type": "RotateWithDirection", + "id": "360d81e7-5126-4dbb-b72d-7bb60eb67400", + "output": { + "RotateWithDirection": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "50b9b31f-c2a8-459a-81fd-c54604f5184e", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "bbfd764c-b419-4c13-aeb0-e753a86318ed", + "identifier": [ + "EL" + ], + "name": "Lovense Ridge" + }, + { + "features": [ + { + "description": "Tip Vibe", + "feature-type": "Vibrate", + "id": "414e5c3e-e52a-4064-b367-893bc0b1fb95", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "Internal Vibe", + "feature-type": "Vibrate", + "id": "be8d8608-d3aa-4fc5-ac5c-8df429f9e63c", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "External Vibe", + "feature-type": "Vibrate", + "id": "8bd37a96-7f7a-450f-aa4b-ffe8aa398d1e", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "ad93f903-a354-40ae-b87e-f8390606a964", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "5454d487-ed23-4067-80e2-9e2f0c01fabf", + "identifier": [ + "U" + ], + "name": "Lovense Lapis" + }, + { + "id": "73fcd02b-fa45-4e11-a62a-598aec256fbd", + "identifier": [ + "SD" + ], + "name": "Lovense Vulse" + }, + { + "features": [ + { + "description": "Stroker Oscillation Speed", + "feature-type": "Oscillate", + "id": "5100187a-40c7-44a4-a0ce-368cc24429cd", + "output": { + "Oscillate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "e4193650-2d46-4e6e-8dd8-b1d8d9a1baff", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "c53de5c8-fc4a-421b-9332-271ec742a156", + "identifier": [ + "H" + ], + "name": "Lovense Solace" + }, + { + "features": [ + { + "description": "Stroker Position Based Movement", + "feature-type": "PositionWithDuration", + "id": "c4b2855d-5ecc-4010-8a8d-17fd3e51cc57", + "output": { + "Oscillate": { + "step-range": [ + 0, + 20 + ] + }, + "PositionWithDuration": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "0b1cba39-8bb7-4f87-9bed-c59f2284d702", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "ed5f76c6-84b9-4fee-891f-28f9f4fa3632", + "identifier": [ + "BA" + ], + "name": "Lovense Solace Pro" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "3f7a25a5-df21-42ca-bf9f-d1c52df1f37e", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "14bd7637-13ed-49ba-9eb9-9c8ba9abec20", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "d3b1219a-aafe-4257-9d5d-3979b5da3c9a", + "name": "Lovense Device" + } + }, + "lovense-connect-service": { + "communication": [ + { + "lovense-connect-service": { + "exists": true + } + } + ], + "configurations": [ + { + "features": [ + { + "description": "Vibrator", + "feature-type": "Vibrate", + "id": "cd1a70b7-d716-41a9-b839-24e0229c25d2", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "Air Pump", + "feature-type": "Constrict", + "id": "e74ae364-c17a-41c4-accf-0e4a4ee94e04", + "output": { + "Constrict": { + "step-range": [ + 0, + 3 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "a2d19eee-211e-4771-b7e1-cfba3e6bb55f", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "c82d6326-c683-496b-b54a-c07cb03434f5", + "identifier": [ + "Max" + ], + "name": "Lovense Max" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "26f7aaa6-4312-487d-aabb-b43e4c87b5c2", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "5410094f-eff4-4b41-bfa2-b4cece3b9101", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "9b31822c-7449-4a3d-bd4d-6cced8440126", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "847c87fa-14a6-416c-95a8-d5b558c92cc0", + "identifier": [ + "Edge" + ], + "name": "Lovense Edge" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "1bfa1705-0193-4393-82f7-1c458e4885b3", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "feature-type": "RotateWithDirection", + "id": "af885c72-ce2b-47d5-87be-3847f24d18a5", + "output": { + "RotateWithDirection": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "1fb626ec-7006-46f5-97b1-db3cc0bc5bb8", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "15dcfcf0-a9c9-4ff4-90c0-37007e7c4809", + "identifier": [ + "Nora" + ], + "name": "Lovense Nora" + }, + { + "id": "68611264-45fb-49ab-9d1a-6a2000fd4b8a", + "identifier": [ + "Ambi" + ], + "name": "Lovense Ambi" + }, + { + "id": "c5063766-bc9c-422c-91e4-18873bc77352", + "identifier": [ + "Lush" + ], + "name": "Lovense Lush" + }, + { + "id": "8cc0f440-8a81-4ae9-951d-050777cb1f33", + "identifier": [ + "Hush" + ], + "name": "Lovense Hush" + }, + { + "id": "0e4f7cc1-5bd6-4f81-8bfc-7da23b0ff483", + "identifier": [ + "Domi" + ], + "name": "Lovense Domi" + }, + { + "id": "0951047c-2ac3-43ea-a24e-2d17174809d0", + "identifier": [ + "Osci" + ], + "name": "Lovense Osci" + }, + { + "id": "93907f90-05d4-4afe-a160-28973069927c", + "identifier": [ + "Mission" + ], + "name": "Lovense Mission" + }, + { + "id": "915d15fb-c47d-494c-af43-b9820e9bd33f", + "identifier": [ + "Ferri" + ], + "name": "Lovense Ferri" + }, + { + "id": "cea4f8b8-43e4-4a73-bab7-179aa2332f85", + "identifier": [ + "Diamo" + ], + "name": "Lovense Diamo" + }, + { + "id": "7194fd0d-e084-4c45-9d49-648b152fe9ba", + "identifier": [ + "ToyS" + ], + "name": "Loveai Dolp" + }, + { + "features": [ + { + "description": "Fucking Machine Oscillation Speed", + "feature-type": "Oscillate", + "id": "0ab80cc0-7a82-4cb6-ba4f-0f18ddb2911f", + "output": { + "Oscillate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "971bd4aa-d6ac-4449-bd1a-862b29ae705e", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "9b52eca4-0e49-426e-a543-2ef735cd803a", + "identifier": [ + "XMachine" + ], + "name": "Lovense Sex Machine" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "59ec4d12-2c6d-4cd9-83b0-8ff1609563d4", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "4e4eead7-9959-4fe2-b629-a535f6bc7ca4", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "b771d1b8-5a68-4a75-8ff2-868380d18fe7", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "d51f41a8-3731-4b06-b320-6cfa2d518940", + "identifier": [ + "Dolce" + ], + "name": "Lovense Dolce" + }, + { + "id": "24a65c79-7a5e-4ab4-82cf-684f54292f89", + "identifier": [ + "Gush" + ], + "name": "Lovense Gush" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "a6ec2f52-780b-4d87-a809-0bdc2ccadcc1", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "c06723f1-f816-442b-8193-a5c407fecabe", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "80d1e022-85a6-46ad-bbe9-1b8085b1e336", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "33a001d2-2879-47f8-89d3-422d262deb53", + "identifier": [ + "Hyphy" + ], + "name": "Lovense Hyphy" + }, + { + "id": "ea035198-1eb8-4fa8-b234-50b9a91c8925", + "identifier": [ + "Calor" + ], + "name": "Lovense Calor" + }, + { + "features": [ + { + "description": "Both Vibes", + "feature-type": "Vibrate", + "id": "bd656e88-abae-49e4-ab45-f75df187bb4a", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "Finger motion", + "feature-type": "Rotate", + "id": "663dedb4-05a1-4391-a666-e59c38ead69c", + "output": { + "Rotate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "735c2164-4fd5-4e82-835d-23251e487d68", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "10995415-c030-4fd1-b5c0-af42d850ff61", + "identifier": [ + "Flexer" + ], + "name": "Lovense Flexer" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "2c186df2-4e8c-491d-b247-fcbaeb763fee", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "81657dab-5fbf-40b4-a6f8-cfecb7906757", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "fe19ad5c-5acb-4ee9-8a09-f6edca06f471", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "7da2f986-8960-4c2c-acf1-d8924878adc0", + "identifier": [ + "Gemini" + ], + "name": "Lovense Gemini" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "fba538eb-784e-4ca7-ad81-e52f3cd0d3f2", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "feature-type": "Oscillate", + "id": "61bd6559-c32d-4c3b-9686-988fa3cd4abf", + "output": { + "Oscillate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "7a794236-85e6-4b13-97c6-d17d1f091f0a", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "75a502f3-6b8f-4d70-97b5-86fff5d45260", + "identifier": [ + "Gravity" + ], + "name": "Lovense Gravity" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "4865ff41-25cd-42a9-b93d-00a7c1e881d5", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "feature-type": "RotateWithDirection", + "id": "d49001e8-5f6b-43ac-9cc7-7e68fab7c323", + "output": { + "RotateWithDirection": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "7fcb01eb-4241-42c1-9799-fdfa190b7edd", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "fcd47b93-ac57-4167-93a5-fb12f223ff28", + "identifier": [ + "Ridge" + ], + "name": "Lovense Ridge" + }, + { + "features": [ + { + "description": "Tip Vibe", + "feature-type": "Vibrate", + "id": "f435ee40-ae30-4fba-9f80-c1143f601993", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "Internal Vibe", + "feature-type": "Vibrate", + "id": "9504ed2b-1baf-4759-922b-a5dcfc16aeb7", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "External Vibe", + "feature-type": "Vibrate", + "id": "1cce6f8f-0301-4e4e-a820-1ed85e11e25d", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "322170f9-b493-4233-9336-e6f7f267450c", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "d99b1620-25cd-40fe-af02-a51d08df33ca", + "identifier": [ + "Lapis" + ], + "name": "Lovense Lapis" + }, + { + "id": "f2c1faec-7d64-48be-9c91-2649c74540c7", + "identifier": [ + "Vulse" + ], + "name": "Lovense Vulse" + }, + { + "features": [ + { + "description": "Stroker Oscillation Speed", + "feature-type": "Oscillate", + "id": "b8b240c0-182d-4889-9200-47c16399c57d", + "output": { + "Oscillate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "37c03e71-1701-4b5a-9697-d62d2dc56e4b", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "665925e2-e895-443f-953a-cae3f371c138", + "identifier": [ + "Solace" + ], + "name": "Lovense Solace" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "387829be-bbd3-4d71-98f2-738dbb685600", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "7202da93-c25d-460a-a863-8d4d38f41fdf", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "caceda00-463b-4981-949f-b7e6b06ed02b", + "name": "Lovense Connect Service Device" + } + }, + "lovenuts": { + "communication": [ + { + "btle": { + "names": [ + "Love_Nuts" + ], + "services": { + "0000fff0-0000-1000-8000-00805f9b34fb": { + "tx": "0000fff1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "45793bae-a3d5-4d76-9f20-f907e82b18df", + "output": { + "Vibrate": { + "step-range": [ + 0, + 15 + ] + } + } + } + ], + "id": "3d5a9edb-e393-4603-8fb9-e038d3c4c0f3", + "name": "Love Nut" + } + }, + "luvmazer": { + "communication": [ + { + "btle": { + "names": [ + "TKLM-W001-BT" + ], + "services": { + "0000ffa0-0000-1000-8000-00805f9b34fb": { + "tx": "0000ffa1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "af257986-e34f-47f9-a69e-7a78afd43d31", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Rotate", + "id": "8f021f8a-a07e-4934-af3b-fa3bafd2a747", + "output": { + "Rotate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "c6d24bef-8263-4e3b-898d-7aeb7e58cc11", + "name": "Luvmazer Finger Magic" + } + }, + "magic-motion-1": { + "communication": [ + { + "btle": { + "names": [ + "Smart Mini Vibe*", + "Flamingo", + "Flamingo T", + "Smart Bean", + "Smart Bean3", + "Magic Cell", + "Magic Wand", + "Fugu", + "Fugu2", + "Gballs2", + "GBalls3", + "FM-LILAC-101", + "Xone", + "CBT002" + ], + "services": { + "0000180f-0000-1000-8000-00805f9b34fb": { + "rxblebattery": "00002a19-0000-1000-8000-00805f9b34fb" + }, + "78667579-7b48-43db-b8c5-7928a6b0a335": { + "tx": "78667579-a914-49a4-8333-aa3c0cd8fedc" + } + } + } + } + ], + "configurations": [ + { + "id": "ef285932-0c7e-4edb-bc81-ce0c59f41c4a", + "identifier": [ + "Smart Bean" + ], + "name": "MagicMotion Smart Bean" + }, + { + "id": "5adced22-1742-4e1e-bf75-225275a500b0", + "identifier": [ + "Smart Bean3" + ], + "name": "FitCute Kegel Rejuve" + }, + { + "id": "0a69e7c1-51ca-49c1-91a3-c58debba037e", + "identifier": [ + "Smart Mini Vibe" + ], + "name": "MagicMotion Smart Mini Vibe" + }, + { + "id": "c006d72e-5fee-4643-b324-35fa6d56e176", + "identifier": [ + "Smart Mini Vibe3" + ], + "name": "MagicMotion Vini" + }, + { + "id": "efa69977-2c7b-4c0f-b9e6-ffa4d9c36630", + "identifier": [ + "Flamingo", + "Flamingo T" + ], + "name": "MagicMotion Flamingo" + }, + { + "id": "7239ca39-f8fd-4727-940b-04483f08cfb9", + "identifier": [ + "Magic Bean" + ], + "name": "MagicMotion Kegel" + }, + { + "id": "5596e91a-e336-4f26-b6da-19858be7ab67", + "identifier": [ + "Magic Cell" + ], + "name": "MagicMotion Dante/Candy/Rise" + }, + { + "id": "91c15cc1-3021-44fb-a64d-3231c007705a", + "identifier": [ + "Magic Wand" + ], + "name": "MagicMotion Wand" + }, + { + "id": "3eefb122-6f5d-4e06-99c5-a89164b1d219", + "identifier": [ + "Magic Fugu", + "Fugu", + "Fugu2" + ], + "name": "MagicMotion Fugu" + }, + { + "id": "a9c33895-4f0a-4ecc-a849-2e632dbc8f29", + "identifier": [ + "Gballs2" + ], + "name": "G Vibe Gballs 2" + }, + { + "id": "c802d1e6-968a-4451-86e0-248e85e3d50d", + "identifier": [ + "GBalls3" + ], + "name": "G Vibe Gballs 3" + }, + { + "id": "ef73c48c-8f6a-44e2-940a-0dd45f69cfb2", + "identifier": [ + "FM-LILAC-101" + ], + "name": "Femometer Lilac" + }, + { + "features": [ + { + "feature-type": "Oscillate", + "id": "ccd72f20-d37a-4e05-bad3-122c5da80b37", + "output": { + "Oscillate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "98a2e5c4-c4de-4ac5-a9db-b3e24a24424a", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "b24d166f-b6e0-4c9b-a056-8296564b19a8", + "identifier": [ + "Xone" + ], + "name": "MagicMotion Xone" + }, + { + "id": "b6dc5c46-0919-4a45-900e-f83afae8b942", + "identifier": [ + "CBT002" + ], + "name": "FunTown Caleo" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "42173db5-95ac-49b5-8a5a-73a63d91fcec", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "bcaf7da8-2e98-47e3-b22c-2204daf40a27", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "2525206c-8bdc-4803-9636-79576f3e692f", + "name": "Magic Motion V1 Device" + } + }, + "magic-motion-2": { + "communication": [ + { + "btle": { + "names": [ + "Eidolon", + "Lipstick", + "Sword", + "Curve", + "Solstice X", + "funwand", + "CBT001" + ], + "services": { + "0000180f-0000-1000-8000-00805f9b34fb": { + "rxblebattery": "00002a19-0000-1000-8000-00805f9b34fb" + }, + "78667579-7b48-43db-b8c5-7928a6b0a335": { + "tx": "78667579-a914-49a4-8333-aa3c0cd8fedc" + } + } + } + } + ], + "configurations": [ + { + "id": "9ed09e5a-945d-4bb0-9813-3e07a8fd7baf", + "identifier": [ + "Lipstick" + ], + "name": "MagicMotion Awaken" + }, + { + "id": "5274feff-b0fa-4c37-9990-8861864fec59", + "identifier": [ + "Sword" + ], + "name": "MagicMotion Equinox" + }, + { + "id": "b639a627-60fc-4eff-afeb-91ccdf2e616b", + "identifier": [ + "Curve" + ], + "name": "MagicMotion Solstice" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "6b96f9d2-87bc-4596-810d-9a96cbd1a2fa", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "86090f46-7c4c-46fe-883f-d3765f477bac", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "6baefd41-de6d-4c60-aedb-0a9b55f34875", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "1093a17d-9596-49b7-945f-c44610244932", + "identifier": [ + "Eidolon" + ], + "name": "MagicMotion Eidolon" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "a245e29e-3f63-4c68-a5c2-c07c7c9970a4", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "70593a3b-2b16-4258-badb-9697074bf10b", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "f966012c-6b68-4dc3-b4a4-16d34fdc30c7", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "adfc6c8c-b7e8-4c0c-9fdc-e7c2bd3b4552", + "identifier": [ + "Solstice X" + ], + "name": "MagicMotion Solstice X" + }, + { + "id": "334f32f6-309e-4e79-a3de-b62aff0f6438", + "identifier": [ + "funwand" + ], + "name": "MagicMotion Zenith" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "81515d54-be1d-42a1-bc7d-5b4e9c20db37", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Oscillate", + "id": "d514fb91-2261-4c5c-a59e-9799fce40d17", + "output": { + "Oscillate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "123954de-a9f1-427a-823a-9b9173ad8856", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "d872f184-a2a4-4869-9506-d34975fa34c3", + "identifier": [ + "CBT001" + ], + "name": "FunTown Jive" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "4fe8ab2c-2811-416c-967c-fce58cb8a2f3", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "014cdffe-d3d5-4bba-acf4-f26e809b45ec", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "33902551-eb44-406b-bc9a-7f9f981a972a", + "name": "Magic Motion V2 Device" + } + }, + "magic-motion-3": { + "communication": [ + { + "btle": { + "names": [ + "Krush" + ], + "services": { + "0000180f-0000-1000-8000-00805f9b34fb": { + "rxblebattery": "00002a19-0000-1000-8000-00805f9b34fb" + }, + "78667579-7b48-43db-b8c5-7928a6b0a335": { + "tx": "78667579-a914-49a4-8333-aa3c0cd8fedc" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "af104b4d-73c3-4d89-95d6-ea7c4e21a3df", + "output": { + "Vibrate": { + "step-range": [ + 0, + 77 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "72bc2f2f-7f67-4636-bc5c-42ac4b55cb59", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "f954c774-3e08-4569-800f-94e454ccd3ca", + "name": "LoveLife Krush" + } + }, + "magic-motion-4": { + "communication": [ + { + "btle": { + "names": [ + "funone", + "Magic Sundi", + "Kegel Coach", + "Magic Lotos", + "nyx", + "umi", + "funkegel", + "bobi2" + ], + "services": { + "0000180f-0000-1000-8000-00805f9b34fb": { + "rxblebattery": "00002a19-0000-1000-8000-00805f9b34fb" + }, + "78667579-7b48-43db-b8c5-7928a6b0a335": { + "tx": "78667579-a914-49a4-8333-aa3c0cd8fedc" + } + } + } + } + ], + "configurations": [ + { + "id": "ae515557-67e1-4527-bd0b-762a2fb47d9b", + "identifier": [ + "funone" + ], + "name": "MagicMotion Bunny" + }, + { + "id": "0e5c564b-02cf-4665-b8e6-d938b8b8d749", + "identifier": [ + "Magic Sundi" + ], + "name": "MagicMotion Sundae" + }, + { + "id": "2ecd285e-9109-403c-b38f-3784629bd7de", + "identifier": [ + "Kegel Coach" + ], + "name": "MagicMotion Kegel Coach" + }, + { + "id": "a66cd42b-c3b3-4b00-bbb2-117961a06bcd", + "identifier": [ + "Magic Lotos" + ], + "name": "MagicMotion Lotos" + }, + { + "id": "69c95fd5-a9c2-4f7d-9fdc-a25f514ba290", + "identifier": [ + "nyx" + ], + "name": "MagicMotion Nyx" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "008a3d35-9b61-4bc2-9554-c3c742f03e12", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "b24eee4d-b3c2-4ce4-8f54-433e3d2a08f5", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "fdc5dc60-ece5-4f81-801c-076b1e1bad57", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "69a69c1d-1e37-49ed-b1a4-07da72939171", + "identifier": [ + "umi" + ], + "name": "MagicMotion Umi" + }, + { + "id": "c22dfa34-5b4d-4c61-a972-fee67b1f60d8", + "identifier": [ + "funkegel" + ], + "name": "MagicMotion Crystal" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "09d1b6fc-834d-4579-9bc7-79813f20d33f", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "04438678-4c82-48e1-a4fa-8dd916ee5469", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "b2b3dedf-5f7a-4069-935f-f210fdf5cafc", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "318ca3d4-0779-47e8-9580-fc3efe1a0556", + "identifier": [ + "bobi2" + ], + "name": "MagicMotion Bobi" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "c8ed6a4c-2dff-4be9-b1c5-b91bfd238bda", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "8ba2798a-4717-4a39-ae5c-f445eb8f4448", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "e53d8751-5993-410c-82d7-edca26dd4c65", + "name": "Magic Motion V4 Device" + } + }, + "mannuo": { + "communication": [ + { + "btle": { + "names": [ + "Sex toys", + "Sex Toys", + "LXCDVP", + "MANO PRODUCT" + ], + "services": { + "0000fff0-0000-1000-8000-00805f9b34fb": { + "rx": "0000fff4-0000-1000-8000-00805f9b34fb", + "tx": "0000fff1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "36daf552-3c59-44b8-b00e-ff1e0e799fc6", + "output": { + "Vibrate": { + "step-range": [ + 0, + 3 + ] + } + } + } + ], + "id": "6fe6ed71-8869-4a38-bfc1-a7adc112e14e", + "name": "ManNuo Device" + } + }, + "maxpro": { + "communication": [ + { + "btle": { + "names": [ + "M2" + ], + "services": { + "6e400001-b5a3-f393-e0a9-e50e24dcca9e": { + "tx": "6e400002-b5a3-f393-e0a9-e50e24dcca9e" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "f3c0255d-2734-4f60-95a7-2e9fc04e399c", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "1f903059-93fd-4160-89a8-cc7a2001d0fa", + "name": "MaxPro 2" + } + }, + "meese": { + "communication": [ + { + "btle": { + "names": [ + "Meese-V389", + "Meese-cd" + ], + "services": { + "0000ffe0-0000-1000-8000-00805f9b34fb": { + "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "8fe479fd-8343-49a2-959b-47f4cd7104ac", + "identifier": [ + "Meese-V389" + ], + "name": "Meese Tera" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "9bdae29d-46fc-4435-8a63-71927e5e1ada", + "output": { + "Vibrate": { + "step-range": [ + 0, + 10 + ] + } + } + } + ], + "id": "db5ab134-ecc8-4f50-9339-20908f8894e6", + "identifier": [ + "Meese-cd" + ], + "name": "Meese Modo" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "86e146ce-8aca-4df1-bfca-67dcf4d241c4", + "output": { + "Vibrate": { + "step-range": [ + 0, + 10 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "d2a0c869-d3c7-4ad7-b1fb-a8c914584abf", + "output": { + "Vibrate": { + "step-range": [ + 0, + 3 + ] + } + } + } + ], + "id": "6ee04bd7-2f57-4ada-b622-b9bb210ff0c1", + "name": "Meese Device" + } + }, + "metaxsire": { + "communication": [ + { + "btle": { + "names": [ + "Rex", + "Cali", + "LY165A01", + "Olis", + "LY213A01", + "LY199B01", + "LY234A01", + "LY271A01", + "LY270A01" + ], + "services": { + "0000ffe0-0000-1000-8000-00805f9b34fb": { + "rx": "0000ffe2-0000-1000-8000-00805f9b34fb", + "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "447c8bda-bafc-472a-9333-8f809bbc48bb", + "identifier": [ + "Rex" + ], + "name": "metaXsire Rex" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "d3e17d91-94d8-449d-b049-91bd0ec3cf71", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Constrict", + "id": "6aceca29-6833-4f61-b5af-1005bb50bdf9", + "output": { + "Constrict": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "e4bb4468-1de1-4f37-a348-5c7177923603", + "identifier": [ + "Cali", + "LY165A01" + ], + "name": "metaXsire Cali" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "2e6d4a73-7847-4a5b-a03c-cdd6f07c39c9", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "c1530d49-07b0-432b-8c08-08e1ef4d2842", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Rotate", + "id": "cbc1187c-2400-4e9b-9fc0-a03744bd7295", + "output": { + "Rotate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "9e874901-c5d7-49d2-910d-3849ab5ff96c", + "identifier": [ + "Olis" + ], + "name": "metaXsire Olis" + }, + { + "features": [ + { + "feature-type": "Oscillate", + "id": "641d8a6a-b068-4089-9632-c81ab872677d", + "output": { + "Oscillate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "15dcc27e-ab6d-407e-8e1a-4b51e445fa5d", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "941a41b2-78d2-45a6-b730-17a8ff8c75e0", + "identifier": [ + "LY213A01" + ], + "name": "metaXsire BuCUE" + }, + { + "id": "0f8e2cac-428a-430c-a9d8-8889ed608c24", + "identifier": [ + "LY199B01" + ], + "name": "Cooxer Bullet Vibe" + }, + { + "id": "de51460a-4c65-4173-8172-8dc7eaccc3a1", + "identifier": [ + "LY234A01" + ], + "name": "metaXsire Tadpole" + }, + { + "id": "5d061d81-98cd-4271-b896-68394a21e97a", + "identifier": [ + "LY271A01" + ], + "name": "metaXsire Upton" + }, + { + "id": "97458f06-7a6f-4f8a-bb7a-93dd6ab53157", + "identifier": [ + "LY270A01" + ], + "name": "metaXsire Una" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "74825924-5e2a-4dd6-a91a-10a24be40c09", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "f595862c-fa49-460c-9667-87f0eac24a6c", + "name": "metaXsire Device" + } + }, + "metaxsire-v2": { + "communication": [ + { + "btle": { + "names": [ + "LY272A01", + "LB-W01", + "HH010" + ], + "services": { + "0000bae0-0000-1000-8000-00805f9b34fb": { + "tx": "0000bae1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "features": [ + { + "feature-type": "Vibrate", + "id": "59cacf4b-ef09-42ad-b3d6-459bc195da26", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + } + ], + "id": "2a4a4daa-5740-425b-b1a4-72b73f746fdf", + "identifier": [ + "LB-W01" + ], + "name": "Libo Miao" + }, + { + "features": [ + { + "feature-type": "Oscillate", + "id": "968f7306-6997-4b76-a40f-acbb431d9582", + "output": { + "Oscillate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "018009d0-b5bf-4f97-a13d-909d0e74fabc", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + } + ], + "id": "0e1f9fe7-22d9-4afb-9fe5-192b8e5508c3", + "identifier": [ + "HH010" + ], + "name": "metaXsire HH010" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "4961e88c-5c2e-4701-95ee-16d58538b65e", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "feature-type": "Oscillate", + "id": "a3cd125d-ac6c-426d-b45a-fe3c7ae1e1d2", + "output": { + "Oscillate": { + "step-range": [ + 0, + 20 + ] + } + } + } + ], + "id": "ce9d4fe0-6614-493d-ac77-02ec5d42947d", + "name": "metaXsire Nolan" + } + }, + "metaxsire-v3": { + "communication": [ + { + "btle": { + "names": [ + "TAY001", + "TAY006", + "TAY009", + "TA-S001A" + ], + "services": { + "0000fff0-0000-1000-8000-00805f9b34fb": { + "tx": "0000fe02-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "c7615c1d-d53f-4d24-82e1-ce08c301da66", + "identifier": [ + "TAY001" + ], + "name": "metaXsire Tay 1" + }, + { + "id": "ddfe0ac7-f275-4e08-b16b-a5cd579e9a9e", + "identifier": [ + "TAY009" + ], + "name": "metaXsire Tay 9" + }, + { + "id": "edfecee1-3b6f-4501-a9d9-717b2bd515a2", + "identifier": [ + "TAY006" + ], + "name": "metaXsire Tay 6" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "11c78de9-800a-4444-9647-0ed33181e63c", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + }, + { + "feature-type": "Oscillate", + "id": "47646747-4dea-47ba-80b2-407e2a276ae2", + "output": { + "Oscillate": { + "step-range": [ + 0, + 20 + ] + } + } + } + ], + "id": "ae1e373f-1a35-476b-8da8-6017dcb7e0de", + "identifier": [ + "TA-S001A" + ], + "name": "metaXsire Zeus" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "074a15d1-2efc-4cd8-8f1f-0f32f1468024", + "output": { + "Vibrate": { + "step-range": [ + 0, + 20 + ] + } + } + } + ], + "id": "2e8ff651-b10d-4686-89b5-b8197e80e159", + "name": "metaXsire Tay" + } + }, + "metaxsire-v4": { + "communication": [ + { + "btle": { + "names": [ + "CFG1 vibrator", + "HJ2024N01" + ], + "services": { + "0000cfa2-0000-1000-8000-00805f9b34fb": { + "tx": "0000cf21-0000-1000-8000-00805f9b34fb" + }, + "0000dba2-0000-1000-8000-00805f9b34fb": { + "tx": "0000db21-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "679cdc1b-9236-4ed1-a3c4-c33b748a8cde", + "identifier": [ + "HJ2024N01" + ], + "name": "VVD Vkini" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "0c9c5a7d-8d28-4003-b1d4-8de5c73c8fe4", + "output": { + "Vibrate": { + "step-range": [ + 0, + 99 + ] + } + } + } + ], + "id": "e69dc695-695d-485b-be16-59161505fd6d", + "name": "metaXsire G1 Vibrator" + } + }, + "metaxsire-v5": { + "communication": [ + { + "btle": { + "names": [ + "CBW02" + ], + "services": { + "0000ffcb-0000-1000-8000-00805f9b34fb": { + "tx": "0000cb02-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "140be13c-4cb3-407f-9597-e03f046f1c1a", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "783bc287-528c-4c58-a7ec-47a49304309e", + "name": "Sexverse Heart" + } + }, + "mizzzee": { + "communication": [ + { + "btle": { + "names": [ + "NFY008" + ], + "services": { + "0000eea0-0000-1000-8000-00805f9b34fb": { + "tx": "0000eea1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "be144c33-8f81-42b7-b43b-1def688feedf", + "output": { + "Vibrate": { + "step-range": [ + 0, + 68 + ] + } + } + } + ], + "id": "d8aa061f-f60d-4e0c-a638-cbbae4493c3b", + "name": "Mizz Zee Device" + } + }, + "mizzzee-v2": { + "communication": [ + { + "btle": { + "names": [ + "XHT" + ], + "services": { + "0000eea0-0000-1000-8000-00805f9b34fb": { + "tx": "0000ee01-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "e120abaf-dd55-4b8a-ba17-ea86155a819c", + "output": { + "Vibrate": { + "step-range": [ + 0, + 68 + ] + } + } + } + ], + "id": "9fc65537-e8ae-4e54-bfcb-adebbe39d7e1", + "name": "Mizz Zee Device" + } + }, + "mizzzee-v3": { + "communication": [ + { + "btle": { + "names": [ + "XHTKJ" + ], + "services": { + "0000ff10-0000-1000-8000-00805f9b34fb": { + "tx": "0000ff12-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "aa417fd0-0ab1-409f-b7a3-05f6c3ede623", + "output": { + "Vibrate": { + "step-range": [ + 0, + 1000 + ] + } + } + } + ], + "id": "4d54f81c-e31f-469a-a17a-ea1d4058a037", + "name": "Mizz Zee Device" + } + }, + "monsterpub": { + "communication": [ + { + "btle": { + "names": [ + "MonsterPub", + "MonsterHub", + "TracyDog" + ], + "services": { + "00006000-0000-1000-8000-00805f9b34fb": { + "generic0": "0000600a-0000-1000-8000-00805f9b34fb", + "tx": "00006001-0000-1000-8000-00805f9b34fb", + "txmode": "00006002-0000-1000-8000-00805f9b34fb", + "txvibrate": "00006003-0000-1000-8000-00805f9b34fb" + }, + "00006010-0000-1000-8000-00805f9b34fb": { + "rxblemodel": "00006014-0000-1000-8000-00805f9b34fb" + }, + "00008000-0000-1000-8000-00805f9b34fb": { + "rx": "00008001-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "features": [ + { + "feature-type": "Vibrate", + "id": "9cf2d977-c1c3-46c0-bb88-c71a3c65f7ae", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "ba941f5c-0946-443c-a6eb-5a0cff38a3b8", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "01eb3034-194f-4c91-88e4-8095bb0f4ff4", + "identifier": [ + "MP2_JK_N_P1" + ], + "name": "Sistalk MonsterPub 2 Doctor Whale" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "d8d639f1-c821-46a6-9eb1-eb1eda9289b5", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "d3c1b259-b884-4a63-ba75-b8d9341398be", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "bdf1fea2-374d-4340-9057-6ee76595cb83", + "identifier": [ + "MP_MW_TL_P2" + ], + "name": "Sistalk MonsterPub Magic Kiss" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "f9f2b6ae-d54d-4d78-a535-3879d96a7fd6", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "8186c4b9-40df-422d-8e70-f0babf32f82b", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "5a8c6ddf-15b2-4d7b-bdcf-38c7c49586bb", + "identifier": [ + "MP2_QC_TL_P1" + ], + "name": "Sistalk MonsterPub 2 Mister Devil" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "51923606-6704-48ca-b083-01ceacf897a1", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "553a765a-e91f-4187-85cb-b2be8311944b", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "fb558c71-beb7-43ec-8b78-2ca975aa7d7b", + "identifier": [ + "MP_BABY_QC_N_P4" + ], + "name": "Sistalk MonsterPub Baby Youth Health" + }, + { + "id": "19e019be-dd3f-4822-8243-288690cae235", + "identifier": [ + "MP_MXY_N_P1" + ], + "name": "Sistalk MonsterPub KiniCat" + }, + { + "id": "640958c5-0fc0-4390-bdda-959c1686084d", + "identifier": [ + "MP1N_QC_TL_P2" + ], + "name": "Sistalk MonsterPub BeatHeart" + }, + { + "id": "f2049034-1515-4008-8cc3-2b6914080a5c", + "identifier": [ + "TDG_LIP_PT2" + ], + "name": "Tracy's Dog Surreal" + }, + { + "id": "1a39cdde-63ba-407a-8307-27b775c3f365", + "identifier": [ + "MP1P_QC_TL_P6" + ], + "name": "Sistalk MonsterPub 1P Mister Devil" + }, + { + "id": "6d613fc2-76b2-4007-af78-e91bfe20e659", + "identifier": [ + "MPMB_QC_TL_P2" + ], + "name": "Sistalk MonsterPub Sweet" + }, + { + "id": "719a2ee0-bf1e-41bc-84c9-6d369b5646dd", + "identifier": [ + "MPAV_QC_TL_P1" + ], + "name": "Sistalk MonsterPub Amazing" + }, + { + "id": "8ee7eb14-bc8b-4f66-ac29-8586fa3d1f04", + "identifier": [ + "MH_TOR_TL_P5" + ], + "name": "Sistalk MonsterHub Tornado" + }, + { + "features": [ + { + "feature-type": "Oscillate", + "id": "6a9d1640-2b72-42f1-8ad1-1e1a97394f82", + "output": { + "Oscillate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "5462d583-6a92-4288-b743-46957be25efb", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "da7e6371-b4cd-475a-9a41-501f4bb06ef3", + "identifier": [ + "MP_SUCKBANG_P5" + ], + "name": "Sistalk MonsterPub Pop" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "3fbc11b2-d07c-4793-a90d-364d62631aca", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "164c2dca-0f5e-4c06-8698-4e65b027a25e", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "8bea0dcd-400c-41a0-819e-bca090caf186", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "8d9c60c2-eb9a-4fd0-8917-78f7d94320b3", + "identifier": [ + "TDG_CRAYBIT_PT" + ], + "name": "Tracy's Dog Craybit Pro" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "79df96bb-25af-422e-a066-c7c3f301a843", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "87e76bfc-ecba-4cda-a574-4a92889a6bc3", + "name": "Sistalk MonsterPub Device" + } + }, + "motorbunny": { + "communication": [ + { + "btle": { + "names": [ + "MB Controller", + "MB LINK 201" + ], + "services": { + "0000fff0-0000-1000-8000-00805f9b34fb": { + "tx": "0000fff6-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "97362be6-5601-4d08-812a-4eb1ffa29980", + "identifier": [ + "MB Controller" + ], + "name": "Motorbunny Classic" + }, + { + "id": "6de31e21-d76c-4d9a-9220-afa36f29d128", + "identifier": [ + "MB LINK 201" + ], + "name": "Motorbunny Buck" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "cb44a214-4c5c-4a04-8b1a-0d91a73a7a3a", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "RotateWithDirection", + "id": "683b450d-bb1a-4fca-b61a-83f8b56086fa", + "output": { + "RotateWithDirection": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "21cb973e-c404-44de-99c8-9cf4bc5538a6", + "name": "Motorbunny Device" + } + }, + "muse": { + "communication": [ + { + "btle": { + "names": [ + "WB-ZDB-WST", + "WB-TDD" + ], + "services": { + "0000aaa0-0000-1000-8000-00805f9b34fb": { + "tx": "0000aaa1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "48b17c67-fb1f-40c7-8dcb-b67dfb041afc", + "identifier": [ + "WB-ZDB-WST" + ], + "name": "Dream Lover Archer 2" + }, + { + "id": "dd40210e-1523-4d61-bdaf-3827635fb181", + "identifier": [ + "WB-TDD" + ], + "name": "Galaku Panty Vib" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "6dcc57e0-8a30-4e90-ba9e-4b8dd488d166", + "output": { + "Vibrate": { + "step-range": [ + 0, + 9 + ] + } + } + } + ], + "id": "94e9d8e0-94cc-42f5-b14d-c55cc91e2e68", + "name": "Muse Device" + } + }, + "mysteryvibe": { + "communication": [ + { + "btle": { + "names": [ + "MV Crescendo", + "MV Tenuto ", + "MV Poco " + ], + "services": { + "f0006900-110c-478b-b74b-6f403b364a9c": { + "txmode": "f0006901-110c-478b-b74b-6f403b364a9c", + "txvibrate": "f0006903-110c-478b-b74b-6f403b364a9c" + } + } + } + } + ], + "configurations": [ + { + "id": "09470af5-da2f-45f4-b540-da653c4c0b40", + "identifier": [ + "MV Crescendo" + ], + "name": "MysteryVibe Crescendo" + }, + { + "id": "1cb2c947-aa77-4aaa-83d4-f987ecb33953", + "identifier": [ + "MV Tenuto " + ], + "name": "MysteryVibe Tenuto" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "78d26150-7355-4633-bdc0-d2d58b2ea2aa", + "output": { + "Vibrate": { + "step-range": [ + 0, + 56 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "8f0c1cc0-b269-4eb6-a87f-34aeaee28906", + "output": { + "Vibrate": { + "step-range": [ + 0, + 56 + ] + } + } + } + ], + "id": "b72b5597-a708-4fe9-919a-99f1d38291ef", + "identifier": [ + "MV Poco " + ], + "name": "MysteryVibe Poco" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "40c417e0-8a0b-4017-a0b5-2b33df4f0acc", + "output": { + "Vibrate": { + "step-range": [ + 0, + 56 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "84057071-af0e-4156-9f82-f7afc794bcde", + "output": { + "Vibrate": { + "step-range": [ + 0, + 56 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "edaa4f3d-71c2-43b3-b9c3-b6a425b27200", + "output": { + "Vibrate": { + "step-range": [ + 0, + 56 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "b977c4f4-1585-49c4-9980-c2e8d329f713", + "output": { + "Vibrate": { + "step-range": [ + 0, + 56 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "ba9c09c7-1948-4b6f-823f-d9fd1380709c", + "output": { + "Vibrate": { + "step-range": [ + 0, + 56 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "5a0a0429-5fb6-4bcb-bb4c-5e14f4338677", + "output": { + "Vibrate": { + "step-range": [ + 0, + 56 + ] + } + } + } + ], + "id": "523391d5-1e0a-42f0-b669-5ad3f3e49902", + "name": "Mysteryvibe Device" + } + }, + "mysteryvibe-v2": { + "communication": [ + { + "btle": { + "names": [ + "6907 MV1", + "6908 MV1", + "6909 MV1", + "6909 MV2", + "6914 MV1", + "6915 MV1" + ], + "services": { + "f0006900-110c-478b-b74b-6f403b364a9c": { + "txmode": "f0006901-110c-478b-b74b-6f403b364a9c", + "txvibrate": "f0006903-110c-478b-b74b-6f403b364a9c" + } + } + } + } + ], + "configurations": [ + { + "id": "9254a628-04a2-4876-856e-182d8badc366", + "identifier": [ + "6907 MV1" + ], + "name": "MysteryVibe Tenuto Mini" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "723b512f-9160-4f5b-b50b-3fb9622dff1e", + "output": { + "Vibrate": { + "step-range": [ + 0, + 56 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "960f8105-2277-4b81-a529-dd050250df80", + "output": { + "Vibrate": { + "step-range": [ + 0, + 56 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "557828e8-e1cf-4f9a-9342-43bc9c34642c", + "output": { + "Vibrate": { + "step-range": [ + 0, + 56 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "f2f6b8f8-7ff7-4928-9385-af1f3c583209", + "output": { + "Vibrate": { + "step-range": [ + 0, + 56 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "a5a287fc-82de-432d-b42d-cc9ee89625ae", + "output": { + "Vibrate": { + "step-range": [ + 0, + 56 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "bbd27d45-3b13-4189-b7a8-ccaa07a405db", + "output": { + "Vibrate": { + "step-range": [ + 0, + 56 + ] + } + } + } + ], + "id": "317cc151-16f9-4ac7-aa69-63a3f0448895", + "identifier": [ + "6908 MV1" + ], + "name": "MysteryVibe Crescendo 2" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "88ddd1f2-6a0b-4fab-b548-5cd4edb55aae", + "output": { + "Vibrate": { + "step-range": [ + 0, + 56 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "e30a128b-3dcb-4f87-beef-8aca7f3b1512", + "output": { + "Vibrate": { + "step-range": [ + 0, + 56 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "3edf88eb-acb9-4852-9a71-3edda23f705d", + "output": { + "Vibrate": { + "step-range": [ + 0, + 56 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "1b3abe40-84d2-4237-830d-44c1927f35c3", + "output": { + "Vibrate": { + "step-range": [ + 0, + 56 + ] + } + } + } + ], + "id": "9a1bcb00-0294-46c2-ac97-0b3f8d50192a", + "identifier": [ + "6909 MV1", + "6909 MV2" + ], + "name": "MysteryVibe Tenuto 2" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "79f4df66-18a2-4fdb-a492-75e908bf978f", + "output": { + "Vibrate": { + "step-range": [ + 0, + 56 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "f149b9be-4616-4552-a0a9-c419cb764988", + "output": { + "Vibrate": { + "step-range": [ + 0, + 56 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "f3553da8-f386-43b4-8998-64b7696c53f4", + "output": { + "Vibrate": { + "step-range": [ + 0, + 56 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "4c1fb245-6f91-4613-895f-5f8cee00ab5b", + "output": { + "Vibrate": { + "step-range": [ + 0, + 56 + ] + } + } + } + ], + "id": "e9187e5a-1491-49db-ba4b-3b6f9fb55977", + "identifier": [ + "6914 MV1" + ], + "name": "MysteryVibe Legato" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "cf40ea50-cddc-40e2-8661-d5252ac29f77", + "output": { + "Vibrate": { + "step-range": [ + 0, + 56 + ] + } + } + } + ], + "id": "ed45ff87-fad1-41fe-8d0a-cfd4daaf1b4e", + "identifier": [ + "6915 MV1" + ], + "name": "MysteryVibe Molto" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "2cd76f8d-963c-4b98-861d-00b560a0ae09", + "output": { + "Vibrate": { + "step-range": [ + 0, + 56 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "525464fd-960b-47ef-b7f3-04196a648963", + "output": { + "Vibrate": { + "step-range": [ + 0, + 56 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "811a2fe9-be54-49ee-89ac-e8e83895e33d", + "output": { + "Vibrate": { + "step-range": [ + 0, + 56 + ] + } + } + } + ], + "id": "2b750693-1766-4448-8c30-9f9fa32830f2", + "name": "Mysteryvibe V2 Device" + } + }, + "nextlevelracing": { + "communication": [ + { + "serial": { + "baud-rate": 115200, + "data-bits": 8, + "parity": "N", + "port": "default", + "stop-bits": 1 + } + } + ], + "defaults": { + "features": [ + { + "description": "Right thigh", + "feature-type": "Vibrate", + "id": "178ade8c-0063-4f37-b37f-c47608f0b1e3", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Left thigh", + "feature-type": "Vibrate", + "id": "f3d43a20-94e8-4e6a-a504-4b2fe87cfbe1", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Right buttock", + "feature-type": "Vibrate", + "id": "00d0b735-ffb6-4964-b963-75b1d4995c89", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Left buttock", + "feature-type": "Vibrate", + "id": "5ba0a42a-8bed-4123-95bd-0d1f4bc5333d", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Right back", + "feature-type": "Vibrate", + "id": "29820b84-4c47-443d-85a5-8706f64d38c1", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Left back", + "feature-type": "Vibrate", + "id": "b930b1ae-2974-4e8f-b95c-b960d848534c", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Right shoulder", + "feature-type": "Vibrate", + "id": "225e1d14-4cc9-4c8c-b6ff-5ae024e3387a", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Left shoulder", + "feature-type": "Vibrate", + "id": "e369bcd9-8e2f-4466-8773-98bdf5fad7c5", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "fc830a11-de0d-4262-8155-99827cb926a9", + "name": "Next Level Racing HF8 Haptic Gaming Pad" + } + }, + "nexus-revo": { + "communication": [ + { + "btle": { + "names": [ + "XW-LW3" + ], + "services": { + "0000c570-0000-1000-8000-00805f9b34fb": { + "tx": "0000c571-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "24125960-c279-4f64-87e3-a819af7319b4", + "output": { + "Vibrate": { + "step-range": [ + 0, + 10 + ] + } + } + }, + { + "feature-type": "RotateWithDirection", + "id": "fabe3961-dc17-4f32-856f-13880c0a29a3", + "output": { + "RotateWithDirection": { + "step-range": [ + 0, + 2 + ] + } + } + } + ], + "id": "622f93f2-53d5-4ada-b6a7-359a9d8aedd0", + "name": "Nexus Revo Stealth" + } + }, + "nintendo-joycon": { + "communication": [ + { + "hid": { + "pairs": [ + { + "product-id": 8199, + "vendor-id": 1406 + }, + { + "product-id": 8198, + "vendor-id": 1406 + }, + { + "product-id": 8201, + "vendor-id": 1406 + } + ] + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "7a3195c9-4c04-4004-9fac-a475983f1dd4", + "output": { + "Vibrate": { + "step-range": [ + 0, + 1000 + ] + } + } + } + ], + "id": "0aae8323-9095-4b71-b151-d5ef93ab8f6d", + "name": "Nintendo Joycon" + } + }, + "nobra": { + "communication": [ + { + "btle": { + "names": [ + "NobraControl*" + ], + "services": { + "0000abf0-0000-1000-8000-00805f9b34fb": { + "tx": "0000abf1-0000-1000-8000-00805f9b34fb" + } + } + } + }, + { + "serial": { + "baud-rate": 19200, + "data-bits": 8, + "parity": "N", + "port": "default", + "stop-bits": 1 + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "3d9a6c96-2f9e-4105-931b-c799c1c9f3e0", + "output": { + "Vibrate": { + "step-range": [ + 0, + 15 + ] + } + } + } + ], + "id": "b548cba6-63cd-4d4c-9124-7e13303a6dec", + "name": "Nobra's Silicone Dreams Toy" + } + }, + "omobo": { + "communication": [ + { + "btle": { + "names": [ + "S6" + ], + "services": { + "0000ffb0-0000-1000-8000-00805f9b34fb": { + "tx": "0000ffb2-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "6ce40ef1-a4bc-4d4f-a3f1-9059e8fd461b", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "550658f8-3cce-4b97-999e-7ddb3357a591", + "name": "Omobo ViVegg Vibrator" + } + }, + "patoo": { + "communication": [ + { + "btle": { + "names": [ + "PTVEA*", + "PBT*", + "PCS*", + "PHT*" + ], + "services": { + "f000aa64-0451-4000-b000-000000000000": { + "tx": "f000aa68-0451-4000-b000-000000000000", + "txmode": "f000aa65-0451-4000-b000-000000000000" + } + } + } + } + ], + "configurations": [ + { + "id": "929310c1-bf4a-4238-b8d9-96ffcca1f954", + "identifier": [ + "PTVEA" + ], + "name": "Patoo Carrot" + }, + { + "id": "91af7b5e-8b16-4489-a916-1584ff1e561c", + "identifier": [ + "PCS" + ], + "name": "Patoo Vibrator" + }, + { + "id": "a4175adb-1086-4a4a-8a43-9d484e231085", + "identifier": [ + "PHT" + ], + "name": "Patoo Bean Sprout" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "f2957620-0a5c-4d69-851c-f9d34544e4cc", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "49f28542-fb54-46e6-a6b8-f412617ce24f", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "70af2af2-ba71-4b41-9e5d-4c3000377a2b", + "identifier": [ + "PBT" + ], + "name": "Patoo Devil" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "328761ed-4dd1-4535-9d37-e805f5eb1a61", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "fbb69ec0-dda6-4fca-ae69-390a91c13c03", + "name": "Patoo Device" + } + }, + "picobong": { + "communication": [ + { + "btle": { + "names": [ + "Blow hole", + "Picobong Male Toy", + "Diver", + "Picobong Egg", + "Life guard", + "Picobong Ring", + "Surfer", + "Picobong Butt Plug", + "Egg driver", + "Surfer_plug" + ], + "services": { + "0000fff0-0000-1000-8000-00805f9b34fb": { + "tx": "0000fff1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "1f59dbcf-b84d-4cf8-ac68-87bacb143b34", + "identifier": [ + "Blow hole", + "Picobong Male Toy" + ], + "name": "Picobong Blow hole" + }, + { + "id": "b3396470-af6e-45df-ad4f-944539d71600", + "identifier": [ + "Diver", + "Picobong Egg" + ], + "name": "Picobong Diver" + }, + { + "id": "88684b6f-6fde-488e-86a5-5c1f50893345", + "identifier": [ + "Life guard", + "Picobong Ring" + ], + "name": "Picobong Life guard" + }, + { + "id": "f7c40c1b-0d86-4d39-9163-34a9a243d614", + "identifier": [ + "Surfer", + "Picobong Butt Plug", + "Egg driver", + "Surfer_plug" + ], + "name": "Picobong Surfer" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "6acffe62-d4ae-4a9e-8610-123d46d26dcc", + "output": { + "Vibrate": { + "step-range": [ + 0, + 10 + ] + } + } + } + ], + "id": "e820a3cc-70e2-4766-98d4-934a00a667db", + "name": "Picobong Device" + } + }, + "pink_punch": { + "communication": [ + { + "btle": { + "names": [ + "Pink_Punch", + "PinkPunch_Peachu", + "PinkPunch_DreamBunny" + ], + "services": { + "0000ffe0-0000-1000-8000-00805f9b34fb": { + "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "7e0338c1-0562-451a-95ce-1b078de2f32e", + "identifier": [ + "Pink_Punch" + ], + "name": "Pink Punch Sunset Mushroom" + }, + { + "id": "b0554241-8f73-45c7-baf8-fa179f1ea4ef", + "identifier": [ + "PinkPunch_Peachu" + ], + "name": "Pink Punch Peachu" + }, + { + "id": "85703d43-c719-4753-ba92-3bb28c150565", + "identifier": [ + "PinkPunch_DreamBunny" + ], + "name": "Pink Punch Dream Bunny" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "71813440-1a8e-4cfb-9753-bf1fdc674579", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "c64c779a-4451-4c55-af1d-e4b40527d678", + "name": "Pink Punch Device" + } + }, + "prettylove": { + "communication": [ + { + "btle": { + "names": [ + "Aogu BLE *", + "AB Shutter3 [Aogu BLE Device]" + ], + "services": { + "0000ffe5-0000-1000-8000-00805f9b34fb": { + "rx": "0000ffe2-0000-1000-8000-00805f9b34fb", + "tx": "0000ffe9-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "349df5c5-1c5d-4de2-a3d9-c9159c640aba", + "output": { + "Vibrate": { + "step-range": [ + 0, + 3 + ] + } + } + } + ], + "id": "abeb7195-dbc2-4bd1-a079-18ffbb04e521", + "name": "Pretty Love Device" + } + }, + "realov": { + "communication": [ + { + "btle": { + "names": [ + "REALOV_VIBE" + ], + "services": { + "0000ffe0-0000-1000-8000-00805f9b34fb": { + "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "7d9d20cd-1a03-487f-b6c7-9b337c49e534", + "output": { + "Vibrate": { + "step-range": [ + 0, + 50 + ] + } + } + } + ], + "id": "79b23444-7e36-4042-bd52-86221c67c988", + "name": "Realov Device" + } + }, + "realtouch": { + "communication": [ + { + "hid": { + "pairs": [ + { + "product-id": 1, + "vendor-id": 8020 + } + ] + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "PositionWithDuration", + "id": "60da884f-131a-4036-ae93-97efc97591e2", + "output": { + "PositionWithDuration": { + "step-range": [ + 0, + 99 + ] + } + } + } + ], + "id": "2b428728-0785-4cbc-a71f-4f48412af194", + "name": "RealTouch" + } + }, + "rez-trancevibrator": { + "communication": [ + { + "usb": { + "pairs": [ + { + "product-id": 1615, + "vendor-id": 2889 + } + ] + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "01e369e0-541d-417a-9809-0600dab964c6", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "04923383-f64b-4b39-bed6-83862c5314d5", + "name": "Rez TranceVibrator" + } + }, + "sakuraneko": { + "communication": [ + { + "btle": { + "names": [ + "sakuraneko-01", + "sakuraneko-02", + "sakuraneko-03", + "sakuraneko-04" + ], + "services": { + "0000ffe0-0000-1000-8000-00805f9b34fb": { + "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "26673810-3196-4733-8071-781c221c1a39", + "identifier": [ + "sakuraneko-01" + ], + "name": "Sakuraneko Korokoro" + }, + { + "id": "e1bcba4b-1f4d-4d57-8a30-ee3696fb206f", + "identifier": [ + "sakuraneko-02" + ], + "name": "Sakuraneko Nukunuku" + }, + { + "id": "7234946a-55ed-483a-8482-a6d6e1e97c4b", + "identifier": [ + "sakuraneko-03" + ], + "name": "Sakuraneko Dokidoki" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "a5eb13a7-1f14-4785-a2ea-86dde4a3e15b", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Rotate", + "id": "62b84b1c-cfcd-4d9a-8dba-4d8210e5ee93", + "output": { + "Rotate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "c45e02cd-b8b6-4617-996e-302db442b228", + "identifier": [ + "sakuraneko-04" + ], + "name": "Sakuraneko Koikoi" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "bb67be77-f219-411d-98b5-d6b358eb94c9", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "0e121fa6-76db-484a-892f-4dc88ac6f333", + "name": "Sakuraneko Device" + } + }, + "satisfyer": { + "communication": [ + { + "btle": { + "manufacturer-data": [ + { + "company": 93, + "data": [ + 0, + 0, + 39 + ] + }, + { + "company": 93, + "data": [ + 0, + 0, + 40 + ] + } + ], + "names": [ + "SF *" + ], + "services": { + "0000180a-0000-1000-8000-00805f9b34fb": { + "rxblemodel": "00002a24-0000-1000-8000-00805f9b34fb" + }, + "51361500-c5e7-47c7-8a6e-47ebc99d80e8": { + "command": "51361501-c5e7-47c7-8a6e-47ebc99d80e8", + "tx": "51361502-c5e7-47c7-8a6e-47ebc99d80e8" + } + } + } + } + ], + "configurations": [ + { + "features": [ + { + "feature-type": "Vibrate", + "id": "b9bcbd6f-9f4a-4738-9a64-08e646fa2297", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "a8a7887f-c5dd-4e2c-ae88-d20e954bc65a", + "identifier": [ + "10005" + ], + "name": "Satisfyer Hot Spot" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "b03a8a9e-13ef-4ed6-820e-cb07d4e3aa30", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "624f9203-ca16-429c-b076-0725a5c04077", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "444d9fc4-23ed-4ea5-a1a5-923680d78af3", + "identifier": [ + "10006" + ], + "name": "Satisfyer Heated Affair" + }, + { + "id": "67f6a3ba-d167-4d44-ac52-0991dbf1df16", + "identifier": [ + "10007" + ], + "name": "Satisfyer Big Heat" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "e5368b0e-00a7-4f20-b338-2a33d65db794", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "4bb68190-ea62-4277-b7f1-3d6f055a939a", + "identifier": [ + "10008" + ], + "name": "Satisfyer Heated Thrill" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "cd889856-c5a8-4d7b-9ff6-5f7e49c13b4a", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "5e8eba19-d6cf-4c85-9824-5afd6191c95a", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "b8219c94-f239-4f12-b3ab-ceeb816bdfb4", + "identifier": [ + "10009" + ], + "name": "Satisfyer Hot Bunny" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "7473ae23-1678-4d6c-bc45-311e126dce65", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "1340347e-7e6a-4c27-a593-7b7a41b09332", + "identifier": [ + "10010" + ], + "name": "Satisfyer Heat Climax" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "715282dc-6919-4a8f-a339-adeb0fa8b4b0", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "1eb40efb-6aa5-4154-a2f4-8cc962cd2682", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "4ffc5fb8-a619-4cbc-8cc9-23104a473ee4", + "identifier": [ + "10011" + ], + "name": "Satisfyer Heat Climax+" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "46c676b0-5dae-4376-b6b3-c3f0b9526260", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "a05e4d51-c296-4395-b5ba-1b8801079a15", + "identifier": [ + "10012" + ], + "name": "Satisfyer Hot Passion" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "dd995a89-a889-40a8-9a88-aa05b8fe3e60", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "d39282bc-910b-40d2-a8f6-2c729ba5e2f2", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "defd08cf-76b3-4957-88ef-5c7fb2a89ff0", + "identifier": [ + "10013" + ], + "name": "Satisfyer Haute Couture+" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "9b18554d-8f0d-4941-8649-7e34375a0005", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "3fba6850-e170-4bbf-b61c-e105b3ea7762", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "d36dda3c-edf3-4ec2-be9a-393934157102", + "identifier": [ + "10014" + ], + "name": "Satisfyer High Fashion+" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "cee6ec1f-1f35-48ef-8864-fa76d2ebb8a5", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "c1a929c7-adf1-4cbe-907e-a24e6164e7af", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "3925e9e4-fc21-4bad-8ecd-4a8780a5ce83", + "identifier": [ + "10015" + ], + "name": "Satisfyer Prêt-à-porter+" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "9dcbc0b0-b076-4b50-9104-c071d52e39ff", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "5ae0c642-bd10-4f21-8fef-60f94ca755c5", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "c771d860-0592-4962-8a05-dc2e7187bff6", + "identifier": [ + "10024", + "10025" + ], + "name": "Satisfyer Love Triangle" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "95143c24-8928-405c-a6d0-1a64b3830498", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "78533341-96c5-4b21-aede-857ec827c1e6", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "4e47a95f-3a70-4bb4-829f-8b617afaaa1d", + "identifier": [ + "10027", + "10028" + ], + "name": "Satisfyer Curvy 1+" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "f0bed160-760d-4d18-b462-247e124c537f", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "81b4e5d2-8fd7-4fed-a6cb-d3df12366040", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "7fa5b1e2-c30f-411f-a9b5-9eeee3d95170", + "identifier": [ + "10030", + "10031" + ], + "name": "Satisfyer Curvy 2+" + }, + { + "id": "942818a5-f94f-4efb-b775-693f8b27ab9b", + "identifier": [ + "10032" + ], + "name": "Satisfyer Double Wand-er" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "0b359281-588c-4aad-bfe1-54d605377120", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "9b9f616a-3219-4424-9ecf-c52520dec964", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "8b5e975e-4215-4b0c-a169-7d6209746d88", + "identifier": [ + "10046", + "10047", + "10048" + ], + "name": "Satisfyer Double Joy" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "d6f94a0f-11cd-4242-b05e-e7f237e6b7c0", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "2fe89205-fb8d-4fb7-93d3-d4169f92875d", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "05f9af5c-d7b9-43f0-8cf5-41f0c09def28", + "identifier": [ + "10049", + "10050", + "10051" + ], + "name": "Satisfyer Double Fun" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "eb62f1da-11a0-48b1-8c8e-2c8ea6e24e61", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "16f5a83d-f0fc-41c1-a4d3-43ce13dd3529", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "82270653-6408-43ef-a148-cdfca58a5d2d", + "identifier": [ + "10052", + "10053", + "10054" + ], + "name": "Satisfyer Double Love" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "5d900545-d8cc-4c32-9ff5-e1d8e0c30b90", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "823f51aa-1766-41f4-b48f-f8b2de4c588e", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "e7c09700-6df1-40c5-b5bb-0203c782dc01", + "identifier": [ + "10055" + ], + "name": "Satisfyer Curvy 3+" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "406de8d0-b6d9-4f5d-b9cd-479092898aac", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "19f2225e-4bc8-4f70-9fb2-734abc8dd5be", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "5c90d251-a2fe-461a-a4ae-0e5172a9739d", + "identifier": [ + "10059", + "10060", + "10061" + ], + "name": "Satisfyer Hot Lover" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "d1bf52af-d49d-42bb-a277-73cc394dce90", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "d1d6a777-21e2-4e6c-9f2e-679d1e75c932", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "44dae430-c6b4-4688-8ab6-9696d82a4b00", + "identifier": [ + "10062", + "10063", + "10064" + ], + "name": "Satisfyer Mono Flex" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "a824a4f4-11c4-4a84-81d6-424a622d1b06", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "7aa798ab-9bc5-47b4-a318-5349c68ebf93", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "467802b9-6e3b-4810-b659-da69885b7366", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "1715eee4-4aa5-4696-9f41-6e6c299061ec", + "identifier": [ + "10065", + "10066", + "10067", + "10068" + ], + "name": "Satisfyer Double Flex" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "704fd1ec-a242-4e02-80ab-9db6f2377a7c", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "c6971493-fa87-45d6-b131-67af138f7b13", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "ce5ebe09-6d9d-44a1-93e9-f5247c03d3f1", + "identifier": [ + "10069", + "10070", + "10071" + ], + "name": "Satisfyer Heat Wave" + }, + { + "id": "b9a13914-c02c-44ac-b9a8-9e95776e3ceb", + "identifier": [ + "10072" + ], + "name": "Satisfyer Little Secret" + }, + { + "id": "c62c869a-8d62-4386-a7f9-ec68ccc99513", + "identifier": [ + "10073" + ], + "name": "Satisfyer Sexy Secret" + }, + { + "id": "03082593-a2ea-455b-9b94-66c3b1953144", + "identifier": [ + "10074" + ], + "name": "Satisfyer Strong One" + }, + { + "id": "e8b06812-88be-4a7d-9581-8ea7210f809a", + "identifier": [ + "10075" + ], + "name": "Satisfyer Mighty One" + }, + { + "id": "d0832c21-c990-4bd8-b06f-32e5768af9d2", + "identifier": [ + "10076" + ], + "name": "Satisfyer Powerful One" + }, + { + "id": "1f6254b1-301c-4455-9a5e-84886d5e3fce", + "identifier": [ + "10077" + ], + "name": "Satisfyer Royal One" + }, + { + "id": "571d6d2c-351a-4870-9a2a-af16bdc97731", + "identifier": [ + "10078" + ], + "name": "Satisfyer Signet Ring" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "39ca4a7a-c9f3-430a-8248-6001719c6a40", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "07ff65a4-ae65-4054-bd70-419ddac6d241", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "8d5afdb3-47d1-4841-92d6-d3c7b1b2238e", + "identifier": [ + "10079", + "10080" + ], + "name": "Satisfyer Dual Love" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "18661df2-7eb2-452a-b611-85433bd99ea0", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "c6b1acf6-511e-44bd-ab1c-b2d944a35cf0", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "d609d09e-86e5-4544-bda3-16b15b532f2d", + "identifier": [ + "10081", + "10082" + ], + "name": "Satisfyer Dual Pleasure" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "ec61550d-e557-4c57-b6a3-02b28bd5e0d6", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "cfcd017c-d3fb-46ab-82d9-55438e96a3d7", + "identifier": [ + "10090" + ], + "name": "Satisfyer Hero+" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "5a8dba5a-ca48-4340-8140-fa1fc4d86b73", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "7fb611fe-6af4-4d0e-a6b8-0d4ee72e34af", + "identifier": [ + "10091" + ], + "name": "Satisfyer Knight+" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "31fb6881-d23e-4f07-b233-c6531ccc79b3", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "98dcb92c-84a1-4a1f-88b9-7c61098020de", + "identifier": [ + "10092", + "10093" + ], + "name": "Satisfyer Newcomer+" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "fec3511d-2fcd-4463-9ef0-b139c8aa8b0a", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "49020dca-5124-4965-9add-4230dfd0fe28", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "c7d1d682-b311-4ce8-b552-d68b8fcde1bc", + "identifier": [ + "10100", + "10101" + ], + "name": "Satisfyer Plug-ilicious 1" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "28f3bea8-f927-46a9-ab45-55daf1f76c87", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "540b8330-f039-4870-a6d2-d536f2415cf2", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "22513021-0cb9-4f30-ada7-f7ca6a86e085", + "identifier": [ + "10102", + "10103", + "10104" + ], + "name": "Satisfyer Plug-ilicious 2" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "0a939b92-0209-4d2f-b658-0db0ac9a2e6e", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "6c07e79d-8842-4e27-88a9-9a471928da5e", + "identifier": [ + "10105" + ], + "name": "Satisfyer E-Love Foreplay" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "e46297ee-6037-44a8-ac06-5f8328d41b19", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "39bfa539-7c58-49a4-87ca-a691a11c16f1", + "identifier": [ + "10108" + ], + "name": "Satisfyer E-Love G-Hunter" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "9248bdf7-d918-4682-b197-59707ac5ea95", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "8d541f70-6595-49b1-b75d-77187f9b75dc", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "0b5bcc9b-b5d7-49d3-9c0a-c8dc82214306", + "identifier": [ + "10109" + ], + "name": "Satisfyer E-Love G-Hunter+" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "8f8b7024-005e-4fda-9c65-adf55dc3c470", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "40653fca-c115-4bd4-b3fa-c3875c41a562", + "identifier": [ + "10110" + ], + "name": "Satisfyer E-Love G-Spotter" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "397a61df-a515-49e1-a14d-af2de7855a3f", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "27720871-f08b-4151-96f1-006a5cc137fc", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "5a5afa20-0518-420e-a5ab-e5b09c5c9842", + "identifier": [ + "10111" + ], + "name": "Satisfyer E-Love G-Spotter+" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "56f7a9fe-d8ef-4a21-b15f-77307a6417ea", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "0bfe78b6-a128-4c68-b874-e85ee18273f0", + "identifier": [ + "10112" + ], + "name": "Satisfyer E-Love Story" + }, + { + "id": "c62ea9ae-dc65-429e-90e4-473fa8c5ffaa", + "identifier": [ + "10119", + "10120", + "10182" + ], + "name": "Satisfyer Love Birds 1" + }, + { + "id": "17b98fe5-4aeb-4c75-b554-701daf147dff", + "identifier": [ + "10121", + "10122", + "10123" + ], + "name": "Satisfyer Love Birds 2" + }, + { + "id": "fde0831c-e1da-46f0-b6fe-8bccfbe9fdae", + "identifier": [ + "10124", + "10125", + "10126" + ], + "name": "Satisfyer Love Birds Vary" + }, + { + "id": "30fb0255-b2e5-424b-bca5-8abdbe864ebf", + "identifier": [ + "10127", + "10128", + "10129", + "10201" + ], + "name": "Satisfyer Ribbed Petal" + }, + { + "id": "b10e2742-01b9-4bc8-8caf-b18f0dc51baa", + "identifier": [ + "10130", + "10131", + "10132", + "10133" + ], + "name": "Satisfyer Shiny Petal" + }, + { + "id": "37096541-c085-4b30-a978-cf1ab8c79198", + "identifier": [ + "10134", + "10135", + "10136", + "10202" + ], + "name": "Satisfyer Smooth Petal" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "54c660d2-c326-4272-a1a8-a6ab0a3f5620", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "992e2870-64ed-4704-a74b-2faf3baa0e4b", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "518071d2-a6b5-4ee9-9d10-9248fcc72d76", + "identifier": [ + "10140" + ], + "name": "Satisfyer Men Vibration+" + }, + { + "id": "fb04247f-1ade-4c3e-816f-1a4c81ae0db4", + "identifier": [ + "10141" + ], + "name": "Satisfyer Power Plug" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "55ed967f-f37b-47e9-acbd-e091ece4a25a", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "4deb6ffc-7ffb-4892-adb9-ff3829cbf7bb", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "16d47710-4849-42b0-aa9b-e7375a533dc5", + "identifier": [ + "10142", + "10143" + ], + "name": "Satisfyer Rotator Plug 1+" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "08a92451-b728-4bf8-bde0-b2af748fc0bd", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "f9b0e791-a348-4485-b1a5-cd90e3503e13", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "7ef01670-5fa3-4bb2-b8b5-3c952f4cf263", + "identifier": [ + "10144", + "10145" + ], + "name": "Satisfyer Rotator Plug 2+" + }, + { + "id": "3e04ed12-9d6e-4f7a-9cc8-09e58a9f760e", + "identifier": [ + "10146", + "10147" + ], + "name": "Satisfyer Deep Diver" + }, + { + "id": "99f4d915-7fea-4be1-893e-3ab74488a383", + "identifier": [ + "10148", + "10149" + ], + "name": "Satisfyer Sweet Seal" + }, + { + "id": "e26a9471-44ab-438a-8290-4793ac6d5ddd", + "identifier": [ + "10150", + "10151" + ], + "name": "Satisfyer Trendsetter" + }, + { + "id": "682c5153-d84c-4a30-b172-42732eaa7081", + "identifier": [ + "10154", + "10155", + "10156" + ], + "name": "Satisfyer Twirling Joy" + }, + { + "id": "b7ed864e-a11d-40de-b3bc-2a28d6ebc2f2", + "identifier": [ + "10157", + "10158" + ], + "name": "Satisfyer Ultra Power Bullet 8" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "c1c09c65-a2d4-4caa-9f56-cec54897758b", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "bc03728b-573a-40d6-ae99-1aa1f508a804", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "17d338a2-dcb1-4170-9a01-ab2250f73b8f", + "identifier": [ + "10160", + "10161", + "10162" + ], + "name": "Satisfyer Double Desire" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "9564b21d-c2ba-444e-85c4-dd9dcd80e3b5", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "c70c801e-980a-4052-a275-f8109058a1ad", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "4729ddda-fb21-4c3a-9868-b0fcbca18480", + "identifier": [ + "10163", + "10164", + "10165", + "10166" + ], + "name": "Satisfyer Double Lust" + }, + { + "id": "b3879662-a471-4bea-ad9a-5d8b59a476a5", + "identifier": [ + "10167" + ], + "name": "Satisfyer Epic Duo" + }, + { + "id": "9404874e-3de2-4696-a620-943f5affb910", + "identifier": [ + "10168" + ], + "name": "Satisfyer Pleasure Wand+" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "9ccf5505-2b55-4386-aa8c-80cb7117f6c2", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "33b12687-c341-47da-81c2-2e2cf9862712", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "98f72ae0-a840-4805-918d-3427541325ca", + "identifier": [ + "10169", + "10170", + "10171" + ], + "name": "Satisfyer Top Secret" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "be9d24ff-8470-481d-aee0-0ea30f0877de", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "ed63da4f-ee14-469c-a47c-12003141716a", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "99cfefd9-fd09-40c6-9a2f-3d68385a04bc", + "identifier": [ + "10172", + "10173", + "10174" + ], + "name": "Satisfyer Top Secret+" + }, + { + "id": "48bb511e-1cc2-4b1d-9497-022b015287bc", + "identifier": [ + "10175", + "10176" + ], + "name": "Satisfyer Bullseye" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "d2786210-46f4-47ce-9f5b-80fa691e0ad2", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "e0dbd014-7415-4d0f-946e-188e239a8154", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "f624b4d4-5fe4-4390-9fbb-8ef170b5846c", + "identifier": [ + "10177", + "10178", + "10179" + ], + "name": "Satisfyer Sunray" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "ff20f721-e6fe-4787-964d-327d29b0c391", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "e8322905-46aa-45f8-b7f7-25a88507a55d", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "69243058-fb93-4791-b78e-f32f50f902b3", + "identifier": [ + "10180", + "10181" + ], + "name": "Satisfyer Curvy Trinity 5+" + }, + { + "id": "20c58cef-83e0-48f2-a352-a3663453403f", + "identifier": [ + "10183", + "10184" + ], + "name": "Satisfyer Intensity Plug" + }, + { + "id": "baa0ad15-08cc-426c-b1f2-02d9768f6e2c", + "identifier": [ + "10185" + ], + "name": "Satisfyer Power Masturbator" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "4019145b-56cf-473e-a286-4a8d040e80cc", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "7dc4760f-3a7c-4c2e-a7da-e7d8d52b196b", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "7d92f936-f672-478a-a26f-616758ff621d", + "identifier": [ + "10186", + "10187" + ], + "name": "Satisfyer Hug me" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "7abb00ea-bb62-4bef-a26f-a7f7135dec2c", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "c77d5b49-6257-4381-900a-9225caea7124", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "d112fbc4-9a5e-4518-b40c-f1200be124cd", + "identifier": [ + "10188" + ], + "name": "Satisfyer Air Pump Bunny 5+" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "1acf7f71-e57a-4a1a-81d3-d8bb977d6b72", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "2278b99f-cee5-48fa-9326-8add9730e1e2", + "identifier": [ + "10189" + ], + "name": "Satisfyer Air Pump Vibrator 5+" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "467accb0-f1f6-4175-afe5-08f48d069fe3", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "4b1b417b-ce44-45fd-be3f-77d939162e18", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "537ce4cb-f8e2-423b-80a5-5bcbb07e6e15", + "identifier": [ + "10190", + "10191" + ], + "name": "Satisfyer Threesome 4" + }, + { + "id": "8ba85779-5b40-48ae-88d5-7744bf852d22", + "identifier": [ + "10192" + ], + "name": "Satisfyer G-Spot Flex 4+" + }, + { + "id": "3844ee0f-94ed-49bf-9a9e-795f407c0ade", + "identifier": [ + "10193", + "10194" + ], + "name": "Satisfyer G-Spot Flex 5+" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "12990ee9-76cc-4b48-b711-f70587f14fd7", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "0687264e-3150-4d0a-818b-be6ad231d54c", + "identifier": [ + "10195" + ], + "name": "Satisfyer Air Pump Booty 5+" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "c8d73535-d37b-4baa-81c6-c301f32390e0", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "304c7318-bd1b-40ba-a475-90b4d7127c46", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "7c33ff57-e4c7-4110-9814-451062806981", + "identifier": [ + "10196" + ], + "name": "Satisfyer Pro+ Wave 4" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "3a37453d-605c-4dd4-a83a-28be69ac55b8", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "42dafbc1-0aac-4348-898a-8d467d903191", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "a1cb3608-d17c-4c5f-b3d5-4c7ee87d5467", + "identifier": [ + "10197", + "10198" + ], + "name": "Satisfyer Mini Wand-er+" + }, + { + "id": "7790e568-454e-45f8-85bb-5f8fd855c554", + "identifier": [ + "10199", + "10200" + ], + "name": "Satisfyer Tropical Tip" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "866a3152-759b-4777-8578-8abaff6aea9a", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "5a7b0180-16b1-41e7-a016-af4a761564de", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "1bc5cd0a-feb7-4cfc-9155-09c7565d85e0", + "identifier": [ + "10203", + "10204" + ], + "name": "Satisfyer Twirling Pro+" + }, + { + "id": "7c2560dc-06d4-4da6-874a-5f6c2c05810d", + "identifier": [ + "10205" + ], + "name": "Satisfyer Perfect Pair 4" + }, + { + "id": "0a682803-b5ad-457a-bbf0-40e48b71cbcf", + "identifier": [ + "10206", + "10207", + "10208" + ], + "name": "Satisfyer Booty Absolute Beginners 5" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "fdb9014d-b7b9-4b28-8804-cdf26b432df1", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "6665fc3b-a8e6-4a36-ad11-46f449abfc90", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "2a429cdd-20f9-4a22-82a9-dd79234e23de", + "identifier": [ + "10241", + "10242" + ], + "name": "Satisfyer Rrrolling Sensation" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "f14fc3ea-05f0-426a-ac01-70cdbadb43ec", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "1a3c8f91-c172-4378-9fe2-64891a06e8d1", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "b0578f68-2b0b-497a-b49a-2e897d3a040a", + "identifier": [ + "10307", + "10308", + "10309" + ], + "name": "Satisfyer Pro 2 Gen 3" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "7153daef-c222-4841-9495-289798fff9ea", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "9a934b7a-b6aa-4ad6-8d5c-e00971d67159", + "name": "Satisfyer Device" + } + }, + "sayberx": { + "communication": [ + { + "btle": { + "names": [ + "SayberX", + "X-Ring *" + ], + "services": { + "0000fff0-0000-1000-8000-00805f9b34fb": { + "rx": "0000fff8-0000-1000-8000-00805f9b34fb", + "tx": "0000fff6-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "features": [ + { + "feature-type": "Vibrate", + "id": "a62d0356-a05f-475c-8a5f-fcfec1327b2a", + "output": { + "Vibrate": { + "step-range": [ + 0, + 4 + ] + } + } + } + ], + "id": "22716d89-5e28-462b-9723-60528fb7373e", + "identifier": [ + "SayberX" + ], + "name": "SayberX" + }, + { + "id": "e77a2f7b-8556-48b8-8245-30c2c80681e7", + "identifier": [ + "X-Ring" + ], + "name": "Sayber X-Ring" + } + ], + "defaults": { + "features": [], + "id": "9635a829-753b-4e5b-825c-24249526af09", + "name": "SayberX Device" + } + }, + "sensee": { + "communication": [ + { + "btle": { + "names": [ + "CTY222S4" + ], + "services": { + "0000fff0-0000-1000-8000-00805f9b34fb": { + "tx": "0000fff5-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "1544b066-a3d3-4749-9081-1b7a26ab54ed", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "a8ffccf6-2d38-4606-abdd-8802a063a2ae", + "name": "Sensee Diandou Rabbit" + } + }, + "sensee-v2": { + "communication": [ + { + "btle": { + "names": [ + "CCPA10S2", + "CCPA18S5", + "Easylive NO8 Cup", + "CTY508S5", + "CTY916S4", + "PTYB22S2", + "CCP322S5", + "CTY823S5" + ], + "services": { + "0000fff0-0000-1000-8000-00805f9b34fb": { + "rx": "0000fff4-0000-1000-8000-00805f9b34fb", + "tx": "0000fff5-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "4629e2a0-553f-4178-a378-8a9a5e88b038", + "identifier": [ + "CCPA10S2" + ], + "name": "Sensee Capsule" + }, + { + "id": "e9be0c9a-43d9-4e95-9d1d-67e22f940a5f", + "identifier": [ + "CCPA18S5" + ], + "name": "Sensee Astronaut" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "1094606e-1407-4249-979c-98d6a6abf97c", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Oscillate", + "id": "542d9822-9617-472c-953b-c9519a59aaac", + "output": { + "Oscillate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "72dcac71-472d-47bc-a408-60567765836c", + "identifier": [ + "Easylive NO8 Cup" + ], + "name": "Sensee No8" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "4a6f2a58-1760-42e6-ae17-6e0c4880a48c", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Oscillate", + "id": "aeab494e-3312-49bd-8f1f-599e3bab7f4d", + "output": { + "Oscillate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "b925cadb-6aef-4896-8b97-1dfa44702a9e", + "identifier": [ + "CCP322S5" + ], + "name": "Easylive Vader" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "c9600c27-1302-449c-9a07-268d59f818f3", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Oscillate", + "id": "377780e3-e3bd-4fe0-a345-6389eb32fbbe", + "output": { + "Oscillate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "fea99f9b-97da-44cf-a898-17e65abf86e3", + "identifier": [ + "CTY508S5" + ], + "name": "Sensee Voice-Interactive Female Vibrator" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "5c8664fd-1113-4d8b-af64-d42f6f303c3e", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Constrict", + "id": "848628c7-b34e-4af4-894f-7f51645dea6a", + "output": { + "Constrict": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "eca4db2b-f7ff-4d59-b73d-f2124786fceb", + "identifier": [ + "PTYB22S2" + ], + "name": "Sensee Moonlight" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "87712e50-fd72-4a3c-b122-ea3866e0942a", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Constrict", + "id": "2a7ce324-34dd-477c-b3e2-6a6632ee4b59", + "output": { + "Constrict": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "4e2ffbbe-8f8f-4593-9eab-3409d85645a2", + "identifier": [ + "CTY823S5" + ], + "name": "Sensee Little Seahorse" + }, + { + "features": [ + { + "feature-type": "Oscillate", + "id": "631815ee-37e9-4de6-9b33-971b9135c718", + "output": { + "Oscillate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "864ef211-1635-41bc-9618-e3989f540287", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "f8032396-8384-448f-88e9-4c754d4ae12e", + "identifier": [ + "CTY916S4" + ], + "name": "Sensee Dream Stick" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "b5865307-0de8-4dd9-bb1a-69e1c2f3c39c", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Constrict", + "id": "cd11ed14-d9ea-4c11-b454-41e5c697f70b", + "output": { + "Constrict": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "d7ba651e-88d6-4452-9fa5-1562b8d8be2a", + "name": "Sensee Device" + } + }, + "serveu": { + "communication": [ + { + "btle": { + "names": [ + "ServeU" + ], + "services": { + "31bb1111-33e3-4f3c-a7fb-104288e7cb77": { + "tx": "31bb2222-33e3-4f3c-a7fb-104288e7cb77" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "PositionWithDuration", + "id": "7e756a59-b13c-4322-bc59-27dacfc73b4d", + "output": { + "PositionWithDuration": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "9967414e-8b34-44ed-8b8a-20fe863e0b50", + "name": "ServeU" + } + }, + "sexverse-lg389": { + "communication": [ + { + "btle": { + "names": [ + "LG389" + ], + "services": { + "0000bae0-0000-1000-8000-00805f9b34fb": { + "rx": "0000bae2-0000-1000-8000-00805f9b34fb", + "tx": "0000bae1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "54ae0f52-dbd7-4fac-8463-f06199b72642", + "output": { + "Vibrate": { + "step-range": [ + 0, + 3 + ] + } + } + }, + { + "feature-type": "Oscillate", + "id": "394cb2f4-9ee5-4fe9-a31c-fd6652479467", + "output": { + "Oscillate": { + "step-range": [ + 0, + 10 + ] + } + } + } + ], + "id": "dd6e5fe8-f53c-4b5c-9614-cedfffc0a40f", + "name": "Sexverse LG389" + } + }, + "svakom-alex": { + "communication": [ + { + "btle": { + "names": [ + "Alex NEO", + "S63E Alex NEO" + ], + "services": { + "0000ffe0-0000-1000-8000-00805f9b34fb": { + "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "323f02f5-f1ab-40b9-ba8b-eba65de178c3", + "output": { + "Vibrate": { + "step-range": [ + 0, + 3 + ] + } + } + } + ], + "id": "39ee59bc-fdc5-47c4-8da6-2c208e30a7b6", + "name": "Svakom Alex Neo" + } + }, + "svakom-alex-v2": { + "communication": [ + { + "btle": { + "names": [ + "Alex NEO 2", + "S63E Alex NEO 2" + ], + "services": { + "0000ffe0-0000-1000-8000-00805f9b34fb": { + "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "807083a6-aca2-499d-84c0-fe1e8884f222", + "output": { + "Vibrate": { + "step-range": [ + 0, + 3 + ] + } + } + } + ], + "id": "632c2055-3c47-439d-8fcc-e3ee0b0288e5", + "name": "Svakom Alex Neo 2" + } + }, + "svakom-avaneo": { + "communication": [ + { + "btle": { + "names": [ + "Ava Neo" + ], + "services": { + "0000ffe0-0000-1000-8000-00805f9b34fb": { + "rx": "0000ffe2-0000-1000-8000-00805f9b34fb", + "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "9dbdf85e-6692-4a95-b8a1-da350327a9a3", + "output": { + "Vibrate": { + "step-range": [ + 0, + 10 + ] + } + } + }, + { + "feature-type": "Oscillate", + "id": "878fb1f8-8c38-4058-bd0f-859584d14cef", + "output": { + "Oscillate": { + "step-range": [ + 0, + 1 + ] + } + } + } + ], + "id": "8254195f-4c38-425d-b5e6-352ad644399a", + "name": "Svakom Ava Neo" + } + }, + "svakom-barnard": { + "communication": [ + { + "btle": { + "names": [ + "DG239A" + ], + "services": { + "0000ffe0-0000-1000-8000-00805f9b34fb": { + "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "7abda591-db6f-492c-a781-5f90d648b561", + "output": { + "Vibrate": { + "step-range": [ + 0, + 3 + ] + } + } + }, + { + "feature-type": "Oscillate", + "id": "5ec8c88b-bd24-4e94-bec1-467735a74b80", + "output": { + "Oscillate": { + "step-range": [ + 0, + 3 + ] + } + } + } + ], + "id": "aaebe699-02dd-461f-879d-c71da8c2d892", + "name": "Fantasy Cup Barnard" + } + }, + "svakom-barney": { + "communication": [ + { + "btle": { + "names": [ + "DJ333A" + ], + "services": { + "0000ffe0-0000-1000-8000-00805f9b34fb": { + "rx": "0000ffe2-0000-1000-8000-00805f9b34fb", + "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "ebbd9a68-1b05-4a21-8f3d-14b3dc7f1f70", + "output": { + "Vibrate": { + "step-range": [ + 0, + 10 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "be5e2510-9b63-4813-9192-2db123b82ac5", + "output": { + "Vibrate": { + "step-range": [ + 0, + 10 + ] + } + } + } + ], + "id": "1b3759c0-ee3b-4f5f-9b3a-3d6bc0cc9594", + "name": "Mutufun Barney" + } + }, + "svakom-dice": { + "communication": [ + { + "btle": { + "names": [ + "ZhiAi" + ], + "services": { + "0000ffe0-0000-1000-8000-00805f9b34fb": { + "rx": "0000ffe2-0000-1000-8000-00805f9b34fb", + "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "60b702d6-d3ff-4554-a3ae-f4638ddc74ef", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "5845f3f5-6943-41df-93df-04b3b1ce7ce2", + "name": "Zemalia Dice for Love" + } + }, + "svakom-dt250a": { + "communication": [ + { + "btle": { + "names": [ + "DT250A" + ], + "services": { + "0000ffe0-0000-1000-8000-00805f9b34fb": { + "rx": "0000ffe2-0000-1000-8000-00805f9b34fb", + "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "608e34f1-69eb-4469-95e2-c56fb26d7db6", + "output": { + "Vibrate": { + "step-range": [ + 0, + 3 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "75e9695f-7049-4ad7-a8db-a85f62868266", + "output": { + "Vibrate": { + "step-range": [ + 0, + 3 + ] + } + } + }, + { + "feature-type": "Constrict", + "id": "5fd9d9a0-4f7c-4ef4-87d5-5081f41499f3", + "output": { + "Constrict": { + "step-range": [ + 0, + 2 + ] + } + } + } + ], + "id": "7897a4fc-e45a-4f23-b04f-91415b3eeef7", + "name": "Coleur Dor DT250A" + } + }, + "svakom-iker": { + "communication": [ + { + "btle": { + "manufacturer-data": [ + { + "company": 39, + "data": [ + 83, + 86, + 65, + 1, + 11, + 18, + 1, + 51, + 68, + 85, + 202, + 8 + ] + } + ], + "names": [ + "Iker" + ], + "services": { + "0000ffe0-0000-1000-8000-00805f9b34fb": { + "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "36af2b39-85ec-4463-9ecd-59fbaff3ba38", + "output": { + "Vibrate": { + "step-range": [ + 0, + 10 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "74e5fb53-383a-4938-81ff-cb84da773882", + "output": { + "Vibrate": { + "step-range": [ + 0, + 5 + ] + } + } + } + ], + "id": "1db55a7c-6133-4b33-bd54-e7fa8dead165", + "name": "Svakom Iker" + } + }, + "svakom-jordan": { + "communication": [ + { + "btle": { + "names": [ + "Jordan" + ], + "services": { + "0000ffe0-0000-1000-8000-00805f9b34fb": { + "rx": "0000ffe2-0000-1000-8000-00805f9b34fb", + "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "f59261c4-39a7-4e13-b7e8-52c0a117ea7f", + "output": { + "Vibrate": { + "step-range": [ + 0, + 10 + ] + } + } + }, + { + "feature-type": "Oscillate", + "id": "84200741-7440-4267-b9a1-519eebe884ed", + "output": { + "Oscillate": { + "step-range": [ + 0, + 5 + ] + } + } + } + ], + "id": "89877d1d-9a8f-4265-93d7-7dbe4c093a58", + "name": "Svakom Jordan" + } + }, + "svakom-pulse": { + "communication": [ + { + "btle": { + "names": [ + "SWK-SX013A", + "Pulse Union", + "Pulse Galaxie", + "SX033APP", + "BX288A", + "QH-SX045A-B", + "SWK-SX067-B", + "QH-HX029A-B" + ], + "services": { + "0000ffe0-0000-1000-8000-00805f9b34fb": { + "rx": "0000ffe2-0000-1000-8000-00805f9b34fb", + "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "5b9918c8-af63-409f-9749-f5e6faf2dca0", + "identifier": [ + "SWK-SX013A" + ], + "name": "Svakom Pulse Lite Neo" + }, + { + "id": "f40b1405-cf40-43c5-a568-24e3d2d70c65", + "identifier": [ + "Pulse Union" + ], + "name": "Svakom Pulse Union" + }, + { + "id": "cd29302f-31f9-4c9f-aa12-ab381f941e82", + "identifier": [ + "Pulse Galaxie" + ], + "name": "Svakom Pulse Galaxie" + }, + { + "id": "ccb6ce6f-5dc7-4ce4-bd31-3e8f3af14a4b", + "identifier": [ + "SX033APP" + ], + "name": "Svakom Mimiki" + }, + { + "id": "ea05be83-2991-4cb5-8ad0-b108e0a52a5a", + "identifier": [ + "BX288A" + ], + "name": "BeYourLover Kyukyu" + }, + { + "id": "8abdd83e-af93-4f82-b240-d9eeed81e976", + "identifier": [ + "QH-SX045A-B" + ], + "name": "Coleur Dor VX045A" + }, + { + "id": "db486014-b4da-4cad-90f4-2ba53a36e335", + "identifier": [ + "SWK-SX067-B" + ], + "name": "Momonii Agatha" + }, + { + "id": "b9851f7f-ddc8-4df5-ad81-3071ec9daab1", + "identifier": [ + "QH-HX029A-B" + ], + "name": "Coleur Dor HX029A" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "0ee3c15e-b05d-4c97-bb4a-523a5475c520", + "output": { + "Vibrate": { + "step-range": [ + 0, + 9 + ] + } + } + } + ], + "id": "91a8f7f5-d774-4beb-ad76-9864b3a46597", + "name": "Svakom Pulse Device" + } + }, + "svakom-sam": { + "communication": [ + { + "btle": { + "names": [ + "Sam Neo" + ], + "services": { + "0000ae00-0000-1000-8000-00805f9b34fb": { + "rx": "0000ae02-0000-1000-8000-00805f9b34fb", + "tx": "0000ae01-0000-1000-8000-00805f9b34fb", + "txmode": "0000ae10-0000-1000-8000-00805f9b34fb" + }, + "0000ffac-0000-1000-8000-00805f9b34fb": { + "firmware": "0000ffb4-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "260f221c-b861-4ee2-bd0f-17a0dd9a14ba", + "output": { + "Vibrate": { + "step-range": [ + 0, + 10 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "cfdf5760-bce0-465c-a2c6-60c86fdd3c95", + "output": { + "Vibrate": { + "step-range": [ + 0, + 1 + ] + } + } + } + ], + "id": "d5fac59d-8e57-43a6-bcc9-61d06f6b8587", + "name": "Svakom Sam Neo" + } + }, + "svakom-sam2": { + "communication": [ + { + "btle": { + "names": [ + "Sam Neo 2", + "Sam Neo 2 Pro" + ], + "services": { + "0000ffe0-0000-1000-8000-00805f9b34fb": { + "rx": "0000ffe2-0000-1000-8000-00805f9b34fb", + "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "f32b4e50-ec7e-4b76-8f29-4b4777da7c22", + "identifier": [ + "Sam Neo 2" + ], + "name": "Svakom Sam Neo 2" + }, + { + "id": "869e4518-1565-4b3b-8d15-45c860c848c2", + "identifier": [ + "Sam Neo 2 Pro" + ], + "name": "Svakom Sam Neo 2 Pro" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "9f584905-3bcb-4a60-9a56-2c2d69c81a8c", + "output": { + "Vibrate": { + "step-range": [ + 0, + 10 + ] + } + } + }, + { + "feature-type": "Constrict", + "id": "7580e615-c22c-4242-b599-9b4041bfa400", + "output": { + "Constrict": { + "step-range": [ + 0, + 5 + ] + } + } + } + ], + "id": "88c0807b-7b34-4f4b-ad95-2e9e31f4f291", + "name": "Svakom Sam Neo 2" + } + }, + "svakom-suitcase": { + "communication": [ + { + "btle": { + "names": [ + "VX357A-BLE-V1.0", + "VX236A-BLE-V1.0" + ], + "services": { + "0000ffe0-0000-1000-8000-00805f9b34fb": { + "rx": "0000ffe2-0000-1000-8000-00805f9b34fb", + "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "e3187cb5-6370-4d29-8850-2d9206889f64", + "identifier": [ + "VX236A-BLE-V1.0" + ], + "name": "Coleur Dor VX236A" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "34836d30-2d4f-4c89-ab42-88dd227f14f0", + "output": { + "Vibrate": { + "step-range": [ + 0, + 30 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "190fc9a8-8d55-45c5-98e0-921246ccbb7d", + "output": { + "Vibrate": { + "step-range": [ + 0, + 1 + ] + } + } + } + ], + "id": "ffefddb3-5697-4ff1-a064-5d33c6f9b214", + "name": "Svakom Magic Suitcase" + } + }, + "svakom-tarax": { + "communication": [ + { + "btle": { + "names": [ + "SX218A" + ], + "services": { + "0000ffe0-0000-1000-8000-00805f9b34fb": { + "rx": "0000ffe2-0000-1000-8000-00805f9b34fb", + "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "description": "Internal vibrator", + "feature-type": "Vibrate", + "id": "8638eed8-37ec-4c54-aa06-a8dd3a832057", + "output": { + "Vibrate": { + "step-range": [ + 0, + 3 + ] + } + } + }, + { + "description": "External pulsator", + "feature-type": "Vibrate", + "id": "a2ad09c0-0042-4f29-875f-464fb83ca916", + "output": { + "Vibrate": { + "step-range": [ + 0, + 3 + ] + } + } + } + ], + "id": "870f69ff-45db-4a13-96e7-1915eef6ac59", + "name": "ToyCod Tara X" + } + }, + "svakom-v1": { + "communication": [ + { + "btle": { + "names": [ + "Aogu SUV", + "Aogu SCB", + "Emma NEO", + "Phoenix NEO" + ], + "services": { + "0000ffe0-0000-1000-8000-00805f9b34fb": { + "rx": "0000ffe2-0000-1000-8000-00805f9b34fb", + "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "46a3fb4f-5e26-45c0-9fd1-176ec896048c", + "identifier": [ + "Aogu SCB" + ], + "name": "Svakom Ella" + }, + { + "id": "c9556aba-5bda-4f23-a690-623c4b9ee04b", + "identifier": [ + "Phoenix NEO" + ], + "name": "Svakom Phoenix Neo" + }, + { + "id": "68d39a06-e350-47ef-8834-e3197178b00e", + "identifier": [ + "Emma NEO" + ], + "name": "Svakom Emma Neo" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "22eb4b95-60f9-4885-80e7-279d02d59804", + "output": { + "Vibrate": { + "step-range": [ + 0, + 19 + ] + } + } + } + ], + "id": "77a1dde5-f31a-4fcb-972b-8094181c187f", + "name": "Svakom Device" + } + }, + "svakom-v2": { + "communication": [ + { + "btle": { + "names": [ + "116", + "117", + "Edeny", + "118", + "Viviana", + "Ella NEO", + "S38A", + "Vick NEO", + "Vick Neo", + "STG05A", + "QH-SJ007A", + "Cici 2", + "Emma Neo 2" + ], + "services": { + "0000ffe0-0000-1000-8000-00805f9b34fb": { + "rx": "0000ffe2-0000-1000-8000-00805f9b34fb", + "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "11905923-4084-4efb-9ac3-a6eba2bf4190", + "identifier": [ + "116" + ], + "name": "Svakom Phoenix Neo" + }, + { + "id": "4bbda06f-ca32-4d34-a11f-d91d8987dc6d", + "identifier": [ + "Viviana" + ], + "name": "Svakom Viviana" + }, + { + "id": "87419e85-5570-41f0-84f2-7f15b138326d", + "identifier": [ + "Ella NEO" + ], + "name": "Svakom Ella Neo" + }, + { + "id": "448ee908-2abc-46cb-aa3f-732830a25139", + "identifier": [ + "117", + "Edeny" + ], + "name": "Svakom Edeny" + }, + { + "id": "b2c3e1ed-0c66-49d7-859d-7c9677c66297", + "identifier": [ + "S38A" + ], + "name": "Svakom Tammy Pro" + }, + { + "id": "c37b8380-dd41-4fd1-8310-8c24230658bf", + "identifier": [ + "Vick NEO", + "Vick Neo" + ], + "name": "Svakom Vick Neo" + }, + { + "id": "63893174-b1fd-4ad3-940f-fbbb939ffa57", + "identifier": [ + "STG05A" + ], + "name": "Svakom Aravinda" + }, + { + "id": "a61ae863-a8fc-4708-b313-b36385926dbf", + "identifier": [ + "118" + ], + "name": "ToyCod Vanesia" + }, + { + "id": "f0609171-5e85-4800-adee-a43ef2e3826a", + "identifier": [ + "QH-SJ007A" + ], + "name": "Svakom Winni 2" + }, + { + "id": "5c03568c-9318-4648-b149-b0fc716d5605", + "identifier": [ + "Cici 2" + ], + "name": "Svakom Cici 2" + }, + { + "id": "a3c23c99-09e7-47d4-898b-9581dfc1f28b", + "identifier": [ + "Emma Neo 2" + ], + "name": "Svakom Emma Neo 2" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "4a225b9d-94c6-437a-a038-3deb4ded5bc5", + "output": { + "Vibrate": { + "step-range": [ + 0, + 10 + ] + } + } + } + ], + "id": "b1189537-2ef1-452b-b6b8-e8e0ba823156", + "name": "Svakom Device v2" + } + }, + "svakom-v3": { + "communication": [ + { + "btle": { + "names": [ + "Phoenix Neo 2", + "FK008A", + "Hannes NEO", + "QH-SX007E" + ], + "services": { + "0000ffe0-0000-1000-8000-00805f9b34fb": { + "rx": "0000ffe2-0000-1000-8000-00805f9b34fb", + "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "14a51507-e4c8-4433-a87b-0a0464c00e31", + "identifier": [ + "Phoenix Neo 2" + ], + "name": "Svakom Phoenix Neo 2" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "737fe419-62fa-4e1b-b6d0-2684cbe8b31f", + "output": { + "Vibrate": { + "step-range": [ + 0, + 10 + ] + } + } + }, + { + "feature-type": "Rotate", + "id": "5e612940-1d00-4680-aa3a-1b052755a01d", + "output": { + "Rotate": { + "step-range": [ + 0, + 1 + ] + } + } + } + ], + "id": "cdd17d02-603a-4a86-af6b-f2c97d09ed84", + "identifier": [ + "FK008A" + ], + "name": "Fantasy Cup Theodore" + }, + { + "id": "d2fda3c5-fa1f-45b5-8f98-a9c33e83922d", + "identifier": [ + "Hannes NEO" + ], + "name": "Svakom Hannes Neo" + }, + { + "features": [ + { + "description": "Vibrating attachments", + "feature-type": "Vibrate", + "id": "1859c6fa-1d2f-46c8-b97c-75a7ca62be8c", + "output": { + "Vibrate": { + "step-range": [ + 0, + 10 + ] + } + } + }, + { + "description": "Suction lens", + "feature-type": "Vibrate", + "id": "63b84610-b32b-4526-a29a-4acb9ad4939d", + "output": { + "Vibrate": { + "step-range": [ + 0, + 1 + ] + } + } + } + ], + "id": "4e1d7b1c-133d-4d7c-9cfe-f4c4e5d0ca01", + "identifier": [ + "QH-SX007E" + ], + "name": "Svakom Alberta" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "1e03f6a5-0197-4a5e-afb5-dcc1266c6a6e", + "output": { + "Vibrate": { + "step-range": [ + 0, + 10 + ] + } + } + } + ], + "id": "58212e06-d13e-461d-a8cd-5bd06cbe5d0c", + "name": "Svakom Device v3" + } + }, + "svakom-v4": { + "communication": [ + { + "btle": { + "names": [ + "B2CM6", + "ERICA", + "Cici+ 2", + "VV468A" + ], + "services": { + "0000ffe0-0000-1000-8000-00805f9b34fb": { + "rx": "0000ffe2-0000-1000-8000-00805f9b34fb", + "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "2e46e18b-5821-4665-9b07-928f4963f16d", + "identifier": [ + "B2CM6" + ], + "name": "ToyCod Barzillai" + }, + { + "id": "22c2f70c-44fa-482f-bfac-1463482bff5d", + "identifier": [ + "ERICA" + ], + "name": "Svakom Erica" + }, + { + "id": "96980e8b-abcf-410e-94e6-d098b13e6192", + "identifier": [ + "Cici+ 2" + ], + "name": "Svakom Cici+ 2" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "65f4d628-cb50-48fa-8d51-39433244ce12", + "output": { + "Vibrate": { + "step-range": [ + 0, + 10 + ] + } + } + } + ], + "id": "8d88601b-6791-4947-94ca-e9d66086f57e", + "identifier": [ + "VV468A" + ], + "name": "ToyCod Clara" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "b61f8bde-2ad3-40a8-8e16-fe6dcec8a887", + "output": { + "Vibrate": { + "step-range": [ + 0, + 10 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "724c247f-733e-4592-9a98-1a37a7c941ba", + "output": { + "Vibrate": { + "step-range": [ + 0, + 10 + ] + } + } + } + ], + "id": "1a43cd07-e5ba-4a9f-8560-d00e1d72c6df", + "name": "Svakom Device v4" + } + }, + "svakom-v5": { + "communication": [ + { + "btle": { + "names": [ + "Chika", + "Mora Neo", + "Trysta Neo", + "Mini Emma Neo" + ], + "services": { + "0000ffe0-0000-1000-8000-00805f9b34fb": { + "rx": "0000ffe2-0000-1000-8000-00805f9b34fb", + "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "4ca8c463-03fc-421d-ab03-27ed6f4283da", + "identifier": [ + "Chika" + ], + "name": "Svakom Chika" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "7d13d266-a8f3-49b5-94d2-ac6242c40b7a", + "output": { + "Vibrate": { + "step-range": [ + 0, + 10 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "3b4e80ae-3ec6-4bb7-aba9-1dc48dd1614b", + "output": { + "Vibrate": { + "step-range": [ + 0, + 10 + ] + } + } + }, + { + "feature-type": "Oscillate", + "id": "41ecfb09-8b4c-4ec1-9f7a-29b9ff1097f7", + "output": { + "Oscillate": { + "step-range": [ + 0, + 3 + ] + } + } + } + ], + "id": "b647f340-bcd1-4d9e-88ac-e064ce86b1ac", + "identifier": [ + "Mora Neo" + ], + "name": "Svakom Mora Neo" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "655ec2b3-ede8-4051-96da-c40eed164372", + "output": { + "Vibrate": { + "step-range": [ + 0, + 10 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "4cc06c03-36d9-4b10-9d51-46417b0d7f3d", + "output": { + "Vibrate": { + "step-range": [ + 0, + 10 + ] + } + } + }, + { + "feature-type": "Oscillate", + "id": "f62fea13-0dfb-4706-8122-9104abf9dca5", + "output": { + "Oscillate": { + "step-range": [ + 0, + 3 + ] + } + } + } + ], + "id": "66d5aa90-b2aa-4552-9777-cbb80aae2b9f", + "identifier": [ + "Trysta Neo" + ], + "name": "Svakom Trysta Neo" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "d957a257-9ae2-45f1-80b2-dbcc4dc2886b", + "output": { + "Vibrate": { + "step-range": [ + 0, + 10 + ] + } + } + } + ], + "id": "396d37c3-dc1e-473d-85ca-95bd9583d9f5", + "identifier": [ + "Mini Emma Neo" + ], + "name": "Svakom Mini Emma Neo" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "4f672189-8169-4114-92cd-ed7f74427548", + "output": { + "Vibrate": { + "step-range": [ + 0, + 10 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "bdd5e445-0d53-47c9-9b9e-c60b83d821fd", + "output": { + "Vibrate": { + "step-range": [ + 0, + 10 + ] + } + } + } + ], + "id": "9b304bb1-b961-4948-937e-4e3ee1b429b0", + "name": "Svakom Device v5" + } + }, + "svakom-v6": { + "communication": [ + { + "btle": { + "names": [ + "CocoPro", + "Echo 2", + "Vick Neo 2", + "Iker Neo", + "VA617A-3", + "VA617A-4" + ], + "services": { + "0000ffe0-0000-1000-8000-00805f9b34fb": { + "rx": "0000ffe2-0000-1000-8000-00805f9b34fb", + "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "4901a610-9b63-47a1-a99a-521ac76e7f99", + "identifier": [ + "CocoPro" + ], + "name": "Svakom Coco Pro" + }, + { + "id": "2613c099-f89f-4936-a26b-e751c8b3be28", + "identifier": [ + "Echo 2" + ], + "name": "Svakom Echo 2" + }, + { + "id": "23bba8d3-9a07-42fc-8606-b165837adbcb", + "identifier": [ + "VA617A-3" + ], + "name": "BeYourLover Naughty Clock Vibrator" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "5ac07e29-37f4-4a7a-8a35-f5b2b59f3dbd", + "output": { + "Vibrate": { + "step-range": [ + 0, + 10 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "263e051e-ed79-4245-b222-2d4888483849", + "output": { + "Vibrate": { + "step-range": [ + 0, + 10 + ] + } + } + } + ], + "id": "f46a3f0e-9d3e-4e5f-9343-bbc0acc8a095", + "identifier": [ + "Vick Neo 2" + ], + "name": "Svakom Vick Neo 2" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "c19b776a-363d-4468-80ec-09bc22ebd06c", + "output": { + "Vibrate": { + "step-range": [ + 0, + 10 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "cbdd56a3-1954-4db0-98c7-535096637868", + "output": { + "Vibrate": { + "step-range": [ + 0, + 10 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "b310a28e-0109-4573-bf4a-259845c518fd", + "output": { + "Vibrate": { + "step-range": [ + 0, + 5 + ] + } + } + } + ], + "id": "2c295a1b-8a26-47dc-9d9c-95961e1cca1b", + "identifier": [ + "Iker Neo" + ], + "name": "Svakom Iker Neo" + }, + { + "features": [ + { + "feature-type": "Constrict", + "id": "38708bd1-466e-48e7-8721-8844aa177959", + "output": { + "Vibrate": { + "step-range": [ + 0, + 10 + ] + } + } + } + ], + "id": "1e587721-7e91-44b2-9612-f9cfd88389fc", + "identifier": [ + "VA617A-4" + ], + "name": "BeYourLover Naughty Clock Sucker" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "5f1d84f8-a44a-43dc-b6f6-8e8682909ff1", + "output": { + "Vibrate": { + "step-range": [ + 0, + 10 + ] + } + } + } + ], + "id": "eafe3786-e15a-4a4d-9b85-bc6e4069c339", + "name": "Svakom Device v6" + } + }, + "synchro": { + "communication": [ + { + "btle": { + "names": [ + "Shinkuro", + "synchro2", + "synchro EX" + ], + "services": { + "0000ffe0-0000-1000-8000-00805f9b34fb": { + "tx": "0000ffe1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "3535446e-779a-496b-8404-e895878cf3e1", + "identifier": [ + "synchro EX" + ], + "name": "Synchro Exchange" + } + ], + "defaults": { + "features": [ + { + "feature-type": "RotateWithDirection", + "id": "b7495351-9101-448a-94c4-4598cf541dca", + "output": { + "RotateWithDirection": { + "step-range": [ + 0, + 6 + ] + } + } + } + ], + "id": "f912a283-7308-4e56-a508-4d47d9caf7d2", + "name": "Synchro" + } + }, + "tcode-v03": { + "communication": [ + { + "serial": { + "baud-rate": 115200, + "data-bits": 8, + "parity": "N", + "port": "default", + "stop-bits": 1 + } + }, + { + "udp": { + "address": "default", + "port": 8000 + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "PositionWithDuration", + "id": "a6e25b9d-4986-4771-8e8c-579ebb472844", + "output": { + "PositionWithDuration": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "211da02e-467c-4788-96bd-689049867e85", + "name": "TCode v0.3 (Single Linear Axis)" + } + }, + "thehandy": { + "communication": [ + { + "btle": { + "names": [ + "The Handy" + ], + "services": { + "1775244d-6b43-439b-877c-060f2d9bed07": { + "firmware": "1775ff51-6b43-439b-877c-060f2d9bed07", + "tx": "1775ff55-6b43-439b-877c-060f2d9bed07" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "PositionWithDuration", + "id": "32309a60-f980-490d-a5f4-467ccae2d586", + "output": { + "PositionWithDuration": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "fc9de0ed-0f9f-402e-a1b5-4d1865e7b87b", + "name": "The Handy" + } + }, + "tryfun": { + "communication": [ + { + "btle": { + "names": [ + "TRYFUN-ONE", + "TF-SPRAY" + ], + "services": { + "0000ff10-0000-1000-8000-00805f9b34fb": { + "tx": "0000fff1-0000-1000-8000-00805f9b34fb" + }, + "0000ffac-0000-1000-8000-00805f9b34fb": { + "tx": "0000ffb5-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "features": [ + { + "feature-type": "Vibrate", + "id": "b9d4420b-9a94-4ea2-8b76-3445d06049f2", + "output": { + "Vibrate": { + "step-range": [ + 0, + 4 + ] + } + } + } + ], + "id": "2cf375ae-7ae9-4d76-be3b-58eff84b67ae", + "identifier": [ + "TF-SPRAY" + ], + "name": "TryFun Surge Pro" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Oscillate", + "id": "e4957d32-e069-4c35-ae3f-e3cce3de6b49", + "output": { + "Oscillate": { + "step-range": [ + 0, + 9 + ] + } + } + }, + { + "feature-type": "Rotate", + "id": "0346e667-8ea2-4cde-80d4-88d498d1ee17", + "output": { + "Rotate": { + "step-range": [ + 0, + 9 + ] + } + } + } + ], + "id": "9b4afa16-a7cf-4fdb-bb95-5f91125ba7e1", + "name": "TryFun Yuan Series" + } + }, + "tryfun-blackhole": { + "communication": [ + { + "btle": { + "names": [ + "TF-BHPLUS" + ], + "services": { + "0000ffac-0000-1000-8000-00805f9b34fb": { + "tx": "0000ffb7-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Oscillate", + "id": "3bf4453c-8ca3-42e5-82c6-409d85cdbacf", + "output": { + "Oscillate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "e10533e6-9aac-4a71-99c1-0b44378d9f06", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "074de6cc-7aee-4b33-8d14-474a61d26548", + "name": "TryFun Black Hole Plus" + } + }, + "tryfun-meta2": { + "communication": [ + { + "btle": { + "names": [ + "TF-META2" + ], + "services": { + "0000ffac-0000-1000-8000-00805f9b34fb": { + "tx": "0000ffb7-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Oscillate", + "id": "0773790b-b629-46b7-af2a-174d75c53fe3", + "output": { + "Oscillate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "bf8f3a67-3403-4d57-90e3-027804c57c4e", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + }, + { + "feature-type": "RotateWithDirection", + "id": "26402ebe-7ee0-4c7d-ae40-205ec4f3a1b0", + "output": { + "RotateWithDirection": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "6b45e5f8-5b23-4c1d-a478-43c17a54cae3", + "name": "TryFun Meta 2" + } + }, + "twerkingbutt": { + "communication": [ + { + "btle": { + "names": [ + "BODIKANG", + "Twerking Butt", + "TwerkingButt" + ], + "services": { + "00000a60-0000-1000-8000-00805f9b34fb": { + "rx": "00000a67-0000-1000-8000-00805f9b34fb", + "tx": "00000a66-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [], + "id": "83e29d7a-6f35-499a-90f8-dfba8b674379", + "name": "Twerking Butt" + } + }, + "vibcrafter": { + "communication": [ + { + "btle": { + "names": [ + "be gentle", + "Janna", + "Hayden", + "Nidalee" + ], + "services": { + "53300051-0060-4bd4-bbe5-a6920e4c5663": { + "rx": "53300053-0060-4bd4-bbe5-a6920e4c5663", + "tx": "53300052-0060-4bd4-bbe5-a6920e4c5663" + } + } + } + } + ], + "configurations": [ + { + "id": "687972b8-e52d-4ce8-8b16-b6d24585915b", + "identifier": [ + "be gentle" + ], + "name": "VibCrafter Harlow" + }, + { + "id": "4006a4fd-2a7a-417e-b64a-66f43ba28b9e", + "identifier": [ + "Hayden" + ], + "name": "VibCrafter Hayden" + }, + { + "id": "3e1e3e00-771b-4657-8450-6e314eed24b3", + "identifier": [ + "Nidalee" + ], + "name": "VibCrafter Nidalee" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "51e20287-006c-4dc9-941a-346b8f960715", + "output": { + "Vibrate": { + "step-range": [ + 0, + 99 + ] + } + } + } + ], + "id": "cb0756c3-111c-463b-a575-edc9204af528", + "identifier": [ + "Janna" + ], + "name": "VibCrafter Janna" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "343a8e18-b76c-4482-b048-32d762bf87c9", + "output": { + "Vibrate": { + "step-range": [ + 0, + 99 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "d92a031e-bd0d-4815-a0bd-6c59566dcce2", + "output": { + "Vibrate": { + "step-range": [ + 0, + 99 + ] + } + } + } + ], + "id": "a44eef0e-b412-44d0-9545-a4b7b0298514", + "name": "VibCrafter Device" + } + }, + "vibratissimo": { + "communication": [ + { + "btle": { + "names": [ + "Vibratissimo" + ], + "services": { + "00001523-1212-efde-1523-785feabcd123": { + "rx": "00001527-1212-efde-1523-785feabcd123", + "txmode": "00001524-1212-efde-1523-785feabcd123", + "txvibrate": "00001526-1212-efde-1523-785feabcd123" + }, + "0000180a-0000-1000-8000-00805f9b34fb": { + "rxblemodel": "00002a24-0000-1000-8000-00805f9b34fb" + }, + "0000180f-0000-1000-8000-00805f9b34fb": { + "rxblebattery": "00002a19-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "features": [ + { + "feature-type": "Vibrate", + "id": "75aa2f87-0d7b-4df1-a661-dd270e92fdd8", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "56fbae53-c57e-4eed-978c-dcf3279b228b", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "0f194120-0912-4d5d-b201-7eee4cc622fe", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "c0f02f4f-5bbb-40ad-94fc-7d81c74c518c", + "identifier": [ + "Licker", + "SecretKiss", + "Womenizer" + ], + "name": "Vibratissimo Licker" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "675d6ccc-8145-40d2-a901-0b683cf8233b", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "c0009e3f-4263-4761-9168-17c9d81479ee", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "16b15667-1598-4194-86b3-7e711f88adab", + "output": { + "Vibrate": { + "step-range": [ + 0, + 2 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "e70bb6fb-9e2c-4970-9483-9f9b661d6e9f", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "2fa1c5bc-85ff-45d5-ada5-23986ad3eab9", + "identifier": [ + "Rabbit" + ], + "name": "Vibratissimo Rabbit" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "c4978273-df69-41b1-8ecd-0b5cdbb6d102", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + }, + { + "description": "Battery Level", + "feature-type": "Battery", + "id": "e0d0a8e6-604a-4d49-bdab-d22fd8658c69", + "input": { + "Battery": { + "input-commands": [ + "Read" + ], + "value-range": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "4b82b175-c139-4af2-b5ad-aa576d9d01a4", + "name": "Vibratissimo Device" + } + }, + "vorze-cyclone-x": { + "communication": [ + { + "hid": { + "pairs": [ + { + "product-id": 22352, + "vendor-id": 1155 + } + ] + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "RotateWithDirection", + "id": "1d1b4dea-ab29-4426-a9f4-dda2c594eefb", + "output": { + "RotateWithDirection": { + "step-range": [ + 0, + 10 + ] + } + } + } + ], + "id": "ac27ce47-6d49-4c43-ac6f-01a19e546305", + "name": "Vorze Cyclone X10 Device" + } + }, + "vorze-sa": { + "communication": [ + { + "btle": { + "names": [ + "Bach smart", + "CycSA", + "UFOSA", + "UFO-TW", + "VorzePiston", + "ROCKET" + ], + "services": { + "40ee1111-63ec-4b7f-8ce7-712efd55b90e": { + "tx": "40ee2222-63ec-4b7f-8ce7-712efd55b90e" + } + } + } + } + ], + "configurations": [ + { + "features": [ + { + "feature-type": "Vibrate", + "id": "447dbcfa-c295-4880-afba-93e24499a78d", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "2923a929-572c-472a-be12-ff5970f0b2b7", + "identifier": [ + "Bach smart" + ], + "name": "Vorze Bach", + "protocol-variant": "vorze-sa-vibrator" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "557d3c89-2e15-4b4a-8480-07f4826a8384", + "output": { + "Vibrate": { + "step-range": [ + 0, + 100 + ] + } + } + } + ], + "id": "756f590f-d2aa-4a4c-ac80-e4ac75a14f15", + "identifier": [ + "ROCKET" + ], + "name": "Adult Festa Rocket", + "protocol-variant": "vorze-sa-vibrator" + }, + { + "features": [ + { + "feature-type": "RotateWithDirection", + "id": "8e249d53-8d80-4f42-bc40-e6edb7779e92", + "output": { + "RotateWithDirection": { + "step-range": [ + 0, + 99 + ] + } + } + } + ], + "id": "390a0e30-0b5f-4b6c-88b4-e4f16383b8a3", + "identifier": [ + "CycSA" + ], + "name": "Vorze A10 Cyclone SA", + "protocol-variant": "vorze-sa-single-rotator" + }, + { + "features": [ + { + "feature-type": "RotateWithDirection", + "id": "2d8d1443-c394-4df4-b9bb-1659d8323b45", + "output": { + "RotateWithDirection": { + "step-range": [ + 0, + 99 + ] + } + } + } + ], + "id": "2ab3b09b-1020-4dcf-86f1-ecd9d5b40ce2", + "identifier": [ + "UFOSA" + ], + "name": "Vorze UFO SA", + "protocol-variant": "vorze-sa-single-rotator" + }, + { + "features": [ + { + "feature-type": "RotateWithDirection", + "id": "a1632ce4-314f-481d-9ae2-2a11a0c4caa4", + "output": { + "RotateWithDirection": { + "step-range": [ + 0, + 99 + ] + } + } + }, + { + "feature-type": "RotateWithDirection", + "id": "4b09a02d-9a4a-4c8b-8340-8e6ca3cecfc2", + "output": { + "RotateWithDirection": { + "step-range": [ + 0, + 99 + ] + } + } + } + ], + "id": "32e92986-3ae4-45f3-9aec-05d6028f1cb7", + "identifier": [ + "UFO-TW" + ], + "name": "Vorze UFO TW", + "protocol-variant": "vorze-sa-dual-rotator" + }, + { + "features": [ + { + "feature-type": "PositionWithDuration", + "id": "7c8d7a1d-9e2f-4a92-83f3-42a0840b90bd", + "output": { + "PositionWithDuration": { + "step-range": [ + 0, + 99 + ] + } + } + } + ], + "id": "b1b17b07-c5b8-4db4-97c4-ef1597cf2e59", + "identifier": [ + "VorzePiston" + ], + "name": "Vorze Piston", + "protocol-variant": "vorze-sa-piston" + } + ], + "defaults": { + "features": [], + "id": "3ed42429-379c-4f48-926e-f297cbe69258", + "name": "Vorze Device" + } + }, + "wetoy": { + "communication": [ + { + "btle": { + "names": [ + "WeToy" + ], + "services": { + "0000fff0-0000-1000-8000-00805f9b34fb": { + "tx": "0000fff3-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "693b0fbc-eee5-4948-b8f4-aa264a78bcc2", + "output": { + "Vibrate": { + "step-range": [ + 0, + 3 + ] + } + } + } + ], + "id": "1c7420e2-1af5-4b1c-8247-6a3702eb2335", + "name": "WeToy MiNa" + } + }, + "wevibe": { + "communication": [ + { + "btle": { + "names": [ + "Cougar", + "4 Plus", + "4_Plus", + "4plus", + "Bloom", + "classic", + "Classic", + "Ditto", + "Gala", + "Jive", + "Nova", + "Pivot", + "Rave", + "Sync", + "Verge", + "Wish" + ], + "services": { + "f000bb03-0451-4000-b000-000000000000": { + "rx": "f000b000-0451-4000-b000-000000000000", + "tx": "f000c000-0451-4000-b000-000000000000" + } + } + } + } + ], + "configurations": [ + { + "id": "cb8bf4cd-b6bd-4499-b977-faf4e2bb9d4e", + "identifier": [ + "Bloom" + ], + "name": "WeVibe Bloom" + }, + { + "id": "0b9e22e7-b79c-4d26-b902-287436673da4", + "identifier": [ + "Ditto" + ], + "name": "WeVibe Ditto" + }, + { + "id": "0d361883-2894-42dd-9268-b36a067564a6", + "identifier": [ + "Jive" + ], + "name": "WeVibe Jive" + }, + { + "id": "5fca5cd6-6336-4eec-bdfc-048266d9f409", + "identifier": [ + "Pivot" + ], + "name": "WeVibe Pivot" + }, + { + "id": "534f442f-396c-4379-b3d0-9c001bcd2891", + "identifier": [ + "Rave" + ], + "name": "WeVibe Rave" + }, + { + "id": "6b31404c-c609-4d75-a312-191c0f7f6a9f", + "identifier": [ + "Verge" + ], + "name": "WeVibe Verge" + }, + { + "id": "a7a85b12-bac4-49da-9d1e-0f5bc739fd3e", + "identifier": [ + "Wish" + ], + "name": "WeVibe Wish" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "c76fd58e-a38c-4f25-a04c-d798e3f892d3", + "output": { + "Vibrate": { + "step-range": [ + 0, + 15 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "027061c3-4d18-4d03-8219-13e3134b8a19", + "output": { + "Vibrate": { + "step-range": [ + 0, + 15 + ] + } + } + } + ], + "id": "11cd7b68-2c94-4fc8-837f-09d47214cee1", + "identifier": [ + "Cougar", + "4 Plus", + "4_Plus", + "4plus", + "classic", + "Classic" + ], + "name": "WeVibe 4 Plus" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "22386dcd-b409-49d2-be03-ad270eae92c4", + "output": { + "Vibrate": { + "step-range": [ + 0, + 15 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "46f2d671-5bbf-49c0-928e-4a8b3cdd892b", + "output": { + "Vibrate": { + "step-range": [ + 0, + 15 + ] + } + } + } + ], + "id": "400ef30a-63eb-4648-b293-c7ecc874f509", + "identifier": [ + "Gala" + ], + "name": "WeVibe Gala" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "e609247a-8c12-422e-8df7-e03373bdbf7a", + "output": { + "Vibrate": { + "step-range": [ + 0, + 15 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "c84081f5-3a72-473a-b2b3-32500014b308", + "output": { + "Vibrate": { + "step-range": [ + 0, + 15 + ] + } + } + } + ], + "id": "b667bb6a-46b1-4534-8c79-83aa0749028a", + "identifier": [ + "Nova" + ], + "name": "WeVibe Nova" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "283b2826-80e3-455f-bec6-7800ebaf2c96", + "output": { + "Vibrate": { + "step-range": [ + 0, + 15 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "64f00297-e4ef-4059-a622-c0bea33d4379", + "output": { + "Vibrate": { + "step-range": [ + 0, + 15 + ] + } + } + } + ], + "id": "0e72dab3-4b87-4bae-ae02-aae0bbb0f035", + "identifier": [ + "Sync" + ], + "name": "WeVibe Sync" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "6c0184bc-93b8-41a9-a976-934256dcdf9d", + "output": { + "Vibrate": { + "step-range": [ + 0, + 15 + ] + } + } + } + ], + "id": "d42dc8a1-bb70-4dd6-b792-710248c00c6e", + "name": "WeVibe Device" + } + }, + "wevibe-8bit": { + "communication": [ + { + "btle": { + "names": [ + "Melt", + "Moxie", + "Vector", + "Wand", + "Wand 2", + "Bond", + "Nelson", + "Nova2", + "Nova_2", + "Nova 2", + "Jive 2" + ], + "services": { + "f000bb03-0451-4000-b000-000000000000": { + "rx": "f000b000-0451-4000-b000-000000000000", + "tx": "f000c000-0451-4000-b000-000000000000" + } + } + } + } + ], + "configurations": [ + { + "features": [ + { + "feature-type": "Vibrate", + "id": "fdf47cba-4429-4944-9bb4-1db4facb8d29", + "output": { + "Vibrate": { + "step-range": [ + 0, + 22 + ] + } + } + } + ], + "id": "4f73e55c-bea8-4069-8409-cba30fbbfc81", + "identifier": [ + "Melt" + ], + "name": "WeVibe Melt" + }, + { + "id": "d29641cb-953a-4d5c-8b43-ba481db2dd42", + "identifier": [ + "Moxie" + ], + "name": "WeVibe Moxie" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "8828bbe0-acf0-4529-9f33-276b23a14afd", + "output": { + "Vibrate": { + "step-range": [ + 0, + 12 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "12702494-a0e9-4929-b928-050d47391cb5", + "output": { + "Vibrate": { + "step-range": [ + 0, + 12 + ] + } + } + } + ], + "id": "52482637-708c-455b-b96b-d4d58af04562", + "identifier": [ + "Vector" + ], + "name": "WeVibe Vector" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "2377d39d-580c-46ea-831c-bb9cb97899d7", + "output": { + "Vibrate": { + "step-range": [ + 0, + 22 + ] + } + } + } + ], + "id": "3829ad7c-be90-49ce-9ecc-fdafa18be3bb", + "identifier": [ + "Wand" + ], + "name": "WeVibe Wand" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "4d92cf70-e464-435c-897e-fd2cd5a918e9", + "output": { + "Vibrate": { + "step-range": [ + 0, + 22 + ] + } + } + } + ], + "id": "3db74c3e-50e1-4dbf-a670-c7297ca52f62", + "identifier": [ + "Wand 2" + ], + "name": "WeVibe Wand 2" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "240a36e0-4791-4676-aa3b-d1c407db2b1b", + "output": { + "Vibrate": { + "step-range": [ + 0, + 27 + ] + } + } + } + ], + "id": "c4b2ecb2-655d-44d9-bfaf-03f314acd3a2", + "identifier": [ + "Bond", + "Nelson" + ], + "name": "WeVibe Bond" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "22172834-1186-4ba2-b221-23f02c3fbd51", + "output": { + "Vibrate": { + "step-range": [ + 0, + 27 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "0972ba1f-0b0e-4738-a050-5333da537b35", + "output": { + "Vibrate": { + "step-range": [ + 0, + 27 + ] + } + } + } + ], + "id": "2292e221-0f17-4d55-8697-f6abebf04ee5", + "identifier": [ + "Nova2", + "Nova_2", + "Nova 2" + ], + "name": "WeVibe Nova 2" + }, + { + "id": "4a0d8ff9-db32-41c7-99e5-8bb005a25bd0", + "identifier": [ + "Jive 2" + ], + "name": "WeVibe Jive 2" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "7b226142-d713-41cd-872a-aea10527482b", + "output": { + "Vibrate": { + "step-range": [ + 0, + 12 + ] + } + } + } + ], + "id": "527527b1-7bf2-40cb-b086-003af792f03f", + "name": "WeVibe 8-bit Device" + } + }, + "wevibe-chorus": { + "communication": [ + { + "btle": { + "names": [ + "Chorus", + "skeena", + "Sync 2", + "Sync Lite" + ], + "services": { + "f000bb03-0451-4000-b000-000000000000": { + "rx": "f000b000-0451-4000-b000-000000000000", + "tx": "f000c000-0451-4000-b000-000000000000" + } + } + } + } + ], + "configurations": [ + { + "features": [ + { + "feature-type": "Vibrate", + "id": "db4d008b-530e-4b8b-937a-bd4e5df4058c", + "output": { + "Vibrate": { + "step-range": [ + 0, + 30 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "27c95f7a-91e7-46c9-90c2-b3d37ed20d6d", + "output": { + "Vibrate": { + "step-range": [ + 0, + 30 + ] + } + } + } + ], + "id": "3d5f001f-d3c0-44d5-9a6a-e4c8e7beb2e1", + "identifier": [ + "Sync 2" + ], + "name": "WeVibe Sync 2" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "62316419-7c01-4ce2-8086-0ca210d26b25", + "output": { + "Vibrate": { + "step-range": [ + 0, + 30 + ] + } + } + } + ], + "id": "36640498-e77c-46f5-9f94-a1b90148f939", + "identifier": [ + "Sync Lite" + ], + "name": "WeVibe Sync Lite" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "52a3c84e-28d4-4750-9a7e-a8618ded617e", + "output": { + "Vibrate": { + "step-range": [ + 0, + 30 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "4aa54a5f-2b85-4178-b671-f4198acf3daf", + "output": { + "Vibrate": { + "step-range": [ + 0, + 30 + ] + } + } + } + ], + "id": "5228aefe-bc48-445c-8129-48c3cebf6729", + "name": "WeVibe Chorus" + } + }, + "xibao": { + "communication": [ + { + "btle": { + "names": [ + "CCYB_*" + ], + "services": { + "0000fff0-0000-1000-8000-00805f9b34fb": { + "tx": "0000fff2-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Oscillate", + "id": "c91a5d82-547c-4bcb-8cd9-1a5085253d11", + "output": { + "Oscillate": { + "step-range": [ + 0, + 99 + ] + } + } + } + ], + "id": "3a3dd2ec-01d9-48d2-afbf-a969c33a147c", + "name": "Xibao Smart Masturbation Cup" + } + }, + "xinput": { + "communication": [ + { + "xinput": { + "exists": true + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "eded54a0-9ef2-49e1-99ec-7ab0ae606604", + "output": { + "Vibrate": { + "step-range": [ + 0, + 65535 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "13b25ae7-4c84-4e9c-bd3e-c2f835bd3edb", + "output": { + "Vibrate": { + "step-range": [ + 0, + 65535 + ] + } + } + } + ], + "id": "0e7844fb-ff3d-4f5d-9e86-03b20f120f94", + "name": "XBox (XInput) Compatible Gamepad" + } + }, + "xiuxiuda": { + "communication": [ + { + "btle": { + "names": [ + "XXD-Lush*" + ], + "services": { + "53300001-0023-4bd4-bbd5-a6920e4c5653": { + "tx": "53300003-0023-4bd4-bbd5-a6920e4c5653" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "da1eb27b-6159-40f8-9662-69d9ca77f768", + "output": { + "Vibrate": { + "step-range": [ + 0, + 19 + ] + } + } + } + ], + "id": "2982ea67-a59f-4490-9a7c-23583a4ec642", + "name": "Xiuxiuda Device" + } + }, + "xuanhuan": { + "communication": [ + { + "btle": { + "names": [ + "QUXIN" + ], + "services": { + "0000fffe-0000-1000-8000-00805f9b34fb": { + "tx": "0000fe02-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "b52a4a37-3eae-40da-a4c2-abe546934900", + "output": { + "Vibrate": { + "step-range": [ + 0, + 10 + ] + } + } + } + ], + "id": "60b567f2-8b50-4673-a295-6dda343a7029", + "name": "Xuanhuan Masturbator" + } + }, + "youcups": { + "communication": [ + { + "btle": { + "names": [ + "Youcups" + ], + "services": { + "0000fee9-0000-1000-8000-00805f9b34fb": { + "tx": "d44bc439-abfd-45a2-b575-925416129600" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "d0c286dc-2608-4f8a-a621-3f65927ed57e", + "output": { + "Vibrate": { + "step-range": [ + 0, + 8 + ] + } + } + } + ], + "id": "f73311e4-69d4-43d7-9781-1294e9d5bf0d", + "name": "Youcups Warrior II" + } + }, + "youou": { + "communication": [ + { + "btle": { + "names": [ + "VX001_*" + ], + "services": { + "0000fff0-0000-1000-8000-00805f9b34fb": { + "tx": "0000fff6-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "19dc8b35-713c-448b-926f-4d56b14f432d", + "output": { + "Vibrate": { + "step-range": [ + 0, + 255 + ] + } + } + } + ], + "id": "6b113fe0-9d26-4dd3-a997-527eb8a048b0", + "name": "Youou Wand Vibrator" + } + }, + "zalo": { + "communication": [ + { + "btle": { + "names": [ + "ZALO-Queen", + "ZALO-King", + "ZALO-Jeanne" + ], + "services": { + "0000fff0-0000-1000-8000-00805f9b34fb": { + "tx": "0000fff1-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "features": [ + { + "feature-type": "Vibrate", + "id": "94357c17-fb2d-4579-a4fa-68d597315887", + "output": { + "Vibrate": { + "step-range": [ + 0, + 8 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "43f2e203-f920-4c59-b7a8-d8902d7efa2f", + "output": { + "Vibrate": { + "step-range": [ + 0, + 8 + ] + } + } + } + ], + "id": "2aaeca64-1ce5-4333-a0ab-609546112d37", + "identifier": [ + "ZALO-Queen" + ], + "name": "Zalo Queen" + }, + { + "features": [ + { + "feature-type": "Vibrate", + "id": "3e1cb89e-43bd-4b57-9f49-79dbb297ce14", + "output": { + "Vibrate": { + "step-range": [ + 0, + 8 + ] + } + } + }, + { + "feature-type": "Vibrate", + "id": "ba694b89-b88e-4029-934f-95d23df42053", + "output": { + "Vibrate": { + "step-range": [ + 0, + 8 + ] + } + } + } + ], + "id": "94254e7a-2666-4e93-8f6d-101fad4a3807", + "identifier": [ + "ZALO-King" + ], + "name": "Zalo King" + }, + { + "id": "743b389e-1eb6-401a-80bc-116b6136c449", + "identifier": [ + "ZALO-Jeanne" + ], + "name": "Zalo Jeanne" + } + ], + "defaults": { + "features": [ + { + "feature-type": "Vibrate", + "id": "e6f5930a-98ee-4ced-9a51-b3938b7b6a0c", + "output": { + "Vibrate": { + "step-range": [ + 0, + 8 + ] + } + } + } + ], + "id": "45648a20-cb18-43a0-9d6c-8bc4ed63ef63", + "name": "Zalo Device" + } + } + } +} diff --git a/crates/buttplug_server_device_config/build.rs b/crates/buttplug_server_device_config/build.rs new file mode 100644 index 000000000..ee06b60c1 --- /dev/null +++ b/crates/buttplug_server_device_config/build.rs @@ -0,0 +1,68 @@ +use std::collections::BTreeMap; + +use serde_yaml; +use serde_json::{self, Value}; +use serde::{Serialize, Deserialize}; +use buttplug_core::util::json::JSONValidator; + +const VERSION_FILE: &str = "./device-config-v4/version.yaml"; +const OUTPUT_FILE: &str = "./build-config/buttplug-device-config-v4.json"; +const PROTOCOL_DIR: &str = "./device-config-v4/protocols/"; +const SCHEMA_FILE: &str = "./device-config-v4/buttplug-device-config-schema-v4.json"; + +#[derive(Serialize, Deserialize, Eq, PartialEq)] +struct VersionFile { + version: BuildVersion +} + +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Copy)] +struct BuildVersion { + pub major: u32, + pub minor: u32 +} + +#[derive(Deserialize, Serialize, Eq, PartialEq)] +struct JsonOutputFile { + version: BuildVersion, + protocols: BTreeMap +} + +fn main() { + println!("cargo:rerun-if-changed={}", PROTOCOL_DIR); + + // Open version file + let mut version: VersionFile = serde_yaml::from_str(&std::fs::read_to_string(VERSION_FILE).unwrap()).unwrap(); + // Bump minor version + version.version.minor += 1; + + // Compile device config file + let mut output = JsonOutputFile { + // lol + version: version.version, + protocols: BTreeMap::new() + }; + + for protocol_file in std::fs::read_dir(PROTOCOL_DIR).unwrap() { + let f = protocol_file.unwrap(); + output.protocols.insert(f.file_name().into_string().unwrap().split(".").next().unwrap().to_owned(), serde_yaml::from_str(&std::fs::read_to_string(f.path()).unwrap()).unwrap()); + } + + let json = serde_json::to_string_pretty(&output).unwrap(); + + // Validate + let validator = JSONValidator::new(&std::fs::read_to_string(SCHEMA_FILE).unwrap()); + validator.validate(&json).unwrap(); + + // See if it's actually different than our last output file + if let Ok(true) = std::fs::exists(OUTPUT_FILE) { + let old_output: JsonOutputFile = serde_json::from_str(&std::fs::read_to_string(OUTPUT_FILE).unwrap()).unwrap(); + if old_output.protocols == output.protocols { + // No actual changes, break out early, don't save + return; + } + } + + // Save it to the build_config directory + std::fs::write(VERSION_FILE, serde_yaml::to_string(&version).unwrap().as_bytes()).unwrap(); + std::fs::write(OUTPUT_FILE, json.as_bytes()).unwrap(); +} \ No newline at end of file diff --git a/buttplug/buttplug-device-config/device-config-v3/buttplug-device-config-schema-v3.json b/crates/buttplug_server_device_config/device-config-v4/buttplug-device-config-schema-v4.json similarity index 71% rename from buttplug/buttplug-device-config/device-config-v3/buttplug-device-config-schema-v3.json rename to crates/buttplug_server_device_config/device-config-v4/buttplug-device-config-schema-v4.json index deb191748..17682e795 100644 --- a/buttplug/buttplug-device-config/device-config-v3/buttplug-device-config-schema-v3.json +++ b/crates/buttplug_server_device_config/device-config-v4/buttplug-device-config-schema-v4.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Buttplug Device Config Schema", - "version": 2, + "version": 4, "description": "JSON format for Buttplug Device Config Files.", "components": { "uuid": { @@ -54,7 +54,6 @@ "advertised-services": { "type": "array", "items": { - "type": "string", "$ref": "#/components/uuid" } }, @@ -163,6 +162,22 @@ "pairs" ] }, + "udp-definition": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "port": { + "type": "integer" + } + }, + "required": [ + "address", + "port" + ], + "additionalProperties": false + }, "step-range": { "description": "Specifies the range of steps to use for a device. Devices will use the low end value as a stop.", "type": "array", @@ -181,57 +196,72 @@ "description": { "type": "string" }, + "id": { + "$ref": "#/components/uuid" + }, "feature-type": { "type": "string", - "pattern": "^(Vibrate|Rotate|Oscillate|Constrict|Inflate|Position|Battery|RSSI|Pressure)$" + "pattern": "^(Vibrate|Rotate|Oscillate|Constrict|Spray|Position|Battery|RSSI|Pressure|RotateWithDirection|PositionWithDuration|Heater|Led)$" }, - "actuator": { + "output": { "type": "object", - "properties": { - "step-range": { - "$ref": "#/components/step-range" - }, - "messages": { - "type": "array", - "items": { - "type": "string", - "pattern": "^(ScalarCmd|RotateCmd|LinearCmd)$" - } + "patternProperties": { + "^(Vibrate|Rotate|Oscillate|Constrict|Spray|Position|RotateWithDirection|PositionWithDuration|Heater|Led)$": { + "type": "object", + "properties": { + "step-range": { + "$ref": "#/components/step-range" + } + }, + "required": [ + "step-range" + ] } - }, - "required": [ - "step-range", - "messages" - ] + } }, - "sensor": { + "input": { "type": "object", - "properties": { - "value-range": { - "type": "array", - "items": { - "$ref": "#/components/step-range" + "patternProperties": { + "^(Battery|RSSI|Pressure)$": { + "type": "object", + "properties": { + "value-range": { + "type": "array", + "items": { + "$ref": "#/components/step-range" + }, + "minItems": 1 + }, + "input-commands": { + "type": "array", + "items": { + "type": "string", + "pattern": "^(Read|Subscribe)$" + } + } }, - "minItems": 1 - }, - "messages": { - "type": "array", - "items": { - "type": "string", - "pattern": "^(SensorReadCmd|SensorSubscribeCmd)$" - } + "required": [ + "value-range", + "input-commands" + ], + "additionalProperties": false } - }, - "required": [ - "value-range", - "messages" - ] + } + }, + "feature-settings": { + "type": "object", + "properties": { + "alt-protocol-index": { + "type": "number" + } + } } }, "required": [ - "feature-type" + "feature-type", + "id" ], - "additionalProperties": false + "additionalProperties": false } }, "user-config-features": { @@ -243,59 +273,61 @@ "description": { "type": "string" }, + "id": { + "$ref": "#/components/uuid" + }, + "base-id": { + "$ref": "#/components/uuid" + }, "feature-type": { - "type": "string", - "pattern": "^(Vibrate|Rotate|Oscillate|Constrict|Inflate|Position|Battery|RSSI|Pressure)$" + "type": "string" }, - "actuator": { + "output": { "type": "object", - "properties": { - "step-range": { - "$ref": "#/components/step-range" - }, - "step-limit": { - "$ref": "#/components/step-range" - }, - "messages": { - "type": "array", - "items": { - "type": "string", - "pattern": "^(ScalarCmd|RotateCmd|LinearCmd)$" + "patternProperties": { + "^(Vibrate|Rotate|Oscillate|Constrict|Spray|Position|RotateWithDirection|PositionWithDuration|Heater|Led)$": { + "type": "object", + "properties": { + "step-limit": { + "$ref": "#/components/step-range" + } } } - }, - "required": [ - "step-range", - "step-limit", - "messages" - ] + } }, - "sensor": { + "input": { "type": "object", - "properties": { - "value-range": { - "type": "array", - "items": { - "$ref": "#/components/step-range" + "patternProperties": { + "^(Battery|RSSI|Pressure)$": { + "type": "object", + "properties": { + "value-range": { + "type": "array", + "items": { + "$ref": "#/components/step-range" + }, + "minItems": 1 + }, + "input-commands": { + "type": "array", + "items": { + "type": "string", + "pattern": "^(Read|Subscribe)$" + } + } }, - "minItems": 1 - }, - "messages": { - "type": "array", - "items": { - "type": "string", - "pattern": "^(SensorReadCmd|SensorSubscribeCmd)$" - } + "required": [ + "value-range", + "input-commands" + ], + "additionalProperties": false } - }, - "required": [ - "value-range", - "messages" - ] + } } }, "required": [ - "feature-type" + "base-id", + "id" ], "additionalProperties": false } @@ -329,6 +361,12 @@ "name": { "type": "string" }, + "id": { + "$ref": "#/components/uuid" + }, + "base-id": { + "$ref": "#/components/uuid" + }, "features": { "$ref": "#/components/user-config-features" }, @@ -336,6 +374,11 @@ "$ref": "#/components/user-config-customization" } }, + "required": [ + "id", + "features", + "user-config" + ], "additionalProperties": false }, "defaults-definition": { @@ -344,13 +387,20 @@ "name": { "type": "string" }, + "id": { + "$ref": "#/components/uuid" + }, + "protocol-variant": { + "type": "string" + }, "features": { "$ref": "#/components/features" } }, "required": [ "name", - "features" + "features", + "id" ] }, "configurations-definition": { @@ -368,13 +418,20 @@ "name": { "type": "string" }, + "id": { + "$ref": "#/components/uuid" + }, + "protocol-variant": { + "type": "string" + }, "features": { "$ref": "#/components/features" } }, "required": [ "name", - "identifier" + "identifier", + "id" ], "additionalProperties": false }, @@ -428,21 +485,19 @@ }, "lovense-connect-service": { "$ref": "#/components/lovense-connect-service-definition" + }, + "udp": { + "$ref": "#/components/udp-definition" } } }, "maxProperties": 1 }, - "devices": { - "type": "object", - "properties": { - "defaults": { - "$ref": "#/components/defaults-definition" - }, - "configurations": { - "$ref": "#/components/configurations-definition" - } - } + "defaults": { + "$ref": "#/components/defaults-definition" + }, + "configurations": { + "$ref": "#/components/configurations-definition" } } } @@ -477,6 +532,9 @@ }, "hid": { "$ref": "#/components/usb-definition" + }, + "udp": { + "$ref": "#/components/udp-definition" } } }, @@ -540,4 +598,4 @@ ], "maxProperties": 2, "additionalProperties": false -} \ No newline at end of file +} diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/activejoy.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/activejoy.yml new file mode 100644 index 000000000..1f33cb54c --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/activejoy.yml @@ -0,0 +1,19 @@ +defaults: + name: IntoYou Remote Egg Vibrator + features: + - feature-type: Vibrate + id: 1fec4773-16a2-4bec-8910-1fcd9a85edaf + output: + Vibrate: + step-range: + - 0 + - 255 + id: 62e7b76d-ab99-42ca-89ea-865a6072451e +communication: + - btle: + names: + - SS-TD-YDTD-001 + services: + 0000f0b0-0000-1000-8000-00805f9b34fb: + tx: 0000f0b1-0000-1000-8000-00805f9b34fb + rx: 0000f0b2-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/adrienlastic.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/adrienlastic.yml new file mode 100644 index 000000000..c8bf9686e --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/adrienlastic.yml @@ -0,0 +1,29 @@ +defaults: + name: Adrien Lastic Device + features: + - feature-type: Vibrate + id: 714132f1-7ddd-420e-bf9f-6927fce0c9c3 + output: + Vibrate: + step-range: + - 0 + - 16 + id: d5c4c815-9226-430d-8b40-915c0e208483 +configurations: + - identifier: + - LVS-S001 + name: Adrien Lastic Palpitation + id: 92c43355-c16f-471a-9c5d-ea30186b75a8 + - identifier: + - LVS-S002 + name: Adrien Lastic Revelation + id: ef491238-d560-46e4-84ed-72c902632bb2 +communication: + - btle: + names: + - Placeholder to avoid conflict with bad attempt to clone a Lovense Lush + advertised-services: + - 00001320-0000-1000-8000-00805f9b34fb + services: + 6e400001-b5a3-f393-e0a9-e50e24dcca9e: + tx: 6e400002-b5a3-f393-e0a9-e50e24dcca9e diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/amorelie-joy.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/amorelie-joy.yml new file mode 100644 index 000000000..3bd0c7c2a --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/amorelie-joy.yml @@ -0,0 +1,56 @@ +defaults: + name: Amorelie Joy Device + features: + - feature-type: Vibrate + id: 9be34b27-431e-47d0-871b-fea3c116d32d + output: + Vibrate: + step-range: + - 0 + - 100 + id: df7c19cc-8e49-4c55-98d1-0b060424260f +configurations: + - identifier: + - 4D02 + name: Amorelie Joy Move + id: b5681266-9f56-4a6f-9985-be33301af6af + - identifier: + - 4D05 + name: Amorelie Joy Cha-Cha + id: 891e1acb-84ec-41e5-8782-2392a1343a34 + - identifier: + - 4D06 + name: Amorelie Joy Boogie + id: fdc21c92-80d8-4cfa-a4e2-a79fef020e1c + - identifier: + - 4D01 + name: Amorelie Joy Shimmer + id: 7a98633a-8b7e-4065-8e10-12b17588f504 + - identifier: + - 4D03 + name: Amorelie Joy Grow + id: bd784815-49d7-4379-98d0-34aa1d9c0097 + - identifier: + - 4D04 + name: Amorelie Joy Shuffle + id: 6124dfc8-b0f4-4db0-b85a-b8d0da53b6a8 + - identifier: + - 4D07 + name: Amorelie Joy Salsa + id: 7e7776a5-98a8-42ef-a9e9-b4aeaf5adbaa +communication: + - btle: + names: + - 4D01 + - 4D02 + - 4D03 + - 4D04 + - 4D05 + - 4D06 + - 4D07 + - 4D08 + - 4D09 + services: + 0000ffe0-0000-1000-8000-00805f9b34fb: + rx: 0000ffe2-0000-1000-8000-00805f9b34fb + tx: 0000ffe3-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/aneros.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/aneros.yml new file mode 100644 index 000000000..c14b3bd55 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/aneros.yml @@ -0,0 +1,27 @@ +defaults: + name: Aneros Vivi + features: + - feature-type: Vibrate + description: Perineum Vibrator + id: a980bc1a-5554-4293-a75f-6d17bf25ebee + output: + Vibrate: + step-range: + - 0 + - 127 + - feature-type: Vibrate + description: Internal Vibrator + id: 811d7d6e-6a75-4925-943a-a06042223e3a + output: + Vibrate: + step-range: + - 0 + - 127 + id: f023f0f4-6629-469e-84c4-171ed4939f3d +communication: + - btle: + names: + - Massage Demo + services: + 0000ff00-0000-1000-8000-00805f9b34fb: + tx: 0000ff01-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/ankni.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/ankni.yml new file mode 100644 index 000000000..d508695dd --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/ankni.yml @@ -0,0 +1,22 @@ +defaults: + name: Roselex Device + features: + - feature-type: Vibrate + id: 2ba5d52d-0f40-4f1f-8738-955f9f7715f3 + output: + Vibrate: + step-range: + - 0 + - 3 + id: 9a26d86b-afd3-4413-ad72-faddf14b7f03 +communication: + - btle: + names: + - DSJM + services: + 0000fe00-0000-1000-8000-00805f9b34fb: + tx: 0000fe01-0000-1000-8000-00805f9b34fb + 0000fffe-0000-1000-8000-00805f9b34fb: + tx: 0000fe02-0000-1000-8000-00805f9b34fb + 0000180a-0000-1000-8000-00805f9b34fb: + generic0: 00002a50-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/bananasome.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/bananasome.yml new file mode 100644 index 000000000..8dc5688ef --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/bananasome.yml @@ -0,0 +1,32 @@ +defaults: + name: Bananasome Rocket X7 + features: + - feature-type: Oscillate + id: 63fa90c4-1ab9-4841-bfa3-45113f2c1d18 + output: + Oscillate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: 3e738dbf-3ff1-495a-a5bf-6d57776d80e8 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: c2a5f510-44fc-4c79-a9e2-ebf4862c45cb + output: + Vibrate: + step-range: + - 0 + - 255 + id: 83c998f8-1a18-48af-aa52-2f310252eb54 +communication: + - btle: + names: + - 火箭X7 + services: + 0000ae00-0000-1000-8000-00805f9b34fb: + tx: 0000ae01-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/cachito.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/cachito.yml new file mode 100644 index 000000000..fc3a00e4b --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/cachito.yml @@ -0,0 +1,35 @@ +defaults: + name: Cachito Device + features: + - feature-type: Vibrate + id: 6e5ce97a-2eae-4807-a857-0e74a9f0d095 + output: + Vibrate: + step-range: + - 0 + - 5 + - feature-type: Vibrate + id: 2ec18700-3fac-4f3b-91c1-ead90bf853d0 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 0ce7063c-f118-44ea-80ed-66f3edb90a57 +configurations: + - identifier: + - CCTSK + name: Cachito Lure Tao + id: 8c4ee478-8dbb-41e6-b41c-a5664eec1532 + - identifier: + - CCTXueGao + name: Cachito Ice Cream + id: 57b25f6e-03d6-44ef-b378-0ef9e69170d4 +communication: + - btle: + names: + - CCTSK + - CCTXueGao + services: + 0000fff0-0000-1000-8000-00805f9b34fb: + tx: 0000fff2-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/cowgirl-cone.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/cowgirl-cone.yml new file mode 100644 index 000000000..3e9f10a10 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/cowgirl-cone.yml @@ -0,0 +1,23 @@ +defaults: + name: The Cowgirl Cone + features: + - feature-type: Vibrate + id: d9247325-2173-4ac7-95c3-6730f0d37964 + output: + Vibrate: + step-range: + - 0 + - 128 + id: 2dc2667a-2305-4dd4-a0a0-9c1dbcf119ea +configurations: + - identifier: + - CG-CONE + name: The Cowgirl Cone + id: 72ec0578-c6dc-4835-a72d-3388816f9611 +communication: + - btle: + names: + - CG-CONE + services: + 0000ffe0-0000-1000-8000-00805f9b34fb: + tx: 0000ffe1-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/cowgirl.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/cowgirl.yml new file mode 100644 index 000000000..b871f9611 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/cowgirl.yml @@ -0,0 +1,35 @@ +defaults: + name: The Cowgirl Device + features: + - feature-type: Vibrate + id: 11c01b64-e6cc-4b19-9a4d-eaf03a317b03 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Rotate + id: 9f3e0837-26e5-4ab1-bb2c-67be33ca920d + output: + Rotate: + step-range: + - 0 + - 255 + id: 5cdfacc3-7a69-415c-aefc-1d889fc5e824 +configurations: + - identifier: + - THE COWGIRL + name: The Cowgirl + id: 188130d5-6ea1-473f-a9f4-a176929221ff + - identifier: + - THE UNICORN + name: The Unicorn + id: 675d61d0-b30f-4f60-abf7-6d5f67a5b56c +communication: + - btle: + names: + - THE COWGIRL + - THE UNICORN + services: + 0000fe00-0000-1000-8000-00805f9b34fb: + tx: 0000fe01-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/cueme.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/cueme.yml new file mode 100644 index 000000000..c46ffc1e8 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/cueme.yml @@ -0,0 +1,109 @@ +defaults: + name: Cueme Device + features: + - feature-type: Vibrate + id: 812c9f59-e9a9-42d9-8c30-1dc91feea5ac + output: + Vibrate: + step-range: + - 0 + - 15 + - feature-type: Vibrate + id: bbd5955a-5c2e-494e-911d-c64708763bea + output: + Vibrate: + step-range: + - 0 + - 15 + - feature-type: Vibrate + id: 9c152f4a-8441-47f4-9b02-d0f64a468517 + output: + Vibrate: + step-range: + - 0 + - 15 + - feature-type: Vibrate + id: f19d9974-0631-4413-a544-7bf02c039743 + output: + Vibrate: + step-range: + - 0 + - 15 + - feature-type: Vibrate + id: ec23bb7f-34df-4480-8eba-3f95dc0d1e0a + output: + Vibrate: + step-range: + - 0 + - 15 + - feature-type: Vibrate + id: 24c910ea-7cfb-486c-8e86-451e8b3bc22f + output: + Vibrate: + step-range: + - 0 + - 15 + - feature-type: Vibrate + id: b8659ec6-6b50-4d74-8a92-2c127856a7ff + output: + Vibrate: + step-range: + - 0 + - 15 + - feature-type: Vibrate + id: 96b18136-9780-4771-b5e6-f090927fbe14 + output: + Vibrate: + step-range: + - 0 + - 15 + id: aeecfe99-106d-4f25-a9b6-4a809971ebfb +configurations: + - identifier: + - '1' + name: Cueme Mens + id: ff44bb15-c9ae-4751-b993-8f325129cbb2 + - identifier: + - '2' + name: Cueme Bra + id: dcb3e162-5271-4737-b2e3-88534daafe05 + - identifier: + - '3' + name: Cueme Womans + features: + - feature-type: Vibrate + id: b4554560-c0ad-42ac-82a8-4a8042fc6ab9 + output: + Vibrate: + step-range: + - 0 + - 15 + - feature-type: Vibrate + id: d666a28d-3701-499f-b0b9-7f6ccf722159 + output: + Vibrate: + step-range: + - 0 + - 15 + - feature-type: Vibrate + id: d2789e16-6771-4046-b5de-500def289894 + output: + Vibrate: + step-range: + - 0 + - 15 + - feature-type: Vibrate + id: c01700e6-1b57-41aa-831b-b3f7a54dbefe + output: + Vibrate: + step-range: + - 0 + - 15 + id: 29364127-d158-411f-9e28-e8f33a5ca4a6 +communication: + - btle: + names: + - FUNCODE_* + services: + 0000fff0-0000-1000-8000-00805f9b34fb: + tx: 0000fff1-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/cupido.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/cupido.yml new file mode 100644 index 000000000..6505c829b --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/cupido.yml @@ -0,0 +1,19 @@ +defaults: + name: Cupido Device + features: + - feature-type: Vibrate + id: 7f645006-1074-415f-8b06-43aa473573c0 + output: + Vibrate: + step-range: + - 0 + - 255 + id: 8ef3fe28-6903-4418-9dd8-5323788ca961 +communication: + - btle: + names: + - MY2607-BLE-V1.0 + services: + 0000f0b0-0000-1000-8000-00805f9b34fb: + tx: 0000f0b1-0000-1000-8000-00805f9b34fb + rx: 0000f0b2-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/deepsire.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/deepsire.yml new file mode 100644 index 000000000..2aa3270cc --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/deepsire.yml @@ -0,0 +1,23 @@ +defaults: + name: DeepSire Device + features: + - feature-type: Vibrate + id: 08e0cd3e-65eb-42a4-8b15-990eb2e4c855 + output: + Vibrate: + step-range: + - 0 + - 255 + id: dd188bc6-784e-4799-b80c-3f568f8794cc +configurations: + - identifier: + - IMP 3 + name: Kuirkish Imp 3 + id: ee9f0605-415e-4b07-8deb-c7252eff7053 +communication: + - btle: + names: + - IMP 3 + services: + 0000ffe0-0000-1000-8000-00805f9b34fb: + tx: 0000ffe1-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/feelingso.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/feelingso.yml new file mode 100644 index 000000000..b701f5d68 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/feelingso.yml @@ -0,0 +1,26 @@ +defaults: + name: FeelingSo Flair Feel + features: + - feature-type: Vibrate + id: ad577b65-e74b-44c3-868b-86e3bfd53dbe + output: + Vibrate: + step-range: + - 0 + - 19 + - feature-type: Oscillate + id: 5a2bd962-a9ab-4bd6-af7b-ae1fd6b39d79 + output: + Oscillate: + step-range: + - 0 + - 19 + id: 2f2d3b3d-e832-40e4-ad74-705c0f02997d +communication: + - btle: + names: + - Flair Feel + services: + 42410001-0000-0101-0000-736278637a72: + tx: 42410002-0000-0101-0000-736278637a72 + rx: 42410003-0000-0101-0000-736278637a72 diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/fleshy-thrust.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/fleshy-thrust.yml new file mode 100644 index 000000000..ff0284f1f --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/fleshy-thrust.yml @@ -0,0 +1,18 @@ +defaults: + name: Fleshy Thrust Sync + features: + - feature-type: PositionWithDuration + id: a8185061-6d41-4eea-bc24-1ff1c5c405b9 + output: + PositionWithDuration: + step-range: + - 0 + - 180 + id: f273ebd5-a698-4c35-9c46-0625fa442960 +communication: + - btle: + names: + - BT05 + services: + 0000ffe0-0000-1000-8000-00805f9b34fb: + tx: 0000ffe1-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/foreo.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/foreo.yml new file mode 100644 index 000000000..054d813b7 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/foreo.yml @@ -0,0 +1,205 @@ +defaults: + name: Foreo Device + features: + - feature-type: Vibrate + id: 0749f306-bd4c-48d7-9c2a-1309817a4dcc + output: + Vibrate: + step-range: + - 0 + - 10 + id: 92d98050-7a3f-45b2-9df1-41e8cda28033 +configurations: + - identifier: + - FOFO + - LUNA fofo + - LUNA FOFO + - LUNA PLAY SMART + name: Foreo LUNA fofo + id: 98f14be3-8938-403a-8f90-d4bf5d15409f + - identifier: + - LUNA PLAYSMART2 + - LUNA PLAY SMART2 + - LUNA play smart2 + - LUNA play smart 2 + name: Foreo LUNA play smart 2 + id: ee014806-a78a-4d83-9c22-25941f13c26e + - identifier: + - LUNA 3 + - LUNA3 + name: Foreo LUNA 3 + id: c711b125-092c-4ece-bb98-83050b3fdf52 + - identifier: + - LUNA3PLUS + - LUNA3 PLUS + - LUNA 3 PLUS + - LUNA 3 plus + name: Foreo LUNA 3 plus + id: da0802b8-f60c-4261-83f7-6c703e587fa2 + - identifier: + - LUNA 3 MEN + - LUNA3MEN + name: Foreo LUNA 3 men + id: de02db79-eba2-48dc-b539-5364aaae4bd2 + - identifier: + - LUNA MINI3 + - LUNA MINI 3 + - LUNA mini 3 + name: Foreo LUNA 3 mini + id: 2ec4a921-d834-4da0-b710-a9d10fba4942 + - identifier: + - LUNA4 + - LUNA 4 + name: Foreo LUNA 4 + id: 695d3e66-e545-43ae-a8fa-8a8883e32439 + - identifier: + - LUNA4PLUS + - LUNA4 PLUS + - LUNA 4 plus + name: Foreo LUNA 4 plus + id: 34503c35-05ef-44f4-875e-e46c9c81a71f + - identifier: + - LUNA4MEN + - LUNA 4 MEN + - LUNA 4 FOR MEN + name: Foreo LUNA 4 men + id: e519d03d-35e4-4e06-84da-a183a516d2bf + - identifier: + - LUNA MINI4 + - LUNA MINI 4 + - LUNA mini 4 + - LUNA 4 mini + name: Foreo LUNA 4 mini + id: 52c53ab8-513a-4cb8-abb5-622086c7b6b0 + - identifier: + - UFO + name: Foreo UFO + id: 67c567c0-1ea2-4093-80bf-a109f6831621 + - identifier: + - UFO mini + - UFO MINI + - UFO MIN + name: Foreo UFO mini + id: 305f6099-c0a7-4eb0-bf0f-7499ef152d8c + - identifier: + - UFO2 + - UFO 2 + name: Foreo UFO 2 + id: 5e5700df-c1b1-448a-822f-1808e453641f + - identifier: + - UFO3 + name: Foreo UFO 3 + id: 3256b258-13cd-4df9-abdb-d8e547c396d5 + - identifier: + - UFO3go + name: Foreo UFO 3 go + id: 1ca37f05-520d-4696-86b1-d0edcf9fa803 + - identifier: + - UFO3eyes + name: Foreo UFO 3 led + id: 77d89601-216c-42ee-9908-c0afd777c9a6 + - identifier: + - UFO3mini + name: Foreo UFO 3 mini + id: 58f9677c-440f-43c9-9ab6-7f938edd3f4a + - identifier: + - UFOMINI2 + - UFO mini 2 + name: Foreo UFO mini 2 + id: d555e823-52aa-4f02-8d8e-788c3dbe3a5e + - identifier: + - BEAR + name: Foreo BEAR + id: a050edb2-71b2-494a-b3db-4f0d9ac20310 + - identifier: + - BEAR_MINI + - BEAR MINI + - BEAR mini + name: Foreo BEAR mini + id: 1231d10c-eee6-4061-8eb2-ffdec6f1523a + - identifier: + - BEAR2 + - BEAR 2 + name: Foreo BEAR 2 + id: c57d9ca7-f3e6-4f48-b65c-fec9a648b699 + - identifier: + - BEAR2go + name: Foreo BEAR 2 go + id: 35a0a090-3085-4f83-b9d2-eb26d0c21ea9 + - identifier: + - BEAR2eyes + name: Foreo BEAR 2 eyes + id: c66dd16e-13e0-4446-809f-a1567fe746c7 + - identifier: + - BEAR2body + name: Foreo BEAR 2 body + id: a837cdd0-6513-4962-85be-d4859e1a7c98 + - identifier: + - KIWI + name: Foreo KIWI + id: d14e7fd0-1da8-44dc-8028-39a5655185fa + - identifier: + - KIWI derma + name: Foreo KIWI derma + id: ee07bc74-21af-455d-a26a-fab22f188f97 +communication: + - btle: + names: + - FOFO + - LUNA fofo + - LUNA FOFO + - LUNA PLAY SMART + - LUNA PLAYSMART2 + - LUNA PLAY SMART2 + - LUNA play smart2 + - LUNA play smart 2 + - LUNA 3 + - LUNA3 + - LUNA3PLUS + - LUNA3 PLUS + - LUNA 3 PLUS + - LUNA 3 plus + - LUNA 3 MEN + - LUNA3MEN + - LUNA MINI3 + - LUNA MINI 3 + - LUNA mini 3 + - LUNA4PLUS + - LUNA4 + - LUNA 4 + - LUNA4PLUS + - LUNA4 PLUS + - LUNA 4 plus + - LUNA4MEN + - LUNA 4 MEN + - LUNA 4 FOR MEN + - LUNA MINI4 + - LUNA MINI 4 + - LUNA mini 4 + - LUNA 4 mini + - UFO + - UFO mini + - UFO MINI + - UFO MIN + - UFO2 + - UFO 2 + - UFOMINI2 + - UFO mini 2 + - UFO3 + - UFO3mini + - UFO3go + - UFO3led + - BEAR + - BEAR_MINI + - BEAR MINI + - BEAR mini + - BEAR2 + - BEAR 2 + - BEAR2go + - BEAR2body + - BEAR2eyes + - KIWI + - KIWI derma + services: + 0000fff0-0000-1000-8000-00805f9b34fb: + tx: 0000fff1-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/fox.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/fox.yml new file mode 100644 index 000000000..98e8424ce --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/fox.yml @@ -0,0 +1,21 @@ +defaults: + name: Fox Device + features: + - feature-type: Vibrate + id: e43828a2-7dc6-4af1-b450-73c50441849f + output: + Vibrate: + step-range: + - 0 + - 3 + id: 4138dc32-5276-47e8-89d4-fddc6ca42c1d +communication: + - btle: + names: + - FOX + - FOX M70 Pro + - FoxM70Pro + - FOX M70-2 + services: + 0000ae00-0000-1000-8000-00805f9b34fb: + tx: 0000ae01-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/fredorch-rotary.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/fredorch-rotary.yml new file mode 100644 index 000000000..4742314ba --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/fredorch-rotary.yml @@ -0,0 +1,20 @@ +defaults: + name: Fredorch Rotary Device + features: + - feature-type: Oscillate + description: Fucking Machine Oscillation Speed + id: 0ec02168-f724-481a-a927-6ea6df4c89b5 + output: + Oscillate: + step-range: + - 0 + - 20 + id: 86b9ab9e-8507-4abf-b6af-8ecd01a94476 +communication: + - btle: + names: + - M1_* + services: + 0000ae10-0000-1000-8000-00805f9b34fb: + tx: 0000ae01-0000-1000-8000-00805f9b34fb + rx: 0000ae02-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/fredorch.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/fredorch.yml new file mode 100644 index 000000000..76eeedaf2 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/fredorch.yml @@ -0,0 +1,19 @@ +defaults: + name: Fredorch Device + features: + - feature-type: PositionWithDuration + id: d3985f07-f95a-4f72-859e-8b0ac76f251f + output: + PositionWithDuration: + step-range: + - 0 + - 150 + id: cbd6a5b5-50c0-4fb5-93e3-408fd027ff4d +communication: + - btle: + names: + - YXlinksSPP + services: + 0000ffb0-0000-1000-8000-00805f9b34fb: + tx: 0000ffb1-0000-1000-8000-00805f9b34fb + rx: 0000ffb2-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/galaku-pump.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/galaku-pump.yml new file mode 100644 index 000000000..ce9facd97 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/galaku-pump.yml @@ -0,0 +1,30 @@ +defaults: + name: Galaku Device + features: + - feature-type: Oscillate + id: 60946646-0160-425f-85ca-9210d35d61fd + output: + Oscillate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 97f24406-d413-43ed-b830-b76c3f912fad + output: + Vibrate: + step-range: + - 0 + - 100 + id: 2e954d01-4f42-4acd-9be8-9fdfa0172998 +configurations: + - identifier: + - V415 + name: Galaku Nebula + id: 7689175c-af6e-4529-a2ae-c4f41f1db595 +communication: + - btle: + names: + - V415 + services: + 00001000-0000-1000-8000-00805f9b34fb: + tx: 00001001-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/galaku.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/galaku.yml new file mode 100644 index 000000000..7ea9c72b1 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/galaku.yml @@ -0,0 +1,1616 @@ +defaults: + name: Galaku Device + features: + - feature-type: Vibrate + description: Vibrate + id: f650b5a9-7413-4ac9-b25e-863180daa04c + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: d9c34cf9-5645-4e04-bf92-51e5df708417 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: c1766383-def6-4bd0-b6ce-1e8f993fa6ae +configurations: + # Type 0 + # - V415 see galaku-pump + - identifier: + - V415 + name: Galaku Nebula + id: 53a117ec-0e2d-43ce-a77b-0ed4fbf82d07 + - identifier: + - GX85 + name: Galaku Shana + id: 6c62e478-d684-4c3a-9d74-0860be907a8e + - identifier: + - GX07 + name: Galaku Miya + id: ccda61b7-8517-4d31-8ef6-a730b1a0ab9a + - identifier: + - GX17 + name: Galaku Capsule lipstick + id: 0f24a925-bad8-48ec-9a35-887f78bc967d + - identifier: + - GX21 + name: Galaku Vitality Cat + id: 9d8d0d14-1507-48ee-8b99-1b5cc6f4a67e + - identifier: + - GX22 + name: Galaku Phantom X + id: 22e21fb8-c399-490f-9680-5abe44c46bc9 + - identifier: + - GX16 + name: Galaku Vitality Strawberry + id: c829fb46-4cf5-4034-bdea-2032e00a34c3 + - identifier: + - GX29 + name: Galaku Little Magic Box + id: aaa2d14e-2b93-46e5-87a0-c622f6f9c82b + - identifier: + - GX23 + name: Galaku Little Whale + id: 859c82eb-9163-426c-90c4-4b567ff34e95 + - identifier: + - GX25 + name: Galaku Happy Vibrator + id: fffd1a38-2ac8-470a-bffb-70360a4099ba + - identifier: + - GX26 + name: Galaku Xiaobao Beans + id: a2e6b3c3-8101-4ff7-8113-4d5c9641f557 + - identifier: + - GK03 + name: Galaku Capsule Vibrator + id: 28e47ecf-6a79-48c0-acd1-82ee75955836 + - identifier: + - GX39 + name: Galaku Ice cone miniAV stick + id: af836ee8-9c73-4759-80f4-d305a14e51c1 + - identifier: + - G321 + name: Galaku mini ice cream cone + id: 9b6a27bd-75d6-42c7-9a71-7f95807eb9c4 + - identifier: + - G304 + name: Galaku Shia's Collar + id: a1042c91-cfa0-41b8-9afa-637599c076ac + - identifier: + - G336 + name: Galaku The Second Generation of Vitality Bird + id: bae928b3-7ff5-45d1-b251-882812d5ef88 + - identifier: + - G331 + name: Galaku Octopus glans massager + id: 074ef604-51bf-4f0a-97ee-16508c582968 + - identifier: + - G326 + name: Galaku Alice + id: ca21391e-6aa2-4480-a1a5-c138318bf44c + - identifier: + - G335 + name: Galaku Unicorn Butt Plug + id: d1a0cd58-1aa2-447c-bd7e-da471fdee5d8 + - identifier: + - G341 + name: Galaku Ace + id: 398c32ab-6498-4358-a25f-8553916719fd + - identifier: + - G355 + name: Galaku Little cute turtle + id: 05dc7803-1513-48d9-9c2f-2719e8b71905 + - identifier: + - G349 + name: Galaku Little Bullet + id: 5e8a289b-9f5f-4865-9f92-d7bd06c68950 + - identifier: + - G407 + name: Galaku Joy Vibrator + id: 9cc769ed-e911-491b-b8ad-1a78ed8675fe + - identifier: + - G204 + name: Galaku Bowling + id: e213ecfd-d0f9-44e1-9c17-d3d78f7c6216 + - identifier: + - G171 + name: Galaku Mixin Controller + id: 299b1c71-e7fc-426b-8d6f-0375685de6a8 + - identifier: + - G12D + name: Galaku Hua Chao Brush + id: aa6c0314-58bc-4b83-b9d7-5988151b0c53 + - identifier: + - G123 + name: Galaku 花sai + id: ed5b32b5-79fa-4d74-8d44-3afc3e71fc38 + - identifier: + - G23A + name: Galaku Dream Vibration + id: 9811b596-7c23-4f18-b0b6-895680d273b0 + - identifier: + - G336 + name: Galaku The Second Generation of Vitality Bird + id: 36d612d2-806c-49f5-85b6-0f291342ea34 + - identifier: + - G23A + name: Galaku Dream Vibration + id: 83521db1-be7a-4ca6-be82-fe218dac73db + - identifier: + - A073 + name: Galaku Joy Vibrator + id: d34943d6-709c-4972-97c8-ffa75c7ff005 + - identifier: + - GLMT + name: Galaku Rogue Rabbit + id: 587af267-9322-4ac6-afe6-8dcd4217ced4 + - identifier: + - G901 + name: Galaku Suck the vibrator + id: 3ee263a4-1aa6-4b6c-8d09-b82d24df4017 + - identifier: + - G912 + name: Galaku Donut + id: 25528b16-8cfa-45d5-b8bc-cd238f2a0416 + - identifier: + - G901 + name: Galaku Suck the vibrator + id: 9593572e-e19d-4863-86ba-3e0542ad54fb + - identifier: + - G20B + name: Galaku Ballet Vibrator + id: 1d5f2345-034e-4d41-93a7-3d0ef80933e0 + - identifier: + - K112 + name: Galaku Donut + id: 52071636-ceb7-4f79-afb1-5d8af4dbf5a2 + - identifier: + - G202 + name: Galaku Flirting Pen + id: d9abd771-c3bc-449a-8c4a-06938231111d + - identifier: + - K118 + name: Galaku Ball vibrator + id: bbb54012-bee5-451a-aea3-98f28ca695a9 + - identifier: + - K107 + name: Galaku Cyberpunk Airplane Cup + id: 104e8fcf-db34-4006-9a27-183ca2b8aaf5 + - identifier: + - G203 + name: Galaku Vitality Cute Pet + id: 48e98efa-7c01-4a8e-a0b5-f721799d78e0 + - identifier: + - TXHL + name: Galaku Little gourd vibrating egg + id: 76ad7e0f-fcbf-4c21-b4f9-c2affe73355a + - identifier: + - TXMM + name: Galaku little kitten + id: 2c2a664d-851d-4686-b432-1e2eef36b713 + - identifier: + - TXKL + name: Galaku Little Dinosaur + id: 7ab1f6e5-ed53-463c-8379-40db8fa580b4 + - identifier: + - K108 + name: Galaku Bell sucking + id: 43e3d3d0-0c9f-46c0-b44b-4d2739a43522 + - identifier: + - K109 + name: Galaku Ring vibration + id: 7ce8bdb5-eebc-44e8-9369-b8a9633a0365 + - identifier: + - KWL2 + name: Galaku Erection Booster + id: 9106168e-1758-424e-8713-7266b96cbf6d + - identifier: + - TFHL + name: Galaku Gyoyo-G (meaning Yue-little gourd) + id: b56b1b77-0174-47f6-8429-06f83a7c2382 + - identifier: + - TFMM + name: Galaku Gyoyo (meaning joy) + id: c90795b9-355b-4cc3-b493-e63c92c4efe5 + - identifier: + - TFKL + name: Galaku Gyoyo (meaning joy) + id: f73faf1a-dc8d-47a6-ba00-435aec9fbfb1 + - identifier: + - K120 + name: Galaku Pinky stick + id: 911b8708-8cc6-406b-8fca-f31dbecb8cbc + - identifier: + - K12A + name: Galaku Little Turtle Stick + id: 03a0ede5-fb62-4f5c-a3e9-c821a9afbfbf + - identifier: + - K12C + name: Galaku Xiao Xian Wan + id: d924b656-3e8e-4742-ab5e-cba345aa6c9b + - identifier: + - LL18 + name: Galaku Mitang + id: 761d7fc2-ba70-4093-8bf7-f3e3ee1d639e + - identifier: + - CYX2 + name: Secret Lover Simon + id: bdd69b72-0c3d-4c14-b923-accd305e9ccc + - identifier: + - RC31 + name: Secret Lover Betty + id: e17ab832-ca1b-430a-b03a-c053c268407e + - identifier: + - MD19 + name: Secret Lover Kevin + id: 546731c9-21c5-4bca-bb85-9fec1c3c627e + - identifier: + - QD48 + name: Adorime Wearable Egg Vibrator + id: f4cdbb77-f674-49df-9b76-75d460179c69 + - identifier: + - BGSF + name: Adorime Male Masturbator + id: c81608ec-36cc-4ca1-9a17-72e4e3ce020f + - identifier: + - BGQS + name: Adorime Penis Vibrator + id: 0c62c632-fbf1-43f1-b3ab-bff41ea88c3a + - identifier: + - AX05 + name: Adorime Anal Vibrator + id: bce17a9d-1cfa-47e7-a6fd-6471896bb1d3 + # Type 1 + - identifier: + - G317 + name: Galaku Zaku Aircraft Cup + features: + - feature-type: Oscillate + description: Oscillate + id: f427019a-a136-45a0-a866-dac460d8770c + output: + Oscillate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: 0fa679ef-eb23-4b10-a456-dd1f99ed7dee + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: 19ac04ae-9d77-4b3b-a706-5df8252569a7 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 58de185f-a52c-42e0-b06f-bb7a293a9d40 + - identifier: + - G312 + name: Galaku Mecha-Original Owner's Aircraft Cup + features: + - feature-type: Oscillate + description: Oscillate + id: 9a04b080-4956-499c-894d-d7538322160e + output: + Oscillate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: a8a8f9c0-f406-4b80-8c8e-3ff1bf9bff72 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: 769865df-58b9-4d0f-8697-4ee78304a10c + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 8c3f6848-0c63-4a56-8f28-ffba313240e3 + - identifier: + - G302 + name: Galaku Little Devil + features: + - feature-type: Vibrate + description: Vibrate + id: c09c7502-7e42-49be-8620-44bf0dda08af + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: ccf2e0e7-4ade-4a9b-8b49-405653f72c7c + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: 22792e4e-bf84-42d4-a1ec-cbffddd3d777 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 1f53344c-173d-4a00-abb4-623969d7b174 + - identifier: + - G320 + name: Galaku Athena + features: + - feature-type: Oscillate + description: Oscillate + id: c86290fd-1271-45d3-98bf-bcd168a1948a + output: + Oscillate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: 70de4e79-4db7-45ee-a7c1-490cdf23bb33 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: a6fb0d1b-9160-40ca-81a7-905776aeff83 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: e8c6ef4f-b574-4fa3-8887-df3415368621 + - identifier: + - G314 + name: Galaku Vitality Octopus II + features: + - feature-type: Vibrate + description: Vibrate + id: 75943039-8932-4a1c-af26-d1f075e78c01 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: 05804a02-980d-4380-b407-a30f56477f8e + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: a104dc8a-7759-4dd9-8113-d3b450b24658 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: a8f4769e-945e-4f32-b2fb-1d15c6be62c6 + - identifier: + - G228 + name: Galaku Little Dolphin + features: + - feature-type: Vibrate + description: Vibrate + id: 7751e53b-a722-49e5-9534-5a5798de081c + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: 68d399dd-a3c9-4423-b244-d231c7e0a131 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: 398eb416-b3d7-4f23-90ec-2f9fb05487f7 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: ead84aad-7180-415d-8740-3a8c84be3fc9 + - identifier: + - G315 + name: Galaku Unicorn + features: + - feature-type: Vibrate + description: Vibrate + id: 02fda4c8-b86c-4131-8d9f-447534785404 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: a21f8a77-22ce-47a3-b220-028f87d3a50d + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: e85a8553-4f3c-49ba-ae88-929d0052e04d + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 9ca11ed6-aa8a-4506-a7f8-78f515075340 + - identifier: + - G307 + name: Galaku Queen Bee Gun + features: + - feature-type: Oscillate + description: Oscillate + id: 3525faff-24d5-4b84-9b4d-b6e92f51f2f4 + output: + Oscillate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: c1150106-9f41-4a80-b30b-6015e1a7e80a + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: 57638eed-03e4-4279-8fc1-cc03a2d9066c + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 113cb4d3-f8a9-45b5-bf66-3e93e5209e4d + - identifier: + - K311 + name: Galaku Freya + features: + - feature-type: Vibrate + description: Vibrate + id: c52a581b-0838-4431-bd39-179628da18d4 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: ba7de25e-d0fd-4431-afc5-e8b72431b025 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: 309ff7a2-aa2f-44e4-ace9-c1d485bf47ae + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 13e7fd6e-2dec-400e-80e5-908a088572fc + - identifier: + - G339 + name: Galaku Rhino Prostate Massager + features: + - feature-type: Vibrate + description: Vibrate + id: 75e8f6e5-a69b-48d4-937b-c202961b464f + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: 3854e366-6eb9-4947-bc90-e246146bec11 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: be8475dd-8928-447d-9e94-1e0543056b29 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 5d47e890-6093-4eae-b7e8-e637dc82a2ea + - identifier: + - G354 + name: Galaku Double-A Aircraft Cup + features: + - feature-type: Vibrate + description: Vibrate + id: dc4348f2-7788-4b63-96f8-80ed74e4f9c2 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: e79abb39-74ab-46cc-9363-41637a43c885 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: 23e5cc47-944a-427c-be33-8611fffc70c8 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 1d9030a8-bfd2-4e49-8e8d-683c7776ae83 + - identifier: + - G12B + name: Galaku Flower Season + features: + - feature-type: Vibrate + description: Vibrate + id: e86333ca-254b-4c40-b448-eeb0e397e2f6 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: f531ad54-4f1f-4fe6-91dd-bba265307fb5 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: f989b7c6-ad5d-49fa-b103-2a21ff2213d5 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 7565ed2f-36c6-4210-830b-c916c4f8132b + - identifier: + - G29C + name: Galaku Little Rubik's Cube + features: + - feature-type: Vibrate + description: Vibrate + id: d8b78598-520b-4d28-9340-1a51d918f31a + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: ddc439b2-dc60-46bd-b6dc-4ce2b92783c0 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: 34bf9651-bbd6-475f-a2ea-536b04c5db62 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 8a41b478-7239-4412-b251-66dcb62f0e98 + - identifier: + - G29D + name: Galaku Small powder cake + features: + - feature-type: Vibrate + description: Vibrate + id: 8dccfd7a-397e-450c-8911-31d2258506f5 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: 6031712c-95a0-457f-93b6-e24b8ab7d335 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: 7e0681c6-7206-41d0-97d2-f3e01d6c8de4 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: fae6c568-0e7f-446f-9523-81964f51728c + - identifier: + - GKML + name: Galaku Milly + features: + - feature-type: Vibrate + description: Vibrate + id: 48936afe-dfda-4a35-bd45-1da66bdc020f + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: f17eba7d-aab9-43d9-a621-4e5b3addd682 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: 67430820-ef54-4821-8d43-37b7ebc6702f + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 722fc3e9-8349-4659-b71b-9c77d437f695 + - identifier: + - G348 + name: Galaku Rhinoceros Back Court + features: + - feature-type: Vibrate + description: Vibrate + id: 8afa26c6-e525-4afc-84f7-a9602d82ddf9 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: ed5039d6-24ea-4adb-becd-ab549aff67ce + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: 8b8b2df2-1f06-4649-b575-ae0abef990dc + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: d546987d-311b-4db1-80d6-b8df1a06b275 + - identifier: + - G913 + name: Galaku Unicorn II + features: + - feature-type: Vibrate + description: Vibrate + id: dff9df20-91d3-478f-b5dd-409db449d9ff + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: f23839bb-69c4-4570-9eb0-ea387a1fa87f + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: 10d3c65c-e6b1-4802-b71f-5843bb6ae4bd + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 536ea0fc-ef97-40a1-be31-56f9cabd489e + - identifier: + - G213 + name: Galaku Phantom + features: + - feature-type: Vibrate + description: Vibrate + id: 5e4c85dc-27df-45fa-a7cc-f2870596b7ed + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: cb5581ba-2f77-49e3-bf0a-856639e045e1 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: f8057621-5690-43fe-8cf9-aa2b1d4ceb07 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: ee326d2c-8241-40b7-9ccd-3662a5901197 + - identifier: + - TFF1 + name: Galaku F1 Aircraft Cup + features: + - feature-type: Oscillate + description: Oscillate + id: 5027b245-170a-47ca-b9b6-d93c48532d56 + output: + Oscillate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: 376aee27-8c1b-4d26-a5e3-9b92be56036d + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: 42b39996-60ac-4ee7-9880-1bc8d73b543a + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: e1516f9a-9f56-4859-832d-6b637c6880e5 + - identifier: + - G310 + name: Galaku Scepter AV Stick + features: + - feature-type: Vibrate + description: Vibrate + id: 7d6f9b0d-2296-42d6-a989-63366e943fff + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: ed69fd16-6951-4176-96b5-e267cb4213e4 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: 76599534-d259-4420-acf8-f172421b684e + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: a849f281-4415-4b0d-a2e2-5b93e8d36833 + - identifier: + - K113 + name: Galaku Unicorn II + features: + - feature-type: Vibrate + description: Vibrate + id: 5debcf2d-4e98-4b5f-88b0-45f4bcd3aaf1 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: 787e3d35-0ea2-407e-8b4b-ecb0680ddfa3 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: c6d8ebc8-bba3-4aaa-b616-3758a6a84b06 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 568f5426-4d6d-4fed-b915-c4ead0dc2b70 + - identifier: + - G228 + name: Galaku Little Dolphin + features: + - feature-type: Vibrate + description: Vibrate + id: 484bcea7-f227-49f3-83f8-ab825c46e0f4 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: 4d68f7a8-2fd1-40f3-8d5f-b932b0fb5d8f + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: f93f3c1d-8046-40f2-a4d3-4c5315c809e6 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: b1a680ee-43ea-44a1-95f0-b287d9b87d07 + - identifier: + - G310 + name: Galaku Scepter AV Stick + features: + - feature-type: Vibrate + description: Vibrate + id: 525a328a-1fe1-4f54-be62-1aade3f4dcab + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: 0f5a8b59-1ba2-4e0f-9de4-272ee2fae908 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: 246cddf5-f04a-45e2-ba07-1f5354d15fdd + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 59723525-29b0-4cfe-b327-c4337e94cce7 + - identifier: + - TFF1 + name: Galaku F1 Aircraft Cup + features: + - feature-type: Vibrate + description: Vibrate + id: e19f5460-6145-48b9-9151-c16765130341 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: f44a3499-e077-41c5-93ba-56a840c8485b + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: 79874bf3-3055-4d5a-a6aa-ea183f434324 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: f989e3e1-6df9-4ab1-a2a6-04aded3fe9a3 + - identifier: + - D358 + name: Galaku Classic vibration-absorbing AV state + features: + - feature-type: Vibrate + description: Vibrate + id: 98b72986-86e9-44dc-a48c-e4b64d5941c0 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: 907f514f-4cfa-4210-88c8-2ae602cade4b + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: 338f4e14-793b-4cb7-b26e-0ff47f2e72cc + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 3ff9c409-8790-4b06-af84-a0ddf103bf23 + - identifier: + - G322 + name: Galaku Unicorn + features: + - feature-type: Vibrate + description: Vibrate + id: d61c7b5a-b021-43bf-a246-9b7dc193cf98 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: 64ecb833-2b8a-46c6-afac-28aa36d05580 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: 87973aa3-f77e-47b1-92dc-1a6b32bba5d5 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: d3c966a9-9341-44b5-a54d-842402010dc5 + - identifier: + - D402 + name: Galaku New series of vibrators + features: + - feature-type: Vibrate + description: Vibrate + id: daedd54d-0d62-434f-8408-d3d9f69cd151 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: 7ebb5f9d-e447-4b67-8b3a-997b46a5f2be + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: b872a7d6-df4c-4d50-8e7b-57cc7102b151 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 9ccc2c45-2762-4005-9de1-f636b44d0e0e + - identifier: + - G40A + name: Galaku New series of vibrators + features: + - feature-type: Vibrate + description: Vibrate + id: 1954d249-a830-4c2f-9a54-73962b0a7f62 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: b0a5e213-8e34-4868-9f93-477d707b555a + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: f5555828-157d-44af-a6f3-61c184adc78b + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 8202daae-1d8f-468e-b772-31f6032e92ff + - identifier: + - G403 + name: Galaku New series of vibrators + features: + - feature-type: Vibrate + description: Vibrate + id: 1db2e6ef-89a9-44a6-b4fe-858c583181cc + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: af1c0858-6f69-49bd-81e0-2b5634cba141 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: 0acf4462-c96b-4dec-b283-d56fdeae3e09 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 5910e68c-1ed0-4dd1-b9b9-74bb2332d3b7 + - identifier: + - G43A + name: Galaku New series of vibrators + features: + - feature-type: Vibrate + description: Vibrate + id: 9204650b-9e73-4423-9de1-94e87cf8cf7b + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: 3e533985-211f-4c4e-996e-6ee5999a8f7b + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: 01388799-5cdf-4127-824b-a51ae1c38e60 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 49ce5f25-f210-43cf-a20e-bb0879b89c63 + - identifier: + - K12B + name: Galaku Little Turtle Stick + features: + - feature-type: Vibrate + description: Vibrate + id: 50c856df-a8d2-4840-bc3d-17f7bc2144e8 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: cc865a89-7a1f-4d9c-ac03-8822ec1ab715 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: 9ecdcaa7-b228-4f67-b04b-a1ff3642ebe2 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: ec43f998-0089-4bef-8a8d-d3ce49747fff + - identifier: + - QCVW + name: Kisstoy Lost (Vibrating) + features: + - feature-type: Vibrate + description: Vibrate + id: cf8ed969-86d5-4597-850f-35c60cfc40e8 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: 13dd1aad-9102-46c9-b126-5293b5da88ad + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: 421f8bf8-6732-405a-b563-139e858bc4fb + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: c61bdc8f-230b-4cc8-9474-c145ecba7682 + - identifier: + - QCSW + name: Kisstoy Lost (Sucking) + features: + - feature-type: Vibrate + description: Vibrate + id: 02b1d882-d47e-4dc2-8062-91e9b6defdd4 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: 1e4691ca-fda3-40da-bad9-b2f7393d5554 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: 0b41e97c-17f9-475d-8a30-d8ed1f52cb67 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 287d283c-d1f6-4dd4-9b53-fc01adafed30 + - identifier: + - QCPW + name: Kisstoy Lost (Insertable) + features: + - feature-type: Vibrate + description: Vibrate + id: 2d070dbf-a2ad-4072-b7ee-a13b278fe4a4 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: cddbd1f6-227d-48e3-a1bc-74332b153a24 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: ad753ac1-6c20-495a-bb0d-409b251fbe26 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 746b8d6f-41ba-433f-b225-b3bf98c7aec9 + - identifier: + - SN80 + name: Adorime G-spot Rabbit Dildo Vibrator + features: + - feature-type: Vibrate + description: Vibrate + id: 42efb235-b450-44a6-97fd-a98b3d9750ad + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrate + id: 76a8c59e-2001-4334-bacd-f436f6858e85 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: 47b24f11-bb92-4173-9123-80a330c76041 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 361c0746-f630-41d3-a0bf-f993e2259217 + # Type 2 + - identifier: + - TFG1 + name: Galaku Aurora Aircraft Cup + features: + - feature-type: Vibrate + description: Vibrate + id: 2b5fdcd4-3b35-4939-b086-950a827141e1 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Constrict + description: Suction Pump + id: 59498f0e-ad39-4701-9197-a5c7428b0acc + output: + Constrict: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: 591ca427-79d4-4d6a-bf00-8596cd9cb493 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 0d60d3a5-bad0-4df7-99ad-4bf4ee442c5d + - identifier: + - GK27 + - GX27 + name: Galaku Cannon-GT + features: + - feature-type: Vibrate + description: Vibrate + id: ff51f8a4-4ac0-434c-b656-d94e0b2eec53 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: e0b9f2c7-68d9-4c7b-9327-6e0802973a44 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 687bbb0e-b5a6-47d8-bca3-3395c510d996 + - identifier: + - GK25 + name: Galaku Phantom PLUS + features: + - feature-type: Vibrate + description: Vibrate + id: d8411669-9823-4755-afe4-969f7a4200cd + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: afb9c389-4624-4871-bfed-c19eccbcd3e3 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 4169f6af-723c-437c-be39-d90508c95e0a + - identifier: + - AC695X_1(BLE) + name: Galaku Vision + features: + - feature-type: Vibrate + description: Vibrate + id: 8626a95c-2ebd-43b4-a592-27282c6cc275 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: b680b236-52f4-4d8e-907e-78e71a0d23e9 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 637fec12-7e76-4107-ba18-931046975976 + - identifier: + - GX33 + name: Galaku Dimension No. 1 + features: + - feature-type: Vibrate + description: Vibrate + id: 90351a28-a5c0-4b77-bd61-d5e667588cf1 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: ab7abe60-7733-4391-a61d-765655275261 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 34c495ac-a36f-4d8c-9823-191895926d49 + - identifier: + - WSXK + name: Galaku Starry Sky CUP + features: + - feature-type: Vibrate + description: Vibrate + id: 80d6340d-70bd-40ba-87bd-014f034a3186 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: 1ef7a2d2-1725-4fd9-9e70-d8e0674ac17f + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 938f9e14-3d1d-4778-821a-a1c17bb42936 +communication: + - btle: + names: + # Type 0 + # - V415 see galaku-pump + - GX85 + - GX07 + - GX17 + - GX21 + - GX22 + - GX16 + - GX29 + - GX23 + - GX25 + - GX26 + - GK03 + - GX39 + - G321 + - G304 + - G336 + - G331 + - G326 + - G335 + - G341 + - G355 + - G349 + - G407 + - G204 + - G171 + - G12D + - G123 + - G23A + - G336 + - G23A + - A073 + - GLMT + - G901 + - G912 + - G901 + - G20B + - K112 + - G202 + - K118 + - K107 + - G203 + - TXHL + - TXMM + - TXKL + - K108 + - K109 + - KWL2 + - TFHL + - TFMM + - TFKL + - K120 + - K12A + - K12C + - LL18 + - CYX2 # Secret Lover Simon + - RC31 # Secret Lover Betty + - MD19 # Secret Lover Kevin + - QD48 # Adorime Wearable Egg Vibrator + - BGSF # Adorime Male Masturbator + - BGQS # Adorime Penis Vibrator + - AX05 # Adorime Anal Vibrator + # Type 1 + - G317 + - G312 + - G302 + - G320 + - G314 + - G228 + - G315 + - G307 + - K311 + - G339 + - G354 + - G12B + - G29C + - G29D + - GKML + - G348 + - G913 + - G213 + - TFF1 + - G310 + - K113 + - G228 + - G310 + - TFF1 + - D358 + - G322 + - D402 + - G40A + - G403 + - G43A + - K12B + - QCVW + - QCSW + - QCPW + - SN80 # Adorime G-spot Rabbit Dildo Vibrator + # Type 2 + - TFG1 + - GK27 + - GX27 # https://discord.com/channels/353303527587708932/1370367465573580881 + - GK25 + - AC695X_1(BLE) + - GX33 + - WSXK + services: + 00001000-0000-1000-8000-00805f9b34fb: + tx: 00001001-0000-1000-8000-00805f9b34fb + rxblebattery: 00001002-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/hgod.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/hgod.yml new file mode 100644 index 000000000..dc8cff655 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/hgod.yml @@ -0,0 +1,19 @@ +defaults: + name: Hgod Device + features: + - feature-type: Vibrate + id: cd638669-9f47-400f-8dcf-80583e7e563a + output: + Vibrate: + step-range: + - 0 + - 10 + id: d786a1cc-7a7c-4b8b-996c-1d2fce573ca2 +communication: + - btle: + names: + - AMN NEO + services: + 0000ffe3-0000-1000-8000-00805f9b34fb: + tx: 0000ffe1-0000-1000-8000-00805f9b34fb + rx: 0000ffe2-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/hismith-mini.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/hismith-mini.yml new file mode 100644 index 000000000..8c01cd9c6 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/hismith-mini.yml @@ -0,0 +1,186 @@ +defaults: + name: Hismith Mini device + features: + - feature-type: Oscillate + description: Fucking Machine Oscillation Speed + id: cd95dc09-627b-489e-841a-39cd5f06bf6d + output: + Oscillate: + step-range: + - 0 + - 100 + id: 195a4797-7b3a-4ecf-bffb-810f9b870a8b +configurations: + - identifier: + - '4001' + name: Auxfun Sex Machine + id: 6227affb-9e0e-49cb-a77b-7913d40f83ce + - identifier: + - '1005' + - '1102' + name: Hismith Sex Machine + id: de78cf6a-30c2-40ce-ac8a-a060735c65ac + - identifier: + - '1004' + name: Hismith Mini Sex Machine + id: fa840f6f-6815-4fed-b238-4260ac21b90f + - identifier: + - '1101' + name: Hismith Servo Sex Machine + id: 330de697-9702-4bc7-89d6-3faf603f0238 + - identifier: + - '1402' + name: Hismith Ukulele + id: 18f342d3-a927-44ac-9605-cf16ec8aad74 + - identifier: + - '1501' + name: Hismith PleasureDrive + id: 5b98725d-56b3-499b-830d-50dc004c27c5 + - identifier: + - '2201' + name: Sinloli Automatic Sex Doll + features: + - feature-type: Constrict + description: Air Pump + id: 1c45bd7c-ca54-483b-9994-f6d4c18cd59f + output: + Constrict: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrator + id: 23c0c1f0-af15-492d-8405-3ce3f24d13a3 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 81341b4e-144b-4427-b5e9-5024b12441c7 + - identifier: + - '3101' + name: Eropair Rabbit Vibrator + features: + - feature-type: Vibrate + description: Internal Vibrator + id: 85ca7d86-a508-4d9e-9ee5-0223a4b68805 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: External Vibrator + id: 950bc937-6be1-4f6c-8d18-36cbd4d25bee + output: + Vibrate: + step-range: + - 0 + - 100 + id: e59964ad-0c44-4301-9148-f8837e197d35 + - identifier: + - '3102' + name: Eropair Thrusting Vibrating Dildo + features: + - feature-type: Oscillate + description: Thruster + id: 6255e8b0-f188-4a8b-9325-4c70af3b20be + output: + Oscillate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrator + id: e0eb75eb-a14b-4947-97de-0bd36517dabd + output: + Vibrate: + step-range: + - 0 + - 100 + id: c1762d51-d2f7-4a03-bb8e-30cde5942831 + - identifier: + - '2101' + name: Eropair Cup + features: + - feature-type: Constrict + description: Air Pump + id: 39ed62dd-77c2-4488-ba09-33792a65b013 + output: + Constrict: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrator + id: d36a28fd-0042-4c5c-a36c-e0a72173e0ab + output: + Vibrate: + step-range: + - 0 + - 100 + id: 8ffeec80-9b8f-4cb5-a70d-6b6d8170a688 + - identifier: + - '2204' + name: Sinloli Cosima + features: + - feature-type: Oscillate + description: Stroker Oscillation Speed + id: 928b7b2b-9e4e-47bc-8196-e304174e78fa + output: + Oscillate: + step-range: + - 0 + - 100 + - feature-type: Constrict + description: Air Pump + id: e9b6dc68-e89a-4f7b-a74f-8a25b31346ee + output: + Constrict: + step-range: + - 0 + - 100 + id: 9eb5977d-38be-4e77-8a26-1d69e8286689 + - identifier: + - '2202' + name: Sinloli Ethel + features: + - feature-type: Oscillate + description: Stroker Oscillation Speed + id: 030bcd37-38f1-415f-b59e-d0013497fadf + output: + Oscillate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + description: Vibrator + id: 19ca1ed9-94ee-46f8-9b70-0e79a013db9d + output: + Vibrate: + step-range: + - 0 + - 100 + id: a14d8479-e4b9-463f-af23-e78bd0c5d2c7 + - identifier: + - '2205' + name: Sinloli Aston + id: d9ced3ed-cc74-4731-baeb-7bbf7fda288e +communication: + - btle: + names: + - Auxfun-Box + - Sinloli + - Sinloli-Sherry + - Eropair * + - HISMITH S1 + - HISMITH S2 + - HISMITH S3 + - Sinloli Cosima + - Sinloli-Ethel + - Sinloli Aston + services: + 0000ffe5-0000-1000-8000-00805f9b34fb: + tx: 0000ffe9-0000-1000-8000-00805f9b34fb + 0000ff90-0000-1000-8000-00805f9b34fb: + rxblemodel: 0000ff96-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/hismith.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/hismith.yml new file mode 100644 index 000000000..5dfde5e8e --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/hismith.yml @@ -0,0 +1,88 @@ +defaults: + name: Hismith device + features: + - feature-type: Oscillate + description: Fucking Machine Oscillation Speed + id: 24291feb-53a7-49ee-898a-8c42f534508f + output: + Oscillate: + step-range: + - 0 + - 100 + id: a8689335-db27-4a23-8724-6973168bb474 +configurations: + - identifier: + - '1001' + name: Hismith Sex Machine + id: 169414bc-55d6-4ada-a9ec-eae862e80e09 + - identifier: + - '1002' + name: Hismith Pro Traveler + id: 33a59054-9a87-4ecb-9893-3b5101b6431b + - identifier: + - '1003' + name: Hismith Capsule + id: 119197ff-5750-40bf-9770-024e75cbe20c + - identifier: + - '2001' + name: Hismith Thrusting Cup + features: + - feature-type: Oscillate + description: Stroker Oscillation Speed + id: 1663c651-cab6-444d-bbd7-39baf190d6ab + output: + Oscillate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: b6a5ed20-e10a-4370-aa9e-0cd85bf1c6f7 + output: + Vibrate: + step-range: + - 0 + - 1 + id: 188ee17a-d776-4f9b-baaa-903b9fea276f + - identifier: + - '1006' + name: Hismith G011 + features: + - feature-type: Oscillate + description: Stroker Oscillation Speed + id: 8621627f-4561-4272-9d95-231d9b8d3440 + output: + Oscillate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 5815777e-11e1-4998-b9a6-68e09656f18c + output: + Vibrate: + step-range: + - 0 + - 1 + id: fb1d1aa1-5a88-4a39-af74-bc127d670ab1 + - identifier: + - '3001' + name: Wildolo Device + features: + - feature-type: Vibrate + id: 5ac186f5-ada6-4ec2-a65a-910b8b2292cc + output: + Vibrate: + step-range: + - 0 + - 100 + id: ef153cf6-130d-43a1-82f1-4a16e457e8ea +communication: + - btle: + names: + - HISMITH + - Wildolo + - "\aHISMITH" + services: + 0000ffe5-0000-1000-8000-00805f9b34fb: + tx: 0000ffe9-0000-1000-8000-00805f9b34fb + 0000ff90-0000-1000-8000-00805f9b34fb: + rxblemodel: 0000ff96-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/htk_bm.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/htk_bm.yml new file mode 100644 index 000000000..02bfb81e5 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/htk_bm.yml @@ -0,0 +1,27 @@ +defaults: + name: HTK Breast Massager + features: + - feature-type: Vibrate + id: 3b33611d-bbba-498e-969d-526106c7e785 + output: + Vibrate: + step-range: + - 0 + - 1 + - feature-type: Vibrate + id: d41e037a-b6ab-4016-a07c-f9eb7e414efb + output: + Vibrate: + step-range: + - 0 + - 1 + id: 3589254d-f271-4059-b2c3-3a5776d1eb02 +communication: + - btle: + names: + - HTK-BLE-BM001 + services: + 0000180f-0000-1000-8000-00805f9b34fb: + rxblebattery: 00002a19-0000-1000-8000-00805f9b34fb + 00001802-0000-1000-8000-00805f9b34fb: + tx: 00002a06-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/itoys.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/itoys.yml new file mode 100644 index 000000000..d41e41747 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/itoys.yml @@ -0,0 +1,56 @@ +defaults: + name: iToys Device + features: + - feature-type: Vibrate + id: 5f1a3edb-6015-404a-865a-c3ee2d568ed4 + output: + Vibrate: + step-range: + - 0 + - 3 + id: 5c58b967-b75f-4f5d-99ef-f581b2579918 +configurations: + - identifier: + - 26-021-B + name: iToys Seagull + id: 2eafb465-e72a-4acd-9344-b9e13fc1f2ed + - identifier: + - SML-2310-SZ-B + name: iToys Twinkling Stars + id: f0458e98-a317-4b1c-af82-bb3f163aeff3 + features: + - feature-type: Vibrate + id: 07601b03-2dc3-4996-aaa7-d23b5aa793f5 + output: + Vibrate: + step-range: + - 0 + - 3 + - feature-type: Oscillate + id: 6d3f5346-4947-41b1-847e-39cd2f485a0a + output: + Oscillate: + step-range: + - 0 + - 255 + - identifier: + - ASF-001-BT-R + name: Defyeah Horizontal Sex Machine ASFO16 + id: 0108797c-1cea-486d-9ed5-3b4412fb6593 + features: + - feature-type: Oscillate + id: c742d608-2110-4377-aaea-7173d7f1dc83 + output: + Oscillate: + step-range: + - 0 + - 255 +communication: + - btle: + names: + - 26-021-B + - SML-2310-SZ-B + - ASF-001-BT-R + services: + 0000ffa0-0000-1000-8000-00805f9b34fb: + tx: 0000ffa1-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/jejoue.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/jejoue.yml new file mode 100644 index 000000000..e03ee2a0e --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/jejoue.yml @@ -0,0 +1,25 @@ +defaults: + name: Je Joue Device + features: + - feature-type: Vibrate + id: a723e382-c32d-4170-b909-50e9ecb9d17f + output: + Vibrate: + step-range: + - 0 + - 5 + - feature-type: Vibrate + id: 79434539-5c1d-459a-abbe-833f0a7403be + output: + Vibrate: + step-range: + - 0 + - 5 + id: 3ad4a393-215b-4cc7-9d77-9541b3b1dab1 +communication: + - btle: + names: + - Je Joue + services: + 0000fff0-0000-1000-8000-00805f9b34fb: + tx: 0000fff1-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/joyhub-v2.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/joyhub-v2.yml new file mode 100644 index 000000000..1c0b80b6a --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/joyhub-v2.yml @@ -0,0 +1,1365 @@ +defaults: + name: JoyHub Device + features: + - feature-type: Vibrate + id: 076c95a5-a869-401b-bd5f-c51ef681c488 + output: + Vibrate: + step-range: + - 0 + - 255 + id: e126925b-4cd6-414c-84fb-dc62464e07bb +configurations: + - identifier: + - J-Pearlconch + name: JoyHub Pearlconch + features: + - feature-type: Rotate + id: ae8e847a-fbe2-4650-8c7e-372399981bac + output: + Rotate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: eb9b02b6-7902-4f4e-8a3d-ae9b6a77595d + output: + Vibrate: + step-range: + - 0 + - 255 + id: 7f324fea-ce2c-4e72-bfc2-b2227251a2c7 + - identifier: + - J-Pearlconch + name: JoyHub Pearlconch + features: + - feature-type: Rotate + id: e5102a93-330d-48b2-a901-79b2b1c6990c + output: + Rotate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: 002b77e4-cef3-4718-98e3-0644cf0461d7 + output: + Vibrate: + step-range: + - 0 + - 255 + id: 9a5b2555-5d9f-4364-8e5b-0e0c2eed9849 + - identifier: + - J-PearlconchL + name: JoyHub Pearlconch L + features: + - feature-type: Rotate + id: a696f55c-376d-4304-aaa4-c25013c4e20f + output: + Rotate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: 597375f8-9698-4c08-8d45-9d732b84b06e + output: + Vibrate: + step-range: + - 0 + - 255 + id: d91c5f72-7a5e-4a38-999a-3118a49ff6d4 + - identifier: + - J-Piet2 + name: JoyHub Piet 2 + features: + - feature-type: Vibrate + id: 00a0dfd6-93a3-40e9-a72f-8c182bb76b67 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Rotate + id: 67e1286e-5572-4c3a-bf11-15f1161f3697 + output: + Rotate: + step-range: + - 0 + - 255 + id: d2aa1980-7943-4c39-b66d-a2f0ba495ce5 + - identifier: + - J-Panther + name: JoyHub Panther + features: + - feature-type: Vibrate + id: 3d236d1d-51b3-4412-bba4-6fc959e5fddf + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Rotate + id: 9307744e-0fcb-4a8a-a5cc-537b4d57c326 + output: + Rotate: + step-range: + - 0 + - 255 + id: 84323f4e-f5f0-48be-9504-cb2798702780 + - identifier: + - J-PetiteRose + name: JoyHub Petite Rose + features: + - feature-type: Vibrate + id: bb3a1f82-2b94-40b7-993b-375c77a92a4f + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Rotate + id: 4b5e922d-f920-43eb-b6f9-2772a4c62496 + output: + Rotate: + step-range: + - 0 + - 255 + id: a8b1f6cd-6b86-488a-a21a-5715669134cc + - identifier: + - J-MoonHorn + name: JoyHub Moon Horn + features: + - feature-type: Vibrate + id: 12048627-fb6c-48af-8fd1-2ab5f40c59df + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Constrict + description: Suction + id: 8b6ce43b-6b60-4497-9c5b-d2b48de13c13 + output: + Constrict: + step-range: + - 0 + - 9 + id: 46fe6203-6b1c-40c5-ba96-91748b35cdd7 + - identifier: + - J-Mecha + name: JoyHub Mecha + features: + - feature-type: Vibrate + id: 23b843f6-801e-48cb-b741-ecfb249ad6a0 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Constrict + description: Suction + id: d67b7e66-080e-4d2c-bbb8-d6e38392961b + output: + Constrict: + step-range: + - 0 + - 7 + id: 764cd060-fd7d-454b-a0bc-10183bb34238 + - identifier: + - J-Lagoon + name: JoyHub Lagoon + features: + - feature-type: Vibrate + id: 4095e42c-1979-42c1-895f-033c3a348a3f + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Constrict + description: Suction + id: c663c71c-befb-4ed1-bb81-d344ee61f3c0 + output: + Constrict: + step-range: + - 0 + - 5 + id: 74ba519b-e31f-4708-8430-6bf0cdea42ac + - identifier: + - J-VibTrefoil + name: JoyHub VibTrefoil + features: + - feature-type: Vibrate + description: External vibrator + id: 8c5ab96c-da9e-419b-ae89-a775ee65fc6d + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + description: Internal vibrator + id: 18af5f39-ea31-43d6-af1e-1b0073576294 + output: + Vibrate: + step-range: + - 0 + - 255 + id: f3b581da-64cd-4643-97d9-0d97683c26f3 + - identifier: + - J-Firedragon + name: JoyHub Firedragon + features: + - feature-type: Oscillate + id: 5bdbe9f5-8075-4afe-8df0-6a960030feeb + output: + Oscillate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: 49429631-a654-4a44-bffe-58c0c2d5289a + output: + Vibrate: + step-range: + - 0 + - 255 + id: 1a1e5e28-5892-4f51-b236-9af6e190cb29 + - identifier: + - J-Dina + name: JoyHub Deena + features: + - feature-type: Oscillate + id: 32860a3d-7370-41ce-9183-046b4fb78f15 + output: + Oscillate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + description: Internal vibrator + id: c88be4c1-7aed-45b5-af68-1f6345d30acb + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + description: External vibrator + id: bebeab4e-9bbd-4064-adb2-d704958c63b0 + output: + Vibrate: + step-range: + - 0 + - 255 + id: bd517815-efb5-427d-88a1-edaff6b0ceba + - identifier: + - J-Vbarbie3f + name: JoyHub Cherly + features: + - feature-type: Vibrate + description: External vibrator + id: 08410e6a-b6f6-4bea-a570-9535407b946b + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + description: Internal vibrator + id: 5a5dc25a-0859-4491-a092-814c71b33b67 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Oscillate + id: 52cc6b42-a1f1-4b8b-ab81-cde582ce1aa9 + output: + Oscillate: + step-range: + - 0 + - 255 + id: ed4f639b-e041-4258-ad8d-4f9ef5f850a7 + - identifier: + - J-CHERLY2c + name: JoyHub Cherly 2c + features: + - feature-type: Vibrate + description: Internal vibrator + id: 3b9cebe0-369d-4086-8a6c-c2d1fe0499a5 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + description: Internal Whip + id: de793e03-1879-40e3-aa8a-5b76a832a56d + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + description: External vibrator + id: ddec3601-be51-490c-a20a-df9a01def1a5 + output: + Vibrate: + step-range: + - 0 + - 255 + id: 0b29424b-d609-4049-b206-831c00bd53c1 + - identifier: + - J-Pathfinder2 + name: JoyHub Pathfinder 2 + features: + - feature-type: Oscillate + id: 2dcf4211-6e27-413a-aa7a-bd9085edb9fe + output: + Oscillate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: 0bde094e-f3d9-48d1-b076-56412838d1c9 + output: + Vibrate: + step-range: + - 0 + - 255 + id: 5b6ebea4-e363-463d-9922-99add3a7c656 + - identifier: + - J-Pathfinder + name: JoyHub Pathfinder + features: + - feature-type: Oscillate + id: b4564c01-12d0-44f9-b3cf-de53068d4692 + output: + Oscillate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: 881dc72c-b2a1-4b0e-9cf7-a351d7b27fe9 + output: + Vibrate: + step-range: + - 0 + - 255 + id: 828d5f2d-9381-4363-bb7e-ffa4964a0970 + - identifier: + - J-VibRipple + name: JoyHub Angela + features: + - feature-type: Vibrate + description: External vibrator + id: 788cb23d-d3c2-4a84-8114-1ee7df4fe367 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + description: Internal vibrator + id: f70b48a2-75ab-44ca-98d3-3f11a2440698 + output: + Vibrate: + step-range: + - 0 + - 255 + id: 9f1be5fa-70c9-4853-bc11-1685304a0d86 + - identifier: + - J-Verax + name: JoyHub Verax + features: + - feature-type: Vibrate + description: Internal Whip + id: 36586dac-a0e5-45ce-a5d5-ff2ec6961e83 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + description: Internal vibrator + id: 76c2ca34-393d-407c-9ae8-954fcc6c13d1 + output: + Vibrate: + step-range: + - 0 + - 255 + id: 07ce35bd-9fc9-4224-8809-13245fe1d3f0 + - identifier: + - J-Verax2 + name: JoyHub Verax 2 + features: + - feature-type: Vibrate + id: be955fe4-d3af-4a0a-a4f9-0c2b3c3cddf7 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Rotate + id: 763324b6-3056-497a-bd07-99c69780358a + output: + Rotate: + step-range: + - 0 + - 255 + id: 258d4904-2feb-4b68-b7fc-7dd4df687a9e + - identifier: + - J-Euphoric2 + name: JoyHub Euphoric 2 + features: + - feature-type: Oscillate + id: 7a437340-eb86-450a-8db3-4c594a638d63 + output: + Oscillate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: 42504b4b-cd77-49c0-abb0-f2ddba7cda72 + output: + Vibrate: + step-range: + - 0 + - 255 + id: f09e8dde-475d-488e-bf21-60bf80f8d2ac + - identifier: + - J-ROSEBUD + name: JoyHub RoseBUD + features: + - feature-type: Vibrate + id: d4c00919-5cd0-434c-9164-62da64967ec8 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Rotate + description: Flicker + id: 727d8c05-7896-4812-9996-36decea2dd49 + output: + Rotate: + step-range: + - 0 + - 255 + - feature-type: Constrict + description: Suction + id: c9f73966-4777-4512-91c2-30349a0bd270 + output: + Constrict: + step-range: + - 0 + - 5 + id: 40a2d620-719e-4d0f-abfc-ec3fa2fe9f92 + - identifier: + - J-Morningbuds2 + name: JoyHub Morningbuds + features: + - feature-type: Rotate + id: 3ecaa10d-338b-4119-bd21-77d662cc1fd1 + output: + Rotate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: f33780a7-56a9-4e8a-b05b-6f92ca0c1366 + output: + Vibrate: + step-range: + - 0 + - 255 + id: 10030c6e-d04d-4613-8feb-41748e638684 + - identifier: + - J-Rhythmic4 + name: JoyHub Rhythmic 4 + features: + - feature-type: Oscillate + id: 77ff9786-c024-4755-af20-0b86a5165269 + output: + Oscillate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: 05de8ce7-24c5-4cb4-8162-5d57f9b46d26 + output: + Vibrate: + step-range: + - 0 + - 255 + id: da2596bc-b8c9-4a47-b671-20095ac1bcdb + - identifier: + - J-Virtuoso2 + name: JoyHub Virtuoso 2 + features: + - feature-type: Vibrate + id: 3391b4b5-a2f5-4bcd-9274-76e8586a4af6 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Rotate + id: e06a6c43-a6ed-4e13-a49e-6375b8aab136 + output: + Rotate: + step-range: + - 0 + - 255 + - feature-type: Constrict + description: Suction + id: 10ca15ff-70e6-4ec4-a258-d7ac8119c47a + output: + Constrict: + step-range: + - 0 + - 3 + id: b73b29bf-5202-4c45-b292-b9a3d538bbb6 + - identifier: + - J-Dyllis + name: JoyHub Dyllis + features: + - feature-type: Oscillate + id: aa769623-c0cb-41d2-bbfa-eb15348422f7 + output: + Oscillate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: e783132a-c6e1-4445-83e2-6ab985c2af66 + output: + Vibrate: + step-range: + - 0 + - 255 + id: a8278c49-58c3-416e-9ae1-072dcfe0f694 + - identifier: + - J-Flamewing + name: JoyHub PhoenixGP + features: + - feature-type: Oscillate + id: 0c1cd9b2-a466-4807-a8be-5b2158a7b04d + output: + Oscillate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: da7ca1ac-4c38-4cc6-aa88-737ff2d4be27 + output: + Vibrate: + step-range: + - 0 + - 255 + id: 1f6a2310-f773-40aa-8a93-bd83f7d78119 + - identifier: + - J-Fabledragon + name: JoyHub Fable Dragon + features: + - feature-type: Oscillate + id: f20ff8eb-afc6-45c4-be6b-0b071141b1bc + output: + Oscillate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: 52eb1885-853a-45f8-85a2-b43a18b79d89 + output: + Vibrate: + step-range: + - 0 + - 255 + id: ee76aeea-337d-44b8-9631-2bd8c8f2acda + - identifier: + - J-Faunus + name: JoyHub Faunus + features: + - feature-type: Oscillate + id: 06b57eb1-50f8-4393-908d-05628120bd14 + output: + Oscillate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: 5a4433de-c45c-46b6-9911-b17948daae74 + output: + Vibrate: + step-range: + - 0 + - 255 + id: 8c4d26b6-f091-4e34-bf13-c6bc303712b5 + - identifier: + - J-VelvetRabbit + name: JoyHub Velvet Rabbit + features: + - feature-type: Vibrate + id: 03b40869-05c1-4d17-9ebf-9566f7f2e9c9 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: 9231af9e-98db-464a-931a-fe80bad3fcaf + output: + Vibrate: + step-range: + - 0 + - 255 + id: 6eae28db-c885-454f-98d4-2e5683bb05d9 + - identifier: + - J-VividPulse + name: JoyHub Vivid Pulse + features: + - feature-type: Vibrate + id: 66e6dd1e-6717-4f47-8868-de317e09b42a + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Oscillate + id: 7e8fc7f6-39c5-469c-b479-dcf85e8deeef + output: + Oscillate: + step-range: + - 0 + - 255 + id: 90caf141-3bee-4024-8d5e-cc854da852d0 + - identifier: + - J-VioletVine + name: JoyHub Violet Vine + features: + - feature-type: Vibrate + id: d45e5cf6-fe20-4eb3-9c48-0c8ed6a4aad6 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: fc78a0c8-262e-4b24-920e-8e91f38417c0 + output: + Vibrate: + step-range: + - 0 + - 255 + id: 4b0128a4-b849-4f60-a0b4-16ebe8500cfe + - identifier: + - J-VibSiren2 + name: JoyHub VibSiren 2 + features: + - feature-type: Vibrate + id: 904e3dfa-d69c-4e0e-9d50-9f119ff959f2 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: ffc701ee-ec1b-42d1-8c99-9a755d595438 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Oscillate + id: 7fafb528-74f3-49df-af78-dc2b64e4bed1 + output: + Oscillate: + step-range: + - 0 + - 255 + id: e2eeccb0-2601-43d1-b1cc-b10234e0004d + - identifier: + - J-Veemy + name: JoyHub Veemy + features: + - feature-type: Vibrate + id: 53ef1d9b-4020-408d-8126-1d484448bccc + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: 88fbe85b-a98a-4965-9f47-c69812fbc66f + output: + Vibrate: + step-range: + - 0 + - 255 + id: 873595ac-acdd-41b2-b162-74ca9776f0f8 + - identifier: + - J-Viball + name: JoyHub Viball + features: + - feature-type: Vibrate + id: 9ac37f94-8129-4c09-83d2-bd2b0d4aae53 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Oscillate + id: fce9a8eb-f227-41f1-bb75-f6dc64573fc5 + output: + Oscillate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: ccecf0fc-e657-432a-8a68-ada09d396934 + output: + Vibrate: + step-range: + - 0 + - 255 + id: e3646777-6550-4984-91bb-3cd738744494 + - identifier: + - J-Vase + name: JoyHub Vase + features: + - feature-type: Vibrate + id: 0d80c22d-a8c4-4f7a-8ec0-0f912653b8a4 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: 21fff2c0-5ccf-459c-9eea-02f95b3174a8 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Oscillate + id: c534acf2-bc28-4384-aa79-f70537b23ab8 + output: + Oscillate: + step-range: + - 0 + - 255 + id: 24d26313-74a9-4515-945f-0f31edb3650a + - identifier: + - J-Vortex2s + name: JoyHub Vortex 2s + features: + - feature-type: Vibrate + id: a0383ad8-05ae-4dae-be06-b384744499f3 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: cddef660-59b2-4f4b-b9ec-16439cd7c12e + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: 14c6efec-d40c-4f21-8459-67a11c079c2d + output: + Vibrate: + step-range: + - 0 + - 255 + id: dbe616e2-478e-4e87-8f7b-4c86835502fe + - identifier: + - J-VortexTongue2 + name: JoyHub Lips + features: + - feature-type: Vibrate + id: e72404a7-9f94-4074-bf3c-40ba5e2a4fbf + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Rotate + id: 25ceb7c6-0dfd-415e-aa74-b1f4ac49d031 + output: + Rotate: + step-range: + - 0 + - 255 + - feature-type: Constrict + description: Air Pump + id: 4bda889f-f1b5-4293-8bd8-f05e30ac188c + output: + Constrict: + step-range: + - 0 + - 3 + id: 83956181-5ebd-4251-bc92-4b10f9bec1f4 + - identifier: + - J-Torin + name: JoyHub Torin + features: + - feature-type: Vibrate + id: 051de0d3-5d2f-4a04-8f4c-a9a6747b2cd1 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: ac0377fa-a7c2-4d5b-bbcc-402d378a1343 + output: + Vibrate: + step-range: + - 0 + - 255 + id: 985e3726-cc4d-4059-972d-654af41a5947 + - identifier: + - J-VBarbiep + name: JoyHub VBarbie p + features: + - feature-type: Vibrate + id: 38c3e4ae-0de5-4e17-9d7a-2e639c293aeb + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: 95db76e1-abc0-4774-a588-9092615291e7 + output: + Vibrate: + step-range: + - 0 + - 255 + id: 1347963d-6bad-41c5-bf3a-314980e3316b + - identifier: + - J-Vbarbie + name: JoyHub VBarbie + features: + - feature-type: Vibrate + id: 058349cf-49ea-453d-8fbd-0b13e880c301 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: 0cbd4cd8-3a5d-4528-b49a-05f199828155 + output: + Vibrate: + step-range: + - 0 + - 255 + id: 73a6f6a2-1fb0-45b0-b379-89eac6aefae5 + - identifier: + - J-Royaleye + name: JoyHub Royaleye + features: + - feature-type: Vibrate + id: 6ee6fa8a-a6a3-4131-8ea9-c35909999167 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: 06a656af-181b-4fa3-94e2-4aa0115cfbc9 + output: + Vibrate: + step-range: + - 0 + - 255 + id: 6f05cc4a-adb1-402d-a392-daa120223257 + - identifier: + - J-VBarbie2t + name: JoyHub Norma + features: + - feature-type: Vibrate + id: d314083c-0588-46ae-aecb-9695305c3439 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: e8afb080-dd64-418a-a07a-197bc6779a9e + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Oscillate + id: 9c9a7901-540d-44b1-ba38-0c8e794e1d9b + output: + Oscillate: + step-range: + - 0 + - 255 + id: 2e417090-ec06-4039-8e60-bf497cec3257 + - identifier: + - J-Pau + name: JoyHub Pau + features: + - feature-type: Oscillate + id: 63355e3e-edef-4317-a679-89b85ced0f4a + output: + Oscillate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: a159d6eb-2e95-4d4b-b74d-537cc77cf7b1 + output: + Vibrate: + step-range: + - 0 + - 255 + id: d693dc6b-3b7a-4ff0-8990-1a10f884ddc4 + - identifier: + - J-Petalwish3 + name: JoyHub Petalwish 3 + features: + - feature-type: Oscillate + id: fe2531e3-3815-4110-9022-06f7f4aa44aa + output: + Oscillate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: 5930bf48-ec9a-4914-b110-47d7e13ddbaf + output: + Vibrate: + step-range: + - 0 + - 255 + id: cb6f0926-32bd-4b48-8676-4cd6df9123a4 + - identifier: + - J-Marshal + name: JoyHub Marshal + features: + - feature-type: Vibrate + id: 29a272ab-f6b6-4a90-ad84-7c21846d7164 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Constrict + description: Air Pump + id: 485b9a41-05d4-440a-a3a4-a3b2bf1ee693 + output: + Constrict: + step-range: + - 0 + - 9 + id: a4d28447-2535-415b-aaab-ebe3ee2e92ba + - identifier: + - J-Vince + name: JoyHub Vince + features: + - feature-type: Vibrate + id: b8bf1392-8a84-4647-a833-be03de144b0a + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: e983d64e-411e-486f-8695-76b4e57b3bd1 + output: + Vibrate: + step-range: + - 0 + - 255 + id: 6dd6c377-c35d-4300-a892-4aace5589ec5 + - identifier: + - J-Dallin + name: JoyHub Dallin + features: + - feature-type: Oscillate + id: 8412021b-0962-4469-b45e-0a59f3272ad0 + output: + Oscillate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: bbc10f1c-171a-4f14-b6e4-520dda5df19f + output: + Vibrate: + step-range: + - 0 + - 255 + id: b559b1ec-d336-45bb-b6e6-cc22344eefd7 + - identifier: + - J-Mace2 + name: JoyHub Maynor + features: + - feature-type: Vibrate + id: f79abcb3-666d-4ba4-b6d3-9cff722b8a1f + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Constrict + description: Air Pump + id: 92fb7f24-e7a2-4bdd-8c93-27610ba1f45d + output: + Constrict: + step-range: + - 0 + - 9 + id: d418dd65-6f41-4af4-a04d-4b343ec778ab + - identifier: + - J-Verax4 + name: JoyHub Verax 4 + features: + - feature-type: Vibrate + id: 9ee6b8e0-a694-4c22-8a82-3fc01f60f99c + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: 514ec2f4-2a2b-4c1e-9eb3-eed3b67c2951 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: 905657e5-fda1-4f0b-9043-a7b3d760e7da + output: + Vibrate: + step-range: + - 0 + - 255 + id: 2a5abb95-efac-45e0-9f56-9fb9f1c9f274 + - identifier: + - J-Palmyra + name: JoyHub Palmyra + features: + - feature-type: Vibrate + id: d7fed551-18b0-4da8-a8b0-596e93fc3e0b + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Oscillate + id: 33414af0-d5bc-461c-821f-54c43d85423b + output: + Oscillate: + step-range: + - 0 + - 255 + id: 8fe7695d-60aa-4af5-92c2-364e8eebf076 + - identifier: + - J-Xylia + name: JoyHub Xylia + features: + - feature-type: Vibrate + id: 8148b859-0acd-4749-a8f3-57ca82d4a156 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Oscillate + id: b1e1444f-e6d7-4045-8565-adff4f25eb87 + output: + Oscillate: + step-range: + - 0 + - 255 + id: bdc796d7-d029-4732-9d8d-037e421f19e8 + - identifier: + - J-Maiden + name: JoyHub Maiden + features: + - feature-type: Rotate + id: 90bf6a90-e1cb-4600-ad00-d4f29bfc4adb + output: + Rotate: + step-range: + - 0 + - 255 + - feature-type: Constrict + id: 0663888b-60c0-491d-aa66-7ec4c2c57b08 + output: + Constrict: + step-range: + - 0 + - 5 + id: c5bd6fb4-b36f-4b3c-865c-943eab645f5e + - identifier: + - J-Viele3 + name: JoyHub Viele 3 + features: + - feature-type: Vibrate + id: 518d1ed4-3b91-4f56-bd29-b7af30598ef1 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Rotate + id: f575f285-a104-4d0d-b5f7-414ea6d67433 + output: + Rotate: + step-range: + - 0 + - 255 + id: 5a23e800-0b33-435b-9139-023533b92880 + - identifier: + - J-Troi + name: JoyHub Troi + features: + - feature-type: Vibrate + id: f48cb279-cbe7-4857-8178-632bd0d1081c + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: 3041d01a-fb7c-48c3-a302-e71d37f5a12e + output: + Vibrate: + step-range: + - 0 + - 255 + id: 01ba3988-0a1c-4afc-b6c7-1c19a2b15ac4 + - identifier: + - J-Tanmouth + name: JoyHub Tanmouth + features: + - feature-type: Vibrate + id: d2f033a7-0805-40e0-acc2-51d4bb635095 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: a44ab42a-fb71-4120-b7a9-705181549ecb + output: + Vibrate: + step-range: + - 0 + - 255 + id: 192325d9-a343-4b9b-bd77-6d9b665a6988 + - identifier: + - J-Marcela + name: JoyHub Marcela + features: + - feature-type: Oscillate + id: aab23df2-2530-488b-8d1a-3bc6429409ae + output: + Oscillate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: cfe637a9-7024-4aa0-9b97-55815f082332 + output: + Vibrate: + step-range: + - 0 + - 255 + id: 1df39ccb-a6d2-41d5-906e-14a42bbd96ed + - identifier: + - J-Vita + name: JoyHub Vita + features: + - feature-type: Vibrate + id: e3308e8e-c0ba-4cf8-a3b3-26cbbea3bea5 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Rotate + id: 95ebe9f7-ad90-4627-bfcc-4ee1f1fdfdba + output: + Rotate: + step-range: + - 0 + - 255 + - feature-type: Oscillate + id: ad45f3ec-513d-423e-a60f-57765c5a07b0 + output: + Oscillate: + step-range: + - 0 + - 255 + id: 1a066cb3-b758-48d2-9296-4dec65115e9a + - identifier: + - J-LACH + name: JoyHub Lach + features: + - feature-type: Vibrate + id: 33aa95b4-e36d-4af8-9de7-cc6447afd03d + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Constrict + id: 5ee461b4-770f-4686-bd6c-c13f12ab0f54 + output: + Constrict: + step-range: + - 0 + - 5 + id: e309c90f-c63a-4883-af14-4a69e899cf12 + - identifier: + - J-Markel + name: JoyHub Markel + features: + - feature-type: Oscillate + id: 90cfdc1e-9bc5-49f9-8993-058f85e5e082 + output: + Oscillate: + step-range: + - 0 + - 255 + - feature-type: Constrict + description: Suction + id: 2cb024d3-33be-4369-bb0c-4c61cc39c62e + output: + Constrict: + step-range: + - 0 + - 9 + - feature-type: Vibrate + id: 22e539e8-4bf0-49e9-883c-112a2d51ea60 + output: + Vibrate: + step-range: + - 0 + - 255 + id: d818b1e1-4270-4e38-8b07-d723c0a97e31 + - identifier: + - J-Pipes + name: JoyHub Pipes + features: + - feature-type: Rotate + id: 558425ee-cf28-48bf-b08f-12568cd3b3ee + output: + Rotate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: 8c8f8f70-e814-4a0e-aa5c-b06b53a9ab80 + output: + Vibrate: + step-range: + - 0 + - 255 + id: 641296fe-8ccc-4a63-8487-790dd419321e + - identifier: + - J-Vigo + name: JoyHub Vigo + features: + - feature-type: Vibrate + id: 89a3e300-3640-4a11-99e4-6585dce725a4 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: a23b9a72-7b22-42ec-ab7d-7936d7141689 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Oscillate + id: 1e6c3008-5efc-4dd1-bee5-95e7e0b016ad + output: + Oscillate: + step-range: + - 0 + - 255 + id: 398a3a62-3bba-433e-80ad-50d129b695db +communication: + - btle: + names: + - J-Pearlconch + - J-PearlconchL + - J-PetiteRose + - J-MoonHorn + - J-VibTrefoil + - J-Panther + - J-Mecha + - J-Lagoon + - J-Firedragon + - J-Dina + - J-Vbarbie3f + - J-CHERLY2c + - J-Pathfinder2 + - J-Pathfinder + - J-VibRipple + - J-Verax + - J-Verax2 + - J-Euphoric2 + - J-ROSEBUD + - J-Morningbuds2 + - J-Rhythmic4 + - J-Virtuoso2 + - J-Dyllis + - J-Flamewing + - J-VelvetRabbit + - J-VividPulse + - J-VioletVine + - J-VibSiren2 + - J-Veemy + - J-Fabledragon + - J-Faunus + - J-VortexTongue2 + - J-Torin + - J-VBarbiep + - J-Vbarbie + - J-Viball + - J-Vase + - J-Vortex2s + - J-Royaleye + - J-VBarbie2t + - J-Pau + - J-Petalwish3 + - J-Marshal + - J-Piet2 + - J-Vince + - J-Dallin + - J-Mace2 + - J-Verax4 + - J-Palmyra + - J-Maiden + - J-Viele3 + - J-Xylia + - J-Troi + - J-Tanmouth + - J-Marcela + - J-Vita + - J-LACH + - J-Markel + - J-Pipes + - J-Vigo + services: + 0000ffa0-0000-1000-8000-00805f9b34fb: + tx: 0000ffa1-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/joyhub-v3.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/joyhub-v3.yml new file mode 100644 index 000000000..a691692ad --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/joyhub-v3.yml @@ -0,0 +1,28 @@ +defaults: + name: JoyHub Device + features: + - feature-type: Vibrate + id: 3adea9b9-8a81-4358-8774-17b621f33907 + output: + Vibrate: + step-range: + - 0 + - 255 + id: acd3b85a-c842-458d-8ff8-eeaaf9be1562 +configurations: + - identifier: + - J-Ringstar + name: JoyHub Starfish + id: 40241a70-ecbd-4c08-8acf-8ee70e7b5d55 + - identifier: + - J-RapidTwist2 + name: JoyHub Resi Ring 2 + id: 4611fa22-18b8-46fe-bece-070e24e1b9e8 +communication: + - btle: + names: + - J-Ringstar + - J-RapidTwist2 + services: + 0000ffa0-0000-1000-8000-00805f9b34fb: + tx: 0000ffa1-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/joyhub-v4.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/joyhub-v4.yml new file mode 100644 index 000000000..f0000962e --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/joyhub-v4.yml @@ -0,0 +1,68 @@ +defaults: + name: JoyHub Device + features: + - feature-type: Vibrate + id: 95e495dc-7b4f-43fd-91ee-b7842f047f59 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Rotate + id: 0f6f75c5-66e8-4293-9ee0-50af9ecfc1b0 + output: + Rotate: + step-range: + - 0 + - 255 + - feature-type: Constrict + description: Suction + id: 487bb0bd-af93-40ff-a92c-6e18772e707f + output: + Constrict: + step-range: + - 0 + - 4 + id: 12907be0-52b2-4df1-a4d1-29c246d72f2f +configurations: + - identifier: + - J-RoseLin + name: JoyHub RoseLin + id: cea67021-dff3-4012-88c0-321706408a55 + - identifier: + - J-Viele + name: JoyHub Viele + features: + - feature-type: Rotate + description: Internal Simulator + id: c731fe0b-3216-428a-9cc5-8e8f2fa21275 + output: + Rotate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + description: Internal Whip + id: 5462e403-9c83-429f-9dd5-db099f18e4e8 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + description: Internal Vibrator + id: f4407e47-4094-41c6-95b8-41f7c20e0f04 + output: + Vibrate: + step-range: + - 0 + - 255 + id: 7c5a1ffd-3228-4513-a180-115c94983eac +communication: + - btle: + names: + - J-RoseLin + - J-Viele + services: + 0000ffa0-0000-1000-8000-00805f9b34fb: + tx: 0000ffa1-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/joyhub-v5.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/joyhub-v5.yml new file mode 100644 index 000000000..5c8f4a80c --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/joyhub-v5.yml @@ -0,0 +1,51 @@ +defaults: + name: JoyHub Device + features: + - feature-type: Rotate + id: 2c03096f-8fd6-4c80-84ba-d07936f76928 + output: + Rotate: + step-range: + - 0 + - 255 + - feature-type: Constrict + description: Suction + id: e9e32817-2cc1-4365-baa6-054fb7f6aa74 + output: + Constrict: + step-range: + - 0 + - 1 + id: abc5309a-008d-41fd-b4db-5fd54614c582 +configurations: + - identifier: + - J-Virtuoso + name: JoyHub Virtuoso + id: fa5a696c-780f-4763-9af2-a619cbae330c + - identifier: + - J-Pathfinder3 + name: JoyHub Pathfinder 3 + features: + - feature-type: Vibrate + id: b91f2775-f628-43c4-bd04-a8844f74d4e1 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Oscillate + id: 3e00301a-c942-4b8d-8f49-fe2af7ecf0b6 + output: + Oscillate: + step-range: + - 0 + - 255 + id: 6e782468-f084-442a-936f-27d7abd5f840 +communication: + - btle: + names: + - J-Virtuoso + - J-Pathfinder3 + services: + 0000ffa0-0000-1000-8000-00805f9b34fb: + tx: 0000ffa1-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/joyhub-v6.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/joyhub-v6.yml new file mode 100644 index 000000000..94387e418 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/joyhub-v6.yml @@ -0,0 +1,31 @@ +defaults: + name: JoyHub Device + features: + - feature-type: Vibrate + id: 9fbf30f4-3f0d-4377-a232-55132d023d11 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Constrict + description: Suction + id: a38653c9-c245-4c98-86c9-3c0da68d646c + output: + Constrict: + step-range: + - 0 + - 9 + id: f89fcd7a-2411-4241-ae81-f4488e926d16 +configurations: + - identifier: + - J-Melody + name: JoyHub Melody + id: 2c33b13e-9d00-4823-bc5b-fda18dbd3691 +communication: + - btle: + names: + - J-Melody + services: + 0000ffa0-0000-1000-8000-00805f9b34fb: + tx: 0000ffa1-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/joyhub.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/joyhub.yml new file mode 100644 index 000000000..da225cfaf --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/joyhub.yml @@ -0,0 +1,469 @@ +defaults: + name: JoyHub Device + features: + - feature-type: Vibrate + id: fc2f0fc2-fb75-4eee-b92b-20eaf7cc9a1e + output: + Vibrate: + step-range: + - 0 + - 255 + feature-settings: + alt-protocol-index: 1 + id: 53cf03db-266d-46c1-964e-0ef505a64200 +configurations: + - identifier: + - JOYHUB-ROSELLA2 + name: JoyHub Rosella 2 + id: 5b78e797-3ff6-4ca8-be15-28a1f3983dca + - identifier: + - J-Velocity + name: JoyHub Velocity + id: bc35f659-b67b-4df5-afdd-46053c2a5366 + - identifier: + - J-ElixirEgg + name: JoyHub ElixirEgg + id: 6cbbce9e-6154-4260-8d2c-69cc52edd2ee + - identifier: + - J-RetroGuard + name: JoyHub Retro Guard + id: 481344d5-9edd-48c4-8867-d0d639648d09 + - identifier: + - J-TrueForm3 + name: JoyHub TrueForm 3 + id: 5a3c541a-2924-44cc-a92d-d48b58cf0159 + - identifier: + - J-TrueForm + name: JoyHub TrueForm + id: 6368a677-6c33-4765-8baf-1cd0cd4bb06e + - identifier: + - J-Rhythmic2 + name: JoyHub Rhythmic 2 + id: 46533dc6-6f1b-4b17-9f31-06b076f417d6 + - identifier: + - J-Rhythmic3 + name: JoyHub Rhythmic 3 + id: 1a5dd035-8107-4db3-924d-503113b1c600 + - identifier: + - J-Rainbow + name: JoyHub Rainbow + id: 907042dc-2681-46a0-9a49-3b8564faa41a + - identifier: + - J-BlackBull + name: JoyHub Black Bull + id: b92595de-f564-4298-a444-9c8bd1a2c7f9 + - identifier: + - J-Peacock + name: JoyHub Peacock + id: 1b560be9-462d-4e08-adb5-2a38690e6ab2 + - identifier: + - J-Mace + name: JoyHub Mace + id: b67fe066-44ff-41be-983d-0ed3e4a7b3ee + - identifier: + - J-Tarian + name: JoyHub Tarian + id: 609b9d5a-45c2-4f6d-a396-34f21e932c12 + - identifier: + - J-Euphoric + name: JoyHub Euphoric + id: c2aea3e0-551b-4e7f-90e6-819878ad6aec + - identifier: + - J-Euphoric3 + name: JoyHub Euphoric3 + id: 4b936259-c2d8-4459-9824-5992c0c22430 + - identifier: + - J-Torrian + name: JoyHub Torrian + id: a0a65312-dc6a-4e7b-a5cb-b1b8499df070 + - identifier: + - J-Rayen + name: JoyHub Rayen + id: 08956682-7cf2-4a01-85d7-7132f8b0690e + - identifier: + - J-Mackay + name: JoyHub Mackay + id: add6c7a5-7a3f-4d3d-abac-da7f9b498ef2 + - identifier: + - J-Rowdy3 + name: JoyHub Rowdy 3 + id: f175684d-3bc2-4c8a-a36b-b68275602179 + - identifier: + - J-Eclipse + name: JoyHub Eclipse + id: 26bab7e2-0a38-4790-bdf0-8d9e1927106a + - identifier: + - J-Scarlett + name: JoyHub Scarlett + id: d7176dba-ce2b-4395-bf26-1b8ab653d8b5 + - identifier: + - J-Tarik + name: JoyHub Tarik + id: f6b8c5db-eca9-4041-9e07-48521ed3a55f + - identifier: + - J-Derik + name: JoyHub Urica Guard + id: 7252d5cc-5f1c-49ca-b2c8-49d7502c1f6b + - identifier: + - J-UricaGuard2 + name: JoyHub Urica Guard 2 + id: a2f973ff-e6cd-4b70-a711-2b24f2d03b6d + - identifier: + - J-Viva + name: JoyHub Viva + id: 6d3ee1c9-0452-4a01-8f73-75d196179e5c + - identifier: + - J-Ryden + name: JoyHub Ryden + id: 25ef0abd-31ed-497f-8fc0-ea374f600ee7 + - identifier: + - J-Peachy + name: JoyHub Peachy + id: eda4e5c4-4a91-4260-a14b-570926e346f6 + - identifier: + - J-Enam + name: JoyHub Enam + id: b1aa4a71-1346-43c4-9de3-7ecc642607d5 + - identifier: + - J-Viv + name: JoyHub Viv + id: 5a4c2185-67a9-4e15-862b-3c913aaecaad + - identifier: + - J-Vivara + name: JoyHub Vivara + id: 307496a5-a0e0-498e-b635-c6bc346cab1c + - identifier: + - J-Explorer2 + name: JoyHub Explorer 2 + id: f499492b-571c-4766-830c-c751706e280d + - identifier: + - J-Petalwish2 + name: JoyHub Petalwish 2 + features: + - feature-type: Oscillate + id: 0d5685ae-95ea-4d2d-849e-b75b7354bc35 + output: + Oscillate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: e092343a-c826-4bc8-a579-e179b50cf65e + output: + Vibrate: + step-range: + - 0 + - 255 + id: 904ef5c8-7030-4c2f-9c12-d69154ab10c3 + - identifier: + - J-VortexTongue + name: JoyHub Vortex Tongue + features: + - feature-type: Vibrate + id: 95313411-9fb3-4df9-b672-c7279ca7d243 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Constrict + description: Air Pump + id: d2f66bd3-96c4-4377-b1f5-45a2f3d99c9e + output: + Constrict: + step-range: + - 0 + - 3 + - feature-type: Rotate + id: 042a4817-348c-4595-9fbc-463ffa903041 + output: + Rotate: + step-range: + - 0 + - 255 + id: c85fd4cf-5bc1-4300-9cb8-a4db4fa8b85f + - identifier: + - J-VibSiren + name: JoyHub VibSiren + features: + - feature-type: Vibrate + description: External vibrator + id: d03ea16f-3126-469d-bf85-843a7c6e2cf6 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Oscillate + id: 115ec3d5-df22-474a-aa5a-32236fcb517e + output: + Oscillate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + description: Internal vibrator + id: cd3828ee-8fe0-4214-acce-9fc4aac9ea46 + output: + Vibrate: + step-range: + - 0 + - 255 + id: 380428d0-73a4-4437-bf48-fb6b26663d1d + - identifier: + - J-Mysticolor + name: JoyHub Mysticolor + features: + - feature-type: Rotate + id: a7a34c6b-5d77-4a38-9708-780ba97cd34f + output: + Rotate: + step-range: + - 0 + - 255 + - feature-type: Constrict + description: Air Pump + id: 7891e1b3-82c3-4e83-936c-2a156f2ba826 + output: + Constrict: + step-range: + - 0 + - 7 + id: 1ca6396e-bee2-42c8-901c-82e975998085 + - identifier: + - J-VividWings + name: JoyHub Vivid Wings + features: + - feature-type: Vibrate + id: 686761a8-fcc9-4a41-9725-045d5cb0dae9 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Oscillate + id: 21c831d4-0956-4b9b-a90e-31a545a89708 + output: + Oscillate: + step-range: + - 0 + - 255 + id: 576095da-d4a5-4f19-9b14-6244cbfe8096 + - identifier: + - J-Mariner + name: JoyHub Mariner + features: + - feature-type: Rotate + id: 439bea28-4c09-4b81-8dd5-dce2ec31781e + output: + Rotate: + step-range: + - 0 + - 255 + - feature-type: Constrict + description: Air Pump + id: 9f386242-41a2-4c86-9167-db6c58840cc7 + output: + Constrict: + step-range: + - 0 + - 2 + id: 67ed28b9-c0fe-4155-b7b8-3829ec12a485 + - identifier: + - J-MarsLion + name: JoyHub MarsLion + features: + - feature-type: Vibrate + id: e43f723f-412d-4c75-8123-2483113a06a8 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Constrict + description: Air Pump + id: 54e3da8e-7f97-46c7-8a1e-9fa549b877c2 + output: + Constrict: + step-range: + - 0 + - 5 + id: 3f3b7c49-94b2-49b6-ba67-3e5539e204b9 + - identifier: + - J-Pul + name: JoyHub Pul + features: + - feature-type: Oscillate + id: a9b7d261-2877-4214-a539-8ce30e038386 + output: + Oscillate: + step-range: + - 0 + - 255 + id: db3efe9b-839c-495e-8c2e-b800b3125b36 + - identifier: + - J-ROSELLA3 + name: JoyHub Rose Love + features: + - feature-type: Constrict + description: Air Pump + id: 0d3b3010-d438-4899-b1c2-d81bff0c6714 + output: + Constrict: + step-range: + - 0 + - 255 + id: ca36d3a7-c305-45e3-b8f7-3106b36b233a + - identifier: + - J-DukeDazzle2 + name: JoyHub Edasich + features: + - feature-type: Vibrate + id: 9fde0544-3307-4a4f-8abf-88ffb1dc3caf + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Oscillate + id: e0ca1697-1e42-4822-925c-691561916bee + output: + Oscillate: + step-range: + - 0 + - 255 + id: 877a8e55-9f08-4bea-826c-20371ba57577 + - identifier: + - J-Mars + name: JoyHub Mars + features: + - feature-type: Oscillate + id: a4a079b4-6cf2-47fc-bfef-0f2921c243db + output: + Oscillate: + step-range: + - 0 + - 255 + id: b4235543-7287-4698-a1e7-9d78c53d4c0a + - identifier: + - J-Martino + name: JoyHub Martino + features: + - feature-type: Oscillate + id: b306148c-c1d9-4281-bae9-fe1ccd876399 + output: + Oscillate: + step-range: + - 0 + - 255 + id: 76d1ddf5-e46b-4912-bea1-a748ce28a18e + - identifier: + - J-MarsLion2 + name: JoyHub Mars Lion 2 + features: + - feature-type: Vibrate + id: b6ffc3b3-9e8a-46cd-82f2-97df7237be83 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Constrict + id: ead93a87-9ad6-448f-a26a-cce980db265e + output: + Constrict: + step-range: + - 0 + - 5 + id: e693fbe3-f697-446e-8fa2-87e99e9e8cb6 + - identifier: + - J-Myrna + name: JoyHub Myrna + features: + - feature-type: Vibrate + id: 393dfa94-e3c8-4962-a053-c39e0447e420 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Constrict + id: b6e89b8c-207d-4588-9fff-f71d42e1a1a5 + output: + Constrict: + step-range: + - 0 + - 9 + id: e6502f8e-73c3-4b1f-9080-4428d6670045 + - identifier: + - J-Vase2 + name: JoyHub Vase 2 + features: + - feature-type: Vibrate + description: Biting lips + id: 7e13af66-c20f-42b3-ba85-764a2cdeaca0 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + description: Sideways flicker + id: f80dc564-7d53-4c6b-991e-ec18051a3207 + output: + Vibrate: + step-range: + - 0 + - 255 + id: cd4e9b09-367e-4ac1-8571-4f0ff4ca8996 +communication: + - btle: + names: + - J-Petalwish2 + - J-VortexTongue + - J-Velocity + - JOYHUB-ROSELLA2 + - J-VibSiren + - J-ElixirEgg + - J-RetroGuard + - J-TrueForm + - J-TrueForm3 + - J-Rhythmic2 + - J-Rhythmic3 + - J-Mysticolor + - J-VividWings + - J-Rainbow + - J-BlackBull + - J-Peacock + - J-Mariner + - J-Mace + - J-MarsLion + - J-Tarian + - J-Pul + - J-Euphoric + - J-Euphoric3 + - J-Torrian + - J-Rayen + - J-ROSELLA3 + - J-Mackay + - J-Rowdy3 + - J-Eclipse + - J-DukeDazzle2 + - J-Scarlett + - J-Tarik + - J-UricaGuard2 + - J-Viva + - J-Ryden + - J-Mars + - J-MarsLion2 + - J-Myrna + - J-Vase2 + - J-Martino + - J-Enam + - J-Viv + - J-Vivara + - J-Explorer2 + - J-Derik + services: + 0000ffa0-0000-1000-8000-00805f9b34fb: + tx: 0000ffa1-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/kgoal-boost.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/kgoal-boost.yml new file mode 100644 index 000000000..802e0c6c6 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/kgoal-boost.yml @@ -0,0 +1,23 @@ +defaults: + name: KGoal Boost + features: + - feature-type: Battery + description: Battery Level + id: 59d2de82-3acf-4316-982f-c2b570afd297 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 1835b668-d778-4552-b75a-95053e06cd5c +communication: + - btle: + names: + - Boost + services: + 0000180f-0000-1000-8000-00805f9b34fb: + rxblebattery: 00002a19-0000-1000-8000-00805f9b34fb + 8e7c6065-7656-17ad-1b41-b53d1a548e0d: + rxpressure: 10c2be2d-d2d5-b7a8-5f42-e2468c9ebbf5 diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/kiiroo-prowand.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/kiiroo-prowand.yml new file mode 100644 index 000000000..b13dd8b30 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/kiiroo-prowand.yml @@ -0,0 +1,36 @@ +defaults: + name: Kiiroo ProWand + features: + - feature-type: Vibrate + id: 2e585349-127b-4536-85b7-9d5b90e44df4 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Battery + description: Battery Level + id: ad812cb2-e04a-4656-9103-a80766601455 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: d1675d72-6d25-4cc4-99dc-a42e4e4fee97 +configurations: + - identifier: + - Luxus + name: Luxus + id: a857e95b-3d5a-4034-92d2-7105c4febb8e +communication: + - btle: + names: + - ProWand + - Luxus + services: + 00001400-0000-1000-8000-00805f9b34fb: + tx: 00001401-0000-1000-8000-00805f9b34fb + 0000180f-0000-1000-8000-00805f9b34fb: + rxblebattery: 00002a19-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/kiiroo-spot.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/kiiroo-spot.yml new file mode 100644 index 000000000..5ea68b152 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/kiiroo-spot.yml @@ -0,0 +1,30 @@ +defaults: + name: Kiiroo Spot + features: + - feature-type: Vibrate + id: a047482e-01d1-477a-bf67-71c1ee667f94 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: 5171bb1b-b234-4a56-96ae-d592d3065d00 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 850e3d26-54df-4eb3-879e-e6f6aa93d335 +communication: + - btle: + names: + - SPOT W1 + services: + 00001400-0000-1000-8000-00805f9b34fb: + tx: 00001401-0000-1000-8000-00805f9b34fb + 0000180f-0000-1000-8000-00805f9b34fb: + rxblebattery: 00002a19-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/kiiroo-v1.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/kiiroo-v1.yml new file mode 100644 index 000000000..8ebb156a3 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/kiiroo-v1.yml @@ -0,0 +1,39 @@ +defaults: + name: Kiiroo V1 Device + features: [] + id: dec656b7-b312-4626-9811-fe2d51ed1242 +configurations: + - identifier: + - PEARL + name: Kiiroo Pearl + features: + - feature-type: Vibrate + id: 31eee57b-a1d8-49de-ac72-0dba46885a28 + output: + Vibrate: + step-range: + - 0 + - 4 + id: aa35c397-8827-44c8-bc9f-a9acc234fba5 + - identifier: + - ONYX + name: Kiiroo Onyx + features: + - feature-type: PositionWithDuration + id: 2fe100ee-4665-4132-b4c6-d70a4037d6ac + output: + PositionWithDuration: + step-range: + - 0 + - 4 + id: f01513ef-a0c9-412d-ae70-b965b65379a8 +communication: + - btle: + names: + - ONYX + - PEARL + services: + 49535343-fe7d-4ae5-8fa9-9fafd205e455: + rx: 49535343-1e4d-4bd9-ba61-23c647249616 + tx: 49535343-8841-43f4-a8d4-ecbe34729bb3 + command: 49535343-aca3-481c-91ec-d85e28a60318 diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/kiiroo-v2-vibrator.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/kiiroo-v2-vibrator.yml new file mode 100644 index 000000000..af7212c5f --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/kiiroo-v2-vibrator.yml @@ -0,0 +1,134 @@ +defaults: + name: Kiiroo V2 Vibrator Device + features: + - feature-type: Vibrate + id: 9a7b7a0b-6601-48d6-adfe-0b39a6f152a8 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: b1c6be0a-efc9-4327-8103-5315ebf3ac95 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 33fd2145-87d1-48fd-aaa9-0188b218d444 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 7dd84343-dfa3-4436-88b8-d3b3cca14064 +configurations: + - identifier: + - Pearl2 + name: Kiiroo Pearl 2 + features: + - feature-type: Vibrate + id: e0374b68-eb67-4ecd-b566-8ca8bb74ce68 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 7581a2c2-0d94-45b4-b427-4a52b0ae3dea + - identifier: + - Fuse + name: OhMiBod Fuse + features: + - feature-type: Vibrate + id: 49587cee-c54e-41ab-9d70-0687ba4e6fec + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: a44beeed-4997-4e52-badc-7e1321338fbc + output: + Vibrate: + step-range: + - 0 + - 100 + id: 31e26147-c9af-45f0-8ee1-edd6c9f9e22e + - identifier: + - Virtual Rabbit + name: PornHub Virtual Rabbit + features: + - feature-type: Vibrate + id: de373981-ea04-4afb-8e58-15e392c7cbdf + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: db2f18c1-0a5f-40b2-b825-ac5a6932334e + output: + Vibrate: + step-range: + - 0 + - 100 + id: 0dbe6911-f95f-4abb-9550-5041a21f2ede + - identifier: + - Virtual Blowbot + name: PornHub Virtual Blowbot + features: + - feature-type: Vibrate + id: 35c2cebd-e539-42f6-be6a-15398bb60a22 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: f6ac9d49-3d48-4709-83ac-2ae0eb5ec74b + output: + Vibrate: + step-range: + - 0 + - 100 + id: d78facf3-706c-44ec-98e8-c4e7baba5966 + - identifier: + - Titan + name: Kiiroo Titan + features: + - feature-type: Vibrate + id: 5c535532-d02d-4acf-9482-fb17a5bc02ad + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 7a5a79b2-ff14-4ee6-ad91-d40649ca9d98 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 9fc946db-8889-403b-b7e1-ce86614b8176 + output: + Vibrate: + step-range: + - 0 + - 100 + id: b588d818-be20-4f01-b3ef-5383f6b60684 +communication: + - btle: + names: + - Pearl2 + - Fuse + - Virtual Blowbot + - Titan + - Virtual Rabbit + services: + 88f82580-0000-01e6-aace-0002a5d5c51b: + tx: 88f82581-0000-01e6-aace-0002a5d5c51b + rxtouch: 88f82582-0000-01e6-aace-0002a5d5c51b + rxaccel: 88f82584-0000-01e6-aace-0002a5d5c51b diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/kiiroo-v2.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/kiiroo-v2.yml new file mode 100644 index 000000000..6fb4dc53b --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/kiiroo-v2.yml @@ -0,0 +1,34 @@ +defaults: + name: Kiiroo v2 Device + features: + - feature-type: PositionWithDuration + id: 49b06ca8-dd4d-4306-91c6-931143dee212 + output: + PositionWithDuration: + step-range: + - 0 + - 99 + id: 1de4322c-86c4-40b1-8e1b-1f51c30392c0 +configurations: + - identifier: + - Launch + name: Fleshlight Launch + id: f54eacbc-d84d-4c58-9410-9fbff25f14e8 + - identifier: + - Onyx2 + name: Kiiroo Onyx 2 + id: 5f3e8a6a-3a47-43a0-aed6-689101509481 +communication: + - btle: + names: + - Launch + - Onyx2 + services: + 88f80580-0000-01e6-aace-0002a5d5c51b: + tx: 88f80581-0000-01e6-aace-0002a5d5c51b + rx: 88f80582-0000-01e6-aace-0002a5d5c51b + firmware: 88f80583-0000-01e6-aace-0002a5d5c51b + f60402a6-0293-4bdb-9f20-6758133f7090: + tx: 02962ac9-e86f-4094-989d-231d69995fc2 + rx: d44d0393-0731-43b3-a373-8fc70b1f3323 + firmware: c7b7a04b-2cc4-40ff-8b10-5d531d1161db diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/kiiroo-v21-initialized.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/kiiroo-v21-initialized.yml new file mode 100644 index 000000000..048e250b1 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/kiiroo-v21-initialized.yml @@ -0,0 +1,71 @@ +defaults: + name: Kiiroo V2.1 Initialized Device + features: [] + id: bd9c7fa4-214b-4871-8373-c5266ace0b90 +configurations: + - identifier: + - Onyx2.1 + name: Kiiroo Onyx 2.1 + features: + - feature-type: PositionWithDuration + id: 8cd94334-adde-4d9b-aad9-c2de93adb2c0 + output: + PositionWithDuration: + step-range: + - 0 + - 99 + id: eac00879-448c-46ed-aaa5-efe86226fb48 + - identifier: + - Onyx+ + name: Kiiroo Onyx+ + features: + - feature-type: PositionWithDuration + id: c66d882d-f752-45b4-806e-166d3e160eb8 + output: + PositionWithDuration: + step-range: + - 0 + - 99 + id: 40dafef9-ef94-4b03-8b8a-e9d7e9fef317 + - identifier: + - KEON + - Keon R2 + name: Kiiroo Keon + features: + - feature-type: PositionWithDuration + id: da002a11-610a-4e13-94c5-4c45d51814f2 + output: + PositionWithDuration: + step-range: + - 0 + - 99 + id: f3675b2e-d7b8-463b-8b91-30a5ebef24f4 + - identifier: + - Rey + - We-Vibe Rocketman + - Realm1.1 + name: Kiiroo Onyx+ Realm Edition + features: + - feature-type: PositionWithDuration + id: 8c896f82-2e17-46f9-9db2-531cc7e42236 + output: + PositionWithDuration: + step-range: + - 0 + - 99 + id: d2fde950-8e0a-4231-8ebc-5c39dcf3349f +communication: + - btle: + names: + - Rey + - We-Vibe Rocketman + - Realm1.1 + - Onyx2.1 + - Onyx+ + - KEON + - Keon R2 + services: + 00001900-0000-1000-8000-00805f9b34fb: + whitelist: 00001901-0000-1000-8000-00805f9b34fb + tx: 00001902-0000-1000-8000-00805f9b34fb + rx: 00001903-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/kiiroo-v21.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/kiiroo-v21.yml new file mode 100644 index 000000000..e731bf3a0 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/kiiroo-v21.yml @@ -0,0 +1,221 @@ +defaults: + name: Kiiroo V2.1 Device + features: [] + id: 189a4912-3c5b-4a0d-ab8b-d44ab6c97f0b +configurations: + - identifier: + - Pearl2.1 + name: Kiiroo Pearl 2.1 + features: + - feature-type: Vibrate + id: ba4166e4-fba3-4eb9-90a2-5b281bb02f1e + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: 61cf5ea0-f9d0-48f0-a337-f905fb89c2c3 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 1e922dde-c4f7-4ca9-96dd-d565135a184f + - identifier: + - Cliona + name: Kiiroo Cliona + features: + - feature-type: Vibrate + id: 222c4e24-d5ee-48c3-bc9d-d3f86d666c2c + output: + Vibrate: + step-range: + - 0 + - 100 + id: 232eab7f-e237-4683-a07f-e05e04b46360 + - identifier: + - OhMiBod 4.0 + - OhMiBod ESCA + name: OhMiBod Esca 2 + features: + - feature-type: Vibrate + id: 75940e97-626d-4016-87eb-2777c29aaec6 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 8d19c7db-4547-4a8d-b4e4-c8bd2379bcd0 + - identifier: + - Titan1.1 + name: Kiiroo Titan 1.1 + features: + - feature-type: Vibrate + id: a5a42b68-553c-4ba4-b68d-322c49d405bc + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: PositionWithDuration + id: b77ed4d9-9350-4868-8cb3-a6c48112f8b2 + output: + PositionWithDuration: + step-range: + - 0 + - 99 + id: 410c22ed-e0f8-4911-8e56-7f23b4e71bcc + - identifier: + - OhMiBod LUMEN + name: OhMiBod Lumen + features: + - feature-type: Vibrate + id: 7d824538-bc5c-47d9-8d4d-8a503bf35284 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 69ae3f47-bb0f-4761-a641-3fc68c7de630 + - identifier: + - OhMiBod NEX2 + name: OhMiBod NEX|2 + features: + - feature-type: Vibrate + id: ba1e86b4-9c6e-42d8-bff5-ac28628b3092 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 73fb1747-2056-403b-a6fb-56c521886a93 + - identifier: + - OhMiBod NEX3 + name: OhMiBod NEX|3 + features: + - feature-type: Vibrate + id: 9172bb5c-bbdc-4b56-a315-cb6b08bcb278 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 00784de1-fb46-4c86-973e-dd12f01e9827 + - identifier: + - Pulse Interactive + name: Hot Octopuss Pulse Solo Interactive + features: + - feature-type: Vibrate + id: b369b6d0-5d5d-40cd-bf7f-3cb7641e1ce7 + output: + Vibrate: + step-range: + - 0 + - 6 + id: e44fdd29-b3a0-4d37-b9af-e732f7934a13 + - identifier: + - Fuse1.1 + name: OhMiBod Fuse 1.1 + features: + - feature-type: Vibrate + id: 0e0820e3-aeec-4df2-ae2a-b4bf82b9a823 + output: + Vibrate: + step-range: + - 0 + - 100 + id: d6675d9e-9ddb-41dc-a0e4-0b0d54fd29cb + - identifier: + - OhMiBod Foxy + name: OhMiBod Foxy + features: + - feature-type: Vibrate + id: 187e471d-3815-4dab-85bc-e81969f26d40 + output: + Vibrate: + step-range: + - 0 + - 100 + id: bdcf6cd9-cc98-46c3-97eb-78b70b2a00a4 + - identifier: + - OhMiBod Chill Panty Vibe + name: OhMiBod Chill + features: + - feature-type: Vibrate + id: 75ed3cd9-8d21-4567-9816-71f7925dcce4 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 50d6c107-7ddf-4adc-9de6-f9fd1e08cdcf + - identifier: + - OhMiBod Sphinx + name: OhMiBod Sphinx + features: + - feature-type: Vibrate + id: 6a78e124-8314-40ec-bcc4-45f10341eaf7 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 15a13fb0-d287-4262-bf7a-26ae019d997b + - identifier: + - Pearl2+ + - Pearl 2+ + name: Kiiroo Pearl 2+ + features: + - feature-type: Vibrate + id: 69d4719c-2342-4d80-a8bc-70f5008b1628 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 5ef95603-09d0-4d44-9714-a7100b319371 + - identifier: + - Pearl3 + - Pearl 3 + name: Kiiroo Pearl 3 + features: + - feature-type: Vibrate + id: b3b2cea4-5987-413f-b611-aa068c76c04c + output: + Vibrate: + step-range: + - 0 + - 100 + id: 8fb6578e-bbbc-42d7-9c2e-7c813bd89f29 +communication: + - btle: + names: + - Titan1.1 + - Cliona + - Pearl2.1 + - Pearl2+ + - Pearl 2+ + - Pearl3 + - Pearl 3 + - OhMiBod 4.0 + - OhMiBod LUMEN + - OhMiBod NEX2 + - OhMiBod NEX3 + - OhMiBod ESCA + - OhMiBod Foxy + - OhMiBod Chill Panty Vibe + - OhMiBod Sphinx + - Pulse Interactive + - Fuse1.1 + services: + 00001900-0000-1000-8000-00805f9b34fb: + whitelist: 00001901-0000-1000-8000-00805f9b34fb + tx: 00001902-0000-1000-8000-00805f9b34fb + rx: 00001903-0000-1000-8000-00805f9b34fb + a0d70001-4c16-4ba7-977a-d394920e13a3: + tx: a0d70002-4c16-4ba7-977a-d394920e13a3 + rx: a0d70003-4c16-4ba7-977a-d394920e13a3 diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/kizuna.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/kizuna.yml new file mode 100644 index 000000000..40aeefa39 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/kizuna.yml @@ -0,0 +1,18 @@ +defaults: + name: Kizuna Smart + features: + - feature-type: Rotate + id: 7077cb50-d3d5-4357-8b5f-42517ffc83b8 + output: + Rotate: + step-range: + - 0 + - 9 + id: 654be6a2-bfe6-4358-bd0a-0d8f2cd9d105 +communication: + - serial: + port: default + baud-rate: 19200 + data-bits: 8 + parity: 'N' + stop-bits: 1 diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/lelo-f1s.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/lelo-f1s.yml new file mode 100644 index 000000000..12512e15b --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/lelo-f1s.yml @@ -0,0 +1,26 @@ +defaults: + name: Lelo F1s + features: + - feature-type: Vibrate + id: 006eb802-d890-4a0f-a566-288d86ec1caf + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 787c4a90-e78c-489a-a0eb-f66b3c70d6d2 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 83c52d23-0532-4b57-8a0b-c8132a5c52bd +communication: + - btle: + names: + - F1s + services: + 0000fff0-0000-1000-8000-00805f9b34fb: + tx: 0000fff1-0000-1000-8000-00805f9b34fb + rx: 00000aa4-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/lelo-f1sv2.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/lelo-f1sv2.yml new file mode 100644 index 000000000..75770d642 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/lelo-f1sv2.yml @@ -0,0 +1,41 @@ +defaults: + name: Lelo F1s V2 + features: + - feature-type: Vibrate + id: 90bd67a5-4601-4c49-97bb-0845ab7011ba + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 05fc758b-a3fe-4156-b3ae-9cdcb9ae95c6 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 108d5cfe-2155-477f-b1b6-c48da6c4b7d8 +configurations: + - identifier: + - F1SV2A + - F1SV2X + name: Lelo F1s V2 + id: 64505ced-309b-4a32-93a8-13ee55e2da2c + - identifier: + - F1SV3 + name: Lelo F1s V3 + id: 36adf7ce-98bf-4fad-b916-b44d20a5d9e1 +communication: + - btle: + names: + - F1SV2A + - F1SV2X + - F1SV3 + services: + 0000fff0-0000-1000-8000-00805f9b34fb: + tx: 0000fff1-0000-1000-8000-00805f9b34fb + whitelist: 00000a10-0000-1000-8000-00805f9b34fb + rx: 00000a04-0000-1000-8000-00805f9b34fb + txvibrate: 0000fff2-0000-1000-8000-00805f9b34fb + generic0: 00000a11-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/lelo-harmony.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/lelo-harmony.yml new file mode 100644 index 000000000..e8043a6f9 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/lelo-harmony.yml @@ -0,0 +1,115 @@ +defaults: + name: Lelo Tiani Harmony + features: + - feature-type: Vibrate + id: 0cf2b478-2235-4f83-897c-d8bbebb822e8 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 0c89262b-0fcd-48c9-9492-a79758da781f + output: + Vibrate: + step-range: + - 0 + - 100 + id: 3bde5251-e810-418a-9ebf-8c3a50684d9a +configurations: + - identifier: + - IdaWave + - Ida Wave + name: Lelo Ida Wave + features: + - feature-type: Vibrate + id: c887327d-e635-4086-83dc-2f21286f485c + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Rotate + id: 5bd48a1d-992e-4c69-ae74-ed94505eec58 + output: + Rotate: + step-range: + - 0 + - 100 + id: a9de3981-7e0d-4b07-b8a9-10031bb6ddae + - identifier: + - TOR3 + name: Lelo Tor 3 + features: + - feature-type: Vibrate + id: d0c39af5-62b4-4bfe-a0bb-71f5c2e86c99 + output: + Vibrate: + step-range: + - 0 + - 100 + id: e0104054-fba7-4ba2-b51f-0f3d95aee1ba + - identifier: + - Hugo2 + name: Lelo Hugo 2 + id: 7d302aee-23cd-4681-b9fc-1275250e8a03 + - identifier: + - DoubleSonic + name: Lelo Enigma Double Sonic + features: + - feature-type: Vibrate + id: 8a9d2c49-1486-4515-a0a4-320c9c903ccc + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Rotate + id: 6fdbe4ae-f0fc-44e0-b0a4-cbb56dee61d8 + output: + Rotate: + step-range: + - 0 + - 100 + id: c6bf86e6-1054-4c14-a3bb-d415edf81834 + - identifier: + - GIGI3 + name: Lelo Gigi 3 + features: + - feature-type: Vibrate + id: ea1ca70a-b3e9-41ba-8863-3f74156fef87 + output: + Vibrate: + step-range: + - 0 + - 100 + id: e722ba98-5c2d-4f77-a56d-ac72b213ed53 + - identifier: + - LIV3 + name: Lelo Liv 3 + features: + - feature-type: Vibrate + id: 1599b3d9-055d-4c9b-a1fe-7cef1fac4c9e + output: + Vibrate: + step-range: + - 0 + - 100 + id: 0daa8498-172c-47bc-b6c4-57414589509b +communication: + - btle: + names: + - IdaWave + - Ida Wave + - TianiHarmony + - Tiani Harmony + - TOR3 + - Hugo2 + - DoubleSonic + - GIGI3 + - LIV3 + services: + 0000fff0-0000-1000-8000-00805f9b34fb: + command: 0000fff1-0000-1000-8000-00805f9b34fb + tx: 0000fff2-0000-1000-8000-00805f9b34fb + whitelist: 00000a11-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/leten.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/leten.yml new file mode 100644 index 000000000..a62de5d81 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/leten.yml @@ -0,0 +1,23 @@ +defaults: + name: Leten Device + features: + - feature-type: Vibrate + id: f9df3044-6d90-4767-97a9-05d15e2f97ec + output: + Vibrate: + step-range: + - 0 + - 25 + id: 8c613401-3bc2-434b-8ffe-881879b1e287 +communication: + - btle: + names: + - T528-LT + - F537-LT + - F520B-LT + - F520A-LT + services: + 0000fff0-0000-1000-8000-00805f9b34fb: + tx: 0000fff1-0000-1000-8000-00805f9b34fb + 0000ffe0-0000-1000-8000-00805f9b34fb: + rx: 0000ffe1-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/libo-elle.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/libo-elle.yml new file mode 100644 index 000000000..d39ab75bb --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/libo-elle.yml @@ -0,0 +1,29 @@ +defaults: + name: Libo Elle Device + features: + - feature-type: Vibrate + id: 1b336a6e-6f35-458f-837e-a0147f67c7f5 + output: + Vibrate: + step-range: + - 0 + - 3 + id: fe54deb6-5c13-4f69-a804-1af5fce5de96 +configurations: + - identifier: + - PiPiJing + name: LiBo Elle + id: af187899-8704-42f1-994e-694616576149 + - identifier: + - Shuidi + name: Libo Elle 2 + id: 98f5289c-98b4-4410-bed2-4d3050a4761e +communication: + - btle: + names: + - PiPiJing + - Shuidi + services: + 00006000-0000-1000-8000-00805f9b34fb: + tx: 00006001-0000-1000-8000-00805f9b34fb + txmode: 00006002-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/libo-karen.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/libo-karen.yml new file mode 100644 index 000000000..9d5e1bf82 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/libo-karen.yml @@ -0,0 +1,14 @@ +defaults: + name: Libo Karen + features: [] + id: 2d9f29c7-7d0d-4319-967c-9f7b89eb7b1d +communication: + - btle: + names: + - SuoYinQiu + services: + 00006000-0000-1000-8000-00805f9b34fb: + tx: 00006001-0000-1000-8000-00805f9b34fb + txmode: 00006002-0000-1000-8000-00805f9b34fb + 00006050-0000-1000-8000-00805f9b34fb: + rxpressure: 00006051-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/libo-shark.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/libo-shark.yml new file mode 100644 index 000000000..1e318a5a0 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/libo-shark.yml @@ -0,0 +1,26 @@ +defaults: + name: Libo Shark + features: + - feature-type: Vibrate + id: 52d614a1-4f43-4946-a7bd-9d413791e642 + output: + Vibrate: + step-range: + - 0 + - 3 + - feature-type: Vibrate + id: 7cebc2d6-3b11-4117-aec4-ced57a738a13 + output: + Vibrate: + step-range: + - 0 + - 3 + id: 44915af5-e3b9-4766-ae2e-b2df758689fd +communication: + - btle: + names: + - ShaYu + services: + 00006000-0000-1000-8000-00805f9b34fb: + tx: 00006001-0000-1000-8000-00805f9b34fb + txmode: 00006002-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/libo-vibes.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/libo-vibes.yml new file mode 100644 index 000000000..cda9f1655 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/libo-vibes.yml @@ -0,0 +1,127 @@ +defaults: + name: Libo Vibes Device + features: + - feature-type: Vibrate + id: db5d9b0a-8498-4f5a-b53b-111a9940367d + output: + Vibrate: + step-range: + - 0 + - 100 + id: 8ba2bd4c-962b-45ff-87e1-3812084c7c1c +configurations: + - identifier: + - XiaoLu + name: Libo Lottie + id: 9c9b46bd-ab5e-4ec2-a9db-c80571074cfb + - identifier: + - LuXiaoHan + name: Libo LuLu + id: 80deea27-6833-4bdc-9d24-02615c3197d9 + - identifier: + - Yuyi + name: Libo Lina + id: 982d708e-788b-4962-b9bb-c253f49becf8 + - identifier: + - LuWuShuang + name: Libo Adel + id: d761eb50-9051-44ce-82ed-d301aa532cc3 + - identifier: + - LiBo + name: Libo Lily + id: f9e758fe-3327-435b-94e3-eda7445d49e1 + - identifier: + - QingTing + name: Libo Lucy + id: 93ce6ac4-2f24-4a8e-ab81-7a046403eb0c + - identifier: + - Huohu + name: Libo Lara + id: f0234003-d8d3-4858-837b-8051109e6770 + - identifier: + - Yuyi + name: Libo Feather + features: + - feature-type: Vibrate + id: 39eca274-5634-4433-9be5-2c688fb9b65c + output: + Vibrate: + step-range: + - 0 + - 99 + id: c63739df-3b00-4602-8d3d-8f1080ec499c + - identifier: + - BaiHu + name: Libo LaLa + features: + - feature-type: Vibrate + id: 4239e32b-b3ad-49e2-a96e-1fb7298b1889 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 5f43a406-9567-43fc-b3b8-5383b5200bfd + output: + Vibrate: + step-range: + - 0 + - 3 + id: 2de690ff-ad02-4272-a2c7-845c3ea8b28c + - identifier: + - Gugudai + name: Libo Carlos + features: + - feature-type: Vibrate + id: 6fc0149e-d041-4987-a66e-dbf36739331f + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 80b80fb2-b458-4661-a1e2-a8f27651d390 + output: + Vibrate: + step-range: + - 0 + - 3 + id: 8e342d89-66d4-4943-ae42-015cb268444b + - identifier: + - Haima + name: Libo Selina + features: + - feature-type: Vibrate + id: 54c02210-8494-40c6-a04c-e0a302aa735e + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: a2fb0a58-895b-49f5-bc88-b0a38bc64e68 + output: + Vibrate: + step-range: + - 0 + - 3 + id: 6d2f4df7-18a1-4568-81be-0e8e545e82a1 +communication: + - btle: + names: + - XiaoLu + - LuXiaoHan + - BaiHu + - Gugudai + - Yuyi + - LuWuShuang + - LiBo + - QingTing + - Huohu + - Yuyi + - Haima + services: + 00006000-0000-1000-8000-00805f9b34fb: + tx: 00006001-0000-1000-8000-00805f9b34fb + txmode: 00006002-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/lioness.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/lioness.yml new file mode 100644 index 000000000..8af15e74e --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/lioness.yml @@ -0,0 +1,21 @@ +defaults: + name: Lioness + features: + - feature-type: Vibrate + id: 30051e05-190c-43e9-a35d-480a7615622d + output: + Vibrate: + step-range: + - 0 + - 100 + id: a35b0291-002b-4382-9eaf-6ebd9d04b668 +communication: + - btle: + names: + - Lioness + - Lioness2 + services: + d973f2ed-b19e-11e2-9e96-0800200c9a66: + tx: d973f2f4-b19e-11e2-9e96-0800200c9a66 + d973f2e5-b19e-11e2-9e96-0800200c9a66: + rx: d973f2e6-b19e-11e2-9e96-0800200c9a66 diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/loob.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/loob.yml new file mode 100644 index 000000000..9f656d04f --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/loob.yml @@ -0,0 +1,18 @@ +defaults: + name: Joyroid Loob + features: + - feature-type: PositionWithDuration + id: 7078c41e-0cd3-4264-8f54-c331ac4c81f9 + output: + PositionWithDuration: + step-range: + - 0 + - 1000 + id: 26c0103c-9b39-4dbb-ad33-5cbdff03c178 +communication: + - btle: + names: + - LOOB + services: + b75c49d2-04a3-4071-a0b5-35853eb08307: + tx: ba5c49d2-04a3-4071-a0b5-35853eb08307 diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/lovedistance.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/lovedistance.yml new file mode 100644 index 000000000..4440e68fb --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/lovedistance.yml @@ -0,0 +1,69 @@ +defaults: + name: Love Distance Device + features: + - feature-type: Vibrate + id: 3eae1a60-e996-4726-858b-2128a1ae376a + output: + Vibrate: + step-range: + - 0 + - 121 + id: 1cd71bad-3cfc-41ee-a6b8-8651bf658489 +configurations: + - identifier: + - REACH G + name: Love Distance Reach G + id: 7b190a71-6667-4b63-9929-42dc3a22d113 + - identifier: + - REACH + name: Love Distance Reach + id: ad11cd1c-7450-4a0e-b7cf-4ff94e53b685 + - identifier: + - MAG + name: Love Distance Mag + id: bae30100-1dfa-4bd9-a2b3-e9415bebd1cb + - identifier: + - SPAN + name: Love Distance Span + id: 84d00425-1a74-4fef-ad06-a5cdf22450d4 + - identifier: + - RANGE + name: Love Distance Range + id: 9cd3854e-03d7-4a32-b189-a97990ef45be + - identifier: + - ORBIT + name: Love Distance Range + id: 04c77f83-87bc-4547-87cc-d2c45c203313 + - identifier: + - JOIN G + name: Love Distance Join G + id: 21f4d6ea-9c83-4d3e-a095-f5761e6c63ed + - identifier: + - LINK + name: Love Distance Link + id: 7dfc44e0-0a77-4725-be94-55ae7fab2601 + - identifier: + - GRASP + name: Love Distance Grasp + id: 57d24ed8-fc9d-4dad-87b0-d978d3ebe8cd + - identifier: + - RECEIVE + name: Love Distance Receive + id: d104ec28-cd82-4fdb-bb9b-96ffc3b639ed +communication: + - btle: + names: + - REACH G + - REACH + - MAG + - SPAN + - RANGE + - ORBIT + - JOIN G + - LINK + - GRASP + - RECEIVE + services: + 0000ff00-0000-1000-8000-00805f9b34fb: + tx: 0000ff01-0000-1000-8000-00805f9b34fb + rx: 0000ff02-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/lovehoney-desire.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/lovehoney-desire.yml new file mode 100644 index 000000000..d6b3c3a92 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/lovehoney-desire.yml @@ -0,0 +1,56 @@ +defaults: + name: Lovehoney Device + features: + - feature-type: Vibrate + id: 716bdae7-2075-4e8a-a2cb-d37b6fc35a5b + output: + Vibrate: + step-range: + - 0 + - 127 + - feature-type: Vibrate + id: ce0315b0-9918-4769-af8e-6ec6258d0e1a + output: + Vibrate: + step-range: + - 0 + - 127 + id: fabcaab7-a38b-4c24-bf36-2ca4905a1e49 +configurations: + - identifier: + - PROSTATE VIBE + name: Lovehoney Desire Prostate Vibrator + id: d7aa359d-a9f0-40b1-8e20-b55e8ef809c0 + - identifier: + - KNICKER VIBE + name: Lovehoney Desire Knicker Vibrator + features: + - feature-type: Vibrate + id: 5e192f37-2beb-4e21-b182-ff113642f465 + output: + Vibrate: + step-range: + - 0 + - 127 + id: 439c5fe2-3e8d-4917-bcd7-8f24824d854b + - identifier: + - LOVE EGG + name: Lovehoney Desire Love Egg + features: + - feature-type: Vibrate + id: 980c9d39-e0bc-45d9-8d41-3e95af348d6c + output: + Vibrate: + step-range: + - 0 + - 127 + id: 00d4e759-900d-4c37-b6a3-ce446bb8f590 +communication: + - btle: + names: + - PROSTATE VIBE + - KNICKER VIBE + - LOVE EGG + services: + 0000ff00-0000-1000-8000-00805f9b34fb: + tx: 0000ff01-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/lovense-connect-service.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/lovense-connect-service.yml new file mode 100644 index 000000000..a37ca27aa --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/lovense-connect-service.yml @@ -0,0 +1,423 @@ +defaults: + name: Lovense Connect Service Device + features: + - feature-type: Vibrate + id: 387829be-bbd3-4d71-98f2-738dbb685600 + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Battery + description: Battery Level + id: 7202da93-c25d-460a-a863-8d4d38f41fdf + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: caceda00-463b-4981-949f-b7e6b06ed02b +configurations: + - identifier: + - Max + name: Lovense Max + features: + - feature-type: Vibrate + description: Vibrator + id: cd1a70b7-d716-41a9-b839-24e0229c25d2 + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Constrict + description: Air Pump + id: e74ae364-c17a-41c4-accf-0e4a4ee94e04 + output: + Constrict: + step-range: + - 0 + - 3 + - feature-type: Battery + description: Battery Level + id: a2d19eee-211e-4771-b7e1-cfba3e6bb55f + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: c82d6326-c683-496b-b54a-c07cb03434f5 + - identifier: + - Edge + name: Lovense Edge + features: + - feature-type: Vibrate + id: 26f7aaa6-4312-487d-aabb-b43e4c87b5c2 + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Vibrate + id: 5410094f-eff4-4b41-bfa2-b4cece3b9101 + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Battery + description: Battery Level + id: 9b31822c-7449-4a3d-bd4d-6cced8440126 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 847c87fa-14a6-416c-95a8-d5b558c92cc0 + - identifier: + - Nora + name: Lovense Nora + features: + - feature-type: Vibrate + id: 1bfa1705-0193-4393-82f7-1c458e4885b3 + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: RotateWithDirection + id: af885c72-ce2b-47d5-87be-3847f24d18a5 + output: + RotateWithDirection: + step-range: + - 0 + - 20 + - feature-type: Battery + description: Battery Level + id: 1fb626ec-7006-46f5-97b1-db3cc0bc5bb8 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 15dcfcf0-a9c9-4ff4-90c0-37007e7c4809 + - identifier: + - Ambi + name: Lovense Ambi + id: 68611264-45fb-49ab-9d1a-6a2000fd4b8a + - identifier: + - Lush + name: Lovense Lush + id: c5063766-bc9c-422c-91e4-18873bc77352 + - identifier: + - Hush + name: Lovense Hush + id: 8cc0f440-8a81-4ae9-951d-050777cb1f33 + - identifier: + - Domi + name: Lovense Domi + id: 0e4f7cc1-5bd6-4f81-8bfc-7da23b0ff483 + - identifier: + - Osci + name: Lovense Osci + id: 0951047c-2ac3-43ea-a24e-2d17174809d0 + - identifier: + - Mission + name: Lovense Mission + id: 93907f90-05d4-4afe-a160-28973069927c + - identifier: + - Ferri + name: Lovense Ferri + id: 915d15fb-c47d-494c-af43-b9820e9bd33f + - identifier: + - Diamo + name: Lovense Diamo + id: cea4f8b8-43e4-4a73-bab7-179aa2332f85 + - identifier: + - ToyS + name: Loveai Dolp + id: 7194fd0d-e084-4c45-9d49-648b152fe9ba + - identifier: + - XMachine + name: Lovense Sex Machine + features: + - feature-type: Oscillate + description: Fucking Machine Oscillation Speed + id: 0ab80cc0-7a82-4cb6-ba4f-0f18ddb2911f + output: + Oscillate: + step-range: + - 0 + - 20 + - feature-type: Battery + description: Battery Level + id: 971bd4aa-d6ac-4449-bd1a-862b29ae705e + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 9b52eca4-0e49-426e-a543-2ef735cd803a + - identifier: + - Dolce + name: Lovense Dolce + features: + - feature-type: Vibrate + id: 59ec4d12-2c6d-4cd9-83b0-8ff1609563d4 + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Vibrate + id: 4e4eead7-9959-4fe2-b629-a535f6bc7ca4 + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Battery + description: Battery Level + id: b771d1b8-5a68-4a75-8ff2-868380d18fe7 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: d51f41a8-3731-4b06-b320-6cfa2d518940 + - identifier: + - Gush + name: Lovense Gush + id: 24a65c79-7a5e-4ab4-82cf-684f54292f89 + - identifier: + - Hyphy + name: Lovense Hyphy + features: + - feature-type: Vibrate + id: a6ec2f52-780b-4d87-a809-0bdc2ccadcc1 + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Vibrate + id: c06723f1-f816-442b-8193-a5c407fecabe + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Battery + description: Battery Level + id: 80d1e022-85a6-46ad-bbe9-1b8085b1e336 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 33a001d2-2879-47f8-89d3-422d262deb53 + - identifier: + - Calor + name: Lovense Calor + id: ea035198-1eb8-4fa8-b234-50b9a91c8925 + - identifier: + - Flexer + name: Lovense Flexer + features: + - feature-type: Vibrate + description: Both Vibes + id: bd656e88-abae-49e4-ab45-f75df187bb4a + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Rotate + description: Finger motion + id: 663dedb4-05a1-4391-a666-e59c38ead69c + output: + Rotate: + step-range: + - 0 + - 20 + - feature-type: Battery + description: Battery Level + id: 735c2164-4fd5-4e82-835d-23251e487d68 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 10995415-c030-4fd1-b5c0-af42d850ff61 + - identifier: + - Gemini + name: Lovense Gemini + features: + - feature-type: Vibrate + id: 2c186df2-4e8c-491d-b247-fcbaeb763fee + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Vibrate + id: 81657dab-5fbf-40b4-a6f8-cfecb7906757 + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Battery + description: Battery Level + id: fe19ad5c-5acb-4ee9-8a09-f6edca06f471 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 7da2f986-8960-4c2c-acf1-d8924878adc0 + - identifier: + - Gravity + name: Lovense Gravity + features: + - feature-type: Vibrate + id: fba538eb-784e-4ca7-ad81-e52f3cd0d3f2 + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Oscillate + id: 61bd6559-c32d-4c3b-9686-988fa3cd4abf + output: + Oscillate: + step-range: + - 0 + - 20 + - feature-type: Battery + description: Battery Level + id: 7a794236-85e6-4b13-97c6-d17d1f091f0a + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 75a502f3-6b8f-4d70-97b5-86fff5d45260 + - identifier: + - Ridge + name: Lovense Ridge + features: + - feature-type: Vibrate + id: 4865ff41-25cd-42a9-b93d-00a7c1e881d5 + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: RotateWithDirection + id: d49001e8-5f6b-43ac-9cc7-7e68fab7c323 + output: + RotateWithDirection: + step-range: + - 0 + - 20 + - feature-type: Battery + description: Battery Level + id: 7fcb01eb-4241-42c1-9799-fdfa190b7edd + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: fcd47b93-ac57-4167-93a5-fb12f223ff28 + - identifier: + - Lapis + name: Lovense Lapis + features: + - feature-type: Vibrate + description: Tip Vibe + id: f435ee40-ae30-4fba-9f80-c1143f601993 + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Vibrate + description: Internal Vibe + id: 9504ed2b-1baf-4759-922b-a5dcfc16aeb7 + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Vibrate + description: External Vibe + id: 1cce6f8f-0301-4e4e-a820-1ed85e11e25d + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Battery + description: Battery Level + id: 322170f9-b493-4233-9336-e6f7f267450c + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: d99b1620-25cd-40fe-af02-a51d08df33ca + - identifier: + - Vulse + name: Lovense Vulse + id: f2c1faec-7d64-48be-9c91-2649c74540c7 + - identifier: + - Solace + name: Lovense Solace + features: + - feature-type: Oscillate + description: Stroker Oscillation Speed + id: b8b240c0-182d-4889-9200-47c16399c57d + output: + Oscillate: + step-range: + - 0 + - 20 + - feature-type: Battery + description: Battery Level + id: 37c03e71-1701-4b5a-9697-d62d2dc56e4b + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 665925e2-e895-443f-953a-cae3f371c138 +communication: + - lovense-connect-service: + exists: true diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/lovense.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/lovense.yml new file mode 100644 index 000000000..15c9fa9f7 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/lovense.yml @@ -0,0 +1,679 @@ +defaults: + name: Lovense Device + features: + - feature-type: Vibrate + id: 3f7a25a5-df21-42ca-bf9f-d1c52df1f37e + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Battery + description: Battery Level + id: 14bd7637-13ed-49ba-9eb9-9c8ba9abec20 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: d3b1219a-aafe-4257-9d5d-3979b5da3c9a +configurations: + - identifier: + - B + name: Lovense Max + features: + - feature-type: Vibrate + description: Vibrator + id: d9c9b4a7-008e-4182-b28c-0984af970c32 + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Constrict + description: Air Pump + id: fed393a9-3ac6-4924-859d-5cb4ae059cea + output: + Constrict: + step-range: + - 0 + - 3 + - feature-type: Battery + description: Battery Level + id: b4be6835-5b91-4540-bc7b-0c3d8dcb89fd + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 99024e29-c0ed-4c26-aede-e0db0679eae5 + - identifier: + - P + name: Lovense Edge + features: + - feature-type: Vibrate + id: cb286b22-998b-4420-82f3-84e8d39db6b5 + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Vibrate + id: c8b72e1d-d7d4-4417-8cbc-e6c0f435889a + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Battery + description: Battery Level + id: 66b31efb-3bd9-4e3a-9972-88c66e9fca28 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 2e309985-6bbf-4b75-866f-76d845b3ce42 + - identifier: + - A + - C + name: Lovense Nora + features: + - feature-type: Vibrate + id: 2c5da93b-36a0-4209-ac8c-cead63b838c6 + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: RotateWithDirection + id: 515e07e2-a6e6-4ac0-a4b0-512504311260 + output: + RotateWithDirection: + step-range: + - 0 + - 20 + - feature-type: Battery + description: Battery Level + id: 820d8fb1-c6ec-434d-b7c4-835bdf36552a + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 463a18b9-42a5-4f7b-8156-0e61346fdb8a + - identifier: + - L + name: Lovense Ambi + id: 7053fde9-0902-4aab-926d-fc51869f6ccc + - identifier: + - S + name: Lovense Lush + id: 670560f0-981e-42cb-b83d-c911dd9826e2 + - identifier: + - Z + name: Lovense Hush + id: 37642e1c-a416-44d3-bada-76b6d9e245c9 + - identifier: + - W + name: Lovense Domi + id: e788f8d5-037a-4ce4-a13f-6b2e8ec31fb6 + - identifier: + - O + name: Lovense Osci + id: 45bf66e7-01e0-48ad-ad1c-2b48d1279da1 + - identifier: + - V + name: Lovense Mission + id: 45e2fc5c-79e8-4228-beba-a97a14d84e7d + - identifier: + - CA + name: Lovense Mission 2 + id: a8f36834-d8eb-48d5-9bad-237e67f6fd5b + - identifier: + - X + name: Lovense Ferri + id: 481b101b-ff4d-4045-84fe-da2b9bba93e2 + - identifier: + - R + name: Lovense Diamo + id: df95c01b-88d3-49b3-b360-69777b341795 + - identifier: + - ToyS + name: Loveai Dolp + id: 30830f67-4550-4133-88a9-b5eccd83083b + - identifier: + - F + name: Lovense Sex Machine + features: + - feature-type: Oscillate + description: Fucking Machine Oscillation Speed + id: f9506652-c4ac-43b1-b184-cd8016b64623 + output: + Oscillate: + step-range: + - 0 + - 20 + - feature-type: Battery + description: Battery Level + id: 7c382c60-0ee2-4315-b8cf-cfd3ab4c9ccd + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 8667f7b6-7baa-4e46-9d76-947fb707f0f3 + - identifier: + - FS + name: Lovense Mini Sex Machine + features: + - feature-type: Oscillate + description: Fucking Machine Oscillation Speed + id: aaf55cab-8ebd-42b3-9bbb-74a57efdf014 + output: + Oscillate: + step-range: + - 0 + - 20 + - feature-type: Battery + description: Battery Level + id: 68defbd8-af87-4f04-97da-edfa8fb576f9 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 48d5c76b-8c0e-4152-9f3b-5ba92ebf30fe + - identifier: + - J + name: Lovense Dolce + features: + - feature-type: Vibrate + id: 930b9aee-0ba5-4268-95ca-2a5691d31239 + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Vibrate + id: 62b2b22c-c028-4aa4-a85c-a7fe8c5f9dcb + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Battery + description: Battery Level + id: 60868f44-3d56-44ed-bcc4-00041a7b5997 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 0bddb3da-2c8d-4af8-9e80-1e0038878f27 + - identifier: + - OC + name: Lovense Osci 3 + features: + - feature-type: Vibrate + id: 4cf78058-44c7-4513-913a-37558a84b91e + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Vibrate + id: f4ada339-8bb2-4b02-b907-69a3257bce3b + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Battery + description: Battery Level + id: 3933bfcb-6daf-4c33-b834-877cb29ce77d + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: a8b175a8-3447-4938-b1df-7215464b56e6 + - identifier: + - ED + name: Lovense Gush + id: 6071cc3a-a8e7-4142-bc80-08fe122452d8 + - identifier: + - EZ + name: Lovense Gush 2 + id: 51de38d3-114f-453e-a440-3958918af423 + - identifier: + - EB + name: Lovense Hyphy + features: + - feature-type: Vibrate + id: 39b063fa-958b-4d1a-bbd1-8480e105dd88 + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Vibrate + id: b40accca-7c73-4bff-9819-45f806a194a8 + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Battery + description: Battery Level + id: 8fa6dc63-430e-42cb-9345-42d37f0c2629 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: a6a0c988-3e04-4fa3-89e2-4f4d2f242ffd + - identifier: + - T + name: Lovense Calor + id: bdab9bf5-25f8-4140-bf4d-3f0edf1883d2 + - identifier: + - EI + name: Lovense Flexer (Firmware update needed) + id: c90a2d78-5b08-40ad-a2c9-ac7eacb43b3d + - identifier: + - EI-FW3 + name: Lovense Flexer + features: + - feature-type: Vibrate + description: Internal Vibe + id: 9b2dcb58-6c2c-46ef-abe4-81631d1a5f66 + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Vibrate + description: External Vibe + id: d8b571fd-614e-4d33-8595-b9fbc81b96bd + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Rotate + description: Finger motion + id: eb6a2d21-93e0-4a08-9674-36fa2d299651 + output: + Rotate: + step-range: + - 0 + - 20 + - feature-type: Battery + description: Battery Level + id: 6548133f-118f-419d-8900-660fde26b42f + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 8f93dd90-1788-4d2c-8b8f-9a339be12c0e + - identifier: + - 'N' + name: Lovense Gemini + features: + - feature-type: Vibrate + id: de8d83b6-76b4-4851-b53d-616d3527040c + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Vibrate + id: 2ea51cd8-b173-408c-bfef-f6508c5b9087 + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Battery + description: Battery Level + id: 710384a5-a7dd-43f1-b55c-147256dc636a + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 9c72451e-1df7-410a-b4b6-e133f3bd9219 + - identifier: + - EA + name: Lovense Gravity + features: + - feature-type: Vibrate + id: 93fa269e-ba3b-4c09-85d0-43385b49ee79 + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Oscillate + id: 475bde3a-4aae-4e84-87be-4df3a634da26 + output: + Oscillate: + step-range: + - 0 + - 20 + - feature-type: Battery + description: Battery Level + id: 104da492-67f1-46fc-b412-b98871ebb518 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: b57dfb65-260d-49b2-bff0-659e38947186 + - identifier: + - Q + name: Lovense Tenera + id: abe8f908-3d93-4ba3-8bb1-3623fcd04202 + - identifier: + - EL + name: Lovense Ridge + features: + - feature-type: Vibrate + id: 0627be5e-8553-4f20-b4cf-15f5e1896e5f + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: RotateWithDirection + id: 360d81e7-5126-4dbb-b72d-7bb60eb67400 + output: + RotateWithDirection: + step-range: + - 0 + - 20 + - feature-type: Battery + description: Battery Level + id: 50b9b31f-c2a8-459a-81fd-c54604f5184e + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: bbfd764c-b419-4c13-aeb0-e753a86318ed + - identifier: + - U + name: Lovense Lapis + features: + - feature-type: Vibrate + description: Tip Vibe + id: 414e5c3e-e52a-4064-b367-893bc0b1fb95 + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Vibrate + description: Internal Vibe + id: be8d8608-d3aa-4fc5-ac5c-8df429f9e63c + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Vibrate + description: External Vibe + id: 8bd37a96-7f7a-450f-aa4b-ffe8aa398d1e + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Battery + description: Battery Level + id: ad93f903-a354-40ae-b87e-f8390606a964 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 5454d487-ed23-4067-80e2-9e2f0c01fabf + - identifier: + - SD + name: Lovense Vulse + id: 73fcd02b-fa45-4e11-a62a-598aec256fbd + - identifier: + - H + name: Lovense Solace + features: + - feature-type: Oscillate + description: Stroker Oscillation Speed + id: 5100187a-40c7-44a4-a0ce-368cc24429cd + output: + Oscillate: + step-range: + - 0 + - 20 + - feature-type: Battery + description: Battery Level + id: e4193650-2d46-4e6e-8dd8-b1d8d9a1baff + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: c53de5c8-fc4a-421b-9332-271ec742a156 + - identifier: + - BA + name: Lovense Solace Pro + features: + - feature-type: PositionWithDuration + description: Stroker Position Based Movement + id: c4b2855d-5ecc-4010-8a8d-17fd3e51cc57 + output: + PositionWithDuration: + step-range: + - 0 + - 100 + Oscillate: + step-range: + - 0 + - 20 + - feature-type: Battery + description: Battery Level + id: 0b1cba39-8bb7-4f87-9bed-c59f2284d702 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: ed5f76c6-84b9-4fee-891f-28f9f4fa3632 +communication: + - btle: + names: + - LVS-* + - LOVE-* + manufacturer-data: + - company: 620 + data: + - 255 + - 33 + advertised-services: + - 6e400001-b5a3-f393-e0a9-e50e24dcca9e + - 50300001-0024-4bd4-bbd5-a6920e4c5653 + - 57300001-0023-4bd4-bbd5-a6920e4c5653 + - 5a300001-0024-4bd4-bbd5-a6920e4c5653 + - 50300001-0023-4bd4-bbd5-a6920e4c5653 + - 53300001-0023-4bd4-bbd5-a6920e4c5653 + - 5a300001-0023-4bd4-bbd5-a6920e4c5653 + - 4f300001-0023-4bd4-bbd5-a6920e4c5653 + - 42300001-0023-4bd4-bbd5-a6920e4c5653 + - 43300001-0023-4bd4-bbd5-a6920e4c5653 + - 4c300001-0023-4bd4-bbd5-a6920e4c5653 + - 4c410001-0023-4bd4-bbd5-a6920e4c5653 + - 56300001-0023-4bd4-bbd5-a6920e4c5653 + - 58300001-0023-4bd4-bbd5-a6920e4c5653 + - 52300001-0023-4bd4-bbd5-a6920e4c5653 + - 46300001-0023-4bd4-bbd5-a6920e4c5653 + - 50300011-0023-4bd4-bbd5-a6920e4c5653 + - 4a300001-0023-4bd4-bbd5-a6920e4c5653 + - 45440001-0023-4bd4-bbd5-a6920e4c5653 + - 45420001-0023-4bd4-bbd5-a6920e4c5653 + - 54300001-0023-4bd4-bbd5-a6920e4c5653 + - 45490001-0023-4bd4-bbd5-a6920e4c5653 + - 4e300001-0023-4bd4-bbd5-a6920e4c5653 + - 45410001-0023-4bd4-bbd5-a6920e4c5653 + - 51300001-0023-4bd4-bbd5-a6920e4c5653 + - 45460001-0023-4bd4-bbd5-a6920e4c5653 + - 454c0001-0023-4bd4-bbd5-a6920e4c5653 + - 55300001-0023-4bd4-bbd5-a6920e4c5653 + - 53440001-0023-4bd4-bbd5-a6920e4c5653 + - 48300001-0023-4bd4-bbd5-a6920e4c5653 + - 46530001-0023-4bd4-bbd5-a6920e4c5653 + - 42410001-0023-4bd4-bbd5-a6920e4c5653 + - 43410001-0023-4bd4-bbd5-a6920e4c5653 + - 4f430001-0023-4bd4-bbd5-a6920e4c5653 + - 455a0001-0023-4bd4-bbd5-a6920e4c5653 + services: + 0000fff0-0000-1000-8000-00805f9b34fb: + tx: 0000fff2-0000-1000-8000-00805f9b34fb + rx: 0000fff1-0000-1000-8000-00805f9b34fb + 6e400001-b5a3-f393-e0a9-e50e24dcca9e: + tx: 6e400002-b5a3-f393-e0a9-e50e24dcca9e + rx: 6e400003-b5a3-f393-e0a9-e50e24dcca9e + 50300001-0024-4bd4-bbd5-a6920e4c5653: + tx: 50300002-0024-4bd4-bbd5-a6920e4c5653 + rx: 50300003-0024-4bd4-bbd5-a6920e4c5653 + 57300001-0023-4bd4-bbd5-a6920e4c5653: + tx: 57300002-0023-4bd4-bbd5-a6920e4c5653 + rx: 57300003-0023-4bd4-bbd5-a6920e4c5653 + 5a300001-0024-4bd4-bbd5-a6920e4c5653: + tx: 5a300002-0024-4bd4-bbd5-a6920e4c5653 + rx: 5a300003-0024-4bd4-bbd5-a6920e4c5653 + 50300001-0023-4bd4-bbd5-a6920e4c5653: + tx: 50300002-0023-4bd4-bbd5-a6920e4c5653 + rx: 50300003-0023-4bd4-bbd5-a6920e4c5653 + 53300001-0023-4bd4-bbd5-a6920e4c5653: + tx: 53300002-0023-4bd4-bbd5-a6920e4c5653 + rx: 53300003-0023-4bd4-bbd5-a6920e4c5653 + 5a300001-0023-4bd4-bbd5-a6920e4c5653: + tx: 5a300002-0023-4bd4-bbd5-a6920e4c5653 + rx: 5a300003-0023-4bd4-bbd5-a6920e4c5653 + 4f300001-0023-4bd4-bbd5-a6920e4c5653: + tx: 4f300002-0023-4bd4-bbd5-a6920e4c5653 + rx: 4f300003-0023-4bd4-bbd5-a6920e4c5653 + 42300001-0023-4bd4-bbd5-a6920e4c5653: + tx: 42300002-0023-4bd4-bbd5-a6920e4c5653 + rx: 42300003-0023-4bd4-bbd5-a6920e4c5653 + 43300001-0023-4bd4-bbd5-a6920e4c5653: + tx: 43300002-0023-4bd4-bbd5-a6920e4c5653 + rx: 43300003-0023-4bd4-bbd5-a6920e4c5653 + 4c300001-0023-4bd4-bbd5-a6920e4c5653: + tx: 4c300002-0023-4bd4-bbd5-a6920e4c5653 + rx: 4c300003-0023-4bd4-bbd5-a6920e4c5653 + 4c410001-0023-4bd4-bbd5-a6920e4c5653: + tx: 4c410002-0023-4bd4-bbd5-a6920e4c5653 + rx: 4c410003-0023-4bd4-bbd5-a6920e4c5653 + 56300001-0023-4bd4-bbd5-a6920e4c5653: + tx: 56300002-0023-4bd4-bbd5-a6920e4c5653 + rx: 56300003-0023-4bd4-bbd5-a6920e4c5653 + 58300001-0023-4bd4-bbd5-a6920e4c5653: + tx: 58300002-0023-4bd4-bbd5-a6920e4c5653 + rx: 58300003-0023-4bd4-bbd5-a6920e4c5653 + 52300001-0023-4bd4-bbd5-a6920e4c5653: + tx: 52300002-0023-4bd4-bbd5-a6920e4c5653 + rx: 52300003-0023-4bd4-bbd5-a6920e4c5653 + 46300001-0023-4bd4-bbd5-a6920e4c5653: + tx: 46300002-0023-4bd4-bbd5-a6920e4c5653 + rx: 46300003-0023-4bd4-bbd5-a6920e4c5653 + 50300011-0023-4bd4-bbd5-a6920e4c5653: + tx: 50300012-0023-4bd4-bbd5-a6920e4c5653 + rx: 50300013-0023-4bd4-bbd5-a6920e4c5653 + 4a300001-0023-4bd4-bbd5-a6920e4c5653: + tx: 4a300002-0023-4bd4-bbd5-a6920e4c5653 + rx: 4a300003-0023-4bd4-bbd5-a6920e4c5653 + 45440001-0023-4bd4-bbd5-a6920e4c5653: + tx: 45440002-0023-4bd4-bbd5-a6920e4c5653 + rx: 45440003-0023-4bd4-bbd5-a6920e4c5653 + 45420001-0023-4bd4-bbd5-a6920e4c5653: + tx: 45420002-0023-4bd4-bbd5-a6920e4c5653 + rx: 45420003-0023-4bd4-bbd5-a6920e4c5653 + 54300001-0023-4bd4-bbd5-a6920e4c5653: + tx: 54300002-0023-4bd4-bbd5-a6920e4c5653 + rx: 54300003-0023-4bd4-bbd5-a6920e4c5653 + 45490001-0023-4bd4-bbd5-a6920e4c5653: + tx: 45490002-0023-4bd4-bbd5-a6920e4c5653 + rx: 45490003-0023-4bd4-bbd5-a6920e4c5653 + 4e300001-0023-4bd4-bbd5-a6920e4c5653: + tx: 4e300002-0023-4bd4-bbd5-a6920e4c5653 + rx: 4e300003-0023-4bd4-bbd5-a6920e4c5653 + 45410001-0023-4bd4-bbd5-a6920e4c5653: + tx: 45410002-0023-4bd4-bbd5-a6920e4c5653 + rx: 45410003-0023-4bd4-bbd5-a6920e4c5653 + 51300001-0023-4bd4-bbd5-a6920e4c5653: + tx: 51300002-0023-4bd4-bbd5-a6920e4c5653 + rx: 51300003-0023-4bd4-bbd5-a6920e4c5653 + 45460001-0023-4bd4-bbd5-a6920e4c5653: + tx: 45460002-0023-4bd4-bbd5-a6920e4c5653 + rx: 45460003-0023-4bd4-bbd5-a6920e4c5653 + 454c0001-0023-4bd4-bbd5-a6920e4c5653: + tx: 454c0002-0023-4bd4-bbd5-a6920e4c5653 + rx: 454c0003-0023-4bd4-bbd5-a6920e4c5653 + 55300001-0023-4bd4-bbd5-a6920e4c5653: + tx: 55300002-0023-4bd4-bbd5-a6920e4c5653 + rx: 55300003-0023-4bd4-bbd5-a6920e4c5653 + 53440001-0023-4bd4-bbd5-a6920e4c5653: + tx: 53440002-0023-4bd4-bbd5-a6920e4c5653 + rx: 53440003-0023-4bd4-bbd5-a6920e4c5653 + 48300001-0023-4bd4-bbd5-a6920e4c5653: + tx: 48300002-0023-4bd4-bbd5-a6920e4c5653 + rx: 48300003-0023-4bd4-bbd5-a6920e4c5653 + 46530001-0023-4bd4-bbd5-a6920e4c5653: + tx: 46530002-0023-4bd4-bbd5-a6920e4c5653 + rx: 46530003-0023-4bd4-bbd5-a6920e4c5653 + 42410001-0023-4bd4-bbd5-a6920e4c5653: + tx: 42410002-0023-4bd4-bbd5-a6920e4c5653 + rx: 42410003-0023-4bd4-bbd5-a6920e4c5653 + 43410001-0023-4bd4-bbd5-a6920e4c5653: + tx: 43410002-0023-4bd4-bbd5-a6920e4c5653 + rx: 43410003-0023-4bd4-bbd5-a6920e4c5653 + 4f430001-0023-4bd4-bbd5-a6920e4c5653: + tx: 4f430002-0023-4bd4-bbd5-a6920e4c5653 + rx: 4f430003-0023-4bd4-bbd5-a6920e4c5653 + 455a0001-0023-4bd4-bbd5-a6920e4c5653: + tx: 455a0002-0023-4bd4-bbd5-a6920e4c5653 + rx: 455a0003-0023-4bd4-bbd5-a6920e4c5653 diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/lovenuts.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/lovenuts.yml new file mode 100644 index 000000000..e7ae9019d --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/lovenuts.yml @@ -0,0 +1,18 @@ +defaults: + name: Love Nut + features: + - feature-type: Vibrate + id: 45793bae-a3d5-4d76-9f20-f907e82b18df + output: + Vibrate: + step-range: + - 0 + - 15 + id: 3d5a9edb-e393-4603-8fb9-e038d3c4c0f3 +communication: + - btle: + names: + - Love_Nuts + services: + 0000fff0-0000-1000-8000-00805f9b34fb: + tx: 0000fff1-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/luvmazer.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/luvmazer.yml new file mode 100644 index 000000000..3b8eb24de --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/luvmazer.yml @@ -0,0 +1,25 @@ +defaults: + name: Luvmazer Finger Magic + features: + - feature-type: Vibrate + id: af257986-e34f-47f9-a69e-7a78afd43d31 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Rotate + id: 8f021f8a-a07e-4934-af3b-fa3bafd2a747 + output: + Rotate: + step-range: + - 0 + - 255 + id: c6d24bef-8263-4e3b-898d-7aeb7e58cc11 +communication: + - btle: + names: + - TKLM-W001-BT + services: + 0000ffa0-0000-1000-8000-00805f9b34fb: + tx: 0000ffa1-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/magic-motion-1.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/magic-motion-1.yml new file mode 100644 index 000000000..bf375d58f --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/magic-motion-1.yml @@ -0,0 +1,121 @@ +defaults: + name: Magic Motion V1 Device + features: + - feature-type: Vibrate + id: 42173db5-95ac-49b5-8a5a-73a63d91fcec + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: bcaf7da8-2e98-47e3-b22c-2204daf40a27 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 2525206c-8bdc-4803-9636-79576f3e692f +configurations: + - identifier: + - Smart Bean + name: MagicMotion Smart Bean + id: ef285932-0c7e-4edb-bc81-ce0c59f41c4a + - identifier: + - Smart Bean3 + name: FitCute Kegel Rejuve + id: 5adced22-1742-4e1e-bf75-225275a500b0 + - identifier: + - Smart Mini Vibe + name: MagicMotion Smart Mini Vibe + id: 0a69e7c1-51ca-49c1-91a3-c58debba037e + - identifier: + - Smart Mini Vibe3 + name: MagicMotion Vini + id: c006d72e-5fee-4643-b324-35fa6d56e176 + - identifier: + - Flamingo + - Flamingo T + name: MagicMotion Flamingo + id: efa69977-2c7b-4c0f-b9e6-ffa4d9c36630 + - identifier: + - Magic Bean + name: MagicMotion Kegel + id: 7239ca39-f8fd-4727-940b-04483f08cfb9 + - identifier: + - Magic Cell + name: MagicMotion Dante/Candy/Rise + id: 5596e91a-e336-4f26-b6da-19858be7ab67 + - identifier: + - Magic Wand + name: MagicMotion Wand + id: 91c15cc1-3021-44fb-a64d-3231c007705a + - identifier: + - Magic Fugu + - Fugu + - Fugu2 + name: MagicMotion Fugu + id: 3eefb122-6f5d-4e06-99c5-a89164b1d219 + - identifier: + - Gballs2 + name: G Vibe Gballs 2 + id: a9c33895-4f0a-4ecc-a849-2e632dbc8f29 + - identifier: + - GBalls3 + name: G Vibe Gballs 3 + id: c802d1e6-968a-4451-86e0-248e85e3d50d + - identifier: + - FM-LILAC-101 + name: Femometer Lilac + id: ef73c48c-8f6a-44e2-940a-0dd45f69cfb2 + - identifier: + - Xone + name: MagicMotion Xone + features: + - feature-type: Oscillate + id: ccd72f20-d37a-4e05-bad3-122c5da80b37 + output: + Oscillate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: 98a2e5c4-c4de-4ac5-a9db-b3e24a24424a + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: b24d166f-b6e0-4c9b-a056-8296564b19a8 + - identifier: + - CBT002 + name: FunTown Caleo + id: b6dc5c46-0919-4a45-900e-f83afae8b942 +communication: + - btle: + names: + - Smart Mini Vibe* + - Flamingo + - Flamingo T + - Smart Bean + - Smart Bean3 + - Magic Cell + - Magic Wand + - Fugu + - Fugu2 + - Gballs2 + - GBalls3 + - FM-LILAC-101 + - Xone + - CBT002 + services: + 78667579-7b48-43db-b8c5-7928a6b0a335: + tx: 78667579-a914-49a4-8333-aa3c0cd8fedc + 0000180f-0000-1000-8000-00805f9b34fb: + rxblebattery: 00002a19-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/magic-motion-2.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/magic-motion-2.yml new file mode 100644 index 000000000..db0b6eafc --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/magic-motion-2.yml @@ -0,0 +1,140 @@ +defaults: + name: Magic Motion V2 Device + features: + - feature-type: Vibrate + id: 4fe8ab2c-2811-416c-967c-fce58cb8a2f3 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: 014cdffe-d3d5-4bba-acf4-f26e809b45ec + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 33902551-eb44-406b-bc9a-7f9f981a972a +configurations: + - identifier: + - Lipstick + name: MagicMotion Awaken + id: 9ed09e5a-945d-4bb0-9813-3e07a8fd7baf + - identifier: + - Sword + name: MagicMotion Equinox + id: 5274feff-b0fa-4c37-9990-8861864fec59 + - identifier: + - Curve + name: MagicMotion Solstice + id: b639a627-60fc-4eff-afeb-91ccdf2e616b + - identifier: + - Eidolon + name: MagicMotion Eidolon + features: + - feature-type: Vibrate + id: 6b96f9d2-87bc-4596-810d-9a96cbd1a2fa + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 86090f46-7c4c-46fe-883f-d3765f477bac + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: 6baefd41-de6d-4c60-aedb-0a9b55f34875 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 1093a17d-9596-49b7-945f-c44610244932 + - identifier: + - Solstice X + name: MagicMotion Solstice X + features: + - feature-type: Vibrate + id: a245e29e-3f63-4c68-a5c2-c07c7c9970a4 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 70593a3b-2b16-4258-badb-9697074bf10b + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: f966012c-6b68-4dc3-b4a4-16d34fdc30c7 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: adfc6c8c-b7e8-4c0c-9fdc-e7c2bd3b4552 + - identifier: + - funwand + name: MagicMotion Zenith + id: 334f32f6-309e-4e79-a3de-b62aff0f6438 + - identifier: + - CBT001 + name: FunTown Jive + features: + - feature-type: Vibrate + id: 81515d54-be1d-42a1-bc7d-5b4e9c20db37 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Oscillate + id: d514fb91-2261-4c5c-a59e-9799fce40d17 + output: + Oscillate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: 123954de-a9f1-427a-823a-9b9173ad8856 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: d872f184-a2a4-4869-9506-d34975fa34c3 +communication: + - btle: + names: + - Eidolon + - Lipstick + - Sword + - Curve + - Solstice X + - funwand + - CBT001 + services: + 78667579-7b48-43db-b8c5-7928a6b0a335: + tx: 78667579-a914-49a4-8333-aa3c0cd8fedc + 0000180f-0000-1000-8000-00805f9b34fb: + rxblebattery: 00002a19-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/magic-motion-3.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/magic-motion-3.yml new file mode 100644 index 000000000..52bcf9c5a --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/magic-motion-3.yml @@ -0,0 +1,30 @@ +defaults: + name: LoveLife Krush + features: + - feature-type: Vibrate + id: af104b4d-73c3-4d89-95d6-ea7c4e21a3df + output: + Vibrate: + step-range: + - 0 + - 77 + - feature-type: Battery + description: Battery Level + id: 72bc2f2f-7f67-4636-bc5c-42ac4b55cb59 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: f954c774-3e08-4569-800f-94e454ccd3ca +communication: + - btle: + names: + - Krush + services: + 78667579-7b48-43db-b8c5-7928a6b0a335: + tx: 78667579-a914-49a4-8333-aa3c0cd8fedc + 0000180f-0000-1000-8000-00805f9b34fb: + rxblebattery: 00002a19-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/magic-motion-4.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/magic-motion-4.yml new file mode 100644 index 000000000..3bfe8eb1f --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/magic-motion-4.yml @@ -0,0 +1,120 @@ +defaults: + name: Magic Motion V4 Device + features: + - feature-type: Vibrate + id: c8ed6a4c-2dff-4be9-b1c5-b91bfd238bda + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: 8ba2798a-4717-4a39-ae5c-f445eb8f4448 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: e53d8751-5993-410c-82d7-edca26dd4c65 +configurations: + - identifier: + - funone + name: MagicMotion Bunny + id: ae515557-67e1-4527-bd0b-762a2fb47d9b + - identifier: + - Magic Sundi + name: MagicMotion Sundae + id: 0e5c564b-02cf-4665-b8e6-d938b8b8d749 + - identifier: + - Kegel Coach + name: MagicMotion Kegel Coach + id: 2ecd285e-9109-403c-b38f-3784629bd7de + - identifier: + - Magic Lotos + name: MagicMotion Lotos + id: a66cd42b-c3b3-4b00-bbb2-117961a06bcd + - identifier: + - nyx + name: MagicMotion Nyx + id: 69c95fd5-a9c2-4f7d-9fdc-a25f514ba290 + - identifier: + - umi + name: MagicMotion Umi + features: + - feature-type: Vibrate + id: 008a3d35-9b61-4bc2-9554-c3c742f03e12 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: b24eee4d-b3c2-4ce4-8f54-433e3d2a08f5 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: fdc5dc60-ece5-4f81-801c-076b1e1bad57 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 69a69c1d-1e37-49ed-b1a4-07da72939171 + - identifier: + - funkegel + name: MagicMotion Crystal + id: c22dfa34-5b4d-4c61-a972-fee67b1f60d8 + - identifier: + - bobi2 + name: MagicMotion Bobi + features: + - feature-type: Vibrate + id: 09d1b6fc-834d-4579-9bc7-79813f20d33f + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 04438678-4c82-48e1-a4fa-8dd916ee5469 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Battery + description: Battery Level + id: b2b3dedf-5f7a-4069-935f-f210fdf5cafc + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 318ca3d4-0779-47e8-9580-fc3efe1a0556 +communication: + - btle: + names: + - funone + - Magic Sundi + - Kegel Coach + - Magic Lotos + - nyx + - umi + - funkegel + - bobi2 + services: + 78667579-7b48-43db-b8c5-7928a6b0a335: + tx: 78667579-a914-49a4-8333-aa3c0cd8fedc + 0000180f-0000-1000-8000-00805f9b34fb: + rxblebattery: 00002a19-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/mannuo.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/mannuo.yml new file mode 100644 index 000000000..75ffd8a69 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/mannuo.yml @@ -0,0 +1,22 @@ +defaults: + name: ManNuo Device + features: + - feature-type: Vibrate + id: 36daf552-3c59-44b8-b00e-ff1e0e799fc6 + output: + Vibrate: + step-range: + - 0 + - 3 + id: 6fe6ed71-8869-4a38-bfc1-a7adc112e14e +communication: + - btle: + names: + - Sex toys + - Sex Toys + - LXCDVP + - MANO PRODUCT + services: + 0000fff0-0000-1000-8000-00805f9b34fb: + tx: 0000fff1-0000-1000-8000-00805f9b34fb + rx: 0000fff4-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/maxpro.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/maxpro.yml new file mode 100644 index 000000000..ce0998a28 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/maxpro.yml @@ -0,0 +1,18 @@ +defaults: + name: MaxPro 2 + features: + - feature-type: Vibrate + id: f3c0255d-2734-4f60-95a7-2e9fc04e399c + output: + Vibrate: + step-range: + - 0 + - 100 + id: 1f903059-93fd-4160-89a8-cc7a2001d0fa +communication: + - btle: + names: + - M2 + services: + 6e400001-b5a3-f393-e0a9-e50e24dcca9e: + tx: 6e400002-b5a3-f393-e0a9-e50e24dcca9e diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/meese.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/meese.yml new file mode 100644 index 000000000..9fa8369b2 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/meese.yml @@ -0,0 +1,43 @@ +defaults: + name: Meese Device + features: + - feature-type: Vibrate + id: 86e146ce-8aca-4df1-bfca-67dcf4d241c4 + output: + Vibrate: + step-range: + - 0 + - 10 + - feature-type: Vibrate + id: d2a0c869-d3c7-4ad7-b1fb-a8c914584abf + output: + Vibrate: + step-range: + - 0 + - 3 + id: 6ee04bd7-2f57-4ada-b622-b9bb210ff0c1 +configurations: + - identifier: + - Meese-V389 + name: Meese Tera + id: 8fe479fd-8343-49a2-959b-47f4cd7104ac + - identifier: + - Meese-cd + name: Meese Modo + features: + - feature-type: Vibrate + id: 9bdae29d-46fc-4435-8a63-71927e5e1ada + output: + Vibrate: + step-range: + - 0 + - 10 + id: db5ab134-ecc8-4f50-9339-20908f8894e6 +communication: + - btle: + names: + - Meese-V389 + - Meese-cd + services: + 0000ffe0-0000-1000-8000-00805f9b34fb: + tx: 0000ffe1-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/metaxsire-v2.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/metaxsire-v2.yml new file mode 100644 index 000000000..f28f2df65 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/metaxsire-v2.yml @@ -0,0 +1,59 @@ +defaults: + name: metaXsire Nolan + features: + - feature-type: Vibrate + id: 4961e88c-5c2e-4701-95ee-16d58538b65e + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Oscillate + id: a3cd125d-ac6c-426d-b45a-fe3c7ae1e1d2 + output: + Oscillate: + step-range: + - 0 + - 20 + id: ce9d4fe0-6614-493d-ac77-02ec5d42947d +configurations: + - identifier: + - LB-W01 + name: Libo Miao + features: + - feature-type: Vibrate + id: 59cacf4b-ef09-42ad-b3d6-459bc195da26 + output: + Vibrate: + step-range: + - 0 + - 20 + id: 2a4a4daa-5740-425b-b1a4-72b73f746fdf + - identifier: + - HH010 + name: metaXsire HH010 + features: + - feature-type: Oscillate + id: 968f7306-6997-4b76-a40f-acbb431d9582 + output: + Oscillate: + step-range: + - 0 + - 20 + - feature-type: Vibrate + id: 018009d0-b5bf-4f97-a13d-909d0e74fabc + output: + Vibrate: + step-range: + - 0 + - 20 + id: 0e1f9fe7-22d9-4afb-9fe5-192b8e5508c3 +communication: + - btle: + names: + - LY272A01 + - LB-W01 + - HH010 + services: + 0000bae0-0000-1000-8000-00805f9b34fb: + tx: 0000bae1-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/metaxsire-v3.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/metaxsire-v3.yml new file mode 100644 index 000000000..18ed0698e --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/metaxsire-v3.yml @@ -0,0 +1,53 @@ +defaults: + name: metaXsire Tay + features: + - feature-type: Vibrate + id: 074a15d1-2efc-4cd8-8f1f-0f32f1468024 + output: + Vibrate: + step-range: + - 0 + - 20 + id: 2e8ff651-b10d-4686-89b5-b8197e80e159 +configurations: + - identifier: + - TAY001 + name: metaXsire Tay 1 + id: c7615c1d-d53f-4d24-82e1-ce08c301da66 + - identifier: + - TAY009 + name: metaXsire Tay 9 + id: ddfe0ac7-f275-4e08-b16b-a5cd579e9a9e + - identifier: + - TAY006 + name: metaXsire Tay 6 + id: edfecee1-3b6f-4501-a9d9-717b2bd515a2 + - identifier: + - TA-S001A + name: metaXsire Zeus + features: + - feature-type: Vibrate + id: 11c78de9-800a-4444-9647-0ed33181e63c + output: + Vibrate: + step-range: + - 0 + - 20 + - feature-type: Oscillate + id: 47646747-4dea-47ba-80b2-407e2a276ae2 + output: + Oscillate: + step-range: + - 0 + - 20 + id: ae1e373f-1a35-476b-8da8-6017dcb7e0de +communication: + - btle: + names: + - TAY001 + - TAY006 + - TAY009 + - TA-S001A + services: + 0000fff0-0000-1000-8000-00805f9b34fb: + tx: 0000fe02-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/metaxsire-v4.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/metaxsire-v4.yml new file mode 100644 index 000000000..ce29d8f5f --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/metaxsire-v4.yml @@ -0,0 +1,26 @@ +defaults: + name: metaXsire G1 Vibrator + features: + - feature-type: Vibrate + id: 0c9c5a7d-8d28-4003-b1d4-8de5c73c8fe4 + output: + Vibrate: + step-range: + - 0 + - 99 + id: e69dc695-695d-485b-be16-59161505fd6d +configurations: + - identifier: + - HJ2024N01 + name: VVD Vkini + id: 679cdc1b-9236-4ed1-a3c4-c33b748a8cde +communication: + - btle: + names: + - CFG1 vibrator + - HJ2024N01 + services: + 0000cfa2-0000-1000-8000-00805f9b34fb: + tx: 0000cf21-0000-1000-8000-00805f9b34fb + 0000dba2-0000-1000-8000-00805f9b34fb: + tx: 0000db21-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/metaxsire-v5.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/metaxsire-v5.yml new file mode 100644 index 000000000..e9672b361 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/metaxsire-v5.yml @@ -0,0 +1,18 @@ +defaults: + name: Sexverse Heart + features: + - feature-type: Vibrate + id: 140be13c-4cb3-407f-9597-e03f046f1c1a + output: + Vibrate: + step-range: + - 0 + - 100 + id: 783bc287-528c-4c58-a7ec-47a49304309e +communication: + - btle: + names: + - CBW02 + services: + 0000ffcb-0000-1000-8000-00805f9b34fb: + tx: 0000cb02-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/metaxsire.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/metaxsire.yml new file mode 100644 index 000000000..6c7baed9c --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/metaxsire.yml @@ -0,0 +1,114 @@ +defaults: + name: metaXsire Device + features: + - feature-type: Vibrate + id: 74825924-5e2a-4dd6-a91a-10a24be40c09 + output: + Vibrate: + step-range: + - 0 + - 255 + id: f595862c-fa49-460c-9667-87f0eac24a6c +configurations: + - identifier: + - Rex + name: metaXsire Rex + id: 447c8bda-bafc-472a-9333-8f809bbc48bb + - identifier: + - Cali + - LY165A01 + name: metaXsire Cali + features: + - feature-type: Vibrate + id: d3e17d91-94d8-449d-b049-91bd0ec3cf71 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Constrict + id: 6aceca29-6833-4f61-b5af-1005bb50bdf9 + output: + Constrict: + step-range: + - 0 + - 255 + id: e4bb4468-1de1-4f37-a348-5c7177923603 + - identifier: + - Olis + name: metaXsire Olis + features: + - feature-type: Vibrate + id: 2e6d4a73-7847-4a5b-a03c-cdd6f07c39c9 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: c1530d49-07b0-432b-8c08-08e1ef4d2842 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Rotate + id: cbc1187c-2400-4e9b-9fc0-a03744bd7295 + output: + Rotate: + step-range: + - 0 + - 255 + id: 9e874901-c5d7-49d2-910d-3849ab5ff96c + - identifier: + - LY213A01 + name: metaXsire BuCUE + features: + - feature-type: Oscillate + id: 641d8a6a-b068-4089-9632-c81ab872677d + output: + Oscillate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: 15dcc27e-ab6d-407e-8e1a-4b51e445fa5d + output: + Vibrate: + step-range: + - 0 + - 255 + id: 941a41b2-78d2-45a6-b730-17a8ff8c75e0 + - identifier: + - LY199B01 + name: Cooxer Bullet Vibe + id: 0f8e2cac-428a-430c-a9d8-8889ed608c24 + - identifier: + - LY234A01 + name: metaXsire Tadpole + id: de51460a-4c65-4173-8172-8dc7eaccc3a1 + - identifier: + - LY271A01 + name: metaXsire Upton + id: 5d061d81-98cd-4271-b896-68394a21e97a + - identifier: + - LY270A01 + name: metaXsire Una + id: 97458f06-7a6f-4f8a-bb7a-93dd6ab53157 +communication: + - btle: + names: + - Rex + - Cali + - LY165A01 + - Olis + - LY213A01 + - LY199B01 + - LY234A01 + - LY271A01 + - LY270A01 + services: + 0000ffe0-0000-1000-8000-00805f9b34fb: + tx: 0000ffe1-0000-1000-8000-00805f9b34fb + rx: 0000ffe2-0000-1000-8000-00805f9b34fb + diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/mizzzee-v2.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/mizzzee-v2.yml new file mode 100644 index 000000000..4f4be3140 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/mizzzee-v2.yml @@ -0,0 +1,18 @@ +defaults: + name: Mizz Zee Device + features: + - feature-type: Vibrate + id: e120abaf-dd55-4b8a-ba17-ea86155a819c + output: + Vibrate: + step-range: + - 0 + - 68 + id: 9fc65537-e8ae-4e54-bfcb-adebbe39d7e1 +communication: + - btle: + names: + - XHT + services: + 0000eea0-0000-1000-8000-00805f9b34fb: + tx: 0000ee01-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/mizzzee-v3.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/mizzzee-v3.yml new file mode 100644 index 000000000..db5aee4e0 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/mizzzee-v3.yml @@ -0,0 +1,18 @@ +defaults: + name: Mizz Zee Device + features: + - feature-type: Vibrate + id: aa417fd0-0ab1-409f-b7a3-05f6c3ede623 + output: + Vibrate: + step-range: + - 0 + - 1000 + id: 4d54f81c-e31f-469a-a17a-ea1d4058a037 +communication: + - btle: + names: + - XHTKJ + services: + 0000ff10-0000-1000-8000-00805f9b34fb: + tx: 0000ff12-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/mizzzee.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/mizzzee.yml new file mode 100644 index 000000000..51fdab653 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/mizzzee.yml @@ -0,0 +1,18 @@ +defaults: + name: Mizz Zee Device + features: + - feature-type: Vibrate + id: be144c33-8f81-42b7-b43b-1def688feedf + output: + Vibrate: + step-range: + - 0 + - 68 + id: d8aa061f-f60d-4e0c-a638-cbbae4493c3b +communication: + - btle: + names: + - NFY008 + services: + 0000eea0-0000-1000-8000-00805f9b34fb: + tx: 0000eea1-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/monsterpub.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/monsterpub.yml new file mode 100644 index 000000000..a70becee0 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/monsterpub.yml @@ -0,0 +1,177 @@ +defaults: + name: Sistalk MonsterPub Device + features: + - feature-type: Vibrate + id: 79df96bb-25af-422e-a066-c7c3f301a843 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 87e76bfc-ecba-4cda-a574-4a92889a6bc3 +configurations: + - identifier: + - MP2_JK_N_P1 + name: Sistalk MonsterPub 2 Doctor Whale + features: + - feature-type: Vibrate + id: 9cf2d977-c1c3-46c0-bb88-c71a3c65f7ae + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: ba941f5c-0946-443c-a6eb-5a0cff38a3b8 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 01eb3034-194f-4c91-88e4-8095bb0f4ff4 + - identifier: + - MP_MW_TL_P2 + name: Sistalk MonsterPub Magic Kiss + features: + - feature-type: Vibrate + id: d8d639f1-c821-46a6-9eb1-eb1eda9289b5 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: d3c1b259-b884-4a63-ba75-b8d9341398be + output: + Vibrate: + step-range: + - 0 + - 100 + id: bdf1fea2-374d-4340-9057-6ee76595cb83 + - identifier: + - MP2_QC_TL_P1 + name: Sistalk MonsterPub 2 Mister Devil + features: + - feature-type: Vibrate + id: f9f2b6ae-d54d-4d78-a535-3879d96a7fd6 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 8186c4b9-40df-422d-8e70-f0babf32f82b + output: + Vibrate: + step-range: + - 0 + - 100 + id: 5a8c6ddf-15b2-4d7b-bdcf-38c7c49586bb + - identifier: + - MP_BABY_QC_N_P4 + name: Sistalk MonsterPub Baby Youth Health + features: + - feature-type: Vibrate + id: 51923606-6704-48ca-b083-01ceacf897a1 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 553a765a-e91f-4187-85cb-b2be8311944b + output: + Vibrate: + step-range: + - 0 + - 100 + id: fb558c71-beb7-43ec-8b78-2ca975aa7d7b + - identifier: + - MP_MXY_N_P1 + name: Sistalk MonsterPub KiniCat + id: 19e019be-dd3f-4822-8243-288690cae235 + - identifier: + - MP1N_QC_TL_P2 + name: Sistalk MonsterPub BeatHeart + id: 640958c5-0fc0-4390-bdda-959c1686084d + - identifier: + - TDG_LIP_PT2 + name: Tracy's Dog Surreal + id: f2049034-1515-4008-8cc3-2b6914080a5c + - identifier: + - MP1P_QC_TL_P6 + name: Sistalk MonsterPub 1P Mister Devil + id: 1a39cdde-63ba-407a-8307-27b775c3f365 + - identifier: + - MPMB_QC_TL_P2 + name: Sistalk MonsterPub Sweet + id: 6d613fc2-76b2-4007-af78-e91bfe20e659 + - identifier: + - MPAV_QC_TL_P1 + name: Sistalk MonsterPub Amazing + id: 719a2ee0-bf1e-41bc-84c9-6d369b5646dd + - identifier: + - MH_TOR_TL_P5 + name: Sistalk MonsterHub Tornado + id: 8ee7eb14-bc8b-4f66-ac29-8586fa3d1f04 + - identifier: + - MP_SUCKBANG_P5 + name: Sistalk MonsterPub Pop + features: + - feature-type: Oscillate + id: 6a9d1640-2b72-42f1-8ad1-1e1a97394f82 + output: + Oscillate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 5462d583-6a92-4288-b743-46957be25efb + output: + Vibrate: + step-range: + - 0 + - 100 + id: da7e6371-b4cd-475a-9a41-501f4bb06ef3 + - identifier: + - TDG_CRAYBIT_PT + name: Tracy's Dog Craybit Pro + features: + - feature-type: Vibrate + id: 3fbc11b2-d07c-4793-a90d-364d62631aca + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 164c2dca-0f5e-4c06-8698-4e65b027a25e + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 8bea0dcd-400c-41a0-819e-bca090caf186 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 8d9c60c2-eb9a-4fd0-8917-78f7d94320b3 +communication: + - btle: + names: + - MonsterPub + - MonsterHub + - TracyDog + services: + 00006000-0000-1000-8000-00805f9b34fb: + tx: 00006001-0000-1000-8000-00805f9b34fb + txmode: 00006002-0000-1000-8000-00805f9b34fb + txvibrate: 00006003-0000-1000-8000-00805f9b34fb + generic0: 0000600a-0000-1000-8000-00805f9b34fb + 00006010-0000-1000-8000-00805f9b34fb: + rxblemodel: 00006014-0000-1000-8000-00805f9b34fb + 00008000-0000-1000-8000-00805f9b34fb: + rx: 00008001-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/motorbunny.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/motorbunny.yml new file mode 100644 index 000000000..b24b3b231 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/motorbunny.yml @@ -0,0 +1,35 @@ +defaults: + name: Motorbunny Device + features: + - feature-type: Vibrate + id: cb44a214-4c5c-4a04-8b1a-0d91a73a7a3a + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: RotateWithDirection + id: 683b450d-bb1a-4fca-b61a-83f8b56086fa + output: + RotateWithDirection: + step-range: + - 0 + - 255 + id: 21cb973e-c404-44de-99c8-9cf4bc5538a6 +configurations: + - identifier: + - MB Controller + name: Motorbunny Classic + id: 97362be6-5601-4d08-812a-4eb1ffa29980 + - identifier: + - MB LINK 201 + name: Motorbunny Buck + id: 6de31e21-d76c-4d9a-9220-afa36f29d128 +communication: + - btle: + names: + - MB Controller + - MB LINK 201 + services: + 0000fff0-0000-1000-8000-00805f9b34fb: + tx: 0000fff6-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/muse.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/muse.yml new file mode 100644 index 000000000..1a92cfbc7 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/muse.yml @@ -0,0 +1,28 @@ +defaults: + name: Muse Device + features: + - feature-type: Vibrate + id: 6dcc57e0-8a30-4e90-ba9e-4b8dd488d166 + output: + Vibrate: + step-range: + - 0 + - 9 + id: 94e9d8e0-94cc-42f5-b14d-c55cc91e2e68 +configurations: + - identifier: + - WB-ZDB-WST + name: Dream Lover Archer 2 + id: 48b17c67-fb1f-40c7-8dcb-b67dfb041afc + - identifier: + - WB-TDD + name: Galaku Panty Vib + id: dd40210e-1523-4d61-bdaf-3827635fb181 +communication: + - btle: + names: + - WB-ZDB-WST + - WB-TDD + services: + 0000aaa0-0000-1000-8000-00805f9b34fb: + tx: 0000aaa1-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/mysteryvibe-v2.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/mysteryvibe-v2.yml new file mode 100644 index 000000000..a07ad969c --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/mysteryvibe-v2.yml @@ -0,0 +1,169 @@ +defaults: + name: Mysteryvibe V2 Device + features: + - feature-type: Vibrate + id: 2cd76f8d-963c-4b98-861d-00b560a0ae09 + output: + Vibrate: + step-range: + - 0 + - 56 + - feature-type: Vibrate + id: 525464fd-960b-47ef-b7f3-04196a648963 + output: + Vibrate: + step-range: + - 0 + - 56 + - feature-type: Vibrate + id: 811a2fe9-be54-49ee-89ac-e8e83895e33d + output: + Vibrate: + step-range: + - 0 + - 56 + id: 2b750693-1766-4448-8c30-9f9fa32830f2 +configurations: + - identifier: + - 6907 MV1 + name: MysteryVibe Tenuto Mini + id: 9254a628-04a2-4876-856e-182d8badc366 + - identifier: + - 6908 MV1 + name: MysteryVibe Crescendo 2 + features: + - feature-type: Vibrate + id: 723b512f-9160-4f5b-b50b-3fb9622dff1e + output: + Vibrate: + step-range: + - 0 + - 56 + - feature-type: Vibrate + id: 960f8105-2277-4b81-a529-dd050250df80 + output: + Vibrate: + step-range: + - 0 + - 56 + - feature-type: Vibrate + id: 557828e8-e1cf-4f9a-9342-43bc9c34642c + output: + Vibrate: + step-range: + - 0 + - 56 + - feature-type: Vibrate + id: f2f6b8f8-7ff7-4928-9385-af1f3c583209 + output: + Vibrate: + step-range: + - 0 + - 56 + - feature-type: Vibrate + id: a5a287fc-82de-432d-b42d-cc9ee89625ae + output: + Vibrate: + step-range: + - 0 + - 56 + - feature-type: Vibrate + id: bbd27d45-3b13-4189-b7a8-ccaa07a405db + output: + Vibrate: + step-range: + - 0 + - 56 + id: 317cc151-16f9-4ac7-aa69-63a3f0448895 + - identifier: + - 6909 MV1 + - 6909 MV2 + name: MysteryVibe Tenuto 2 + features: + - feature-type: Vibrate + id: 88ddd1f2-6a0b-4fab-b548-5cd4edb55aae + output: + Vibrate: + step-range: + - 0 + - 56 + - feature-type: Vibrate + id: e30a128b-3dcb-4f87-beef-8aca7f3b1512 + output: + Vibrate: + step-range: + - 0 + - 56 + - feature-type: Vibrate + id: 3edf88eb-acb9-4852-9a71-3edda23f705d + output: + Vibrate: + step-range: + - 0 + - 56 + - feature-type: Vibrate + id: 1b3abe40-84d2-4237-830d-44c1927f35c3 + output: + Vibrate: + step-range: + - 0 + - 56 + id: 9a1bcb00-0294-46c2-ac97-0b3f8d50192a + - identifier: + - 6914 MV1 + name: MysteryVibe Legato + features: + - feature-type: Vibrate + id: 79f4df66-18a2-4fdb-a492-75e908bf978f + output: + Vibrate: + step-range: + - 0 + - 56 + - feature-type: Vibrate + id: f149b9be-4616-4552-a0a9-c419cb764988 + output: + Vibrate: + step-range: + - 0 + - 56 + - feature-type: Vibrate + id: f3553da8-f386-43b4-8998-64b7696c53f4 + output: + Vibrate: + step-range: + - 0 + - 56 + - feature-type: Vibrate + id: 4c1fb245-6f91-4613-895f-5f8cee00ab5b + output: + Vibrate: + step-range: + - 0 + - 56 + id: e9187e5a-1491-49db-ba4b-3b6f9fb55977 + - identifier: + - 6915 MV1 + name: MysteryVibe Molto + features: + - feature-type: Vibrate + id: cf40ea50-cddc-40e2-8661-d5252ac29f77 + output: + Vibrate: + step-range: + - 0 + - 56 + id: ed45ff87-fad1-41fe-8d0a-cfd4daaf1b4e +communication: + - btle: + names: + - 6907 MV1 + - 6908 MV1 + - 6909 MV1 + - 6909 MV2 + - 6914 MV1 + - 6915 MV1 + services: + f0006900-110c-478b-b74b-6f403b364a9c: + txmode: f0006901-110c-478b-b74b-6f403b364a9c + txvibrate: f0006903-110c-478b-b74b-6f403b364a9c diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/mysteryvibe.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/mysteryvibe.yml new file mode 100644 index 000000000..e96c5b292 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/mysteryvibe.yml @@ -0,0 +1,84 @@ +defaults: + name: Mysteryvibe Device + features: + - feature-type: Vibrate + id: 40c417e0-8a0b-4017-a0b5-2b33df4f0acc + output: + Vibrate: + step-range: + - 0 + - 56 + - feature-type: Vibrate + id: 84057071-af0e-4156-9f82-f7afc794bcde + output: + Vibrate: + step-range: + - 0 + - 56 + - feature-type: Vibrate + id: edaa4f3d-71c2-43b3-b9c3-b6a425b27200 + output: + Vibrate: + step-range: + - 0 + - 56 + - feature-type: Vibrate + id: b977c4f4-1585-49c4-9980-c2e8d329f713 + output: + Vibrate: + step-range: + - 0 + - 56 + - feature-type: Vibrate + id: ba9c09c7-1948-4b6f-823f-d9fd1380709c + output: + Vibrate: + step-range: + - 0 + - 56 + - feature-type: Vibrate + id: 5a0a0429-5fb6-4bcb-bb4c-5e14f4338677 + output: + Vibrate: + step-range: + - 0 + - 56 + id: 523391d5-1e0a-42f0-b669-5ad3f3e49902 +configurations: + - identifier: + - MV Crescendo + name: MysteryVibe Crescendo + id: 09470af5-da2f-45f4-b540-da653c4c0b40 + - identifier: + - 'MV Tenuto ' + name: MysteryVibe Tenuto + id: 1cb2c947-aa77-4aaa-83d4-f987ecb33953 + - identifier: + - 'MV Poco ' + name: MysteryVibe Poco + features: + - feature-type: Vibrate + id: 78d26150-7355-4633-bdc0-d2d58b2ea2aa + output: + Vibrate: + step-range: + - 0 + - 56 + - feature-type: Vibrate + id: 8f0c1cc0-b269-4eb6-a87f-34aeaee28906 + output: + Vibrate: + step-range: + - 0 + - 56 + id: b72b5597-a708-4fe9-919a-99f1d38291ef +communication: + - btle: + names: + - MV Crescendo + - 'MV Tenuto ' + - 'MV Poco ' + services: + f0006900-110c-478b-b74b-6f403b364a9c: + txmode: f0006901-110c-478b-b74b-6f403b364a9c + txvibrate: f0006903-110c-478b-b74b-6f403b364a9c diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/nextlevelracing.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/nextlevelracing.yml new file mode 100644 index 000000000..43b911d46 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/nextlevelracing.yml @@ -0,0 +1,75 @@ +defaults: + name: Next Level Racing HF8 Haptic Gaming Pad + features: + - feature-type: Vibrate + description: Right thigh + id: 178ade8c-0063-4f37-b37f-c47608f0b1e3 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + description: Left thigh + id: f3d43a20-94e8-4e6a-a504-4b2fe87cfbe1 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + description: Right buttock + id: 00d0b735-ffb6-4964-b963-75b1d4995c89 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + description: Left buttock + id: 5ba0a42a-8bed-4123-95bd-0d1f4bc5333d + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + description: Right back + id: 29820b84-4c47-443d-85a5-8706f64d38c1 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + description: Left back + id: b930b1ae-2974-4e8f-b95c-b960d848534c + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + description: Right shoulder + id: 225e1d14-4cc9-4c8c-b6ff-5ae024e3387a + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + description: Left shoulder + id: e369bcd9-8e2f-4466-8773-98bdf5fad7c5 + output: + Vibrate: + step-range: + - 0 + - 255 + id: fc830a11-de0d-4262-8155-99827cb926a9 +communication: + - serial: + port: default + baud-rate: 115200 + data-bits: 8 + parity: 'N' + stop-bits: 1 diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/nexus-revo.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/nexus-revo.yml new file mode 100644 index 000000000..4056efb15 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/nexus-revo.yml @@ -0,0 +1,25 @@ +defaults: + name: Nexus Revo Stealth + features: + - feature-type: Vibrate + id: 24125960-c279-4f64-87e3-a819af7319b4 + output: + Vibrate: + step-range: + - 0 + - 10 + - feature-type: RotateWithDirection + id: fabe3961-dc17-4f32-856f-13880c0a29a3 + output: + RotateWithDirection: + step-range: + - 0 + - 2 + id: 622f93f2-53d5-4ada-b6a7-359a9d8aedd0 +communication: + - btle: + names: + - XW-LW3 + services: + 0000c570-0000-1000-8000-00805f9b34fb: + tx: 0000c571-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/nintendo-joycon.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/nintendo-joycon.yml new file mode 100644 index 000000000..eb816c3c3 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/nintendo-joycon.yml @@ -0,0 +1,20 @@ +defaults: + name: Nintendo Joycon + features: + - feature-type: Vibrate + id: 7a3195c9-4c04-4004-9fac-a475983f1dd4 + output: + Vibrate: + step-range: + - 0 + - 1000 + id: 0aae8323-9095-4b71-b151-d5ef93ab8f6d +communication: + - hid: + pairs: + - vendor-id: 1406 + product-id: 8199 + - vendor-id: 1406 + product-id: 8198 + - vendor-id: 1406 + product-id: 8201 diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/nobra.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/nobra.yml new file mode 100644 index 000000000..9c6f5074e --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/nobra.yml @@ -0,0 +1,24 @@ +defaults: + name: Nobra's Silicone Dreams Toy + features: + - feature-type: Vibrate + id: 3d9a6c96-2f9e-4105-931b-c799c1c9f3e0 + output: + Vibrate: + step-range: + - 0 + - 15 + id: b548cba6-63cd-4d4c-9124-7e13303a6dec +communication: + - btle: + names: + - NobraControl* + services: + 0000abf0-0000-1000-8000-00805f9b34fb: + tx: 0000abf1-0000-1000-8000-00805f9b34fb + - serial: + port: default + baud-rate: 19200 + data-bits: 8 + parity: 'N' + stop-bits: 1 diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/omobo.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/omobo.yml new file mode 100644 index 000000000..b7228d61f --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/omobo.yml @@ -0,0 +1,18 @@ +defaults: + name: Omobo ViVegg Vibrator + features: + - feature-type: Vibrate + id: 6ce40ef1-a4bc-4d4f-a3f1-9059e8fd461b + output: + Vibrate: + step-range: + - 0 + - 100 + id: 550658f8-3cce-4b97-999e-7ddb3357a591 +communication: + - btle: + names: + - S6 + services: + 0000ffb0-0000-1000-8000-00805f9b34fb: + tx: 0000ffb2-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/patoo.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/patoo.yml new file mode 100644 index 000000000..65130cf43 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/patoo.yml @@ -0,0 +1,54 @@ +defaults: + name: Patoo Device + features: + - feature-type: Vibrate + id: 328761ed-4dd1-4535-9d37-e805f5eb1a61 + output: + Vibrate: + step-range: + - 0 + - 100 + id: fbb69ec0-dda6-4fca-ae69-390a91c13c03 +configurations: + - identifier: + - PTVEA + name: Patoo Carrot + id: 929310c1-bf4a-4238-b8d9-96ffcca1f954 + - identifier: + - PCS + name: Patoo Vibrator + id: 91af7b5e-8b16-4489-a916-1584ff1e561c + - identifier: + - PHT + name: Patoo Bean Sprout + id: a4175adb-1086-4a4a-8a43-9d484e231085 + - identifier: + - PBT + name: Patoo Devil + features: + - feature-type: Vibrate + id: f2957620-0a5c-4d69-851c-f9d34544e4cc + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 49f28542-fb54-46e6-a6b8-f412617ce24f + output: + Vibrate: + step-range: + - 0 + - 100 + id: 70af2af2-ba71-4b41-9e5d-4c3000377a2b +communication: + - btle: + names: + - PTVEA* + - PBT* + - PCS* + - PHT* + services: + f000aa64-0451-4000-b000-000000000000: + txmode: f000aa65-0451-4000-b000-000000000000 + tx: f000aa68-0451-4000-b000-000000000000 diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/picobong.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/picobong.yml new file mode 100644 index 000000000..5b97eff07 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/picobong.yml @@ -0,0 +1,50 @@ +defaults: + name: Picobong Device + features: + - feature-type: Vibrate + id: 6acffe62-d4ae-4a9e-8610-123d46d26dcc + output: + Vibrate: + step-range: + - 0 + - 10 + id: e820a3cc-70e2-4766-98d4-934a00a667db +configurations: + - identifier: + - Blow hole + - Picobong Male Toy + name: Picobong Blow hole + id: 1f59dbcf-b84d-4cf8-ac68-87bacb143b34 + - identifier: + - Diver + - Picobong Egg + name: Picobong Diver + id: b3396470-af6e-45df-ad4f-944539d71600 + - identifier: + - Life guard + - Picobong Ring + name: Picobong Life guard + id: 88684b6f-6fde-488e-86a5-5c1f50893345 + - identifier: + - Surfer + - Picobong Butt Plug + - Egg driver + - Surfer_plug + name: Picobong Surfer + id: f7c40c1b-0d86-4d39-9163-34a9a243d614 +communication: + - btle: + names: + - Blow hole + - Picobong Male Toy + - Diver + - Picobong Egg + - Life guard + - Picobong Ring + - Surfer + - Picobong Butt Plug + - Egg driver + - Surfer_plug + services: + 0000fff0-0000-1000-8000-00805f9b34fb: + tx: 0000fff1-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/pink_punch.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/pink_punch.yml new file mode 100644 index 000000000..1ddec2444 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/pink_punch.yml @@ -0,0 +1,33 @@ +defaults: + name: Pink Punch Device + features: + - feature-type: Vibrate + id: 71813440-1a8e-4cfb-9753-bf1fdc674579 + output: + Vibrate: + step-range: + - 0 + - 100 + id: c64c779a-4451-4c55-af1d-e4b40527d678 +configurations: + - identifier: + - Pink_Punch + name: Pink Punch Sunset Mushroom + id: 7e0338c1-0562-451a-95ce-1b078de2f32e + - identifier: + - PinkPunch_Peachu + name: Pink Punch Peachu + id: b0554241-8f73-45c7-baf8-fa179f1ea4ef + - identifier: + - PinkPunch_DreamBunny + name: Pink Punch Dream Bunny + id: 85703d43-c719-4753-ba92-3bb28c150565 +communication: + - btle: + names: + - Pink_Punch + - PinkPunch_Peachu + - PinkPunch_DreamBunny + services: + 0000ffe0-0000-1000-8000-00805f9b34fb: + tx: 0000ffe1-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/prettylove.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/prettylove.yml new file mode 100644 index 000000000..6a59aefe8 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/prettylove.yml @@ -0,0 +1,20 @@ +defaults: + name: Pretty Love Device + features: + - feature-type: Vibrate + id: 349df5c5-1c5d-4de2-a3d9-c9159c640aba + output: + Vibrate: + step-range: + - 0 + - 3 + id: abeb7195-dbc2-4bd1-a079-18ffbb04e521 +communication: + - btle: + names: + - Aogu BLE * + - AB Shutter3 [Aogu BLE Device] + services: + 0000ffe5-0000-1000-8000-00805f9b34fb: + tx: 0000ffe9-0000-1000-8000-00805f9b34fb + rx: 0000ffe2-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/realov.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/realov.yml new file mode 100644 index 000000000..24e92c19a --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/realov.yml @@ -0,0 +1,18 @@ +defaults: + name: Realov Device + features: + - feature-type: Vibrate + id: 7d9d20cd-1a03-487f-b6c7-9b337c49e534 + output: + Vibrate: + step-range: + - 0 + - 50 + id: 79b23444-7e36-4042-bd52-86221c67c988 +communication: + - btle: + names: + - REALOV_VIBE + services: + 0000ffe0-0000-1000-8000-00805f9b34fb: + tx: 0000ffe1-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/realtouch.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/realtouch.yml new file mode 100644 index 000000000..8dcc5e4e7 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/realtouch.yml @@ -0,0 +1,16 @@ +defaults: + name: RealTouch + features: + - feature-type: PositionWithDuration + id: 60da884f-131a-4036-ae93-97efc97591e2 + output: + PositionWithDuration: + step-range: + - 0 + - 99 + id: 2b428728-0785-4cbc-a71f-4f48412af194 +communication: + - hid: + pairs: + - vendor-id: 8020 + product-id: 1 diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/rez-trancevibrator.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/rez-trancevibrator.yml new file mode 100644 index 000000000..51360ea9b --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/rez-trancevibrator.yml @@ -0,0 +1,16 @@ +defaults: + name: Rez TranceVibrator + features: + - feature-type: Vibrate + id: 01e369e0-541d-417a-9809-0600dab964c6 + output: + Vibrate: + step-range: + - 0 + - 255 + id: 04923383-f64b-4b39-bed6-83862c5314d5 +communication: + - usb: + pairs: + - vendor-id: 2889 + product-id: 1615 diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/sakuraneko.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/sakuraneko.yml new file mode 100644 index 000000000..f0107c6c6 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/sakuraneko.yml @@ -0,0 +1,53 @@ +defaults: + name: Sakuraneko Device + features: + - feature-type: Vibrate + id: bb67be77-f219-411d-98b5-d6b358eb94c9 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 0e121fa6-76db-484a-892f-4dc88ac6f333 +configurations: + - identifier: + - sakuraneko-01 + name: Sakuraneko Korokoro + id: 26673810-3196-4733-8071-781c221c1a39 + - identifier: + - sakuraneko-02 + name: Sakuraneko Nukunuku + id: e1bcba4b-1f4d-4d57-8a30-ee3696fb206f + - identifier: + - sakuraneko-03 + name: Sakuraneko Dokidoki + id: 7234946a-55ed-483a-8482-a6d6e1e97c4b + - identifier: + - sakuraneko-04 + name: Sakuraneko Koikoi + features: + - feature-type: Vibrate + id: a5eb13a7-1f14-4785-a2ea-86dde4a3e15b + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Rotate + id: 62b84b1c-cfcd-4d9a-8dba-4d8210e5ee93 + output: + Rotate: + step-range: + - 0 + - 100 + id: c45e02cd-b8b6-4617-996e-302db442b228 +communication: + - btle: + names: + - sakuraneko-01 + - sakuraneko-02 + - sakuraneko-03 + - sakuraneko-04 + services: + 0000ffe0-0000-1000-8000-00805f9b34fb: + tx: 0000ffe1-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/satisfyer.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/satisfyer.yml new file mode 100644 index 000000000..cadba8605 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/satisfyer.yml @@ -0,0 +1,1152 @@ +defaults: + name: Satisfyer Device + features: + - feature-type: Vibrate + id: 7153daef-c222-4841-9495-289798fff9ea + output: + Vibrate: + step-range: + - 0 + - 100 + id: 9a934b7a-b6aa-4ad6-8d5c-e00971d67159 +configurations: + - identifier: + - '10005' + name: Satisfyer Hot Spot + features: + - feature-type: Vibrate + id: b9bcbd6f-9f4a-4738-9a64-08e646fa2297 + output: + Vibrate: + step-range: + - 0 + - 100 + id: a8a7887f-c5dd-4e2c-ae88-d20e954bc65a + - identifier: + - '10006' + name: Satisfyer Heated Affair + features: + - feature-type: Vibrate + id: b03a8a9e-13ef-4ed6-820e-cb07d4e3aa30 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 624f9203-ca16-429c-b076-0725a5c04077 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 444d9fc4-23ed-4ea5-a1a5-923680d78af3 + - identifier: + - '10007' + name: Satisfyer Big Heat + id: 67f6a3ba-d167-4d44-ac52-0991dbf1df16 + - identifier: + - '10008' + name: Satisfyer Heated Thrill + features: + - feature-type: Vibrate + id: e5368b0e-00a7-4f20-b338-2a33d65db794 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 4bb68190-ea62-4277-b7f1-3d6f055a939a + - identifier: + - '10009' + name: Satisfyer Hot Bunny + features: + - feature-type: Vibrate + id: cd889856-c5a8-4d7b-9ff6-5f7e49c13b4a + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 5e8eba19-d6cf-4c85-9824-5afd6191c95a + output: + Vibrate: + step-range: + - 0 + - 100 + id: b8219c94-f239-4f12-b3ab-ceeb816bdfb4 + - identifier: + - '10010' + name: Satisfyer Heat Climax + features: + - feature-type: Vibrate + id: 7473ae23-1678-4d6c-bc45-311e126dce65 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 1340347e-7e6a-4c27-a593-7b7a41b09332 + - identifier: + - '10011' + name: Satisfyer Heat Climax+ + features: + - feature-type: Vibrate + id: 715282dc-6919-4a8f-a339-adeb0fa8b4b0 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 1eb40efb-6aa5-4154-a2f4-8cc962cd2682 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 4ffc5fb8-a619-4cbc-8cc9-23104a473ee4 + - identifier: + - '10012' + name: Satisfyer Hot Passion + features: + - feature-type: Vibrate + id: 46c676b0-5dae-4376-b6b3-c3f0b9526260 + output: + Vibrate: + step-range: + - 0 + - 100 + id: a05e4d51-c296-4395-b5ba-1b8801079a15 + - identifier: + - '10013' + name: Satisfyer Haute Couture+ + features: + - feature-type: Vibrate + id: dd995a89-a889-40a8-9a88-aa05b8fe3e60 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: d39282bc-910b-40d2-a8f6-2c729ba5e2f2 + output: + Vibrate: + step-range: + - 0 + - 100 + id: defd08cf-76b3-4957-88ef-5c7fb2a89ff0 + - identifier: + - '10014' + name: Satisfyer High Fashion+ + features: + - feature-type: Vibrate + id: 9b18554d-8f0d-4941-8649-7e34375a0005 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 3fba6850-e170-4bbf-b61c-e105b3ea7762 + output: + Vibrate: + step-range: + - 0 + - 100 + id: d36dda3c-edf3-4ec2-be9a-393934157102 + - identifier: + - '10015' + name: Satisfyer Prêt-à-porter+ + features: + - feature-type: Vibrate + id: cee6ec1f-1f35-48ef-8864-fa76d2ebb8a5 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: c1a929c7-adf1-4cbe-907e-a24e6164e7af + output: + Vibrate: + step-range: + - 0 + - 100 + id: 3925e9e4-fc21-4bad-8ecd-4a8780a5ce83 + - identifier: + - '10024' + - '10025' + name: Satisfyer Love Triangle + features: + - feature-type: Vibrate + id: 9dcbc0b0-b076-4b50-9104-c071d52e39ff + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 5ae0c642-bd10-4f21-8fef-60f94ca755c5 + output: + Vibrate: + step-range: + - 0 + - 100 + id: c771d860-0592-4962-8a05-dc2e7187bff6 + - identifier: + - '10027' + - '10028' + name: Satisfyer Curvy 1+ + features: + - feature-type: Vibrate + id: 95143c24-8928-405c-a6d0-1a64b3830498 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 78533341-96c5-4b21-aede-857ec827c1e6 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 4e47a95f-3a70-4bb4-829f-8b617afaaa1d + - identifier: + - '10030' + - '10031' + name: Satisfyer Curvy 2+ + features: + - feature-type: Vibrate + id: f0bed160-760d-4d18-b462-247e124c537f + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 81b4e5d2-8fd7-4fed-a6cb-d3df12366040 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 7fa5b1e2-c30f-411f-a9b5-9eeee3d95170 + - identifier: + - '10032' + name: Satisfyer Double Wand-er + id: 942818a5-f94f-4efb-b775-693f8b27ab9b + - identifier: + - '10046' + - '10047' + - '10048' + name: Satisfyer Double Joy + features: + - feature-type: Vibrate + id: 0b359281-588c-4aad-bfe1-54d605377120 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 9b9f616a-3219-4424-9ecf-c52520dec964 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 8b5e975e-4215-4b0c-a169-7d6209746d88 + - identifier: + - '10049' + - '10050' + - '10051' + name: Satisfyer Double Fun + features: + - feature-type: Vibrate + id: d6f94a0f-11cd-4242-b05e-e7f237e6b7c0 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 2fe89205-fb8d-4fb7-93d3-d4169f92875d + output: + Vibrate: + step-range: + - 0 + - 100 + id: 05f9af5c-d7b9-43f0-8cf5-41f0c09def28 + - identifier: + - '10052' + - '10053' + - '10054' + name: Satisfyer Double Love + features: + - feature-type: Vibrate + id: eb62f1da-11a0-48b1-8c8e-2c8ea6e24e61 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 16f5a83d-f0fc-41c1-a4d3-43ce13dd3529 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 82270653-6408-43ef-a148-cdfca58a5d2d + - identifier: + - '10055' + name: Satisfyer Curvy 3+ + features: + - feature-type: Vibrate + id: 5d900545-d8cc-4c32-9ff5-e1d8e0c30b90 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 823f51aa-1766-41f4-b48f-f8b2de4c588e + output: + Vibrate: + step-range: + - 0 + - 100 + id: e7c09700-6df1-40c5-b5bb-0203c782dc01 + - identifier: + - '10059' + - '10060' + - '10061' + name: Satisfyer Hot Lover + features: + - feature-type: Vibrate + id: 406de8d0-b6d9-4f5d-b9cd-479092898aac + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 19f2225e-4bc8-4f70-9fb2-734abc8dd5be + output: + Vibrate: + step-range: + - 0 + - 100 + id: 5c90d251-a2fe-461a-a4ae-0e5172a9739d + - identifier: + - '10062' + - '10063' + - '10064' + name: Satisfyer Mono Flex + features: + - feature-type: Vibrate + id: d1bf52af-d49d-42bb-a277-73cc394dce90 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: d1d6a777-21e2-4e6c-9f2e-679d1e75c932 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 44dae430-c6b4-4688-8ab6-9696d82a4b00 + - identifier: + - '10065' + - '10066' + - '10067' + - '10068' + name: Satisfyer Double Flex + features: + - feature-type: Vibrate + id: a824a4f4-11c4-4a84-81d6-424a622d1b06 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 7aa798ab-9bc5-47b4-a318-5349c68ebf93 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 467802b9-6e3b-4810-b659-da69885b7366 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 1715eee4-4aa5-4696-9f41-6e6c299061ec + - identifier: + - '10069' + - '10070' + - '10071' + name: Satisfyer Heat Wave + features: + - feature-type: Vibrate + id: 704fd1ec-a242-4e02-80ab-9db6f2377a7c + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: c6971493-fa87-45d6-b131-67af138f7b13 + output: + Vibrate: + step-range: + - 0 + - 100 + id: ce5ebe09-6d9d-44a1-93e9-f5247c03d3f1 + - identifier: + - '10072' + name: Satisfyer Little Secret + id: b9a13914-c02c-44ac-b9a8-9e95776e3ceb + - identifier: + - '10073' + name: Satisfyer Sexy Secret + id: c62c869a-8d62-4386-a7f9-ec68ccc99513 + - identifier: + - '10074' + name: Satisfyer Strong One + id: 03082593-a2ea-455b-9b94-66c3b1953144 + - identifier: + - '10075' + name: Satisfyer Mighty One + id: e8b06812-88be-4a7d-9581-8ea7210f809a + - identifier: + - '10076' + name: Satisfyer Powerful One + id: d0832c21-c990-4bd8-b06f-32e5768af9d2 + - identifier: + - '10077' + name: Satisfyer Royal One + id: 1f6254b1-301c-4455-9a5e-84886d5e3fce + - identifier: + - '10078' + name: Satisfyer Signet Ring + id: 571d6d2c-351a-4870-9a2a-af16bdc97731 + - identifier: + - '10079' + - '10080' + name: Satisfyer Dual Love + features: + - feature-type: Vibrate + id: 39ca4a7a-c9f3-430a-8248-6001719c6a40 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 07ff65a4-ae65-4054-bd70-419ddac6d241 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 8d5afdb3-47d1-4841-92d6-d3c7b1b2238e + - identifier: + - '10081' + - '10082' + name: Satisfyer Dual Pleasure + features: + - feature-type: Vibrate + id: 18661df2-7eb2-452a-b611-85433bd99ea0 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: c6b1acf6-511e-44bd-ab1c-b2d944a35cf0 + output: + Vibrate: + step-range: + - 0 + - 100 + id: d609d09e-86e5-4544-bda3-16b15b532f2d + - identifier: + - '10090' + name: Satisfyer Hero+ + features: + - feature-type: Vibrate + id: ec61550d-e557-4c57-b6a3-02b28bd5e0d6 + output: + Vibrate: + step-range: + - 0 + - 100 + id: cfcd017c-d3fb-46ab-82d9-55438e96a3d7 + - identifier: + - '10091' + name: Satisfyer Knight+ + features: + - feature-type: Vibrate + id: 5a8dba5a-ca48-4340-8140-fa1fc4d86b73 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 7fb611fe-6af4-4d0e-a6b8-0d4ee72e34af + - identifier: + - '10092' + - '10093' + name: Satisfyer Newcomer+ + features: + - feature-type: Vibrate + id: 31fb6881-d23e-4f07-b233-c6531ccc79b3 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 98dcb92c-84a1-4a1f-88b9-7c61098020de + - identifier: + - '10100' + - '10101' + name: Satisfyer Plug-ilicious 1 + features: + - feature-type: Vibrate + id: fec3511d-2fcd-4463-9ef0-b139c8aa8b0a + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 49020dca-5124-4965-9add-4230dfd0fe28 + output: + Vibrate: + step-range: + - 0 + - 100 + id: c7d1d682-b311-4ce8-b552-d68b8fcde1bc + - identifier: + - '10102' + - '10103' + - '10104' + name: Satisfyer Plug-ilicious 2 + features: + - feature-type: Vibrate + id: 28f3bea8-f927-46a9-ab45-55daf1f76c87 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 540b8330-f039-4870-a6d2-d536f2415cf2 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 22513021-0cb9-4f30-ada7-f7ca6a86e085 + - identifier: + - '10105' + name: Satisfyer E-Love Foreplay + features: + - feature-type: Vibrate + id: 0a939b92-0209-4d2f-b658-0db0ac9a2e6e + output: + Vibrate: + step-range: + - 0 + - 100 + id: 6c07e79d-8842-4e27-88a9-9a471928da5e + - identifier: + - '10108' + name: Satisfyer E-Love G-Hunter + features: + - feature-type: Vibrate + id: e46297ee-6037-44a8-ac06-5f8328d41b19 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 39bfa539-7c58-49a4-87ca-a691a11c16f1 + - identifier: + - '10109' + name: Satisfyer E-Love G-Hunter+ + features: + - feature-type: Vibrate + id: 9248bdf7-d918-4682-b197-59707ac5ea95 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 8d541f70-6595-49b1-b75d-77187f9b75dc + output: + Vibrate: + step-range: + - 0 + - 100 + id: 0b5bcc9b-b5d7-49d3-9c0a-c8dc82214306 + - identifier: + - '10110' + name: Satisfyer E-Love G-Spotter + features: + - feature-type: Vibrate + id: 8f8b7024-005e-4fda-9c65-adf55dc3c470 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 40653fca-c115-4bd4-b3fa-c3875c41a562 + - identifier: + - '10111' + name: Satisfyer E-Love G-Spotter+ + features: + - feature-type: Vibrate + id: 397a61df-a515-49e1-a14d-af2de7855a3f + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 27720871-f08b-4151-96f1-006a5cc137fc + output: + Vibrate: + step-range: + - 0 + - 100 + id: 5a5afa20-0518-420e-a5ab-e5b09c5c9842 + - identifier: + - '10112' + name: Satisfyer E-Love Story + features: + - feature-type: Vibrate + id: 56f7a9fe-d8ef-4a21-b15f-77307a6417ea + output: + Vibrate: + step-range: + - 0 + - 100 + id: 0bfe78b6-a128-4c68-b874-e85ee18273f0 + - identifier: + - '10119' + - '10120' + - '10182' + name: Satisfyer Love Birds 1 + id: c62ea9ae-dc65-429e-90e4-473fa8c5ffaa + - identifier: + - '10121' + - '10122' + - '10123' + name: Satisfyer Love Birds 2 + id: 17b98fe5-4aeb-4c75-b554-701daf147dff + - identifier: + - '10124' + - '10125' + - '10126' + name: Satisfyer Love Birds Vary + id: fde0831c-e1da-46f0-b6fe-8bccfbe9fdae + - identifier: + - '10127' + - '10128' + - '10129' + - '10201' + name: Satisfyer Ribbed Petal + id: 30fb0255-b2e5-424b-bca5-8abdbe864ebf + - identifier: + - '10130' + - '10131' + - '10132' + - '10133' + name: Satisfyer Shiny Petal + id: b10e2742-01b9-4bc8-8caf-b18f0dc51baa + - identifier: + - '10134' + - '10135' + - '10136' + - '10202' + name: Satisfyer Smooth Petal + id: 37096541-c085-4b30-a978-cf1ab8c79198 + - identifier: + - '10140' + name: Satisfyer Men Vibration+ + features: + - feature-type: Vibrate + id: 54c660d2-c326-4272-a1a8-a6ab0a3f5620 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 992e2870-64ed-4704-a74b-2faf3baa0e4b + output: + Vibrate: + step-range: + - 0 + - 100 + id: 518071d2-a6b5-4ee9-9d10-9248fcc72d76 + - identifier: + - '10141' + name: Satisfyer Power Plug + id: fb04247f-1ade-4c3e-816f-1a4c81ae0db4 + - identifier: + - '10142' + - '10143' + name: Satisfyer Rotator Plug 1+ + features: + - feature-type: Vibrate + id: 55ed967f-f37b-47e9-acbd-e091ece4a25a + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 4deb6ffc-7ffb-4892-adb9-ff3829cbf7bb + output: + Vibrate: + step-range: + - 0 + - 100 + id: 16d47710-4849-42b0-aa9b-e7375a533dc5 + - identifier: + - '10144' + - '10145' + name: Satisfyer Rotator Plug 2+ + features: + - feature-type: Vibrate + id: 08a92451-b728-4bf8-bde0-b2af748fc0bd + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: f9b0e791-a348-4485-b1a5-cd90e3503e13 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 7ef01670-5fa3-4bb2-b8b5-3c952f4cf263 + - identifier: + - '10146' + - '10147' + name: Satisfyer Deep Diver + id: 3e04ed12-9d6e-4f7a-9cc8-09e58a9f760e + - identifier: + - '10148' + - '10149' + name: Satisfyer Sweet Seal + id: 99f4d915-7fea-4be1-893e-3ab74488a383 + - identifier: + - '10150' + - '10151' + name: Satisfyer Trendsetter + id: e26a9471-44ab-438a-8290-4793ac6d5ddd + - identifier: + - '10154' + - '10155' + - '10156' + name: Satisfyer Twirling Joy + id: 682c5153-d84c-4a30-b172-42732eaa7081 + - identifier: + - '10157' + - '10158' + name: Satisfyer Ultra Power Bullet 8 + id: b7ed864e-a11d-40de-b3bc-2a28d6ebc2f2 + - identifier: + - '10160' + - '10161' + - '10162' + name: Satisfyer Double Desire + features: + - feature-type: Vibrate + id: c1c09c65-a2d4-4caa-9f56-cec54897758b + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: bc03728b-573a-40d6-ae99-1aa1f508a804 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 17d338a2-dcb1-4170-9a01-ab2250f73b8f + - identifier: + - '10163' + - '10164' + - '10165' + - '10166' + name: Satisfyer Double Lust + features: + - feature-type: Vibrate + id: 9564b21d-c2ba-444e-85c4-dd9dcd80e3b5 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: c70c801e-980a-4052-a275-f8109058a1ad + output: + Vibrate: + step-range: + - 0 + - 100 + id: 4729ddda-fb21-4c3a-9868-b0fcbca18480 + - identifier: + - '10167' + name: Satisfyer Epic Duo + id: b3879662-a471-4bea-ad9a-5d8b59a476a5 + - identifier: + - '10168' + name: Satisfyer Pleasure Wand+ + id: 9404874e-3de2-4696-a620-943f5affb910 + - identifier: + - '10169' + - '10170' + - '10171' + name: Satisfyer Top Secret + features: + - feature-type: Vibrate + id: 9ccf5505-2b55-4386-aa8c-80cb7117f6c2 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 33b12687-c341-47da-81c2-2e2cf9862712 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 98f72ae0-a840-4805-918d-3427541325ca + - identifier: + - '10172' + - '10173' + - '10174' + name: Satisfyer Top Secret+ + features: + - feature-type: Vibrate + id: be9d24ff-8470-481d-aee0-0ea30f0877de + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: ed63da4f-ee14-469c-a47c-12003141716a + output: + Vibrate: + step-range: + - 0 + - 100 + id: 99cfefd9-fd09-40c6-9a2f-3d68385a04bc + - identifier: + - '10175' + - '10176' + name: Satisfyer Bullseye + id: 48bb511e-1cc2-4b1d-9497-022b015287bc + - identifier: + - '10177' + - '10178' + - '10179' + name: Satisfyer Sunray + features: + - feature-type: Vibrate + id: d2786210-46f4-47ce-9f5b-80fa691e0ad2 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: e0dbd014-7415-4d0f-946e-188e239a8154 + output: + Vibrate: + step-range: + - 0 + - 100 + id: f624b4d4-5fe4-4390-9fbb-8ef170b5846c + - identifier: + - '10180' + - '10181' + name: Satisfyer Curvy Trinity 5+ + features: + - feature-type: Vibrate + id: ff20f721-e6fe-4787-964d-327d29b0c391 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: e8322905-46aa-45f8-b7f7-25a88507a55d + output: + Vibrate: + step-range: + - 0 + - 100 + id: 69243058-fb93-4791-b78e-f32f50f902b3 + - identifier: + - '10183' + - '10184' + name: Satisfyer Intensity Plug + id: 20c58cef-83e0-48f2-a352-a3663453403f + - identifier: + - '10185' + name: Satisfyer Power Masturbator + id: baa0ad15-08cc-426c-b1f2-02d9768f6e2c + - identifier: + - '10186' + - '10187' + name: Satisfyer Hug me + features: + - feature-type: Vibrate + id: 4019145b-56cf-473e-a286-4a8d040e80cc + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 7dc4760f-3a7c-4c2e-a7da-e7d8d52b196b + output: + Vibrate: + step-range: + - 0 + - 100 + id: 7d92f936-f672-478a-a26f-616758ff621d + - identifier: + - '10188' + name: Satisfyer Air Pump Bunny 5+ + features: + - feature-type: Vibrate + id: 7abb00ea-bb62-4bef-a26f-a7f7135dec2c + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: c77d5b49-6257-4381-900a-9225caea7124 + output: + Vibrate: + step-range: + - 0 + - 100 + id: d112fbc4-9a5e-4518-b40c-f1200be124cd + - identifier: + - '10189' + name: Satisfyer Air Pump Vibrator 5+ + features: + - feature-type: Vibrate + id: 1acf7f71-e57a-4a1a-81d3-d8bb977d6b72 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 2278b99f-cee5-48fa-9326-8add9730e1e2 + - identifier: + - '10190' + - '10191' + name: Satisfyer Threesome 4 + features: + - feature-type: Vibrate + id: 467accb0-f1f6-4175-afe5-08f48d069fe3 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 4b1b417b-ce44-45fd-be3f-77d939162e18 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 537ce4cb-f8e2-423b-80a5-5bcbb07e6e15 + - identifier: + - '10192' + name: Satisfyer G-Spot Flex 4+ + id: 8ba85779-5b40-48ae-88d5-7744bf852d22 + - identifier: + - '10193' + - '10194' + name: Satisfyer G-Spot Flex 5+ + id: 3844ee0f-94ed-49bf-9a9e-795f407c0ade + - identifier: + - '10195' + name: Satisfyer Air Pump Booty 5+ + features: + - feature-type: Vibrate + id: 12990ee9-76cc-4b48-b711-f70587f14fd7 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 0687264e-3150-4d0a-818b-be6ad231d54c + - identifier: + - '10196' + name: Satisfyer Pro+ Wave 4 + features: + - feature-type: Vibrate + id: c8d73535-d37b-4baa-81c6-c301f32390e0 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 304c7318-bd1b-40ba-a475-90b4d7127c46 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 7c33ff57-e4c7-4110-9814-451062806981 + - identifier: + - '10197' + - '10198' + name: Satisfyer Mini Wand-er+ + features: + - feature-type: Vibrate + id: 3a37453d-605c-4dd4-a83a-28be69ac55b8 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 42dafbc1-0aac-4348-898a-8d467d903191 + output: + Vibrate: + step-range: + - 0 + - 100 + id: a1cb3608-d17c-4c5f-b3d5-4c7ee87d5467 + - identifier: + - '10199' + - '10200' + name: Satisfyer Tropical Tip + id: 7790e568-454e-45f8-85bb-5f8fd855c554 + - identifier: + - '10203' + - '10204' + name: Satisfyer Twirling Pro+ + features: + - feature-type: Vibrate + id: 866a3152-759b-4777-8578-8abaff6aea9a + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 5a7b0180-16b1-41e7-a016-af4a761564de + output: + Vibrate: + step-range: + - 0 + - 100 + id: 1bc5cd0a-feb7-4cfc-9155-09c7565d85e0 + - identifier: + - '10205' + name: Satisfyer Perfect Pair 4 + id: 7c2560dc-06d4-4da6-874a-5f6c2c05810d + - identifier: + - '10206' + - '10207' + - '10208' + name: Satisfyer Booty Absolute Beginners 5 + id: 0a682803-b5ad-457a-bbf0-40e48b71cbcf + - identifier: + - '10241' + - '10242' + name: Satisfyer Rrrolling Sensation + features: + - feature-type: Vibrate + id: fdb9014d-b7b9-4b28-8804-cdf26b432df1 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 6665fc3b-a8e6-4a36-ad11-46f449abfc90 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 2a429cdd-20f9-4a22-82a9-dd79234e23de + - identifier: + - '10307' + - '10308' + - '10309' + name: Satisfyer Pro 2 Gen 3 + features: + - feature-type: Vibrate + id: f14fc3ea-05f0-426a-ac01-70cdbadb43ec + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 1a3c8f91-c172-4378-9fe2-64891a06e8d1 + output: + Vibrate: + step-range: + - 0 + - 100 + id: b0578f68-2b0b-497a-b49a-2e897d3a040a +communication: + - btle: + names: + - SF * + manufacturer-data: + - company: 93 + data: + - 0 + - 0 + - 39 + - company: 93 + data: + - 0 + - 0 + - 40 + services: + 0000180a-0000-1000-8000-00805f9b34fb: + rxblemodel: 00002a24-0000-1000-8000-00805f9b34fb + 51361500-c5e7-47c7-8a6e-47ebc99d80e8: + command: 51361501-c5e7-47c7-8a6e-47ebc99d80e8 + tx: 51361502-c5e7-47c7-8a6e-47ebc99d80e8 diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/sayberx.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/sayberx.yml new file mode 100644 index 000000000..19db19adc --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/sayberx.yml @@ -0,0 +1,30 @@ +defaults: + name: SayberX Device + features: [] + id: 9635a829-753b-4e5b-825c-24249526af09 +configurations: + - identifier: + - SayberX + name: SayberX + features: + - feature-type: Vibrate + id: a62d0356-a05f-475c-8a5f-fcfec1327b2a + output: + Vibrate: + step-range: + - 0 + - 4 + id: 22716d89-5e28-462b-9723-60528fb7373e + - identifier: + - X-Ring + name: Sayber X-Ring + id: e77a2f7b-8556-48b8-8245-30c2c80681e7 +communication: + - btle: + names: + - SayberX + - X-Ring * + services: + 0000fff0-0000-1000-8000-00805f9b34fb: + tx: 0000fff6-0000-1000-8000-00805f9b34fb + rx: 0000fff8-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/sensee-v2.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/sensee-v2.yml new file mode 100644 index 000000000..80f3ba9bb --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/sensee-v2.yml @@ -0,0 +1,156 @@ +defaults: + name: Sensee Device + features: + - feature-type: Vibrate + id: b5865307-0de8-4dd9-bb1a-69e1c2f3c39c + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Constrict + id: cd11ed14-d9ea-4c11-b454-41e5c697f70b + output: + Constrict: + step-range: + - 0 + - 100 + id: d7ba651e-88d6-4452-9fa5-1562b8d8be2a +configurations: + - identifier: + - CCPA10S2 + name: Sensee Capsule + id: 4629e2a0-553f-4178-a378-8a9a5e88b038 + - identifier: + - CCPA18S5 + name: Sensee Astronaut + id: e9be0c9a-43d9-4e95-9d1d-67e22f940a5f + - identifier: + - Easylive NO8 Cup + name: Sensee No8 + features: + - feature-type: Vibrate + id: 1094606e-1407-4249-979c-98d6a6abf97c + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Oscillate + id: 542d9822-9617-472c-953b-c9519a59aaac + output: + Oscillate: + step-range: + - 0 + - 100 + id: 72dcac71-472d-47bc-a408-60567765836c + - identifier: + - CCP322S5 + name: Easylive Vader + features: + - feature-type: Vibrate + id: 4a6f2a58-1760-42e6-ae17-6e0c4880a48c + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Oscillate + id: aeab494e-3312-49bd-8f1f-599e3bab7f4d + output: + Oscillate: + step-range: + - 0 + - 100 + id: b925cadb-6aef-4896-8b97-1dfa44702a9e + - identifier: + - CTY508S5 + name: Sensee Voice-Interactive Female Vibrator + features: + - feature-type: Vibrate + id: c9600c27-1302-449c-9a07-268d59f818f3 + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Oscillate + id: 377780e3-e3bd-4fe0-a345-6389eb32fbbe + output: + Oscillate: + step-range: + - 0 + - 100 + id: fea99f9b-97da-44cf-a898-17e65abf86e3 + - identifier: + - PTYB22S2 + name: Sensee Moonlight + features: + - feature-type: Vibrate + id: 5c8664fd-1113-4d8b-af64-d42f6f303c3e + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Constrict + id: 848628c7-b34e-4af4-894f-7f51645dea6a + output: + Constrict: + step-range: + - 0 + - 100 + id: eca4db2b-f7ff-4d59-b73d-f2124786fceb + - identifier: + - CTY823S5 + name: Sensee Little Seahorse + features: + - feature-type: Vibrate + id: 87712e50-fd72-4a3c-b122-ea3866e0942a + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: Constrict + id: 2a7ce324-34dd-477c-b3e2-6a6632ee4b59 + output: + Constrict: + step-range: + - 0 + - 100 + id: 4e2ffbbe-8f8f-4593-9eab-3409d85645a2 + - identifier: + - CTY916S4 + name: Sensee Dream Stick + features: + - feature-type: Oscillate + id: 631815ee-37e9-4de6-9b33-971b9135c718 + output: + Oscillate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: 864ef211-1635-41bc-9618-e3989f540287 + output: + Vibrate: + step-range: + - 0 + - 100 + id: f8032396-8384-448f-88e9-4c754d4ae12e +communication: + - btle: + names: + - CCPA10S2 + - CCPA18S5 + - Easylive NO8 Cup + - CTY508S5 + - CTY916S4 + - PTYB22S2 + - CCP322S5 + - CTY823S5 + services: + 0000fff0-0000-1000-8000-00805f9b34fb: + tx: 0000fff5-0000-1000-8000-00805f9b34fb + rx: 0000fff4-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/sensee.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/sensee.yml new file mode 100644 index 000000000..be5201351 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/sensee.yml @@ -0,0 +1,18 @@ +defaults: + name: Sensee Diandou Rabbit + features: + - feature-type: Vibrate + id: 1544b066-a3d3-4749-9081-1b7a26ab54ed + output: + Vibrate: + step-range: + - 0 + - 100 + id: a8ffccf6-2d38-4606-abdd-8802a063a2ae +communication: + - btle: + names: + - CTY222S4 + services: + 0000fff0-0000-1000-8000-00805f9b34fb: + tx: 0000fff5-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/serveu.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/serveu.yml new file mode 100644 index 000000000..e61b45717 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/serveu.yml @@ -0,0 +1,18 @@ +defaults: + name: ServeU + features: + - feature-type: PositionWithDuration + id: 7e756a59-b13c-4322-bc59-27dacfc73b4d + output: + PositionWithDuration: + step-range: + - 0 + - 100 + id: 9967414e-8b34-44ed-8b8a-20fe863e0b50 +communication: + - btle: + names: + - ServeU + services: + 31bb1111-33e3-4f3c-a7fb-104288e7cb77: + tx: 31bb2222-33e3-4f3c-a7fb-104288e7cb77 diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/sexverse-lg389.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/sexverse-lg389.yml new file mode 100644 index 000000000..8fd968a72 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/sexverse-lg389.yml @@ -0,0 +1,26 @@ +defaults: + name: Sexverse LG389 + features: + - feature-type: Vibrate + id: 54ae0f52-dbd7-4fac-8463-f06199b72642 + output: + Vibrate: + step-range: + - 0 + - 3 + - feature-type: Oscillate + id: 394cb2f4-9ee5-4fe9-a31c-fd6652479467 + output: + Oscillate: + step-range: + - 0 + - 10 + id: dd6e5fe8-f53c-4b5c-9614-cedfffc0a40f +communication: + - btle: + names: + - LG389 + services: + 0000bae0-0000-1000-8000-00805f9b34fb: + tx: 0000bae1-0000-1000-8000-00805f9b34fb + rx: 0000bae2-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-alex-v2.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-alex-v2.yml new file mode 100644 index 000000000..1eabf52d6 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-alex-v2.yml @@ -0,0 +1,19 @@ +defaults: + name: Svakom Alex Neo 2 + features: + - feature-type: Vibrate + id: 807083a6-aca2-499d-84c0-fe1e8884f222 + output: + Vibrate: + step-range: + - 0 + - 3 + id: 632c2055-3c47-439d-8fcc-e3ee0b0288e5 +communication: + - btle: + names: + - Alex NEO 2 + - S63E Alex NEO 2 + services: + 0000ffe0-0000-1000-8000-00805f9b34fb: + tx: 0000ffe1-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-alex.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-alex.yml new file mode 100644 index 000000000..4cdb0dd0f --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-alex.yml @@ -0,0 +1,19 @@ +defaults: + name: Svakom Alex Neo + features: + - feature-type: Vibrate + id: 323f02f5-f1ab-40b9-ba8b-eba65de178c3 + output: + Vibrate: + step-range: + - 0 + - 3 + id: 39ee59bc-fdc5-47c4-8da6-2c208e30a7b6 +communication: + - btle: + names: + - Alex NEO + - S63E Alex NEO + services: + 0000ffe0-0000-1000-8000-00805f9b34fb: + tx: 0000ffe1-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-avaneo.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-avaneo.yml new file mode 100644 index 000000000..bd77ce9fb --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-avaneo.yml @@ -0,0 +1,26 @@ +defaults: + name: Svakom Ava Neo + features: + - feature-type: Vibrate + id: 9dbdf85e-6692-4a95-b8a1-da350327a9a3 + output: + Vibrate: + step-range: + - 0 + - 10 + - feature-type: Oscillate + id: 878fb1f8-8c38-4058-bd0f-859584d14cef + output: + Oscillate: + step-range: + - 0 + - 1 + id: 8254195f-4c38-425d-b5e6-352ad644399a +communication: + - btle: + names: + - Ava Neo + services: + 0000ffe0-0000-1000-8000-00805f9b34fb: + tx: 0000ffe1-0000-1000-8000-00805f9b34fb + rx: 0000ffe2-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-barnard.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-barnard.yml new file mode 100644 index 000000000..196b0e347 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-barnard.yml @@ -0,0 +1,25 @@ +defaults: + name: Fantasy Cup Barnard + features: + - feature-type: Vibrate + id: 7abda591-db6f-492c-a781-5f90d648b561 + output: + Vibrate: + step-range: + - 0 + - 3 + - feature-type: Oscillate + id: 5ec8c88b-bd24-4e94-bec1-467735a74b80 + output: + Oscillate: + step-range: + - 0 + - 3 + id: aaebe699-02dd-461f-879d-c71da8c2d892 +communication: + - btle: + names: + - DG239A + services: + 0000ffe0-0000-1000-8000-00805f9b34fb: + tx: 0000ffe1-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-barney.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-barney.yml new file mode 100644 index 000000000..f8166aeda --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-barney.yml @@ -0,0 +1,26 @@ +defaults: + name: Mutufun Barney + features: + - feature-type: Vibrate + id: ebbd9a68-1b05-4a21-8f3d-14b3dc7f1f70 + output: + Vibrate: + step-range: + - 0 + - 10 + - feature-type: Vibrate + id: be5e2510-9b63-4813-9192-2db123b82ac5 + output: + Vibrate: + step-range: + - 0 + - 10 + id: 1b3759c0-ee3b-4f5f-9b3a-3d6bc0cc9594 +communication: + - btle: + names: + - DJ333A + services: + 0000ffe0-0000-1000-8000-00805f9b34fb: + tx: 0000ffe1-0000-1000-8000-00805f9b34fb + rx: 0000ffe2-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-dice.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-dice.yml new file mode 100644 index 000000000..a96403ae7 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-dice.yml @@ -0,0 +1,19 @@ +defaults: + name: Zemalia Dice for Love + features: + - feature-type: Vibrate + id: 60b702d6-d3ff-4554-a3ae-f4638ddc74ef + output: + Vibrate: + step-range: + - 0 + - 255 + id: 5845f3f5-6943-41df-93df-04b3b1ce7ce2 +communication: + - btle: + names: + - ZhiAi + services: + 0000ffe0-0000-1000-8000-00805f9b34fb: + tx: 0000ffe1-0000-1000-8000-00805f9b34fb + rx: 0000ffe2-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-dt250a.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-dt250a.yml new file mode 100644 index 000000000..f3eddbcc5 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-dt250a.yml @@ -0,0 +1,33 @@ +defaults: + name: Coleur Dor DT250A + features: + - feature-type: Vibrate + id: 608e34f1-69eb-4469-95e2-c56fb26d7db6 + output: + Vibrate: + step-range: + - 0 + - 3 + - feature-type: Vibrate + id: 75e9695f-7049-4ad7-a8db-a85f62868266 + output: + Vibrate: + step-range: + - 0 + - 3 + - feature-type: Constrict + id: 5fd9d9a0-4f7c-4ef4-87d5-5081f41499f3 + output: + Constrict: + step-range: + - 0 + - 2 + id: 7897a4fc-e45a-4f23-b04f-91415b3eeef7 +communication: + - btle: + names: + - DT250A + services: + 0000ffe0-0000-1000-8000-00805f9b34fb: + tx: 0000ffe1-0000-1000-8000-00805f9b34fb + rx: 0000ffe2-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-iker.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-iker.yml new file mode 100644 index 000000000..6b28b01b9 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-iker.yml @@ -0,0 +1,40 @@ +defaults: + name: Svakom Iker + features: + - feature-type: Vibrate + id: 36af2b39-85ec-4463-9ecd-59fbaff3ba38 + output: + Vibrate: + step-range: + - 0 + - 10 + - feature-type: Vibrate + id: 74e5fb53-383a-4938-81ff-cb84da773882 + output: + Vibrate: + step-range: + - 0 + - 5 + id: 1db55a7c-6133-4b33-bd54-e7fa8dead165 +communication: + - btle: + names: + - Iker + manufacturer-data: + - company: 39 + data: + - 83 + - 86 + - 65 + - 1 + - 11 + - 18 + - 1 + - 51 + - 68 + - 85 + - 202 + - 8 + services: + 0000ffe0-0000-1000-8000-00805f9b34fb: + tx: 0000ffe1-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-jordan.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-jordan.yml new file mode 100644 index 000000000..fbd8ac049 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-jordan.yml @@ -0,0 +1,26 @@ +defaults: + name: Svakom Jordan + features: + - feature-type: Vibrate + id: f59261c4-39a7-4e13-b7e8-52c0a117ea7f + output: + Vibrate: + step-range: + - 0 + - 10 + - feature-type: Oscillate + id: 84200741-7440-4267-b9a1-519eebe884ed + output: + Oscillate: + step-range: + - 0 + - 5 + id: 89877d1d-9a8f-4265-93d7-7dbe4c093a58 +communication: + - btle: + names: + - Jordan + services: + 0000ffe0-0000-1000-8000-00805f9b34fb: + tx: 0000ffe1-0000-1000-8000-00805f9b34fb + rx: 0000ffe2-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-pulse.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-pulse.yml new file mode 100644 index 000000000..67b7dfee5 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-pulse.yml @@ -0,0 +1,59 @@ +defaults: + name: Svakom Pulse Device + features: + - feature-type: Vibrate + id: 0ee3c15e-b05d-4c97-bb4a-523a5475c520 + output: + Vibrate: + step-range: + - 0 + - 9 + id: 91a8f7f5-d774-4beb-ad76-9864b3a46597 +configurations: + - identifier: + - SWK-SX013A + name: Svakom Pulse Lite Neo + id: 5b9918c8-af63-409f-9749-f5e6faf2dca0 + - identifier: + - Pulse Union + name: Svakom Pulse Union + id: f40b1405-cf40-43c5-a568-24e3d2d70c65 + - identifier: + - Pulse Galaxie + name: Svakom Pulse Galaxie + id: cd29302f-31f9-4c9f-aa12-ab381f941e82 + - identifier: + - SX033APP + name: Svakom Mimiki + id: ccb6ce6f-5dc7-4ce4-bd31-3e8f3af14a4b + - identifier: + - BX288A + name: BeYourLover Kyukyu + id: ea05be83-2991-4cb5-8ad0-b108e0a52a5a + - identifier: + - QH-SX045A-B + name: Coleur Dor VX045A + id: 8abdd83e-af93-4f82-b240-d9eeed81e976 + - identifier: + - SWK-SX067-B + name: Momonii Agatha + id: db486014-b4da-4cad-90f4-2ba53a36e335 + - identifier: + - QH-HX029A-B + name: Coleur Dor HX029A + id: b9851f7f-ddc8-4df5-ad81-3071ec9daab1 +communication: + - btle: + names: + - SWK-SX013A + - Pulse Union + - Pulse Galaxie + - SX033APP + - BX288A + - QH-SX045A-B + - SWK-SX067-B + - QH-HX029A-B + services: + 0000ffe0-0000-1000-8000-00805f9b34fb: + tx: 0000ffe1-0000-1000-8000-00805f9b34fb + rx: 0000ffe2-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-sam.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-sam.yml new file mode 100644 index 000000000..6328d35a9 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-sam.yml @@ -0,0 +1,29 @@ +defaults: + name: Svakom Sam Neo + features: + - feature-type: Vibrate + id: 260f221c-b861-4ee2-bd0f-17a0dd9a14ba + output: + Vibrate: + step-range: + - 0 + - 10 + - feature-type: Vibrate + id: cfdf5760-bce0-465c-a2c6-60c86fdd3c95 + output: + Vibrate: + step-range: + - 0 + - 1 + id: d5fac59d-8e57-43a6-bcc9-61d06f6b8587 +communication: + - btle: + names: + - Sam Neo + services: + 0000ae00-0000-1000-8000-00805f9b34fb: + tx: 0000ae01-0000-1000-8000-00805f9b34fb + rx: 0000ae02-0000-1000-8000-00805f9b34fb + txmode: 0000ae10-0000-1000-8000-00805f9b34fb + 0000ffac-0000-1000-8000-00805f9b34fb: + firmware: 0000ffb4-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-sam2.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-sam2.yml new file mode 100644 index 000000000..8d7201801 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-sam2.yml @@ -0,0 +1,36 @@ +defaults: + name: Svakom Sam Neo 2 + features: + - feature-type: Vibrate + id: 9f584905-3bcb-4a60-9a56-2c2d69c81a8c + output: + Vibrate: + step-range: + - 0 + - 10 + - feature-type: Constrict + id: 7580e615-c22c-4242-b599-9b4041bfa400 + output: + Constrict: + step-range: + - 0 + - 5 + id: 88c0807b-7b34-4f4b-ad95-2e9e31f4f291 +configurations: + - identifier: + - Sam Neo 2 + name: Svakom Sam Neo 2 + id: f32b4e50-ec7e-4b76-8f29-4b4777da7c22 + - identifier: + - Sam Neo 2 Pro + name: Svakom Sam Neo 2 Pro + id: 869e4518-1565-4b3b-8d15-45c860c848c2 +communication: + - btle: + names: + - Sam Neo 2 + - Sam Neo 2 Pro + services: + 0000ffe0-0000-1000-8000-00805f9b34fb: + tx: 0000ffe1-0000-1000-8000-00805f9b34fb + rx: 0000ffe2-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-suitcase.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-suitcase.yml new file mode 100644 index 000000000..feab31201 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-suitcase.yml @@ -0,0 +1,32 @@ +defaults: + name: Svakom Magic Suitcase + features: + - feature-type: Vibrate + id: 34836d30-2d4f-4c89-ab42-88dd227f14f0 + output: + Vibrate: + step-range: + - 0 + - 30 + - feature-type: Vibrate + id: 190fc9a8-8d55-45c5-98e0-921246ccbb7d + output: + Vibrate: + step-range: + - 0 + - 1 + id: ffefddb3-5697-4ff1-a064-5d33c6f9b214 +configurations: + - identifier: + - VX236A-BLE-V1.0 + name: Coleur Dor VX236A + id: e3187cb5-6370-4d29-8850-2d9206889f64 +communication: + - btle: + names: + - VX357A-BLE-V1.0 + - VX236A-BLE-V1.0 + services: + 0000ffe0-0000-1000-8000-00805f9b34fb: + tx: 0000ffe1-0000-1000-8000-00805f9b34fb + rx: 0000ffe2-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-tarax.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-tarax.yml new file mode 100644 index 000000000..9d513c9b2 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-tarax.yml @@ -0,0 +1,28 @@ +defaults: + name: ToyCod Tara X + features: + - feature-type: Vibrate + description: Internal vibrator + id: 8638eed8-37ec-4c54-aa06-a8dd3a832057 + output: + Vibrate: + step-range: + - 0 + - 3 + - feature-type: Vibrate + description: External pulsator + id: a2ad09c0-0042-4f29-875f-464fb83ca916 + output: + Vibrate: + step-range: + - 0 + - 3 + id: 870f69ff-45db-4a13-96e7-1915eef6ac59 +communication: + - btle: + names: + - SX218A + services: + 0000ffe0-0000-1000-8000-00805f9b34fb: + tx: 0000ffe1-0000-1000-8000-00805f9b34fb + rx: 0000ffe2-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-v1.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-v1.yml new file mode 100644 index 000000000..87a5fa565 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-v1.yml @@ -0,0 +1,35 @@ +defaults: + name: Svakom Device + features: + - feature-type: Vibrate + id: 22eb4b95-60f9-4885-80e7-279d02d59804 + output: + Vibrate: + step-range: + - 0 + - 19 + id: 77a1dde5-f31a-4fcb-972b-8094181c187f +configurations: + - identifier: + - Aogu SCB + name: Svakom Ella + id: 46a3fb4f-5e26-45c0-9fd1-176ec896048c + - identifier: + - Phoenix NEO + name: Svakom Phoenix Neo + id: c9556aba-5bda-4f23-a690-623c4b9ee04b + - identifier: + - Emma NEO + name: Svakom Emma Neo + id: 68d39a06-e350-47ef-8834-e3197178b00e +communication: + - btle: + names: + - Aogu SUV + - Aogu SCB + - Emma NEO + - Phoenix NEO + services: + 0000ffe0-0000-1000-8000-00805f9b34fb: + tx: 0000ffe1-0000-1000-8000-00805f9b34fb + rx: 0000ffe2-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-v2.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-v2.yml new file mode 100644 index 000000000..327417a3a --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-v2.yml @@ -0,0 +1,78 @@ +defaults: + name: Svakom Device v2 + features: + - feature-type: Vibrate + id: 4a225b9d-94c6-437a-a038-3deb4ded5bc5 + output: + Vibrate: + step-range: + - 0 + - 10 + id: b1189537-2ef1-452b-b6b8-e8e0ba823156 +configurations: + - identifier: + - '116' + name: Svakom Phoenix Neo + id: 11905923-4084-4efb-9ac3-a6eba2bf4190 + - identifier: + - Viviana + name: Svakom Viviana + id: 4bbda06f-ca32-4d34-a11f-d91d8987dc6d + - identifier: + - Ella NEO + name: Svakom Ella Neo + id: 87419e85-5570-41f0-84f2-7f15b138326d + - identifier: + - '117' + - Edeny + name: Svakom Edeny + id: 448ee908-2abc-46cb-aa3f-732830a25139 + - identifier: + - S38A + name: Svakom Tammy Pro + id: b2c3e1ed-0c66-49d7-859d-7c9677c66297 + - identifier: + - Vick NEO + - Vick Neo + name: Svakom Vick Neo + id: c37b8380-dd41-4fd1-8310-8c24230658bf + - identifier: + - STG05A + name: Svakom Aravinda + id: 63893174-b1fd-4ad3-940f-fbbb939ffa57 + - identifier: + - '118' + name: ToyCod Vanesia + id: a61ae863-a8fc-4708-b313-b36385926dbf + - identifier: + - QH-SJ007A + name: Svakom Winni 2 + id: f0609171-5e85-4800-adee-a43ef2e3826a + - identifier: + - Cici 2 + name: Svakom Cici 2 + id: 5c03568c-9318-4648-b149-b0fc716d5605 + - identifier: + - Emma Neo 2 + name: Svakom Emma Neo 2 + id: a3c23c99-09e7-47d4-898b-9581dfc1f28b +communication: + - btle: + names: + - '116' + - '117' + - Edeny + - '118' + - Viviana + - Ella NEO + - S38A + - Vick NEO + - Vick Neo + - STG05A + - QH-SJ007A + - Cici 2 + - Emma Neo 2 + services: + 0000ffe0-0000-1000-8000-00805f9b34fb: + tx: 0000ffe1-0000-1000-8000-00805f9b34fb + rx: 0000ffe2-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-v3.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-v3.yml new file mode 100644 index 000000000..6303c6d0a --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-v3.yml @@ -0,0 +1,71 @@ +defaults: + name: Svakom Device v3 + features: + - feature-type: Vibrate + id: 1e03f6a5-0197-4a5e-afb5-dcc1266c6a6e + output: + Vibrate: + step-range: + - 0 + - 10 + id: 58212e06-d13e-461d-a8cd-5bd06cbe5d0c +configurations: + - identifier: + - Phoenix Neo 2 + name: Svakom Phoenix Neo 2 + id: 14a51507-e4c8-4433-a87b-0a0464c00e31 + - identifier: + - FK008A + name: Fantasy Cup Theodore + features: + - feature-type: Vibrate + id: 737fe419-62fa-4e1b-b6d0-2684cbe8b31f + output: + Vibrate: + step-range: + - 0 + - 10 + - feature-type: Rotate + id: 5e612940-1d00-4680-aa3a-1b052755a01d + output: + Rotate: + step-range: + - 0 + - 1 + id: cdd17d02-603a-4a86-af6b-f2c97d09ed84 + - identifier: + - Hannes NEO + name: Svakom Hannes Neo + id: d2fda3c5-fa1f-45b5-8f98-a9c33e83922d + - identifier: + - QH-SX007E + name: Svakom Alberta + features: + - feature-type: Vibrate + description: Vibrating attachments + id: 1859c6fa-1d2f-46c8-b97c-75a7ca62be8c + output: + Vibrate: + step-range: + - 0 + - 10 + - feature-type: Vibrate + description: Suction lens + id: 63b84610-b32b-4526-a29a-4acb9ad4939d + output: + Vibrate: + step-range: + - 0 + - 1 + id: 4e1d7b1c-133d-4d7c-9cfe-f4c4e5d0ca01 +communication: + - btle: + names: + - Phoenix Neo 2 + - FK008A + - Hannes NEO + - QH-SX007E + services: + 0000ffe0-0000-1000-8000-00805f9b34fb: + tx: 0000ffe1-0000-1000-8000-00805f9b34fb + rx: 0000ffe2-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-v4.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-v4.yml new file mode 100644 index 000000000..e772a057b --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-v4.yml @@ -0,0 +1,54 @@ +defaults: + name: Svakom Device v4 + features: + - feature-type: Vibrate + id: b61f8bde-2ad3-40a8-8e16-fe6dcec8a887 + output: + Vibrate: + step-range: + - 0 + - 10 + - feature-type: Vibrate + id: 724c247f-733e-4592-9a98-1a37a7c941ba + output: + Vibrate: + step-range: + - 0 + - 10 + id: 1a43cd07-e5ba-4a9f-8560-d00e1d72c6df +configurations: + - identifier: + - B2CM6 + name: ToyCod Barzillai + id: 2e46e18b-5821-4665-9b07-928f4963f16d + - identifier: + - ERICA + name: Svakom Erica + id: 22c2f70c-44fa-482f-bfac-1463482bff5d + - identifier: + - Cici+ 2 + name: Svakom Cici+ 2 + id: 96980e8b-abcf-410e-94e6-d098b13e6192 + - identifier: + - VV468A + name: ToyCod Clara + features: + - feature-type: Vibrate + id: 65f4d628-cb50-48fa-8d51-39433244ce12 + output: + Vibrate: + step-range: + - 0 + - 10 + id: 8d88601b-6791-4947-94ca-e9d66086f57e +communication: + - btle: + names: + - B2CM6 + - ERICA + - Cici+ 2 + - VV468A + services: + 0000ffe0-0000-1000-8000-00805f9b34fb: + tx: 0000ffe1-0000-1000-8000-00805f9b34fb + rx: 0000ffe2-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-v5.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-v5.yml new file mode 100644 index 000000000..f58ad3ce5 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-v5.yml @@ -0,0 +1,98 @@ +defaults: + name: Svakom Device v5 + features: + - feature-type: Vibrate + id: 4f672189-8169-4114-92cd-ed7f74427548 + output: + Vibrate: + step-range: + - 0 + - 10 + - feature-type: Vibrate + id: bdd5e445-0d53-47c9-9b9e-c60b83d821fd + output: + Vibrate: + step-range: + - 0 + - 10 + id: 9b304bb1-b961-4948-937e-4e3ee1b429b0 +configurations: + - identifier: + - Chika + name: Svakom Chika + id: 4ca8c463-03fc-421d-ab03-27ed6f4283da + - identifier: + - Mora Neo + name: Svakom Mora Neo + features: + - feature-type: Vibrate + id: 7d13d266-a8f3-49b5-94d2-ac6242c40b7a + output: + Vibrate: + step-range: + - 0 + - 10 + - feature-type: Vibrate + id: 3b4e80ae-3ec6-4bb7-aba9-1dc48dd1614b + output: + Vibrate: + step-range: + - 0 + - 10 + - feature-type: Oscillate + id: 41ecfb09-8b4c-4ec1-9f7a-29b9ff1097f7 + output: + Oscillate: + step-range: + - 0 + - 3 + id: b647f340-bcd1-4d9e-88ac-e064ce86b1ac + - identifier: + - Trysta Neo + name: Svakom Trysta Neo + features: + - feature-type: Vibrate + id: 655ec2b3-ede8-4051-96da-c40eed164372 + output: + Vibrate: + step-range: + - 0 + - 10 + - feature-type: Vibrate + id: 4cc06c03-36d9-4b10-9d51-46417b0d7f3d + output: + Vibrate: + step-range: + - 0 + - 10 + - feature-type: Oscillate + id: f62fea13-0dfb-4706-8122-9104abf9dca5 + output: + Oscillate: + step-range: + - 0 + - 3 + id: 66d5aa90-b2aa-4552-9777-cbb80aae2b9f + - identifier: + - Mini Emma Neo + name: Svakom Mini Emma Neo + features: + - feature-type: Vibrate + id: d957a257-9ae2-45f1-80b2-dbcc4dc2886b + output: + Vibrate: + step-range: + - 0 + - 10 + id: 396d37c3-dc1e-473d-85ca-95bd9583d9f5 +communication: + - btle: + names: + - Chika + - Mora Neo + - Trysta Neo + - Mini Emma Neo + services: + 0000ffe0-0000-1000-8000-00805f9b34fb: + tx: 0000ffe1-0000-1000-8000-00805f9b34fb + rx: 0000ffe2-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-v6.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-v6.yml new file mode 100644 index 000000000..3c437865c --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/svakom-v6.yml @@ -0,0 +1,94 @@ +defaults: + name: Svakom Device v6 + features: + - feature-type: Vibrate + id: 5f1d84f8-a44a-43dc-b6f6-8e8682909ff1 + output: + Vibrate: + step-range: + - 0 + - 10 + id: eafe3786-e15a-4a4d-9b85-bc6e4069c339 +configurations: + - identifier: + - CocoPro + name: Svakom Coco Pro + id: 4901a610-9b63-47a1-a99a-521ac76e7f99 + - identifier: + - Echo 2 + name: Svakom Echo 2 + id: 2613c099-f89f-4936-a26b-e751c8b3be28 + - identifier: + - VA617A-3 + name: BeYourLover Naughty Clock Vibrator + id: 23bba8d3-9a07-42fc-8606-b165837adbcb + - identifier: + - Vick Neo 2 + name: Svakom Vick Neo 2 + features: + - feature-type: Vibrate + id: 5ac07e29-37f4-4a7a-8a35-f5b2b59f3dbd + output: + Vibrate: + step-range: + - 0 + - 10 + - feature-type: Vibrate + id: 263e051e-ed79-4245-b222-2d4888483849 + output: + Vibrate: + step-range: + - 0 + - 10 + id: f46a3f0e-9d3e-4e5f-9343-bbc0acc8a095 + - identifier: + - Iker Neo + name: Svakom Iker Neo + features: + - feature-type: Vibrate + id: c19b776a-363d-4468-80ec-09bc22ebd06c + output: + Vibrate: + step-range: + - 0 + - 10 + - feature-type: Vibrate + id: cbdd56a3-1954-4db0-98c7-535096637868 + output: + Vibrate: + step-range: + - 0 + - 10 + - feature-type: Vibrate + id: b310a28e-0109-4573-bf4a-259845c518fd + output: + Vibrate: + step-range: + - 0 + - 5 + id: 2c295a1b-8a26-47dc-9d9c-95961e1cca1b + - identifier: + - VA617A-4 + name: BeYourLover Naughty Clock Sucker + features: + - feature-type: Constrict + id: 38708bd1-466e-48e7-8721-8844aa177959 + output: + Vibrate: + step-range: + - 0 + - 10 + id: 1e587721-7e91-44b2-9612-f9cfd88389fc +communication: + - btle: + names: + - CocoPro + - Echo 2 + - Vick Neo 2 + - Iker Neo + - VA617A-3 + - VA617A-4 + services: + 0000ffe0-0000-1000-8000-00805f9b34fb: + tx: 0000ffe1-0000-1000-8000-00805f9b34fb + rx: 0000ffe2-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/synchro.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/synchro.yml new file mode 100644 index 000000000..8ee6be84a --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/synchro.yml @@ -0,0 +1,25 @@ +defaults: + name: Synchro + features: + - feature-type: RotateWithDirection + id: b7495351-9101-448a-94c4-4598cf541dca + output: + RotateWithDirection: + step-range: + - 0 + - 6 + id: f912a283-7308-4e56-a508-4d47d9caf7d2 +configurations: + - identifier: + - synchro EX + name: Synchro Exchange + id: 3535446e-779a-496b-8404-e895878cf3e1 +communication: + - btle: + names: + - Shinkuro + - synchro2 + - synchro EX + services: + 0000ffe0-0000-1000-8000-00805f9b34fb: + tx: 0000ffe1-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/tcode-v03.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/tcode-v03.yml new file mode 100644 index 000000000..66fe2be5c --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/tcode-v03.yml @@ -0,0 +1,21 @@ +defaults: + name: TCode v0.3 (Single Linear Axis) + features: + - feature-type: PositionWithDuration + id: a6e25b9d-4986-4771-8e8c-579ebb472844 + output: + PositionWithDuration: + step-range: + - 0 + - 100 + id: 211da02e-467c-4788-96bd-689049867e85 +communication: + - serial: + port: default + baud-rate: 115200 + data-bits: 8 + parity: 'N' + stop-bits: 1 + - udp: + address: default + port: 8000 diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/thehandy.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/thehandy.yml new file mode 100644 index 000000000..38c901253 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/thehandy.yml @@ -0,0 +1,19 @@ +defaults: + name: The Handy + features: + - feature-type: PositionWithDuration + id: 32309a60-f980-490d-a5f4-467ccae2d586 + output: + PositionWithDuration: + step-range: + - 0 + - 100 + id: fc9de0ed-0f9f-402e-a1b5-4d1865e7b87b +communication: + - btle: + names: + - The Handy + services: + 1775244d-6b43-439b-877c-060f2d9bed07: + firmware: 1775ff51-6b43-439b-877c-060f2d9bed07 + tx: 1775ff55-6b43-439b-877c-060f2d9bed07 diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/tryfun-blackhole.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/tryfun-blackhole.yml new file mode 100644 index 000000000..cb202273b --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/tryfun-blackhole.yml @@ -0,0 +1,25 @@ +defaults: + name: TryFun Black Hole Plus + features: + - feature-type: Oscillate + id: 3bf4453c-8ca3-42e5-82c6-409d85cdbacf + output: + Oscillate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: e10533e6-9aac-4a71-99c1-0b44378d9f06 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 074de6cc-7aee-4b33-8d14-474a61d26548 +communication: + - btle: + names: + - TF-BHPLUS + services: + 0000ffac-0000-1000-8000-00805f9b34fb: + tx: 0000ffb7-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/tryfun-meta2.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/tryfun-meta2.yml new file mode 100644 index 000000000..acd4364d2 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/tryfun-meta2.yml @@ -0,0 +1,32 @@ +defaults: + name: TryFun Meta 2 + features: + - feature-type: Oscillate + id: 0773790b-b629-46b7-af2a-174d75c53fe3 + output: + Oscillate: + step-range: + - 0 + - 100 + - feature-type: Vibrate + id: bf8f3a67-3403-4d57-90e3-027804c57c4e + output: + Vibrate: + step-range: + - 0 + - 100 + - feature-type: RotateWithDirection + id: 26402ebe-7ee0-4c7d-ae40-205ec4f3a1b0 + output: + RotateWithDirection: + step-range: + - 0 + - 100 + id: 6b45e5f8-5b23-4c1d-a478-43c17a54cae3 +communication: + - btle: + names: + - TF-META2 + services: + 0000ffac-0000-1000-8000-00805f9b34fb: + tx: 0000ffb7-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/tryfun.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/tryfun.yml new file mode 100644 index 000000000..d327c365d --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/tryfun.yml @@ -0,0 +1,41 @@ +defaults: + name: TryFun Yuan Series + features: + - feature-type: Oscillate + id: e4957d32-e069-4c35-ae3f-e3cce3de6b49 + output: + Oscillate: + step-range: + - 0 + - 9 + - feature-type: Rotate + id: 0346e667-8ea2-4cde-80d4-88d498d1ee17 + output: + Rotate: + step-range: + - 0 + - 9 + id: 9b4afa16-a7cf-4fdb-bb95-5f91125ba7e1 +configurations: + - identifier: + - TF-SPRAY + name: TryFun Surge Pro + features: + - feature-type: Vibrate + id: b9d4420b-9a94-4ea2-8b76-3445d06049f2 + output: + Vibrate: + step-range: + - 0 + - 4 + id: 2cf375ae-7ae9-4d76-be3b-58eff84b67ae +communication: + - btle: + names: + - TRYFUN-ONE + - TF-SPRAY + services: + 0000ff10-0000-1000-8000-00805f9b34fb: + tx: 0000fff1-0000-1000-8000-00805f9b34fb + 0000ffac-0000-1000-8000-00805f9b34fb: + tx: 0000ffb5-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/twerkingbutt.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/twerkingbutt.yml new file mode 100644 index 000000000..69840782e --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/twerkingbutt.yml @@ -0,0 +1,14 @@ +defaults: + name: Twerking Butt + features: [] + id: 83e29d7a-6f35-499a-90f8-dfba8b674379 +communication: + - btle: + names: + - BODIKANG + - Twerking Butt + - TwerkingButt + services: + 00000a60-0000-1000-8000-00805f9b34fb: + tx: 00000a66-0000-1000-8000-00805f9b34fb + rx: 00000a67-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/vibcrafter.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/vibcrafter.yml new file mode 100644 index 000000000..c9956a152 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/vibcrafter.yml @@ -0,0 +1,54 @@ +defaults: + name: VibCrafter Device + features: + - feature-type: Vibrate + id: 343a8e18-b76c-4482-b048-32d762bf87c9 + output: + Vibrate: + step-range: + - 0 + - 99 + - feature-type: Vibrate + id: d92a031e-bd0d-4815-a0bd-6c59566dcce2 + output: + Vibrate: + step-range: + - 0 + - 99 + id: a44eef0e-b412-44d0-9545-a4b7b0298514 +configurations: + - identifier: + - be gentle + name: VibCrafter Harlow + id: 687972b8-e52d-4ce8-8b16-b6d24585915b + - identifier: + - Hayden + name: VibCrafter Hayden + id: 4006a4fd-2a7a-417e-b64a-66f43ba28b9e + - identifier: + - Nidalee + name: VibCrafter Nidalee + id: 3e1e3e00-771b-4657-8450-6e314eed24b3 + - identifier: + - Janna + name: VibCrafter Janna + features: + - feature-type: Vibrate + id: 51e20287-006c-4dc9-941a-346b8f960715 + output: + Vibrate: + step-range: + - 0 + - 99 + id: cb0756c3-111c-463b-a575-edc9204af528 +communication: + - btle: + names: + - be gentle + - Janna + - Hayden + - Nidalee + services: + 53300051-0060-4bd4-bbe5-a6920e4c5663: + tx: 53300052-0060-4bd4-bbe5-a6920e4c5663 + rx: 53300053-0060-4bd4-bbe5-a6920e4c5663 diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/vibratissimo.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/vibratissimo.yml new file mode 100644 index 000000000..2136758b0 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/vibratissimo.yml @@ -0,0 +1,102 @@ +defaults: + name: Vibratissimo Device + features: + - feature-type: Vibrate + id: c4978273-df69-41b1-8ecd-0b5cdbb6d102 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Battery + description: Battery Level + id: e0d0a8e6-604a-4d49-bdab-d22fd8658c69 + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 4b82b175-c139-4af2-b5ad-aa576d9d01a4 +configurations: + - identifier: + - Licker + - SecretKiss + - Womenizer + name: Vibratissimo Licker + features: + - feature-type: Vibrate + id: 75aa2f87-0d7b-4df1-a661-dd270e92fdd8 + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: 56fbae53-c57e-4eed-978c-dcf3279b228b + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Battery + description: Battery Level + id: 0f194120-0912-4d5d-b201-7eee4cc622fe + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: c0f02f4f-5bbb-40ad-94fc-7d81c74c518c + - identifier: + - Rabbit + name: Vibratissimo Rabbit + features: + - feature-type: Vibrate + id: 675d6ccc-8145-40d2-a901-0b683cf8233b + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: c0009e3f-4263-4761-9168-17c9d81479ee + output: + Vibrate: + step-range: + - 0 + - 255 + - feature-type: Vibrate + id: 16b15667-1598-4194-86b3-7e711f88adab + output: + Vibrate: + step-range: + - 0 + - 2 + - feature-type: Battery + description: Battery Level + id: e70bb6fb-9e2c-4970-9483-9f9b661d6e9f + input: + Battery: + value-range: + - - 0 + - 100 + input-commands: + - Read + id: 2fa1c5bc-85ff-45d5-ada5-23986ad3eab9 +communication: + - btle: + names: + - Vibratissimo + services: + 00001523-1212-efde-1523-785feabcd123: + txmode: 00001524-1212-efde-1523-785feabcd123 + txvibrate: 00001526-1212-efde-1523-785feabcd123 + rx: 00001527-1212-efde-1523-785feabcd123 + 0000180a-0000-1000-8000-00805f9b34fb: + rxblemodel: 00002a24-0000-1000-8000-00805f9b34fb + 0000180f-0000-1000-8000-00805f9b34fb: + rxblebattery: 00002a19-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/vorze-cyclone-x.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/vorze-cyclone-x.yml new file mode 100644 index 000000000..237fd0e67 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/vorze-cyclone-x.yml @@ -0,0 +1,16 @@ +defaults: + name: Vorze Cyclone X10 Device + features: + - feature-type: RotateWithDirection + id: 1d1b4dea-ab29-4426-a9f4-dda2c594eefb + output: + RotateWithDirection: + step-range: + - 0 + - 10 + id: ac27ce47-6d49-4c43-ac6f-01a19e546305 +communication: + - hid: + pairs: + - vendor-id: 1155 + product-id: 22352 diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/vorze-sa.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/vorze-sa.yml new file mode 100644 index 000000000..eba129bb0 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/vorze-sa.yml @@ -0,0 +1,102 @@ +defaults: + name: Vorze Device + features: [] + id: 3ed42429-379c-4f48-926e-f297cbe69258 +configurations: + - identifier: + - Bach smart + protocol-variant: vorze-sa-vibrator + name: Vorze Bach + features: + - feature-type: Vibrate + id: 447dbcfa-c295-4880-afba-93e24499a78d + output: + Vibrate: + step-range: + - 0 + - 100 + id: 2923a929-572c-472a-be12-ff5970f0b2b7 + - identifier: + - ROCKET + name: Adult Festa Rocket + protocol-variant: vorze-sa-vibrator + features: + - feature-type: Vibrate + id: 557d3c89-2e15-4b4a-8480-07f4826a8384 + output: + Vibrate: + step-range: + - 0 + - 100 + id: 756f590f-d2aa-4a4c-ac80-e4ac75a14f15 + - identifier: + - CycSA + name: Vorze A10 Cyclone SA + protocol-variant: vorze-sa-single-rotator + features: + - feature-type: RotateWithDirection + id: 8e249d53-8d80-4f42-bc40-e6edb7779e92 + output: + RotateWithDirection: + step-range: + - 0 + - 99 + id: 390a0e30-0b5f-4b6c-88b4-e4f16383b8a3 + - identifier: + - UFOSA + name: Vorze UFO SA + protocol-variant: vorze-sa-single-rotator + features: + - feature-type: RotateWithDirection + id: 2d8d1443-c394-4df4-b9bb-1659d8323b45 + output: + RotateWithDirection: + step-range: + - 0 + - 99 + id: 2ab3b09b-1020-4dcf-86f1-ecd9d5b40ce2 + - identifier: + - UFO-TW + name: Vorze UFO TW + protocol-variant: vorze-sa-dual-rotator + features: + - feature-type: RotateWithDirection + id: a1632ce4-314f-481d-9ae2-2a11a0c4caa4 + output: + RotateWithDirection: + step-range: + - 0 + - 99 + - feature-type: RotateWithDirection + id: 4b09a02d-9a4a-4c8b-8340-8e6ca3cecfc2 + output: + RotateWithDirection: + step-range: + - 0 + - 99 + id: 32e92986-3ae4-45f3-9aec-05d6028f1cb7 + - identifier: + - VorzePiston + protocol-variant: vorze-sa-piston + name: Vorze Piston + features: + - feature-type: PositionWithDuration + id: 7c8d7a1d-9e2f-4a92-83f3-42a0840b90bd + output: + PositionWithDuration: + step-range: + - 0 + - 99 + id: b1b17b07-c5b8-4db4-97c4-ef1597cf2e59 +communication: + - btle: + names: + - Bach smart + - CycSA + - UFOSA + - UFO-TW + - VorzePiston + - ROCKET + services: + 40ee1111-63ec-4b7f-8ce7-712efd55b90e: + tx: 40ee2222-63ec-4b7f-8ce7-712efd55b90e \ No newline at end of file diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/wetoy.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/wetoy.yml new file mode 100644 index 000000000..ee21c649f --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/wetoy.yml @@ -0,0 +1,18 @@ +defaults: + name: WeToy MiNa + features: + - feature-type: Vibrate + id: 693b0fbc-eee5-4948-b8f4-aa264a78bcc2 + output: + Vibrate: + step-range: + - 0 + - 3 + id: 1c7420e2-1af5-4b1c-8247-6a3702eb2335 +communication: + - btle: + names: + - WeToy + services: + 0000fff0-0000-1000-8000-00805f9b34fb: + tx: 0000fff3-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/wevibe-8bit.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/wevibe-8bit.yml new file mode 100644 index 000000000..2056a835f --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/wevibe-8bit.yml @@ -0,0 +1,127 @@ +defaults: + name: WeVibe 8-bit Device + features: + - feature-type: Vibrate + id: 7b226142-d713-41cd-872a-aea10527482b + output: + Vibrate: + step-range: + - 0 + - 12 + id: 527527b1-7bf2-40cb-b086-003af792f03f +configurations: + - identifier: + - Melt + name: WeVibe Melt + features: + - feature-type: Vibrate + id: fdf47cba-4429-4944-9bb4-1db4facb8d29 + output: + Vibrate: + step-range: + - 0 + - 22 + id: 4f73e55c-bea8-4069-8409-cba30fbbfc81 + - identifier: + - Moxie + name: WeVibe Moxie + id: d29641cb-953a-4d5c-8b43-ba481db2dd42 + - identifier: + - Vector + name: WeVibe Vector + features: + - feature-type: Vibrate + id: 8828bbe0-acf0-4529-9f33-276b23a14afd + output: + Vibrate: + step-range: + - 0 + - 12 + - feature-type: Vibrate + id: 12702494-a0e9-4929-b928-050d47391cb5 + output: + Vibrate: + step-range: + - 0 + - 12 + id: 52482637-708c-455b-b96b-d4d58af04562 + - identifier: + - Wand + name: WeVibe Wand + features: + - feature-type: Vibrate + id: 2377d39d-580c-46ea-831c-bb9cb97899d7 + output: + Vibrate: + step-range: + - 0 + - 22 + id: 3829ad7c-be90-49ce-9ecc-fdafa18be3bb + - identifier: + - Wand 2 + name: WeVibe Wand 2 + features: + - feature-type: Vibrate + id: 4d92cf70-e464-435c-897e-fd2cd5a918e9 + output: + Vibrate: + step-range: + - 0 + - 22 + id: 3db74c3e-50e1-4dbf-a670-c7297ca52f62 + - identifier: + - Bond + - Nelson + name: WeVibe Bond + features: + - feature-type: Vibrate + id: 240a36e0-4791-4676-aa3b-d1c407db2b1b + output: + Vibrate: + step-range: + - 0 + - 27 + id: c4b2ecb2-655d-44d9-bfaf-03f314acd3a2 + - identifier: + - Nova2 + - Nova_2 + - Nova 2 + name: WeVibe Nova 2 + features: + - feature-type: Vibrate + id: 22172834-1186-4ba2-b221-23f02c3fbd51 + output: + Vibrate: + step-range: + - 0 + - 27 + - feature-type: Vibrate + id: 0972ba1f-0b0e-4738-a050-5333da537b35 + output: + Vibrate: + step-range: + - 0 + - 27 + id: 2292e221-0f17-4d55-8697-f6abebf04ee5 + - identifier: + - Jive 2 + name: WeVibe Jive 2 + id: 4a0d8ff9-db32-41c7-99e5-8bb005a25bd0 +communication: + - btle: + names: + - Melt + - Moxie + - Vector + - Wand + - Wand 2 + - Bond + - Nelson + - Nova2 + - Nova_2 + - Nova 2 + - Jive 2 + services: + f000bb03-0451-4000-b000-000000000000: + tx: f000c000-0451-4000-b000-000000000000 + rx: f000b000-0451-4000-b000-000000000000 diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/wevibe-chorus.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/wevibe-chorus.yml new file mode 100644 index 000000000..97c7f6fb5 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/wevibe-chorus.yml @@ -0,0 +1,61 @@ +defaults: + name: WeVibe Chorus + features: + - feature-type: Vibrate + id: 52a3c84e-28d4-4750-9a7e-a8618ded617e + output: + Vibrate: + step-range: + - 0 + - 30 + - feature-type: Vibrate + id: 4aa54a5f-2b85-4178-b671-f4198acf3daf + output: + Vibrate: + step-range: + - 0 + - 30 + id: 5228aefe-bc48-445c-8129-48c3cebf6729 +configurations: + - identifier: + - Sync 2 + name: WeVibe Sync 2 + features: + - feature-type: Vibrate + id: db4d008b-530e-4b8b-937a-bd4e5df4058c + output: + Vibrate: + step-range: + - 0 + - 30 + - feature-type: Vibrate + id: 27c95f7a-91e7-46c9-90c2-b3d37ed20d6d + output: + Vibrate: + step-range: + - 0 + - 30 + id: 3d5f001f-d3c0-44d5-9a6a-e4c8e7beb2e1 + - identifier: + - Sync Lite + name: WeVibe Sync Lite + features: + - feature-type: Vibrate + id: 62316419-7c01-4ce2-8086-0ca210d26b25 + output: + Vibrate: + step-range: + - 0 + - 30 + id: 36640498-e77c-46f5-9f94-a1b90148f939 +communication: + - btle: + names: + - Chorus + - skeena + - Sync 2 + - Sync Lite + services: + f000bb03-0451-4000-b000-000000000000: + tx: f000c000-0451-4000-b000-000000000000 + rx: f000b000-0451-4000-b000-000000000000 diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/wevibe.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/wevibe.yml new file mode 100644 index 000000000..6d73dbd56 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/wevibe.yml @@ -0,0 +1,144 @@ +defaults: + name: WeVibe Device + features: + - feature-type: Vibrate + id: 6c0184bc-93b8-41a9-a976-934256dcdf9d + output: + Vibrate: + step-range: + - 0 + - 15 + id: d42dc8a1-bb70-4dd6-b792-710248c00c6e +configurations: + - identifier: + - Bloom + name: WeVibe Bloom + id: cb8bf4cd-b6bd-4499-b977-faf4e2bb9d4e + - identifier: + - Ditto + name: WeVibe Ditto + id: 0b9e22e7-b79c-4d26-b902-287436673da4 + - identifier: + - Jive + name: WeVibe Jive + id: 0d361883-2894-42dd-9268-b36a067564a6 + - identifier: + - Pivot + name: WeVibe Pivot + id: 5fca5cd6-6336-4eec-bdfc-048266d9f409 + - identifier: + - Rave + name: WeVibe Rave + id: 534f442f-396c-4379-b3d0-9c001bcd2891 + - identifier: + - Verge + name: WeVibe Verge + id: 6b31404c-c609-4d75-a312-191c0f7f6a9f + - identifier: + - Wish + name: WeVibe Wish + id: a7a85b12-bac4-49da-9d1e-0f5bc739fd3e + - identifier: + - Cougar + - 4 Plus + - 4_Plus + - 4plus + - classic + - Classic + name: WeVibe 4 Plus + features: + - feature-type: Vibrate + id: c76fd58e-a38c-4f25-a04c-d798e3f892d3 + output: + Vibrate: + step-range: + - 0 + - 15 + - feature-type: Vibrate + id: 027061c3-4d18-4d03-8219-13e3134b8a19 + output: + Vibrate: + step-range: + - 0 + - 15 + id: 11cd7b68-2c94-4fc8-837f-09d47214cee1 + - identifier: + - Gala + name: WeVibe Gala + features: + - feature-type: Vibrate + id: 22386dcd-b409-49d2-be03-ad270eae92c4 + output: + Vibrate: + step-range: + - 0 + - 15 + - feature-type: Vibrate + id: 46f2d671-5bbf-49c0-928e-4a8b3cdd892b + output: + Vibrate: + step-range: + - 0 + - 15 + id: 400ef30a-63eb-4648-b293-c7ecc874f509 + - identifier: + - Nova + name: WeVibe Nova + features: + - feature-type: Vibrate + id: e609247a-8c12-422e-8df7-e03373bdbf7a + output: + Vibrate: + step-range: + - 0 + - 15 + - feature-type: Vibrate + id: c84081f5-3a72-473a-b2b3-32500014b308 + output: + Vibrate: + step-range: + - 0 + - 15 + id: b667bb6a-46b1-4534-8c79-83aa0749028a + - identifier: + - Sync + name: WeVibe Sync + features: + - feature-type: Vibrate + id: 283b2826-80e3-455f-bec6-7800ebaf2c96 + output: + Vibrate: + step-range: + - 0 + - 15 + - feature-type: Vibrate + id: 64f00297-e4ef-4059-a622-c0bea33d4379 + output: + Vibrate: + step-range: + - 0 + - 15 + id: 0e72dab3-4b87-4bae-ae02-aae0bbb0f035 +communication: + - btle: + names: + - Cougar + - 4 Plus + - 4_Plus + - 4plus + - Bloom + - classic + - Classic + - Ditto + - Gala + - Jive + - Nova + - Pivot + - Rave + - Sync + - Verge + - Wish + services: + f000bb03-0451-4000-b000-000000000000: + tx: f000c000-0451-4000-b000-000000000000 + rx: f000b000-0451-4000-b000-000000000000 diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/xibao.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/xibao.yml new file mode 100644 index 000000000..4f453574e --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/xibao.yml @@ -0,0 +1,18 @@ +defaults: + name: Xibao Smart Masturbation Cup + features: + - feature-type: Oscillate + id: c91a5d82-547c-4bcb-8cd9-1a5085253d11 + output: + Oscillate: + step-range: + - 0 + - 99 + id: 3a3dd2ec-01d9-48d2-afbf-a969c33a147c +communication: + - btle: + names: + - CCYB_* + services: + 0000fff0-0000-1000-8000-00805f9b34fb: + tx: 0000fff2-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/xinput.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/xinput.yml new file mode 100644 index 000000000..ea304b596 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/xinput.yml @@ -0,0 +1,21 @@ +defaults: + name: XBox (XInput) Compatible Gamepad + features: + - feature-type: Vibrate + id: eded54a0-9ef2-49e1-99ec-7ab0ae606604 + output: + Vibrate: + step-range: + - 0 + - 65535 + - feature-type: Vibrate + id: 13b25ae7-4c84-4e9c-bd3e-c2f835bd3edb + output: + Vibrate: + step-range: + - 0 + - 65535 + id: 0e7844fb-ff3d-4f5d-9e86-03b20f120f94 +communication: + - xinput: + exists: true diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/xiuxiuda.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/xiuxiuda.yml new file mode 100644 index 000000000..f50d2e0a2 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/xiuxiuda.yml @@ -0,0 +1,18 @@ +defaults: + name: Xiuxiuda Device + features: + - feature-type: Vibrate + id: da1eb27b-6159-40f8-9662-69d9ca77f768 + output: + Vibrate: + step-range: + - 0 + - 19 + id: 2982ea67-a59f-4490-9a7c-23583a4ec642 +communication: + - btle: + names: + - XXD-Lush* + services: + 53300001-0023-4bd4-bbd5-a6920e4c5653: + tx: 53300003-0023-4bd4-bbd5-a6920e4c5653 diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/xuanhuan.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/xuanhuan.yml new file mode 100644 index 000000000..900df3c79 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/xuanhuan.yml @@ -0,0 +1,18 @@ +defaults: + name: Xuanhuan Masturbator + features: + - feature-type: Vibrate + id: b52a4a37-3eae-40da-a4c2-abe546934900 + output: + Vibrate: + step-range: + - 0 + - 10 + id: 60b567f2-8b50-4673-a295-6dda343a7029 +communication: + - btle: + names: + - QUXIN + services: + 0000fffe-0000-1000-8000-00805f9b34fb: + tx: 0000fe02-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/youcups.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/youcups.yml new file mode 100644 index 000000000..a28cb9e6b --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/youcups.yml @@ -0,0 +1,18 @@ +defaults: + name: Youcups Warrior II + features: + - feature-type: Vibrate + id: d0c286dc-2608-4f8a-a621-3f65927ed57e + output: + Vibrate: + step-range: + - 0 + - 8 + id: f73311e4-69d4-43d7-9781-1294e9d5bf0d +communication: + - btle: + names: + - Youcups + services: + 0000fee9-0000-1000-8000-00805f9b34fb: + tx: d44bc439-abfd-45a2-b575-925416129600 diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/youou.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/youou.yml new file mode 100644 index 000000000..fa95743ba --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/youou.yml @@ -0,0 +1,18 @@ +defaults: + name: Youou Wand Vibrator + features: + - feature-type: Vibrate + id: 19dc8b35-713c-448b-926f-4d56b14f432d + output: + Vibrate: + step-range: + - 0 + - 255 + id: 6b113fe0-9d26-4dd3-a997-527eb8a048b0 +communication: + - btle: + names: + - VX001_* + services: + 0000fff0-0000-1000-8000-00805f9b34fb: + tx: 0000fff6-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/protocols/zalo.yml b/crates/buttplug_server_device_config/device-config-v4/protocols/zalo.yml new file mode 100644 index 000000000..3632e7c6e --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/protocols/zalo.yml @@ -0,0 +1,63 @@ +defaults: + name: Zalo Device + features: + - feature-type: Vibrate + id: e6f5930a-98ee-4ced-9a51-b3938b7b6a0c + output: + Vibrate: + step-range: + - 0 + - 8 + id: 45648a20-cb18-43a0-9d6c-8bc4ed63ef63 +configurations: + - identifier: + - ZALO-Queen + name: Zalo Queen + features: + - feature-type: Vibrate + id: 94357c17-fb2d-4579-a4fa-68d597315887 + output: + Vibrate: + step-range: + - 0 + - 8 + - feature-type: Vibrate + id: 43f2e203-f920-4c59-b7a8-d8902d7efa2f + output: + Vibrate: + step-range: + - 0 + - 8 + id: 2aaeca64-1ce5-4333-a0ab-609546112d37 + - identifier: + - ZALO-King + name: Zalo King + features: + - feature-type: Vibrate + id: 3e1cb89e-43bd-4b57-9f49-79dbb297ce14 + output: + Vibrate: + step-range: + - 0 + - 8 + - feature-type: Vibrate + id: ba694b89-b88e-4029-934f-95d23df42053 + output: + Vibrate: + step-range: + - 0 + - 8 + id: 94254e7a-2666-4e93-8f6d-101fad4a3807 + - identifier: + - ZALO-Jeanne + name: Zalo Jeanne + id: 743b389e-1eb6-401a-80bc-116b6136c449 +communication: + - btle: + names: + - ZALO-Queen + - ZALO-King + - ZALO-Jeanne + services: + 0000fff0-0000-1000-8000-00805f9b34fb: + tx: 0000fff1-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config-v4/version.yaml b/crates/buttplug_server_device_config/device-config-v4/version.yaml new file mode 100644 index 000000000..51a0cd264 --- /dev/null +++ b/crates/buttplug_server_device_config/device-config-v4/version.yaml @@ -0,0 +1,3 @@ +version: + major: 4 + minor: 59 diff --git a/buttplug/src/util/device_configuration.rs b/crates/buttplug_server_device_config/src/device_configuration.rs similarity index 73% rename from buttplug/src/util/device_configuration.rs rename to crates/buttplug_server_device_config/src/device_configuration.rs index 3717c3c3a..bc39d3d4d 100644 --- a/buttplug/src/util/device_configuration.rs +++ b/crates/buttplug_server_device_config/src/device_configuration.rs @@ -5,31 +5,33 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use super::json::JSONValidator; -use crate::{ - core::{ - errors::{ButtplugDeviceError, ButtplugError}, - message::DeviceFeature, - }, - server::device::configuration::{ - BaseDeviceDefinition, - BaseDeviceIdentifier, - DeviceConfigurationManager, - DeviceConfigurationManagerBuilder, - ProtocolCommunicationSpecifier, - UserDeviceDefinition, - UserDeviceIdentifier, - }, +use super::{ + BaseDeviceDefinition, + BaseDeviceIdentifier, + DeviceConfigurationManager, + DeviceConfigurationManagerBuilder, + DeviceSettings, + ProtocolCommunicationSpecifier, + ServerBaseDeviceFeature, + UserDeviceCustomization, + UserDeviceDefinition, + UserDeviceIdentifier, +}; +use buttplug_core::{ + errors::{ButtplugDeviceError, ButtplugError}, + message::OutputType, + util::json::JSONValidator, }; use dashmap::DashMap; use getset::{CopyGetters, Getters, MutGetters, Setters}; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, fmt::Display}; +use std::{collections::HashMap, fmt::Display, ops::RangeInclusive}; +use uuid::Uuid; pub static DEVICE_CONFIGURATION_JSON: &str = - include_str!("../../buttplug-device-config/build-config/buttplug-device-config-v3.json"); + include_str!("../build-config/buttplug-device-config-v4.json"); static DEVICE_CONFIGURATION_JSON_SCHEMA: &str = include_str!( - "../../buttplug-device-config/device-config-v3/buttplug-device-config-schema-v3.json" + "../device-config-v4/buttplug-device-config-schema-v4.json" ); /// The top level configuration for a protocol. Contains all data about devices that can use the @@ -67,8 +69,13 @@ struct ProtocolAttributes { #[serde(skip_serializing_if = "Option::is_none")] identifier: Option>, name: String, + id: Uuid, + #[serde(skip_serializing_if = "Option::is_none", rename = "protocol-variant")] + protocol_variant: Option, + #[serde(skip_serializing_if = "Option::is_none")] + features: Option>, #[serde(skip_serializing_if = "Option::is_none")] - features: Option>, + device_settings: Option, } #[derive(Deserialize, Serialize, Debug, Clone, Default, Getters, Setters, MutGetters)] @@ -82,6 +89,35 @@ struct ProtocolDefinition { pub configurations: Vec, } +#[derive(Deserialize, Serialize, Debug, Clone, Getters, Setters, MutGetters)] +#[getset(get = "pub", set = "pub", get_mut = "pub(crate)")] +struct UserFeatureOutputCustomization { + step_limit: RangeInclusive, + reverse_position: bool, +} + +#[derive(Deserialize, Serialize, Debug, Clone, Getters, Setters, MutGetters)] +#[getset(get = "pub", set = "pub", get_mut = "pub(crate)")] +struct UserFeatureCustomization { + id: Uuid, + #[serde(rename = "base-id")] + base_id: Uuid, + output: HashMap, +} + +#[derive(Deserialize, Serialize, Debug, Clone, Getters, Setters, MutGetters)] +#[getset(get = "pub", set = "pub", get_mut = "pub(crate)")] +struct SerializedUserDeviceDefinition { + id: Uuid, + #[serde(rename = "base-id")] + base_id: Uuid, + /// Message attributes for this device instance. + features: Vec, + /// Per-user configurations specific to this device instance. + #[serde(rename = "user-config")] + user_config: UserDeviceCustomization, +} + #[derive(Deserialize, Serialize, Debug, Clone, Getters, Setters, MutGetters)] #[getset(get = "pub", set = "pub", get_mut = "pub(crate)")] struct UserDeviceConfigPair { @@ -105,31 +141,36 @@ impl From for ProtocolDeviceConfiguration { if let Some(defaults) = protocol_def.defaults() { let config_attrs = BaseDeviceDefinition::new( &defaults.name, + &defaults.id, + &defaults.protocol_variant, defaults .features .as_ref() .expect("This is a default, therefore we'll always have features."), + &defaults.device_settings, ); configurations.insert(None, config_attrs); - for config in &protocol_def.configurations { - if let Some(identifiers) = &config.identifier { - for identifier in identifiers { - let config_attrs = BaseDeviceDefinition::new( - // Even subconfigurations always have names - &config.name, - config + } + for config in &protocol_def.configurations { + if let Some(identifiers) = &config.identifier { + for identifier in identifiers { + let config_attrs = BaseDeviceDefinition::new( + // Even subconfigurations always have names + &config.name, + &config.id, + &config.protocol_variant, + config.features.as_ref().unwrap_or( + protocol_def + .defaults() + .as_ref() + .unwrap_or(&ProtocolAttributes::default()) .features .as_ref() - .or(Some( - defaults - .features - .as_ref() - .expect("Defaults always have features"), - )) - .unwrap(), - ); - configurations.insert(Some(identifier.to_owned()), config_attrs); - } + .unwrap_or(&vec![]), + ), + &config.device_settings, + ); + configurations.insert(Some(identifier.to_owned()), config_attrs); } } } @@ -263,13 +304,11 @@ where } } Err(err) => Err(ButtplugDeviceError::DeviceConfigurationError(format!( - "{}", - err + "{err}" ))), }, Err(err) => Err(ButtplugDeviceError::DeviceConfigurationError(format!( - "{}", - err + "{err}" ))), } } @@ -285,7 +324,7 @@ fn load_main_config( } // Start by loading the main config let main_config = load_protocol_config_from_json::( - &main_config_str + main_config_str .as_ref() .unwrap_or(&DEVICE_CONFIGURATION_JSON.to_owned()), skip_version_check, @@ -336,7 +375,7 @@ fn load_user_config( ) -> Result<(), ButtplugDeviceError> { info!("Loading user configuration from string."); let user_config_file = - load_protocol_config_from_json::(&user_config_str, skip_version_check)?; + load_protocol_config_from_json::(user_config_str, skip_version_check)?; if user_config_file.user_configs.is_none() { info!("No user configurations provided in user config."); @@ -347,12 +386,33 @@ fn load_user_config( .user_configs .expect("Just checked validity"); - for (protocol, specifier) in user_config.protocols.unwrap_or_default() { - if let Some(comm_specifiers) = specifier.communication() { - dcm_builder.user_communication_specifier(&protocol, comm_specifiers); + let mut protocol_specifiers = HashMap::new(); + let mut protocol_features = HashMap::new(); + + for (protocol_name, protocol_def) in user_config.protocols.unwrap_or_default() { + if let Some(comm_specifiers) = protocol_def.communication() { + dcm_builder.user_communication_specifier(&protocol_name, comm_specifiers); + } + info!("{:?}", protocol_def); + info!("Adding {}", protocol_name); + let protocol_device_config: ProtocolDeviceConfiguration = protocol_def.into(); + protocol_specifiers.insert( + protocol_name.clone(), + protocol_device_config.specifiers().clone(), + ); + info!("{:?}", protocol_device_config); + for (config_ident, config) in protocol_device_config.configurations() { + info!("Adding {:?}", config_ident); + let ident = BaseDeviceIdentifier::new(&protocol_name, config_ident); + protocol_features.insert(ident, config.clone()); } } + for (ident, features) in protocol_features { + info!("Adding {}", features.id()); + dcm_builder.protocol_features(&ident, &features); + } + for user_device_config_pair in user_config.user_device_configs.unwrap_or_default() { dcm_builder.user_protocol_features( user_device_config_pair.identifier(), @@ -386,7 +446,7 @@ pub fn save_user_config(dcm: &DeviceConfigurationManager) -> Result Result, +} + +impl DeviceSettings { + pub fn is_none(&self) -> bool { + self.message_gap_ms.is_none() + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default, CopyGetters)] +pub struct BaseFeatureSettings { + #[serde( + rename = "alt-protocol-index", + skip_serializing_if = "Option::is_none", + default + )] + #[getset(get_copy = "pub")] + alt_protocol_index: Option, +} + +impl BaseFeatureSettings { + pub fn is_none(&self) -> bool { + self.alt_protocol_index.is_none() + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct UserFeatureSettings { + #[serde( + rename = "reverse-position", + skip_serializing_if = "Option::is_none", + default + )] + reverse_position: Option, +} + +impl UserFeatureSettings { + pub fn is_none(&self) -> bool { + self.reverse_position.is_none() + } +} + +#[derive(Debug, Clone, Getters, CopyGetters)] +pub struct BaseDeviceDefinition { + #[getset(get = "pub")] + /// Given name of the device this instance represents. + name: String, + #[getset(get = "pub")] + /// Message attributes for this device instance. + features: Vec, + #[getset(get_copy = "pub")] + id: Uuid, + #[getset(get = "pub")] + protocol_variant: Option, + #[getset(get = "pub")] + device_settings: DeviceSettings, +} + +impl BaseDeviceDefinition { + /// Create a new instance + pub fn new( + name: &str, + id: &Uuid, + protocol_variant: &Option, + features: &[ServerBaseDeviceFeature], + device_settings: &Option, + ) -> Self { + Self { + name: name.to_owned(), + features: features.into(), + id: *id, + protocol_variant: protocol_variant.clone(), + device_settings: device_settings.clone().unwrap_or_default(), + } + } +} + +#[derive(Serialize, Deserialize, Debug, Getters, CopyGetters, Default, Clone, MutGetters)] +pub struct UserDeviceCustomization { + #[serde( + rename = "display-name", + default, + skip_serializing_if = "Option::is_none" + )] + #[getset(get = "pub")] + display_name: Option, + #[serde(default)] + #[getset(get_copy = "pub")] + allow: bool, + #[serde(default)] + #[getset(get_copy = "pub")] + deny: bool, + #[getset(get_copy = "pub", get_mut = "pub")] + index: u32, + #[getset(get_copy = "pub")] + #[serde( + rename = "message-gap-ms", + default, + skip_serializing_if = "Option::is_none" + )] + message_gap_ms: Option, +} + +impl UserDeviceCustomization { + pub fn new( + display_name: &Option, + allow: bool, + deny: bool, + index: u32, + message_gap_ms: Option, + ) -> Self { + Self { + display_name: display_name.clone(), + allow, + deny, + index, + message_gap_ms, + } + } + + pub fn default_with_index(index: u32) -> Self { + Self::new(&None, false, false, index, None) + } +} + +#[derive(Debug, Clone, Getters, MutGetters, Serialize, Deserialize, CopyGetters)] +pub struct UserDeviceDefinition { + #[getset(get_copy = "pub")] + id: Uuid, + #[getset(get_copy = "pub")] + #[serde(rename = "base-id")] + base_id: Uuid, + #[getset(get = "pub")] + /// Message attributes for this device instance. + #[getset(get = "pub", get_mut = "pub")] + features: Vec, + #[getset(get = "pub", get_mut = "pub")] + #[serde(rename = "user-config")] + /// Per-user configurations specific to this device instance. + user_config: UserDeviceCustomization, +} + +impl UserDeviceDefinition { + fn new(index: u32, base_id: Uuid, features: &Vec) -> Self { + Self { + id: Uuid::new_v4(), + base_id, + features: features.clone(), + user_config: UserDeviceCustomization::default_with_index(index), + } + } +} + +#[derive(Debug, Clone, Getters, CopyGetters, MutGetters)] +pub struct DeviceDefinition { + #[getset(get = "pub")] + base_device: BaseDeviceDefinition, + #[getset(get = "pub", get_mut = "pub")] + user_device: UserDeviceDefinition, +} + +impl DeviceDefinition { + /// Create a new instance + pub fn new(base_device: &BaseDeviceDefinition, user_device: &UserDeviceDefinition) -> Self { + Self { + base_device: base_device.clone(), + user_device: user_device.clone(), + } + } + + pub fn id(&self) -> Uuid { + self.user_device.id() + } + + pub fn name(&self) -> &str { + self.base_device.name() + } + + pub fn protocol_variant(&self) -> &Option { + self.base_device.protocol_variant() + } + + pub fn features(&self) -> Vec { + // TODO Gross way to do this. + let mut features: Vec = vec![]; + self.base_device.features().iter().for_each(|x| { + if let Some(user_feature) = self + .user_device + .features + .iter() + .find(|user_feature| user_feature.base_id() == x.id()) + { + features.push(ServerDeviceFeature::new(x, user_feature)); + } + }); + features + } + + pub fn user_config(&self) -> &UserDeviceCustomization { + self.user_device.user_config() + } + + pub fn message_gap(&self) -> Option { + if let Some(gap) = self.user_device.user_config().message_gap_ms() { + Some(Duration::from_millis(gap.into())) + } else if let Some(gap) = self.base_device.device_settings.message_gap_ms() { + Some(Duration::from_millis(gap.into())) + } else { + None + } + } + + pub fn new_from_base_definition(def: &BaseDeviceDefinition, index: u32) -> Self { + let user_features = def + .features() + .iter() + .map(|x| x.as_user_device_feature()) + .collect(); + Self::new( + def, + &UserDeviceDefinition::new(index, def.id(), &user_features), + ) + } + + pub fn update_user_output( + &mut self, + feature_id: Uuid, + output_type: OutputType, + user_output: ServerUserDeviceFeatureOutput, + ) { + if let Some(feature) = self + .user_device + .features_mut() + .iter_mut() + .find(|x| x.id() == feature_id) + { + feature.update_output(output_type, &user_output); + } + } +} diff --git a/crates/buttplug_server_device_config/src/device_feature.rs b/crates/buttplug_server_device_config/src/device_feature.rs new file mode 100644 index 000000000..93f825af5 --- /dev/null +++ b/crates/buttplug_server_device_config/src/device_feature.rs @@ -0,0 +1,402 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 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 super::BaseFeatureSettings; +use buttplug_core::{ + errors::ButtplugDeviceError, + message::{ + DeviceFeature, + DeviceFeatureInput, + DeviceFeatureOutput, + FeatureType, + InputCommandType, + InputType, + OutputType, + }, +}; +use getset::{CopyGetters, Getters, MutGetters, Setters}; +use serde::{ + ser::{self, SerializeSeq}, + Deserialize, + Serialize, + Serializer, +}; +use std::{ + collections::{HashMap, HashSet}, + ops::RangeInclusive, +}; +use uuid::Uuid; + +fn range_serialize(range: &Option>, serializer: S) -> Result +where + S: Serializer, +{ + if let Some(range) = range { + let mut seq = serializer.serialize_seq(Some(2))?; + seq.serialize_element(&range.start())?; + seq.serialize_element(&range.end())?; + seq.end() + } else { + Err(ser::Error::custom( + "shouldn't be serializing if range is None", + )) + } +} + +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()])?; + } + seq.end() +} + +#[derive( + Clone, Debug, Default, Getters, MutGetters, Setters, Serialize, Deserialize, CopyGetters, +)] +pub struct ServerBaseDeviceFeature { + #[getset(get = "pub", get_mut = "pub(super)")] + #[serde(default)] + description: String, + #[getset(get_copy = "pub")] + #[serde(rename = "feature-type")] + feature_type: FeatureType, + #[getset(get = "pub")] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "output")] + output: Option>, + #[getset(get = "pub")] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "input")] + input: Option>, + #[getset(get_copy = "pub")] + id: Uuid, + #[getset(get = "pub")] + #[serde( + rename = "feature-settings", + skip_serializing_if = "BaseFeatureSettings::is_none", + default + )] + feature_settings: BaseFeatureSettings, +} + +impl ServerBaseDeviceFeature { + pub fn as_user_device_feature(&self) -> ServerUserDeviceFeature { + ServerUserDeviceFeature { + id: Uuid::new_v4(), + base_id: self.id, + output: self.output.as_ref().and_then(|x| { + Some( + x.keys() + .map(|x| (*x, ServerUserDeviceFeatureOutput::default())) + .collect(), + ) + }), + } + } +} + +#[derive( + Clone, Debug, Default, Getters, MutGetters, Setters, Serialize, Deserialize, CopyGetters, +)] +pub struct ServerUserDeviceFeature { + #[getset(get_copy = "pub")] + id: Uuid, + #[getset(get_copy = "pub")] + #[serde(rename = "base-id")] + base_id: Uuid, + #[getset(get = "pub")] + #[serde(rename = "output", skip_serializing_if = "Option::is_none")] + output: Option>, +} + +impl ServerUserDeviceFeature { + pub fn update_output(&mut self, output_type: OutputType, output: &ServerUserDeviceFeatureOutput) { + if let Some(ref mut output_map) = self.output { + if output_map.contains_key(&output_type) { + output_map.insert(output_type, output.clone()); + } + } + } +} + +#[derive(Clone, Debug, Getters, MutGetters, Setters, Serialize, Deserialize, CopyGetters)] +pub struct ServerBaseDeviceFeatureOutput { + #[getset(get = "pub")] + #[serde(rename = "step-range")] + step_range: RangeInclusive, +} + +impl ServerBaseDeviceFeatureOutput { + pub fn new(step_range: &RangeInclusive) -> Self { + Self { + step_range: step_range.clone(), + } + } +} + +#[derive( + Clone, Debug, Default, Getters, MutGetters, Setters, Serialize, Deserialize, CopyGetters, +)] +pub struct ServerUserDeviceFeatureOutput { + #[getset(get = "pub")] + #[serde( + rename = "step-limit", + default, + skip_serializing_if = "Option::is_none", + serialize_with = "range_serialize" + )] + step_limit: Option>, + #[getset(get = "pub")] + #[serde( + rename = "reverse-position", + default, + skip_serializing_if = "Option::is_none" + )] + reverse_position: Option, + #[getset(get = "pub")] + #[serde(rename = "ignore", default, skip_serializing_if = "Option::is_none")] + ignore: Option, +} + +impl ServerUserDeviceFeatureOutput { + pub fn new( + step_limit: Option>, + reverse_position: Option, + ignore: Option, + ) -> Self { + Self { + step_limit, + reverse_position, + ignore, + } + } +} + +#[derive(Clone, Debug, Default, Getters, MutGetters, Setters, CopyGetters)] +pub struct ServerDeviceFeature { + base_feature: ServerBaseDeviceFeature, + #[getset(get_mut = "pub")] + user_feature: ServerUserDeviceFeature, + #[getset(get = "pub")] + output: Option>, + // input doesn't specialize across Base/User right now so we just return the base device input +} + +impl PartialEq for ServerDeviceFeature { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} + +impl Eq for ServerDeviceFeature { +} + +impl ServerDeviceFeature { + pub fn new( + base_feature: &ServerBaseDeviceFeature, + user_feature: &ServerUserDeviceFeature, + ) -> Self { + if base_feature.id() != user_feature.base_id() { + // TODO panic! + } + let output = { + if let Some(output_map) = base_feature.output() { + let mut output = HashMap::new(); + if let Some(user_output_map) = user_feature.output() { + for (output_type, output_feature) in output_map { + // TODO What if we have a key in the user map that isn't in the base map? We should remove it. + if user_output_map.contains_key(output_type) { + output.insert( + *output_type, + ServerDeviceFeatureOutput::new( + output_feature, + user_output_map.get(output_type).clone().unwrap(), + ), + ); + } + } + } + Some(output) + } else { + None + } + }; + + Self { + output, + base_feature: base_feature.clone(), + user_feature: user_feature.clone(), + } + } + + pub fn description(&self) -> &String { + self.base_feature.description() + } + + pub fn feature_type(&self) -> FeatureType { + self.base_feature.feature_type + } + + pub fn id(&self) -> Uuid { + self.user_feature.id() + } + + pub fn base_id(&self) -> Uuid { + self.base_feature.id() + } + + pub fn alt_protocol_index(&self) -> Option { + self.base_feature.feature_settings().alt_protocol_index() + } + + pub fn input(&self) -> &Option> { + self.base_feature.input() + } + + pub fn is_valid(&self) -> Result<(), ButtplugDeviceError> { + if let Some(output_map) = &self.output { + for actuator in output_map.values() { + actuator.is_valid()?; + } + } + Ok(()) + } + + pub fn as_device_feature(&self, index: u32) -> DeviceFeature { + DeviceFeature::new( + index, + self.description(), + self.feature_type(), + &self.output.clone().map(|x| { + x.iter() + .map(|(t, a)| (*t, DeviceFeatureOutput::from(a.clone()))) + .collect() + }), + &self.base_feature.input().clone().map(|x| { + x.iter() + .map(|(t, a)| (*t, DeviceFeatureInput::from(a.clone()))) + .collect() + }), + ) + } +} + +#[derive(Clone, Debug, Getters, MutGetters)] +#[getset(get = "pub")] +pub struct ServerDeviceFeatureOutput { + base_feature: ServerBaseDeviceFeatureOutput, + #[getset(get_mut = "pub")] + user_feature: ServerUserDeviceFeatureOutput, +} + +impl ServerDeviceFeatureOutput { + pub fn new( + base_feature: &ServerBaseDeviceFeatureOutput, + user_feature: &ServerUserDeviceFeatureOutput, + ) -> Self { + Self { + base_feature: base_feature.clone(), + user_feature: user_feature.clone(), + } + } + + pub fn step_range(&self) -> &RangeInclusive { + self.base_feature.step_range() + } + + pub fn step_limit(&self) -> &RangeInclusive { + if let Some(limit) = self.user_feature.step_limit() { + limit + } else { + self.step_range() + } + } + + pub fn step_count(&self) -> u32 { + if let Some(step_limit) = self.user_feature.step_limit() { + step_limit.end() - step_limit.start() + } else { + self.base_feature.step_range.end() - self.base_feature.step_range.start() + } + } + + pub fn reverse_position(&self) -> bool { + *self + .user_feature + .reverse_position() + .as_ref() + .unwrap_or(&false) + } + + pub fn is_valid(&self) -> Result<(), ButtplugDeviceError> { + let step_range = self.base_feature.step_range(); + if step_range.is_empty() { + Err(ButtplugDeviceError::DeviceConfigurationError( + "Step range empty.".to_string(), + )) + } else if let Some(step_limit) = self.user_feature.step_limit() { + if step_limit.is_empty() { + Err(ButtplugDeviceError::DeviceConfigurationError( + "Step limit empty.".to_string(), + )) + } else if step_limit.start() < step_range.start() || step_limit.end() > step_range.end() { + Err(ButtplugDeviceError::DeviceConfigurationError( + "Step limit outside step range.".to_string(), + )) + } else { + Ok(()) + } + } else { + Ok(()) + } + } +} + +impl From for DeviceFeatureOutput { + fn from(value: ServerDeviceFeatureOutput) -> Self { + DeviceFeatureOutput::new(value.step_count()) + } +} + +#[derive( + Clone, Debug, Default, PartialEq, Eq, Getters, MutGetters, Setters, Serialize, Deserialize, +)] +pub struct ServerDeviceFeatureInput { + #[getset(get = "pub", get_mut = "pub(super)")] + #[serde(rename = "value-range")] + #[serde(serialize_with = "range_sequence_serialize")] + value_range: Vec>, + #[getset(get = "pub")] + #[serde(rename = "input-commands")] + input_commands: HashSet, +} + +impl ServerDeviceFeatureInput { + pub fn new( + value_range: &Vec>, + sensor_commands: &HashSet, + ) -> Self { + Self { + value_range: value_range.clone(), + input_commands: sensor_commands.clone(), + } + } +} + +impl From for DeviceFeatureInput { + fn from(value: ServerDeviceFeatureInput) -> Self { + // Unlike actuator, this is just a straight copy. + DeviceFeatureInput::new(value.value_range(), value.input_commands()) + } +} diff --git a/buttplug/src/core/message/endpoint.rs b/crates/buttplug_server_device_config/src/endpoint.rs similarity index 97% rename from buttplug/src/core/message/endpoint.rs rename to crates/buttplug_server_device_config/src/endpoint.rs index 0ec4e9fcb..348b8c71e 100644 --- a/buttplug/src/core/message/endpoint.rs +++ b/crates/buttplug_server_device_config/src/endpoint.rs @@ -137,7 +137,7 @@ impl Serialize for Endpoint { struct EndpointVisitor; -impl<'de> Visitor<'de> for EndpointVisitor { +impl Visitor<'_> for EndpointVisitor { type Value = Endpoint; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { @@ -148,7 +148,7 @@ impl<'de> Visitor<'de> for EndpointVisitor { where E: de::Error, { - Endpoint::from_str(value).map_err(|e| E::custom(format!("{}", e))) + Endpoint::from_str(value).map_err(|e| E::custom(format!("{e}"))) } } diff --git a/buttplug/src/server/device/configuration/identifiers.rs b/crates/buttplug_server_device_config/src/identifiers.rs similarity index 100% rename from buttplug/src/server/device/configuration/identifiers.rs rename to crates/buttplug_server_device_config/src/identifiers.rs diff --git a/buttplug/src/server/device/configuration/mod.rs b/crates/buttplug_server_device_config/src/lib.rs similarity index 62% rename from buttplug/src/server/device/configuration/mod.rs rename to crates/buttplug_server_device_config/src/lib.rs index c9472eba4..9ff879a80 100644 --- a/buttplug/src/server/device/configuration/mod.rs +++ b/crates/buttplug_server_device_config/src/lib.rs @@ -93,7 +93,6 @@ //! //! - Protocol device specifiers and attributes //! - Factory/Builder instances for [ButtplugProtocols](crate::device::protocol::ButtplugProtocol) -//! - Whether or not Raw Messages are allowed //! - User configuration information (allow/deny lists, per-device protocol attributes, etc...) //! //! The [DeviceConfigurationManager] is created when a ButtplugServer comes up, and which time @@ -132,47 +131,42 @@ //! [ButtplugDevice](crate::device::ButtplugDevice) instance used by the //! [ButtplugServer](crate::server::ButtplugServer). //! -//! ### Raw Messages -//! //! ### User Configurations //! +#[macro_use] +extern crate strum_macros; + mod specifier; pub use specifier::*; mod identifiers; pub use identifiers::*; mod device_definitions; pub use device_definitions::*; - -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::protocol::{ - get_default_protocol_map, - ProtocolIdentifierFactory, - ProtocolSpecializer, - }, -}; +mod device_feature; +pub use device_feature::*; +mod device_configuration; +pub use device_configuration::*; +mod endpoint; +pub use endpoint::*; + +use buttplug_core::errors::ButtplugDeviceError; use dashmap::DashMap; use getset::Getters; use std::{ collections::HashMap, fmt::{self, Debug}, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, }; +#[macro_use] +extern crate log; + #[derive(Default, Clone)] pub struct DeviceConfigurationManagerBuilder { - skip_default_protocols: bool, - allow_raw_messages: bool, communication_specifiers: HashMap>, user_communication_specifiers: DashMap>, base_device_definitions: HashMap, - user_device_definitions: DashMap, - /// Map of protocol names to their respective protocol instance factories - protocols: Vec<(String, Arc)>, + user_device_definitions: DashMap, } impl DeviceConfigurationManagerBuilder { @@ -218,68 +212,39 @@ impl DeviceConfigurationManagerBuilder { identifier: &UserDeviceIdentifier, features: &UserDeviceDefinition, ) -> &mut Self { - self - .user_device_definitions - .insert(identifier.clone(), features.clone()); - self - } - - /// Add a protocol instance factory for a [ButtplugProtocol] - pub fn protocol_factory(&mut self, factory: T) -> &mut Self - where - T: ProtocolIdentifierFactory + 'static, - { - self - .protocols - .push((factory.identifier().to_owned(), Arc::new(factory))); - self - } - - pub fn skip_default_protocols(&mut self) -> &mut Self { - self.skip_default_protocols = true; - self - } - - pub fn allow_raw_messages(&mut self, allow: bool) -> &mut Self { - self.allow_raw_messages = allow; + if let Some((_, base_definition)) = self + .base_device_definitions + .iter() + .find(|(_, x)| x.id() == features.base_id()) + { + self.user_device_definitions.insert( + identifier.clone(), + DeviceDefinition::new(base_definition, features), + ); + } else { + error!( + "Cannot find protocol with base id {} for user id {}", + features.base_id(), + features.id() + ) + } self } pub fn finish(&mut self) -> Result { - // Map of protocol names to their respective protocol instance factories - let mut protocol_map = if !self.skip_default_protocols { - get_default_protocol_map() - } else { - HashMap::new() - }; - - for (name, protocol) in &self.protocols { - if protocol_map.contains_key(name) { - // TODO Fill in error - } - protocol_map.insert(name.clone(), protocol.clone()); - } - // Build and validate the protocol attributes tree. let mut attribute_tree_map = HashMap::new(); // Add all the defaults first, they won't have parent attributes. for (ident, attr) in &self.base_device_definitions { - // If we don't have a protocol loaded for this configuration block, just drop it. We can't do - // anything with it anyways. - if !protocol_map.contains_key(ident.protocol()) { - debug!( - "Protocol {:?} in base configurations does not exist in system, discarding definition.", - ident.protocol() - ); - continue; - } + /* for feature in attr.features() { if let Err(e) = feature.is_valid() { error!("Feature {attr:?} for ident {ident:?} is not valid, skipping addition: {e:?}"); continue; } } + */ attribute_tree_map.insert(ident.clone(), attr.clone()); } @@ -287,15 +252,6 @@ impl DeviceConfigurationManagerBuilder { // Finally, add in user configurations, which will have an address. for kv in &self.user_device_definitions { let (ident, attr) = (kv.key(), kv.value()); - // If we don't have a protocol loaded for this configuration block, just drop it. We can't do - // anything with it anyways. - if !protocol_map.contains_key(ident.protocol()) { - warn!( - "Protocol {:?} in user configurations does not exist in system, discarding definition.", - ident.protocol() - ); - continue; - } for feature in attr.features() { if let Err(e) = feature.is_valid() { error!("Feature {attr:?} for ident {ident:?} is not valid, skipping addition: {e:?}"); @@ -306,12 +262,11 @@ impl DeviceConfigurationManagerBuilder { } Ok(DeviceConfigurationManager { - allow_raw_messages: Arc::new(AtomicBool::new(self.allow_raw_messages)), base_communication_specifiers: self.communication_specifiers.clone(), user_communication_specifiers: self.user_communication_specifiers.clone(), base_device_definitions: attribute_tree_map, user_device_definitions: user_attribute_tree_map, - protocol_map, + //protocol_map, }) } } @@ -328,11 +283,8 @@ impl DeviceConfigurationManagerBuilder { /// information about what commands can be sent to the device (Vibrate, Rotate, etc...), and the /// parameters for those commands (number of power levels, stroke distances, etc...). #[derive(Getters)] +#[getset(get = "pub")] pub struct DeviceConfigurationManager { - /// If true, add raw message support to connected devices - allow_raw_messages: Arc, - /// Map of protocol names to their respective protocol instance factories - protocol_map: HashMap>, /// Communication specifiers from the base device config, mapped from protocol name to vector of /// specifiers. Should not change/update during a session. base_communication_specifiers: HashMap>, @@ -340,12 +292,10 @@ pub struct DeviceConfigurationManager { base_device_definitions: HashMap, /// Communication specifiers provided by the user, mapped from protocol name to vector of /// specifiers. Loaded at session start, may change over life of session. - #[getset(get = "pub")] user_communication_specifiers: DashMap>, - /// Device definitions from the base device config. Loaded at session start, may change over life + /// Device definitions from the user device config. Loaded at session start, may change over life /// of session. - #[getset(get = "pub")] - user_device_definitions: DashMap, + user_device_definitions: DashMap, } impl Debug for DeviceConfigurationManager { @@ -355,7 +305,6 @@ impl Debug for DeviceConfigurationManager { } impl Default for DeviceConfigurationManager { - /// Create a new instance with Raw Message support turned off fn default() -> Self { // Unwrap allowed here because we assume our built in device config will // always work. System won't pass tests or possibly even build otherwise. @@ -366,16 +315,12 @@ impl Default for DeviceConfigurationManager { } impl DeviceConfigurationManager { - pub fn set_allow_raw_messages(&self, allow: bool) { - self.allow_raw_messages.store(allow, Ordering::Relaxed) - } - pub fn add_user_communication_specifier( &self, protocol: &str, specifier: &ProtocolCommunicationSpecifier, ) -> Result<(), ButtplugDeviceError> { - if !self.protocol_map.contains_key(protocol) {} + //self.protocol_map.contains_key(protocol); self .user_communication_specifiers .entry(protocol.to_owned()) @@ -402,13 +347,24 @@ impl DeviceConfigurationManager { pub fn add_user_device_definition( &self, identifier: &UserDeviceIdentifier, - definition: &UserDeviceDefinition, + definition: &DeviceDefinition, ) -> Result<(), ButtplugDeviceError> { - if !self.protocol_map.contains_key(identifier.protocol()) {} + //self.protocol_map.contains_key(identifier.protocol()); + // Check validity of device + let mut index = definition.user_config().index(); + let indexes: Vec = self.user_device_definitions().iter().map(|x| x.value().user_config().index()).collect(); + // If we just added 1 to the maximum value of the current indexes, someone decides to set an + // index to u32::MAX-1, then we'd have a problem. This is kind of a shit solution but it'll work + // quickly for anyone that's not actively fucking with us by manually playing with user config files. + while indexes.contains(&index) { + index = index.wrapping_add(1); + } + let mut def = definition.clone(); + *def.user_device_mut().user_config_mut().index_mut() = index; self .user_device_definitions .entry(identifier.clone()) - .insert(definition.clone()); + .insert(def); Ok(()) } @@ -470,7 +426,7 @@ impl DeviceConfigurationManager { let mut index = 0; while current_indexes.contains(&index) { - index = index + 1; + index += 1; } debug!("Generating and assigning index {index:?} for device {identifier:?}"); index @@ -485,81 +441,30 @@ impl DeviceConfigurationManager { self.base_communication_specifiers.clone() } - pub fn protocol_specializers( - &self, - specifier: &ProtocolCommunicationSpecifier, - ) -> Vec { - debug!( - "Looking for protocol that matches specifier: {:?}", - specifier - ); - let mut specializers = vec![]; - - let mut update_specializer_map = - |name: &str, specifiers: &Vec| { - if specifiers.contains(specifier) { - info!( - "Found protocol {:?} for user specifier {:?}.", - name, specifier - ); - - if self.protocol_map.contains_key(name) { - specializers.push(ProtocolSpecializer::new( - specifiers.clone(), - self - .protocol_map - .get(name) - .expect("already checked existence") - .create(), - )); - } else { - warn!( - "No protocol implementation for {:?} found for specifier {:?}.", - name, specifier - ); - } - } - }; - - // Loop through both maps, as chaining between DashMap and HashMap gets kinda gross. - for spec in self.user_communication_specifiers.iter() { - update_specializer_map(spec.key(), spec.value()); - } - for (name, specifiers) in self.base_communication_specifiers.iter() { - update_specializer_map(name, specifiers); - } - specializers - } - - pub fn device_definition( - &self, - identifier: &UserDeviceIdentifier, - raw_endpoints: &[Endpoint], - ) -> Option { - let mut features = if let Some(attrs) = self.user_device_definitions.get(identifier) { + pub fn device_definition(&self, identifier: &UserDeviceIdentifier) -> Option { + let features = if let Some(attrs) = self.user_device_definitions.get(identifier) { debug!("User device config found for {:?}", identifier); attrs.clone() } else if let Some(attrs) = self.base_device_definitions.get(&BaseDeviceIdentifier::new( - &identifier.protocol(), - &identifier.identifier(), + identifier.protocol(), + identifier.identifier(), )) { debug!( "Protocol + Identifier device config found for {:?}", identifier ); - UserDeviceDefinition::new_from_base_definition(attrs, self.device_index(identifier)) + DeviceDefinition::new_from_base_definition(attrs, self.device_index(identifier)) } else if let Some(attrs) = self .base_device_definitions - .get(&BaseDeviceIdentifier::new(&identifier.protocol(), &None)) + .get(&BaseDeviceIdentifier::new(identifier.protocol(), &None)) { debug!("Protocol device config found for {:?}", identifier); - UserDeviceDefinition::new_from_base_definition(attrs, self.device_index(identifier)) + DeviceDefinition::new_from_base_definition(attrs, self.device_index(identifier)) } else { return None; }; - // If this is a new device, it needs to be added to the user device definition map. Make sure we - // do this before we add raw message features. + // If this is a new device, it needs to be added to the user device definition map. // // Device definitions are looked up before we fully initialize a device, mostly for algorithm // preparation. There is a very small chance we may save the device config then error out when @@ -570,173 +475,6 @@ impl DeviceConfigurationManager { .insert(identifier.clone(), features.clone()); } - if self.allow_raw_messages.load(Ordering::Relaxed) { - features.add_raw_messages(raw_endpoints); - } - Some(features) } } - -#[cfg(test)] -mod test { - use super::*; - use crate::core::message::{ - ButtplugActuatorFeatureMessageType, - DeviceFeature, - DeviceFeatureActuator, - FeatureType, - }; - use std::{ - collections::{HashMap, HashSet}, - ops::RangeInclusive, - }; - - fn create_unit_test_dcm(allow_raw_messages: bool) -> DeviceConfigurationManager { - let mut builder = DeviceConfigurationManagerBuilder::default(); - let specifiers = ProtocolCommunicationSpecifier::BluetoothLE(BluetoothLESpecifier::new( - HashSet::from(["LVS-*".to_owned(), "LovenseDummyTestName".to_owned()]), - vec![], - HashSet::new(), - HashMap::new(), - )); - builder - .allow_raw_messages(allow_raw_messages) - .communication_specifier("lovense", &[specifiers]) - .protocol_features( - &BaseDeviceIdentifier::new("lovense", &Some("P".to_owned())), - &BaseDeviceDefinition::new( - "Lovense Edge", - &vec![ - DeviceFeature::new( - "Edge Vibration 1", - FeatureType::Vibrate, - &Some(DeviceFeatureActuator::new( - &RangeInclusive::new(0, 20), - &RangeInclusive::new(0, 20), - &HashSet::from_iter([ButtplugActuatorFeatureMessageType::ScalarCmd]), - )), - &None, - ), - DeviceFeature::new( - "Edge Vibration 2", - FeatureType::Vibrate, - &Some(DeviceFeatureActuator::new( - &RangeInclusive::new(0, 20), - &RangeInclusive::new(0, 20), - &HashSet::from_iter([ButtplugActuatorFeatureMessageType::ScalarCmd]), - )), - &None, - ), - ], - ), - ) - .finish() - .unwrap() - } - - #[test] - fn test_config_equals() { - let config = create_unit_test_dcm(false); - let spec = ProtocolCommunicationSpecifier::BluetoothLE(BluetoothLESpecifier::new_from_device( - "LovenseDummyTestName", - &HashMap::new(), - &[], - )); - assert!(!config.protocol_specializers(&spec).is_empty()); - } - - #[test] - fn test_config_wildcard_equals() { - let config = create_unit_test_dcm(false); - let spec = ProtocolCommunicationSpecifier::BluetoothLE(BluetoothLESpecifier::new_from_device( - "LVS-Whatever", - &HashMap::new(), - &[], - )); - assert!(!config.protocol_specializers(&spec).is_empty()); - } - /* - #[test] - fn test_specific_device_config_creation() { - let dcm = create_unit_test_dcm(false); - let spec = ProtocolCommunicationSpecifier::BluetoothLE(BluetoothLESpecifier::new_from_device( - "LVS-Whatever", - &HashMap::new(), - &[], - )); - assert!(!dcm.protocol_specializers(&spec).is_empty()); - let config: ProtocolDeviceAttributes = dcm - .device_definition( - &UserDeviceIdentifier::new("Whatever", "lovense", &Some("P".to_owned())), - &[], - ) - .expect("Should be found") - .into(); - // Make sure we got the right name - assert_eq!(config.name(), "Lovense Edge"); - // Make sure we overwrote the default of 1 - assert_eq!( - config - .message_attributes() - .scalar_cmd() - .as_ref() - .expect("Test, assuming infallible") - .get(0) - .expect("Test, assuming infallible") - .step_count(), - 20 - ); - } - - #[test] - fn test_raw_device_config_creation() { - let dcm = create_unit_test_dcm(true); - let spec = ProtocolCommunicationSpecifier::BluetoothLE(BluetoothLESpecifier::new_from_device( - "LVS-Whatever", - &HashMap::new(), - &[], - )); - assert!(!dcm.protocol_specializers(&spec).is_empty()); - let config: ProtocolDeviceAttributes = dcm - .device_definition( - &UserDeviceIdentifier::new("Whatever", "lovense", &Some("P".to_owned())), - &[], - ) - .expect("Should be found") - .into(); - // Make sure we got the right name - assert_eq!(config.name(), "Lovense Edge"); - // Make sure we overwrote the default of 1 - assert!(config.message_attributes().raw_read_cmd().is_some()); - assert!(config.message_attributes().raw_write_cmd().is_some()); - assert!(config.message_attributes().raw_subscribe_cmd().is_some()); - assert!(config.message_attributes().raw_unsubscribe_cmd().is_some()); - } - - #[test] - fn test_non_raw_device_config_creation() { - let dcm = create_unit_test_dcm(false); - let spec = ProtocolCommunicationSpecifier::BluetoothLE(BluetoothLESpecifier::new_from_device( - "LVS-Whatever", - &HashMap::new(), - &[], - )); - assert!(!dcm.protocol_specializers(&spec).is_empty()); - let config: ProtocolDeviceAttributes = dcm - .device_definition( - &UserDeviceIdentifier::new("Whatever", "lovense", &Some("P".to_owned())), - &[], - ) - .expect("Should be found") - .into(); - // Make sure we got the right name - assert_eq!(config.name(), "Lovense Edge"); - // Make sure we overwrote the default of 1 - assert!(config.message_attributes().raw_read_cmd().is_none()); - assert!(config.message_attributes().raw_write_cmd().is_none()); - assert!(config.message_attributes().raw_subscribe_cmd().is_none()); - assert!(config.message_attributes().raw_unsubscribe_cmd().is_none()); - } - */ -} diff --git a/buttplug/src/server/device/configuration/specifier.rs b/crates/buttplug_server_device_config/src/specifier.rs similarity index 93% rename from buttplug/src/server/device/configuration/specifier.rs rename to crates/buttplug_server_device_config/src/specifier.rs index 48eec03da..45d812c34 100644 --- a/buttplug/src/server/device/configuration/specifier.rs +++ b/crates/buttplug_server_device_config/src/specifier.rs @@ -5,7 +5,7 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::core::message::Endpoint; +use super::Endpoint; use getset::{Getters, MutGetters, Setters}; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; @@ -345,6 +345,43 @@ impl PartialEq for SerialSpecifier { } } + +/// Specifier for UDP devices +/// +/// Handles udp device identification (via address:port) and configuration. +#[derive(Serialize, Deserialize, Debug, Clone, Default, Getters, Setters, MutGetters)] +#[getset(get = "pub", set = "pub", get_mut = "pub(crate)")] +pub struct UdpSpecifier { + address: String, + port: u16, +} + +impl UdpSpecifier { + pub fn new(address: &str, port: u16) -> Self { + Self { + address: address.to_owned(), + port, + } + } +} + +impl ToString for UdpSpecifier { + fn to_string(&self) -> String + { + format!("{}:{}", self.address, self.port) + } +} + +impl PartialEq for UdpSpecifier { + fn eq(&self, other: &Self) -> bool { + if *self.address == *other.address && self.port == other.port { + return true; + } + false + } +} + + /// Specifier for Websocket Device Manager devices /// /// The websocket device manager is a network based manager, so we have no info other than possibly @@ -392,6 +429,8 @@ pub enum ProtocolCommunicationSpecifier { LovenseConnectService(LovenseConnectServiceSpecifier), #[serde(rename = "websocket")] Websocket(WebsocketSpecifier), + #[serde(rename = "udp")] + Udp(UdpSpecifier), } impl PartialEq for ProtocolCommunicationSpecifier { @@ -404,6 +443,7 @@ impl PartialEq for ProtocolCommunicationSpecifier { (HID(self_spec), HID(other_spec)) => self_spec == other_spec, (XInput(self_spec), XInput(other_spec)) => self_spec == other_spec, (Websocket(self_spec), Websocket(other_spec)) => self_spec == other_spec, + (Udp(self_spec), Udp(other_spec)) => self_spec == other_spec, (LovenseConnectService(self_spec), LovenseConnectService(other_spec)) => { self_spec == other_spec } diff --git a/crates/buttplug_server_hwmgr_btleplug/Cargo.toml b/crates/buttplug_server_hwmgr_btleplug/Cargo.toml new file mode 100644 index 000000000..19e424903 --- /dev/null +++ b/crates/buttplug_server_hwmgr_btleplug/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "buttplug_server_hwmgr_btleplug" +version = "10.0.0" +authors = ["Nonpolynomial Labs, LLC "] +description = "Buttplug Intimate Hardware Control Library - Core Library" +license = "BSD-3-Clause" +homepage = "http://buttplug.io" +repository = "https://github.com/buttplugio/buttplug.git" +readme = "./README.md" +keywords = ["usb", "serial", "hardware", "bluetooth", "teledildonics"] +edition = "2024" +exclude = ["examples/**"] + +[lib] +name = "buttplug_server_hwmgr_btleplug" +path = "src/lib.rs" +test = true +doctest = true +doc = true + + +[dependencies] +buttplug_derive = "0.8.1" +# buttplug_derive = { path = "../buttplug_derive" } +buttplug_core = { path = "../buttplug_core" } +buttplug_server = { path = "../buttplug_server" } +buttplug_server_device_config = { path = "../buttplug_server_device_config" } +futures = "0.3.31" +futures-util = "0.3.31" +log = "0.4.27" +tokio = { version = "1.46.1", features = ["sync", "time"] } +btleplug = "0.11.8" +async-trait = "0.1.88" +uuid = { version = "1.17.0", features = ["serde", "v4"] } +dashmap = { version = "6.1.0", features = ["serde"] } +tracing = "0.1.41" + +[target.'cfg(target_os = "windows")'.dependencies] +windows = { version = "0.61.3", features = ["Devices_Bluetooth", "Foundation"] } diff --git a/buttplug/src/server/device/hardware/communication/btleplug/btleplug_adapter_task.rs b/crates/buttplug_server_hwmgr_btleplug/src/btleplug_adapter_task.rs similarity index 91% rename from buttplug/src/server/device/hardware/communication/btleplug/btleplug_adapter_task.rs rename to crates/buttplug_server_hwmgr_btleplug/src/btleplug_adapter_task.rs index cf4564b55..d11ec0867 100644 --- a/buttplug/src/server/device/hardware/communication/btleplug/btleplug_adapter_task.rs +++ b/crates/buttplug_server_hwmgr_btleplug/src/btleplug_adapter_task.rs @@ -6,12 +6,12 @@ // for full license information. use super::btleplug_hardware::BtleplugHardwareConnector; -use crate::server::device::hardware::communication::HardwareCommunicationManagerEvent; use btleplug::{ api::{Central, CentralEvent, Manager as _, Peripheral, ScanFilter}, platform::{Adapter, Manager, PeripheralId}, }; -use futures::{future::FutureExt, StreamExt}; +use buttplug_server::device::hardware::communication::HardwareCommunicationManagerEvent; +use futures::StreamExt; use std::{ collections::HashMap, sync::{ @@ -21,9 +21,12 @@ use std::{ time::Duration, }; use tokio::{ + select, sync::mpsc::{Receiver, Sender}, time::sleep, }; +use tracing::info_span; +use uuid::Uuid; #[derive(Debug, Clone, Copy)] pub enum BtleplugAdapterCommand { @@ -36,7 +39,7 @@ struct PeripheralInfo { name: Option, peripheral_id: PeripheralId, manufacturer_data: HashMap>, - services: Vec, + services: Vec, } pub struct BtleplugAdapterTask { @@ -102,7 +105,7 @@ impl BtleplugAdapterTask { { let span = info_span!( "btleplug enumeration", - address = tracing::field::display(format!("{:?}", peripheral_id)), + address = tracing::field::display(format!("{peripheral_id:?}")), name = tracing::field::display(&device_name) ); let _enter = span.enter(); @@ -124,7 +127,7 @@ impl BtleplugAdapterTask { .event_sender .send(HardwareCommunicationManagerEvent::DeviceFound { name: device_name, - address: format!("{:?}", peripheral_id), + address: format!("{peripheral_id:?}"), creator: device_creator, }) .await @@ -151,12 +154,12 @@ impl BtleplugAdapterTask { // Start by assuming we'll find the adapter on the first try. If not, we'll print an error // message then loop while trying to find it. - self.adapter_connected.store(true, Ordering::SeqCst); + self.adapter_connected.store(true, Ordering::Relaxed); let adapter; loop { - let adapter_found = self.adapter_connected.load(Ordering::SeqCst); + let adapter_found = self.adapter_connected.load(Ordering::Relaxed); if !adapter_found { sleep(Duration::from_secs(1)).await; } @@ -202,7 +205,7 @@ impl BtleplugAdapterTask { adapter } else { if adapter_found { - self.adapter_connected.store(false, Ordering::SeqCst); + self.adapter_connected.store(false, Ordering::Relaxed); warn!("Bluetooth LE adapter not found, will not be using bluetooth scanning until found. Buttplug will continue polling for the adapter, but no more warning messages will be posted."); } continue; @@ -210,7 +213,7 @@ impl BtleplugAdapterTask { } Err(e) => { if adapter_found { - self.adapter_connected.store(false, Ordering::SeqCst); + self.adapter_connected.store(false, Ordering::Relaxed); error!("Error retreiving BTLE adapters: {:?}", e); } continue; @@ -230,7 +233,7 @@ impl BtleplugAdapterTask { let event_fut = events.next(); select! { - event = event_fut.fuse() => { + event = event_fut => { if let Some(event) = event { match event { CentralEvent::DeviceDiscovered(peripheral_id) | CentralEvent::DeviceUpdated(peripheral_id) => { @@ -249,13 +252,13 @@ impl BtleplugAdapterTask { return; } }, - command = self.command_receiver.recv().fuse() => { + command = self.command_receiver.recv() => { if let Some(cmd) = command { match cmd { BtleplugAdapterCommand::StartScanning => { tried_addresses.clear(); if let Err(err) = adapter.start_scan(ScanFilter::default()).await { - error!("Start scanning request failed: {}", err); + error!("Start scanning request failed. Ensure Bluetooth is enabled and permissions are granted: {}", err); } } BtleplugAdapterCommand::StopScanning => { diff --git a/buttplug/src/server/device/hardware/communication/btleplug/btleplug_comm_manager.rs b/crates/buttplug_server_hwmgr_btleplug/src/btleplug_comm_manager.rs similarity index 88% rename from buttplug/src/server/device/hardware/communication/btleplug/btleplug_comm_manager.rs rename to crates/buttplug_server_hwmgr_btleplug/src/btleplug_comm_manager.rs index c2d961321..f32d025bd 100644 --- a/buttplug/src/server/device/hardware/communication/btleplug/btleplug_comm_manager.rs +++ b/crates/buttplug_server_hwmgr_btleplug/src/btleplug_comm_manager.rs @@ -6,14 +6,11 @@ // for full license information. use super::btleplug_adapter_task::{BtleplugAdapterCommand, BtleplugAdapterTask}; -use crate::{ - core::{errors::ButtplugDeviceError, ButtplugResultFuture}, - server::device::hardware::communication::{ - HardwareCommunicationManager, - HardwareCommunicationManagerBuilder, - HardwareCommunicationManagerEvent, - }, - util::async_manager, +use buttplug_core::{errors::ButtplugDeviceError, util::async_manager, ButtplugResultFuture}; +use buttplug_server::device::hardware::communication::{ + HardwareCommunicationManager, + HardwareCommunicationManagerBuilder, + HardwareCommunicationManagerEvent, }; use futures::future::FutureExt; use std::sync::{ @@ -86,7 +83,7 @@ impl HardwareCommunicationManager for BtlePlugCommunicationManager { let adapter_event_sender = self.adapter_event_sender.clone(); let scanning_status = self.scanning_status.clone(); // Set to true just to make sure we don't call ScanningFinished too early. - scanning_status.store(true, Ordering::SeqCst); + scanning_status.store(true, Ordering::Relaxed); async move { if adapter_event_sender .send(BtleplugAdapterCommand::StartScanning) @@ -94,7 +91,7 @@ impl HardwareCommunicationManager for BtlePlugCommunicationManager { .is_err() { error!("Error starting scan, cannot send to btleplug event loop."); - scanning_status.store(false, Ordering::SeqCst); + scanning_status.store(false, Ordering::Relaxed); Err( ButtplugDeviceError::DeviceConnectionError( "Cannot send start scanning request to event loop.".to_owned(), @@ -111,7 +108,7 @@ impl HardwareCommunicationManager for BtlePlugCommunicationManager { fn stop_scanning(&mut self) -> ButtplugResultFuture { let adapter_event_sender = self.adapter_event_sender.clone(); // Just assume any outcome of this means we're done scanning. - self.scanning_status.store(false, Ordering::SeqCst); + self.scanning_status.store(false, Ordering::Relaxed); async move { if adapter_event_sender .send(BtleplugAdapterCommand::StopScanning) @@ -133,11 +130,11 @@ impl HardwareCommunicationManager for BtlePlugCommunicationManager { } fn scanning_status(&self) -> bool { - self.scanning_status.load(Ordering::SeqCst) + self.scanning_status.load(Ordering::Relaxed) } fn can_scan(&self) -> bool { - self.adapter_connected.load(Ordering::SeqCst) + self.adapter_connected.load(Ordering::Relaxed) } } /* diff --git a/buttplug/src/server/device/hardware/communication/btleplug/btleplug_hardware.rs b/crates/buttplug_server_hwmgr_btleplug/src/btleplug_hardware.rs similarity index 84% rename from buttplug/src/server/device/hardware/communication/btleplug/btleplug_hardware.rs rename to crates/buttplug_server_hwmgr_btleplug/src/btleplug_hardware.rs index 299b25e50..1cd99ec5f 100644 --- a/buttplug/src/server/device/hardware/communication/btleplug/btleplug_hardware.rs +++ b/crates/buttplug_server_hwmgr_btleplug/src/btleplug_hardware.rs @@ -5,32 +5,27 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::hardware::communication::HardwareSpecificError, - server::device::{ - configuration::{BluetoothLESpecifier, ProtocolCommunicationSpecifier}, - hardware::{ - Hardware, - HardwareConnector, - HardwareEvent, - HardwareInternal, - HardwareReadCmd, - HardwareReading, - HardwareSpecializer, - HardwareSubscribeCmd, - HardwareUnsubscribeCmd, - HardwareWriteCmd, - }, - }, - util::async_manager, -}; use async_trait::async_trait; use btleplug::api::CharPropFlags; use btleplug::{ api::{Central, CentralEvent, Characteristic, Peripheral, ValueNotification, WriteType}, platform::Adapter, }; +use buttplug_core::{errors::ButtplugDeviceError, util::async_manager}; +use buttplug_server::device::hardware::{ + communication::HardwareSpecificError, + Hardware, + HardwareConnector, + HardwareEvent, + HardwareInternal, + HardwareReadCmd, + HardwareReading, + HardwareSpecializer, + HardwareSubscribeCmd, + HardwareUnsubscribeCmd, + HardwareWriteCmd, +}; +use buttplug_server_device_config::{BluetoothLESpecifier, ProtocolCommunicationSpecifier, Endpoint}; use dashmap::DashSet; use futures::{ future::{self, BoxFuture, FutureExt}, @@ -42,8 +37,9 @@ use std::{ fmt::{self, Debug}, pin::Pin, sync::Arc, + time::Duration, }; -use tokio::sync::broadcast; +use tokio::{select, sync::broadcast}; use uuid::Uuid; pub(super) struct BtleplugHardwareConnector { @@ -104,17 +100,17 @@ impl HardwareConnector for BtleplugHardwareConnector { .await .expect("If we crash here it's Bluez's fault. Use something else please.") { - if let Err(err) = self.device.connect().await { + if let Err(e) = self.device.connect().await { let return_err = ButtplugDeviceError::DeviceSpecificError( - HardwareSpecificError::BtleplugError(format!("{:?}", err)), + HardwareSpecificError::HardwareSpecificError("btleplug".to_owned(), format!("{e:?}")) + .to_string(), ); return Err(return_err); } if let Err(err) = self.device.discover_services().await { error!("BTLEPlug error discovering characteristics: {:?}", err); return Err(ButtplugDeviceError::DeviceConnectionError(format!( - "BTLEPlug error discovering characteristics: {:?}", - err + "BTLEPlug error discovering characteristics: {err:?}" ))); } } @@ -212,18 +208,14 @@ impl HardwareSpecializer for BtleplugHardwareSpecializer { endpoints.clone(), uuid_map, ); - let mut hardware = Hardware::new( + Ok(Hardware::new( &self.name, - &format!("{:?}", address), + &format!("{address:?}"), &endpoints.keys().cloned().collect::>(), + &Some(Duration::from_millis(75)), + self.requires_keepalive, Box::new(device_internal_impl), - ); - - // Let the hardware know if we need command resends or whatever. Fucking iOS. - if self.requires_keepalive { - hardware.set_requires_keepalive(); - } - Ok(hardware) + )) } } @@ -247,11 +239,11 @@ impl BtlePlugHardware { let event_stream_clone = event_stream.clone(); let address = device.id(); let name_clone = name.to_owned(); - async_manager::spawn(async move { + let _ = async_manager::spawn(async move { let mut error_notification = false; loop { select! { - notification = notification_stream.next().fuse() => { + notification = notification_stream.next() => { if let Some(notification) = notification { let endpoint = if let Some(endpoint) = uuid_map.get(¬ification.uuid) { *endpoint @@ -270,7 +262,7 @@ impl BtlePlugHardware { continue; } if let Err(err) = event_stream_clone.send(HardwareEvent::Notification( - format!("{:?}", address), + format!("{address:?}"), endpoint, notification.value, )) { @@ -282,7 +274,7 @@ impl BtlePlugHardware { } } } - adapter_event = adapter_event_stream.next().fuse() => { + adapter_event = adapter_event_stream.next() => { if let Some(CentralEvent::DeviceDisconnected(addr)) = adapter_event { if address == addr { info!( @@ -292,7 +284,7 @@ impl BtlePlugHardware { if event_stream_clone.receiver_count() != 0 { if let Err(err) = event_stream_clone .send(HardwareEvent::Disconnected( - format!("{:?}", address) + format!("{address:?}") )) { error!( "Cannot send notification, device object disappeared: {:?}", @@ -340,15 +332,15 @@ impl HardwareInternal for BtlePlugHardware { &self, msg: &HardwareWriteCmd, ) -> BoxFuture<'static, Result<(), ButtplugDeviceError>> { - let characteristic = match self.endpoints.get(&msg.endpoint) { + let characteristic = match self.endpoints.get(&msg.endpoint()) { Some(chr) => chr.clone(), None => { - return future::ready(Err(ButtplugDeviceError::InvalidEndpoint(msg.endpoint))).boxed(); + return future::ready(Err(ButtplugDeviceError::InvalidEndpoint(msg.endpoint().to_string()))).boxed(); } }; let device = self.device.clone(); - let mut write_type = if msg.write_with_response { + let mut write_type = if msg.write_with_response() { WriteType::WithResponse } else { WriteType::WithoutResponse @@ -383,7 +375,7 @@ impl HardwareInternal for BtlePlugHardware { } } - let data = msg.data.clone(); + let data = msg.data().clone(); async move { match device.write(&characteristic, &data, write_type).await { Ok(()) => { @@ -395,10 +387,11 @@ impl HardwareInternal for BtlePlugHardware { ); Ok(()) } - Err(err) => { - error!("BTLEPlug device write error: {:?}", err); + Err(e) => { + error!("BTLEPlug device write error: {:?}", e); Err(ButtplugDeviceError::DeviceSpecificError( - HardwareSpecificError::BtleplugError(format!("{:?}", err)), + HardwareSpecificError::HardwareSpecificError("btleplug".to_owned(), format!("{e:?}")) + .to_string(), )) } } @@ -412,24 +405,25 @@ impl HardwareInternal for BtlePlugHardware { ) -> BoxFuture<'static, Result> { // Right now we only need read for doing a whitelist check on devices. We // don't care about the data we get back. - let characteristic = match self.endpoints.get(&msg.endpoint) { + let characteristic = match self.endpoints.get(&msg.endpoint()) { Some(chr) => chr.clone(), None => { - return future::ready(Err(ButtplugDeviceError::InvalidEndpoint(msg.endpoint))).boxed(); + return future::ready(Err(ButtplugDeviceError::InvalidEndpoint(msg.endpoint().to_string()))).boxed(); } }; let device = self.device.clone(); - let endpoint = msg.endpoint; + let endpoint = msg.endpoint(); async move { match device.read(&characteristic).await { Ok(data) => { trace!("Got reading: {:?}", data); Ok(HardwareReading::new(endpoint, &data)) } - Err(err) => { - error!("BTLEPlug device read error: {:?}", err); + Err(e) => { + error!("BTLEPlug device read error: {:?}", e); Err(ButtplugDeviceError::DeviceSpecificError( - HardwareSpecificError::BtleplugError(format!("{:?}", err)), + HardwareSpecificError::HardwareSpecificError("btleplug".to_owned(), format!("{e:?}")) + .to_string(), )) } } @@ -441,7 +435,7 @@ impl HardwareInternal for BtlePlugHardware { &self, msg: &HardwareSubscribeCmd, ) -> BoxFuture<'static, Result<(), ButtplugDeviceError>> { - let endpoint = msg.endpoint; + let endpoint = msg.endpoint(); if self.subscribed_endpoints.contains(&endpoint) { debug!( "Endpoint {} already subscribed, ignoring and returning Ok.", @@ -452,17 +446,17 @@ impl HardwareInternal for BtlePlugHardware { let characteristic = match self.endpoints.get(&endpoint) { Some(chr) => chr.clone(), None => { - return future::ready(Err(ButtplugDeviceError::InvalidEndpoint(msg.endpoint))).boxed(); + return future::ready(Err(ButtplugDeviceError::InvalidEndpoint(msg.endpoint().to_string()))).boxed(); } }; let endpoints = self.subscribed_endpoints.clone(); let device = self.device.clone(); async move { device.subscribe(&characteristic).await.map_err(|e| { - ButtplugDeviceError::DeviceSpecificError(HardwareSpecificError::BtleplugError(format!( - "{:?}", - e - ))) + ButtplugDeviceError::DeviceSpecificError( + HardwareSpecificError::HardwareSpecificError("btleplug".to_owned(), format!("{e:?}")) + .to_string(), + ) })?; endpoints.insert(endpoint); Ok(()) @@ -474,7 +468,7 @@ impl HardwareInternal for BtlePlugHardware { &self, msg: &HardwareUnsubscribeCmd, ) -> BoxFuture<'static, Result<(), ButtplugDeviceError>> { - let endpoint = msg.endpoint; + let endpoint = msg.endpoint(); if !self.subscribed_endpoints.contains(&endpoint) { debug!( "Endpoint {} already unsubscribed, ignoring and returning Ok.", @@ -482,20 +476,20 @@ impl HardwareInternal for BtlePlugHardware { ); return future::ready(Ok(())).boxed(); } - let characteristic = match self.endpoints.get(&msg.endpoint) { + let characteristic = match self.endpoints.get(&msg.endpoint()) { Some(chr) => chr.clone(), None => { - return future::ready(Err(ButtplugDeviceError::InvalidEndpoint(msg.endpoint))).boxed(); + return future::ready(Err(ButtplugDeviceError::InvalidEndpoint(msg.endpoint().to_string()))).boxed(); } }; let endpoints = self.subscribed_endpoints.clone(); let device = self.device.clone(); async move { device.unsubscribe(&characteristic).await.map_err(|e| { - ButtplugDeviceError::DeviceSpecificError(HardwareSpecificError::BtleplugError(format!( - "{:?}", - e - ))) + ButtplugDeviceError::DeviceSpecificError( + HardwareSpecificError::HardwareSpecificError("btleplug".to_owned(), format!("{e:?}")) + .to_string(), + ) })?; endpoints.remove(&endpoint); Ok(()) diff --git a/buttplug/src/server/device/hardware/communication/btleplug/mod.rs b/crates/buttplug_server_hwmgr_btleplug/src/lib.rs similarity index 92% rename from buttplug/src/server/device/hardware/communication/btleplug/mod.rs rename to crates/buttplug_server_hwmgr_btleplug/src/lib.rs index db6b809f9..fdd525b2f 100644 --- a/buttplug/src/server/device/hardware/communication/btleplug/mod.rs +++ b/crates/buttplug_server_hwmgr_btleplug/src/lib.rs @@ -5,6 +5,9 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. +#[macro_use] +extern crate log; + pub mod btleplug_comm_manager; pub use btleplug_comm_manager::BtlePlugCommunicationManagerBuilder; mod btleplug_adapter_task; diff --git a/crates/buttplug_server_hwmgr_hid/Cargo.toml b/crates/buttplug_server_hwmgr_hid/Cargo.toml new file mode 100644 index 000000000..5e450e56e --- /dev/null +++ b/crates/buttplug_server_hwmgr_hid/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "buttplug_server_hwmgr_hid" +version = "10.0.0" +authors = ["Nonpolynomial Labs, LLC "] +description = "Buttplug Intimate Hardware Control Library - Core Library" +license = "BSD-3-Clause" +homepage = "http://buttplug.io" +repository = "https://github.com/buttplugio/buttplug.git" +readme = "./README.md" +keywords = ["usb", "serial", "hardware", "bluetooth", "teledildonics"] +edition = "2024" +exclude = ["examples/**"] + +[lib] +name = "buttplug_server_hwmgr_hid" +path = "src/lib.rs" +test = true +doctest = true +doc = true + + +[dependencies] +buttplug_derive = "0.8.1" +# buttplug_derive = { path = "../buttplug_derive" } +buttplug_core = { path = "../buttplug_core" } +buttplug_server = { path = "../buttplug_server" } +buttplug_server_device_config = { path = "../buttplug_server_device_config" } +futures = "0.3.31" +futures-util = "0.3.31" +log = "0.4.27" +tokio = { version = "1.46.1", features = ["sync", "time"] } +async-trait = "0.1.88" +uuid = { version = "1.17.0", features = ["serde", "v4"] } +dashmap = { version = "6.1.0", features = ["serde"] } +tracing = "0.1.41" +thiserror = "2.0.12" + +[target.'cfg(target_os = "windows")'.dependencies] +hidapi = { version = "2.6.3", default-features = false, features = ["windows-native"] } + +[target.'cfg(target_os = "linux")'.dependencies] +# Linux hidraw is needed here in order to work with the lovense dongle. libusb breaks it on linux. +# Other platforms are not affected by the feature changes. +hidapi = { version = "2.6.3", default-features = false, features = ["linux-static-hidraw"] } + +[target.'cfg(target_os = "macos")'.dependencies] +hidapi = { version = "2.6.3", default-features = false, features = ["macos-shared-device"] } diff --git a/buttplug/src/server/device/hardware/communication/hid/hid_comm_manager.rs b/crates/buttplug_server_hwmgr_hid/src/hid_comm_manager.rs similarity index 87% rename from buttplug/src/server/device/hardware/communication/hid/hid_comm_manager.rs rename to crates/buttplug_server_hwmgr_hid/src/hid_comm_manager.rs index 8a008a995..01a34e091 100644 --- a/buttplug/src/server/device/hardware/communication/hid/hid_comm_manager.rs +++ b/crates/buttplug_server_hwmgr_hid/src/hid_comm_manager.rs @@ -1,15 +1,14 @@ -use crate::{ - core::errors::ButtplugDeviceError, - server::device::hardware::communication::{ - HardwareCommunicationManager, - HardwareCommunicationManagerBuilder, - HardwareCommunicationManagerEvent, - TimedRetryCommunicationManager, - TimedRetryCommunicationManagerImpl, - }, -}; use async_trait::async_trait; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server::device::hardware::communication::{ + HardwareCommunicationManager, + HardwareCommunicationManagerBuilder, + HardwareCommunicationManagerEvent, + TimedRetryCommunicationManager, + TimedRetryCommunicationManagerImpl, +}; use hidapi::HidApi; +use log::*; use std::sync::Arc; use tokio::sync::mpsc::Sender; @@ -64,7 +63,7 @@ impl TimedRetryCommunicationManagerImpl for HidCommunicationManager { continue; } seen_addresses.push(serial_number.clone()); - let device_creator = HidHardwareConnector::new(api.clone(), &device); + let device_creator = HidHardwareConnector::new(api.clone(), device); if device_sender .send(HardwareCommunicationManagerEvent::DeviceFound { name: device.product_string().unwrap().to_owned(), diff --git a/buttplug/src/server/device/hardware/communication/hid/hid_device_impl.rs b/crates/buttplug_server_hwmgr_hid/src/hid_device_impl.rs similarity index 81% rename from buttplug/src/server/device/hardware/communication/hid/hid_device_impl.rs rename to crates/buttplug_server_hwmgr_hid/src/hid_device_impl.rs index 4ce736642..33044d2d6 100644 --- a/buttplug/src/server/device/hardware/communication/hid/hid_device_impl.rs +++ b/crates/buttplug_server_hwmgr_hid/src/hid_device_impl.rs @@ -1,25 +1,20 @@ use super::hidapi_async::HidAsyncDevice; -use crate::{ - core::errors::ButtplugDeviceError, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, VIDPIDSpecifier}, - hardware::{ - Endpoint, - GenericHardwareSpecializer, - Hardware, - HardwareConnector, - HardwareEvent, - HardwareInternal, - HardwareReadCmd, - HardwareReading, - HardwareSpecializer, - HardwareSubscribeCmd, - HardwareUnsubscribeCmd, - HardwareWriteCmd, - }, - }, -}; use async_trait::async_trait; +use buttplug_core::{errors::ButtplugDeviceError}; +use buttplug_server::device::hardware::{ + GenericHardwareSpecializer, + Hardware, + HardwareConnector, + HardwareEvent, + HardwareInternal, + HardwareReadCmd, + HardwareReading, + HardwareSpecializer, + HardwareSubscribeCmd, + HardwareUnsubscribeCmd, + HardwareWriteCmd, +}; +use buttplug_server_device_config::{ProtocolCommunicationSpecifier, VIDPIDSpecifier, Endpoint}; use futures::{future::BoxFuture, AsyncWriteExt}; use hidapi::{DeviceInfo, HidApi}; use std::{ @@ -77,9 +72,11 @@ impl HardwareConnector for HidHardwareConnector { self.device_info.product_string().unwrap() ); let hardware = Hardware::new( - &self.device_info.product_string().unwrap(), - &self.device_info.serial_number().unwrap(), + self.device_info.product_string().unwrap(), + self.device_info.serial_number().unwrap(), &[Endpoint::Rx, Endpoint::Tx], + &None, + false, Box::new(device_impl_internal), ); Ok(Box::new(GenericHardwareSpecializer::new(hardware))) @@ -111,7 +108,7 @@ impl HardwareInternal for HIDDeviceImpl { fn disconnect(&self) -> BoxFuture<'static, Result<(), ButtplugDeviceError>> { let connected = self.connected.clone(); Box::pin(async move { - connected.store(false, Ordering::SeqCst); + connected.store(false, Ordering::Relaxed); Ok(()) }) } @@ -128,13 +125,10 @@ impl HardwareInternal for HIDDeviceImpl { msg: &HardwareWriteCmd, ) -> BoxFuture<'static, Result<(), ButtplugDeviceError>> { let device = self.device.clone(); - let data = msg.data.clone(); + let data = msg.data().clone(); Box::pin(async move { device.lock().await.write(&data).await.map_err(|e| { - ButtplugDeviceError::DeviceCommunicationError(format!( - "Cannot write to HID Device: {:?}.", - e - )) + ButtplugDeviceError::DeviceCommunicationError(format!("Cannot write to HID Device: {e:?}.")) })?; Ok(()) }) diff --git a/buttplug/src/server/device/hardware/communication/hid/hidapi_async.rs b/crates/buttplug_server_hwmgr_hid/src/hidapi_async.rs similarity index 89% rename from buttplug/src/server/device/hardware/communication/hid/hidapi_async.rs rename to crates/buttplug_server_hwmgr_hid/src/hidapi_async.rs index 462df035d..b0a25c378 100644 --- a/buttplug/src/server/device/hardware/communication/hid/hidapi_async.rs +++ b/crates/buttplug_server_hwmgr_hid/src/hidapi_async.rs @@ -55,7 +55,7 @@ pub struct HidAsyncDevice { impl Clone for HidAsyncDevice { fn clone(&self) -> Self { Self { - inner: self.inner.as_ref().map(|dev| Arc::clone(&dev)), + inner: self.inner.as_ref().map(Arc::clone), } } } @@ -70,12 +70,10 @@ impl Drop for HidAsyncDevice { drop(req_tx); // Wait for the reader thread to finish - match guard.read_thread.take() { - Some(jh) => match jh.join() { - Ok(_) => info!("device read thread joined"), - Err(_) => {} //error!("failed to join device read thread"), - }, - None => {} //error!("already joined"), + if let Some(jh) = guard.read_thread.take() { + if jh.join().is_ok() { + info!("device read thread joined") + } } } else { //error!("Failed to take lock on device"); @@ -126,7 +124,7 @@ impl HidAsyncDevice { continue; } //debug!("Read data"); - if let Err(_) = data_tx.send(Some(buf)) { + if data_tx.send(Some(buf)).is_err() { //error!("Sending internally: {}", e); break; } @@ -179,21 +177,15 @@ impl AsyncWrite for HidAsyncDevice { //let this: &mut Self = &mut self; //debug!("Will write {} bytes: {:?}", buf.len(), &buf[..]); match self.inner.as_mut().unwrap().lock() { - Ok(guard) => match guard.device.lock() { - Ok(guard) => { + Ok(guard) => { + if let Ok(guard) = guard.device.lock() { guard - .write(&buf[..]) - .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("hidapi failed: {}", e)))?; + .write(buf) + .map_err(|e| io::Error::other(format!("hidapi failed: {e}")))?; //debug!("Wrote: {:?}", &buf[0..max_len]); } - Err(_) => {} //error!("{:?}", e), - }, - Err(e) => { - return Poll::Ready(Err(io::Error::new( - io::ErrorKind::Other, - format!("Mutex broken: {:?}", e), - ))) } + Err(e) => return Poll::Ready(Err(io::Error::other(format!("Mutex broken: {e:?}")))), } buf = &buf[max_len..]; if buf.is_empty() { @@ -234,7 +226,7 @@ impl AsyncRead for HidAsyncDevice { .as_mut() .unwrap() .lock() - .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("Mutex broken: {:?}", e)))?; + .map_err(|e| io::Error::other(format!("Mutex broken: {e:?}")))?; loop { let waker = cx.waker().clone(); match this.rstate { @@ -278,10 +270,7 @@ impl AsyncRead for HidAsyncDevice { } Err(e) => match e { mpsc::TryRecvError::Disconnected => { - return Poll::Ready(Err(io::Error::new( - io::ErrorKind::Other, - "Inner channel dead", - ))); + return Poll::Ready(Err(io::Error::other("Inner channel dead"))); } mpsc::TryRecvError::Empty => { return Poll::Pending; diff --git a/buttplug/src/server/device/hardware/communication/hid/mod.rs b/crates/buttplug_server_hwmgr_hid/src/lib.rs similarity index 82% rename from buttplug/src/server/device/hardware/communication/hid/mod.rs rename to crates/buttplug_server_hwmgr_hid/src/lib.rs index 3a5c4a766..321fe9ddb 100644 --- a/buttplug/src/server/device/hardware/communication/hid/mod.rs +++ b/crates/buttplug_server_hwmgr_hid/src/lib.rs @@ -1,3 +1,6 @@ +#[macro_use] +extern crate log; + pub mod hid_comm_manager; pub mod hid_device_impl; mod hidapi_async; diff --git a/crates/buttplug_server_hwmgr_lovense_connect/Cargo.toml b/crates/buttplug_server_hwmgr_lovense_connect/Cargo.toml new file mode 100644 index 000000000..0916ecea4 --- /dev/null +++ b/crates/buttplug_server_hwmgr_lovense_connect/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "buttplug_server_hwmgr_lovense_connect" +version = "10.0.0" +authors = ["Nonpolynomial Labs, LLC "] +description = "Buttplug Intimate Hardware Control Library - Core Library" +license = "BSD-3-Clause" +homepage = "http://buttplug.io" +repository = "https://github.com/buttplugio/buttplug.git" +readme = "./README.md" +keywords = ["usb", "serial", "hardware", "bluetooth", "teledildonics"] +edition = "2024" +exclude = ["examples/**"] + +[lib] +name = "buttplug_server_hwmgr_lovense_connect" +path = "src/lib.rs" +test = true +doctest = true +doc = true + + + +# Only build docs on one platform (linux) +[package.metadata.docs.rs] +targets = [] +# Features to pass to Cargo (default: []) +features = ["default", "unstable"] + +[dependencies] +buttplug_derive = "0.8.1" +# buttplug_derive = { path = "../buttplug_derive" } +buttplug_core = { path = "../buttplug_core" } +buttplug_server = { path = "../buttplug_server" } +buttplug_server_device_config = { path = "../buttplug_server_device_config" } +futures = "0.3.31" +futures-util = "0.3.31" +log = "0.4.27" +tokio = { version = "1.46.1", features = ["sync", "time"] } +async-trait = "0.1.88" +uuid = { version = "1.17.0", features = ["serde", "v4"] } +dashmap = { version = "6.1.0", features = ["serde"] } +tracing = "0.1.41" +thiserror = "2.0.12" +reqwest = { version = "0.12.22", default-features = false, features = ["rustls-tls"] } +rustls = { version = "0.23.29", default-features = false, features = ["ring"]} +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" +serde-aux = "4.7.0" diff --git a/buttplug/src/server/device/hardware/communication/lovense_connect_service/mod.rs b/crates/buttplug_server_hwmgr_lovense_connect/src/lib.rs similarity index 94% rename from buttplug/src/server/device/hardware/communication/lovense_connect_service/mod.rs rename to crates/buttplug_server_hwmgr_lovense_connect/src/lib.rs index e3aefd3d9..29d76a88e 100644 --- a/buttplug/src/server/device/hardware/communication/lovense_connect_service/mod.rs +++ b/crates/buttplug_server_hwmgr_lovense_connect/src/lib.rs @@ -5,6 +5,9 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. +#[macro_use] +extern crate log; + mod lovense_connect_service_comm_manager; mod lovense_connect_service_hardware; pub use lovense_connect_service_comm_manager::{ diff --git a/buttplug/src/server/device/hardware/communication/lovense_connect_service/lovense_connect_service_comm_manager.rs b/crates/buttplug_server_hwmgr_lovense_connect/src/lovense_connect_service_comm_manager.rs similarity index 96% rename from buttplug/src/server/device/hardware/communication/lovense_connect_service/lovense_connect_service_comm_manager.rs rename to crates/buttplug_server_hwmgr_lovense_connect/src/lovense_connect_service_comm_manager.rs index 2b23ac785..4a9db06ce 100644 --- a/buttplug/src/server/device/hardware/communication/lovense_connect_service/lovense_connect_service_comm_manager.rs +++ b/crates/buttplug_server_hwmgr_lovense_connect/src/lovense_connect_service_comm_manager.rs @@ -6,17 +6,15 @@ // for full license information. use super::lovense_connect_service_hardware::LovenseServiceHardwareConnector; -use crate::{ - core::errors::ButtplugDeviceError, - server::device::hardware::communication::{ - HardwareCommunicationManager, - HardwareCommunicationManagerBuilder, - HardwareCommunicationManagerEvent, - TimedRetryCommunicationManager, - TimedRetryCommunicationManagerImpl, - }, -}; use async_trait::async_trait; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server::device::hardware::communication::{ + HardwareCommunicationManager, + HardwareCommunicationManagerBuilder, + HardwareCommunicationManagerEvent, + TimedRetryCommunicationManager, + TimedRetryCommunicationManagerImpl, +}; use dashmap::DashSet; use reqwest::StatusCode; use serde::{Deserialize, Deserializer}; @@ -130,7 +128,7 @@ pub struct LovenseConnectServiceCommunicationManager { } pub(super) async fn get_local_info(host: &str) -> Option { - match reqwest::get(format!("{}/GetToys", host)).await { + match reqwest::get(format!("{host}/GetToys")).await { Ok(res) => { if res.status() != StatusCode::OK { error!( diff --git a/buttplug/src/server/device/hardware/communication/lovense_connect_service/lovense_connect_service_hardware.rs b/crates/buttplug_server_hwmgr_lovense_connect/src/lovense_connect_service_hardware.rs similarity index 88% rename from buttplug/src/server/device/hardware/communication/lovense_connect_service/lovense_connect_service_hardware.rs rename to crates/buttplug_server_hwmgr_lovense_connect/src/lovense_connect_service_hardware.rs index 2ec150113..d901ac1ca 100644 --- a/buttplug/src/server/device/hardware/communication/lovense_connect_service/lovense_connect_service_hardware.rs +++ b/crates/buttplug_server_hwmgr_lovense_connect/src/lovense_connect_service_hardware.rs @@ -6,27 +6,26 @@ // for full license information. use super::lovense_connect_service_comm_manager::{get_local_info, LovenseServiceToyInfo}; -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - configuration::{LovenseConnectServiceSpecifier, ProtocolCommunicationSpecifier}, - hardware::{ - GenericHardwareSpecializer, - Hardware, - HardwareConnector, - HardwareEvent, - HardwareInternal, - HardwareReadCmd, - HardwareReading, - HardwareSpecializer, - HardwareSubscribeCmd, - HardwareUnsubscribeCmd, - HardwareWriteCmd, - }, - }, - util::async_manager, -}; use async_trait::async_trait; +use buttplug_core::{errors::ButtplugDeviceError, util::async_manager}; +use buttplug_server::device::hardware::{ + GenericHardwareSpecializer, + Hardware, + HardwareConnector, + HardwareEvent, + HardwareInternal, + HardwareReadCmd, + HardwareReading, + HardwareSpecializer, + HardwareSubscribeCmd, + HardwareUnsubscribeCmd, + HardwareWriteCmd, +}; +use buttplug_server_device_config::{ + LovenseConnectServiceSpecifier, + ProtocolCommunicationSpecifier, + Endpoint, +}; use futures::future::{self, BoxFuture, FutureExt}; use std::{ fmt::{self, Debug}, @@ -71,6 +70,8 @@ impl HardwareConnector for LovenseServiceHardwareConnector { &self.toy_info.name, &self.toy_info.id, &[Endpoint::Tx], + &None, + false, Box::new(hardware_internal), ); Ok(Box::new(GenericHardwareSpecializer::new(hardware))) @@ -107,7 +108,7 @@ impl LovenseServiceHardware { info!("Exiting lovense service device connection check loop."); break; } - battery_level_clone.store(toy.battery.clamp(0, 100) as u8, Ordering::SeqCst); + battery_level_clone.store(toy.battery.clamp(0, 100) as u8, Ordering::Relaxed); break; } } @@ -145,7 +146,7 @@ impl HardwareInternal for LovenseServiceHardware { async move { Ok(HardwareReading::new( Endpoint::Rx, - &[battery_level.load(Ordering::SeqCst)], + &[battery_level.load(Ordering::Relaxed)], )) } .boxed() @@ -158,7 +159,7 @@ impl HardwareInternal for LovenseServiceHardware { let command_url = format!( "{}/{}", self.http_host, - std::str::from_utf8(&msg.data) + std::str::from_utf8(&msg.data()) .expect("We build this in the protocol then have to serialize to [u8], but it's a string.") ); @@ -169,7 +170,7 @@ impl HardwareInternal for LovenseServiceHardware { async_manager::spawn(async move { trace!( "Got http response: {}", - res.text().await.unwrap_or(format!("no response")) + res.text().await.unwrap_or("no response".to_string()) ); }); Ok(()) diff --git a/crates/buttplug_server_hwmgr_lovense_dongle/Cargo.toml b/crates/buttplug_server_hwmgr_lovense_dongle/Cargo.toml new file mode 100644 index 000000000..7c624117d --- /dev/null +++ b/crates/buttplug_server_hwmgr_lovense_dongle/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "buttplug_server_hwmgr_lovense_dongle" +version = "10.0.0" +authors = ["Nonpolynomial Labs, LLC "] +description = "Buttplug Intimate Hardware Control Library - Core Library" +license = "BSD-3-Clause" +homepage = "http://buttplug.io" +repository = "https://github.com/buttplugio/buttplug.git" +readme = "./README.md" +keywords = ["usb", "serial", "hardware", "bluetooth", "teledildonics"] +edition = "2024" +exclude = ["examples/**"] + +[lib] +name = "buttplug_server_hwmgr_lovense_dongle" +path = "src/lib.rs" +test = true +doctest = true +doc = true + + +[dependencies] +buttplug_derive = "0.8.1" +# buttplug_derive = { path = "../buttplug_derive" } +buttplug_core = { path = "../buttplug_core" } +buttplug_server = { path = "../buttplug_server" } +buttplug_server_device_config = { path = "../buttplug_server_device_config" } +futures = "0.3.31" +futures-util = "0.3.31" +log = "0.4.27" +tokio = { version = "1.46.1", features = ["sync", "time", "rt"] } +async-trait = "0.1.88" +uuid = { version = "1.17.0", features = ["serde", "v4"] } +dashmap = { version = "6.1.0", features = ["serde"] } +tracing = "0.1.41" +thiserror = "2.0.12" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" +serde_repr = "0.1.20" +tokio-util = "0.7.15" +tracing-futures = "0.2.5" + +[target.'cfg(target_os = "windows")'.dependencies] +hidapi = { version = "2.6.3", default-features = false, features = ["windows-native"] } + +[target.'cfg(target_os = "linux")'.dependencies] +# Linux hidraw is needed here in order to work with the lovense dongle. libusb breaks it on linux. +# Other platforms are not affected by the feature changes. +hidapi = { version = "2.6.3", default-features = false, features = ["linux-static-hidraw"] } + +[target.'cfg(target_os = "macos")'.dependencies] +hidapi = { version = "2.6.3", default-features = false, features = ["macos-shared-device"] } diff --git a/buttplug/src/server/device/hardware/communication/lovense_dongle/mod.rs b/crates/buttplug_server_hwmgr_lovense_dongle/src/lib.rs similarity index 77% rename from buttplug/src/server/device/hardware/communication/lovense_dongle/mod.rs rename to crates/buttplug_server_hwmgr_lovense_dongle/src/lib.rs index f1dcb4b89..0050f7894 100644 --- a/buttplug/src/server/device/hardware/communication/lovense_dongle/mod.rs +++ b/crates/buttplug_server_hwmgr_lovense_dongle/src/lib.rs @@ -5,18 +5,16 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. +#[macro_use] +extern crate log; + pub mod lovense_dongle_hardware; mod lovense_dongle_messages; mod lovense_dongle_state_machine; pub mod lovense_hid_dongle_comm_manager; -pub mod lovense_serial_dongle_comm_manager; pub use lovense_dongle_hardware::{LovenseDongleHardware, LovenseDongleHardwareConnector}; pub use lovense_hid_dongle_comm_manager::{ LovenseHIDDongleCommunicationManager, LovenseHIDDongleCommunicationManagerBuilder, }; -pub use lovense_serial_dongle_comm_manager::{ - LovenseSerialDongleCommunicationManager, - LovenseSerialDongleCommunicationManagerBuilder, -}; diff --git a/buttplug/src/server/device/hardware/communication/lovense_dongle/lovense_dongle_hardware.rs b/crates/buttplug_server_hwmgr_lovense_dongle/src/lovense_dongle_hardware.rs similarity index 92% rename from buttplug/src/server/device/hardware/communication/lovense_dongle/lovense_dongle_hardware.rs rename to crates/buttplug_server_hwmgr_lovense_dongle/src/lovense_dongle_hardware.rs index 8deb422a4..d04728bb5 100644 --- a/buttplug/src/server/device/hardware/communication/lovense_dongle/lovense_dongle_hardware.rs +++ b/crates/buttplug_server_hwmgr_lovense_dongle/src/lovense_dongle_hardware.rs @@ -12,27 +12,22 @@ use super::lovense_dongle_messages::{ LovenseDongleOutgoingMessage, OutgoingLovenseData, }; -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - configuration::{BluetoothLESpecifier, ProtocolCommunicationSpecifier}, - hardware::{ - GenericHardwareSpecializer, - Hardware, - HardwareConnector, - HardwareEvent, - HardwareInternal, - HardwareReadCmd, - HardwareReading, - HardwareSpecializer, - HardwareSubscribeCmd, - HardwareUnsubscribeCmd, - HardwareWriteCmd, - }, - }, - util::async_manager, -}; use async_trait::async_trait; +use buttplug_core::{errors::ButtplugDeviceError, util::async_manager}; +use buttplug_server::device::hardware::{ + GenericHardwareSpecializer, + Hardware, + HardwareConnector, + HardwareEvent, + HardwareInternal, + HardwareReadCmd, + HardwareReading, + HardwareSpecializer, + HardwareSubscribeCmd, + HardwareUnsubscribeCmd, + HardwareWriteCmd, +}; +use buttplug_server_device_config::{BluetoothLESpecifier, ProtocolCommunicationSpecifier, Endpoint}; use futures::future::{self, BoxFuture, FutureExt}; use std::{ collections::HashMap, @@ -41,6 +36,7 @@ use std::{ atomic::{AtomicBool, Ordering}, Arc, }, + time::Duration, }; use tokio::sync::{broadcast, mpsc}; @@ -103,6 +99,8 @@ impl HardwareConnector for LovenseDongleHardwareConnector { "Lovense Dongle Device", &self.id, &[Endpoint::Rx, Endpoint::Tx], + &Some(Duration::from_millis(75)), + false, Box::new(hardware_internal), ); Ok(Box::new(GenericHardwareSpecializer::new(device))) @@ -176,7 +174,7 @@ impl HardwareInternal for LovenseDongleHardware { fn disconnect(&self) -> BoxFuture<'static, Result<(), ButtplugDeviceError>> { let connected = self.connected.clone(); async move { - connected.store(false, Ordering::SeqCst); + connected.store(false, Ordering::Relaxed); Ok(()) } .boxed() @@ -198,7 +196,7 @@ impl HardwareInternal for LovenseDongleHardware { ) -> BoxFuture<'static, Result<(), ButtplugDeviceError>> { let port_sender = self.device_outgoing.clone(); let address = self.address.clone(); - let data = msg.data.clone(); + let data = msg.data().clone(); async move { let outgoing_msg = LovenseDongleOutgoingMessage { func: LovenseDongleMessageFunc::Command, diff --git a/buttplug/src/server/device/hardware/communication/lovense_dongle/lovense_dongle_messages.rs b/crates/buttplug_server_hwmgr_lovense_dongle/src/lovense_dongle_messages.rs similarity index 100% rename from buttplug/src/server/device/hardware/communication/lovense_dongle/lovense_dongle_messages.rs rename to crates/buttplug_server_hwmgr_lovense_dongle/src/lovense_dongle_messages.rs diff --git a/buttplug/src/server/device/hardware/communication/lovense_dongle/lovense_dongle_state_machine.rs b/crates/buttplug_server_hwmgr_lovense_dongle/src/lovense_dongle_state_machine.rs similarity index 98% rename from buttplug/src/server/device/hardware/communication/lovense_dongle/lovense_dongle_state_machine.rs rename to crates/buttplug_server_hwmgr_lovense_dongle/src/lovense_dongle_state_machine.rs index dc7cd492f..fb7b6942d 100644 --- a/buttplug/src/server/device/hardware/communication/lovense_dongle/lovense_dongle_state_machine.rs +++ b/crates/buttplug_server_hwmgr_lovense_dongle/src/lovense_dongle_state_machine.rs @@ -6,9 +6,9 @@ // for full license information. use super::{lovense_dongle_hardware::*, lovense_dongle_messages::*}; -use crate::server::device::hardware::communication::HardwareCommunicationManagerEvent; use async_trait::async_trait; -use futures::{select, FutureExt}; +use buttplug_server::device::hardware::communication::HardwareCommunicationManagerEvent; +use futures::{pin_mut, select, FutureExt}; use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, @@ -61,7 +61,7 @@ impl ChannelHub { } pub fn create_new_wait_for_dongle_state(self) -> Option> { - self.is_scanning.store(false, Ordering::SeqCst); + self.is_scanning.store(false, Ordering::Relaxed); Some(Box::new(LovenseDongleWaitForDongle::new( self.comm_manager_incoming, self.event_outgoing, @@ -154,7 +154,7 @@ impl ChannelHub { } pub fn set_scanning_status(&self, is_scanning: bool) { - self.is_scanning.store(is_scanning, Ordering::SeqCst); + self.is_scanning.store(is_scanning, Ordering::Relaxed); } } @@ -244,12 +244,12 @@ impl LovenseDongleState for LovenseDongleWaitForDongle { } LovenseDeviceCommand::StartScanning => { debug!("Lovense dongle not found, storing StartScanning command until found."); - self.is_scanning.store(true, Ordering::SeqCst); + self.is_scanning.store(true, Ordering::Relaxed); should_scan = true; } LovenseDeviceCommand::StopScanning => { debug!("Lovense dongle not found, clearing StartScanning command and emitting ScanningFinished."); - self.is_scanning.store(false, Ordering::SeqCst); + self.is_scanning.store(false, Ordering::Relaxed); should_scan = false; // If we were requested to scan and then asked to stop, act like we at least tried. if self diff --git a/buttplug/src/server/device/hardware/communication/lovense_dongle/lovense_hid_dongle_comm_manager.rs b/crates/buttplug_server_hwmgr_lovense_dongle/src/lovense_hid_dongle_comm_manager.rs similarity index 94% rename from buttplug/src/server/device/hardware/communication/lovense_dongle/lovense_hid_dongle_comm_manager.rs rename to crates/buttplug_server_hwmgr_lovense_dongle/src/lovense_hid_dongle_comm_manager.rs index ceb01f7ab..7744c3860 100644 --- a/buttplug/src/server/device/hardware/communication/lovense_dongle/lovense_hid_dongle_comm_manager.rs +++ b/crates/buttplug_server_hwmgr_lovense_dongle/src/lovense_hid_dongle_comm_manager.rs @@ -13,14 +13,11 @@ use super::{ }, lovense_dongle_state_machine::create_lovense_dongle_machine, }; -use crate::{ - core::{errors::ButtplugDeviceError, ButtplugResultFuture}, - server::device::hardware::communication::{ - HardwareCommunicationManager, - HardwareCommunicationManagerBuilder, - HardwareCommunicationManagerEvent, - }, - util::async_manager, +use buttplug_core::{errors::ButtplugDeviceError, util::async_manager, ButtplugResultFuture}; +use buttplug_server::device::hardware::communication::{ + HardwareCommunicationManager, + HardwareCommunicationManagerBuilder, + HardwareCommunicationManagerEvent, }; use futures::FutureExt; use hidapi::{HidApi, HidDevice}; @@ -34,6 +31,7 @@ use std::{ }; use tokio::{ runtime, + select, sync::{ mpsc::{channel, Receiver, Sender}, Mutex, @@ -75,8 +73,8 @@ fn hid_write_thread( while let Some(data) = rt.block_on(async { select! { - _ = token.cancelled().fuse() => None, - data = receiver.recv().fuse() => data + _ = token.cancelled() => None, + data = receiver.recv() => data } }) { match data { @@ -242,7 +240,7 @@ impl LovenseHIDDongleCommunicationManager { ButtplugDeviceError::DeviceConnectionError("Cannot find lovense HID Dongle.".to_owned()) })?; - dongle_available.store(true, Ordering::SeqCst); + dongle_available.store(true, Ordering::Relaxed); let read_thread = thread::Builder::new() .name("Lovense Dongle HID Reader Thread".to_string()) @@ -288,7 +286,7 @@ impl HardwareCommunicationManager for LovenseHIDDongleCommunicationManager { fn start_scanning(&mut self) -> ButtplugResultFuture { debug!("Lovense Dongle Manager scanning for devices"); let sender = self.machine_sender.clone(); - self.is_scanning.store(true, Ordering::SeqCst); + self.is_scanning.store(true, Ordering::Relaxed); async move { sender .send(LovenseDeviceCommand::StartScanning) @@ -312,11 +310,11 @@ impl HardwareCommunicationManager for LovenseHIDDongleCommunicationManager { } fn scanning_status(&self) -> bool { - self.is_scanning.load(Ordering::SeqCst) + self.is_scanning.load(Ordering::Relaxed) } fn can_scan(&self) -> bool { - self.dongle_available.load(Ordering::SeqCst) + self.dongle_available.load(Ordering::Relaxed) } } diff --git a/crates/buttplug_server_hwmgr_serial/Cargo.toml b/crates/buttplug_server_hwmgr_serial/Cargo.toml new file mode 100644 index 000000000..d30e265a3 --- /dev/null +++ b/crates/buttplug_server_hwmgr_serial/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "buttplug_server_hwmgr_serial" +version = "10.0.0" +authors = ["Nonpolynomial Labs, LLC "] +description = "Buttplug Intimate Hardware Control Library - Core Library" +license = "BSD-3-Clause" +homepage = "http://buttplug.io" +repository = "https://github.com/buttplugio/buttplug.git" +readme = "./README.md" +keywords = ["usb", "serial", "hardware", "bluetooth", "teledildonics"] +edition = "2024" +exclude = ["examples/**"] + +[lib] +name = "buttplug_server_hwmgr_serial" +path = "src/lib.rs" +test = true +doctest = true +doc = true + + +[dependencies] +buttplug_derive = "0.8.1" +# buttplug_derive = { path = "../buttplug_derive" } +buttplug_core = { path = "../buttplug_core" } +buttplug_server = { path = "../buttplug_server" } +buttplug_server_device_config = { path = "../buttplug_server_device_config" } +futures = "0.3.31" +futures-util = "0.3.31" +log = "0.4.27" +tokio = { version = "1.46.1", features = ["sync", "time"] } +async-trait = "0.1.88" +uuid = { version = "1.17.0", features = ["serde", "v4"] } +dashmap = { version = "6.1.0", features = ["serde"] } +tracing = "0.1.41" +thiserror = "2.0.12" +serialport = { version = "4.7.2" } +tokio-util = "0.7.15" diff --git a/buttplug/src/server/device/hardware/communication/serialport/mod.rs b/crates/buttplug_server_hwmgr_serial/src/lib.rs similarity index 94% rename from buttplug/src/server/device/hardware/communication/serialport/mod.rs rename to crates/buttplug_server_hwmgr_serial/src/lib.rs index d3ff40f80..04b80639b 100644 --- a/buttplug/src/server/device/hardware/communication/serialport/mod.rs +++ b/crates/buttplug_server_hwmgr_serial/src/lib.rs @@ -5,6 +5,9 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. +#[macro_use] +extern crate log; + mod serialport_comm_manager; mod serialport_hardware; diff --git a/buttplug/src/server/device/hardware/communication/serialport/serialport_comm_manager.rs b/crates/buttplug_server_hwmgr_serial/src/serialport_comm_manager.rs similarity index 89% rename from buttplug/src/server/device/hardware/communication/serialport/serialport_comm_manager.rs rename to crates/buttplug_server_hwmgr_serial/src/serialport_comm_manager.rs index 2858a3dc9..9105b93b9 100644 --- a/buttplug/src/server/device/hardware/communication/serialport/serialport_comm_manager.rs +++ b/crates/buttplug_server_hwmgr_serial/src/serialport_comm_manager.rs @@ -8,17 +8,15 @@ use std::time::Duration; use super::SerialPortHardwareConnector; -use crate::{ - core::errors::ButtplugDeviceError, - server::device::hardware::communication::{ - HardwareCommunicationManager, - HardwareCommunicationManagerBuilder, - HardwareCommunicationManagerEvent, - TimedRetryCommunicationManager, - TimedRetryCommunicationManagerImpl, - }, -}; use async_trait::async_trait; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server::device::hardware::communication::{ + HardwareCommunicationManager, + HardwareCommunicationManagerBuilder, + HardwareCommunicationManagerEvent, + TimedRetryCommunicationManager, + TimedRetryCommunicationManagerImpl, +}; use serialport::available_ports; use tokio::sync::mpsc::Sender; diff --git a/buttplug/src/server/device/hardware/communication/serialport/serialport_hardware.rs b/crates/buttplug_server_hwmgr_serial/src/serialport_hardware.rs similarity index 92% rename from buttplug/src/server/device/hardware/communication/serialport/serialport_hardware.rs rename to crates/buttplug_server_hwmgr_serial/src/serialport_hardware.rs index efc1bf59a..7a7d7f9b8 100644 --- a/buttplug/src/server/device/hardware/communication/serialport/serialport_hardware.rs +++ b/crates/buttplug_server_hwmgr_serial/src/serialport_hardware.rs @@ -5,27 +5,22 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::hardware::communication::HardwareSpecificError, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, SerialSpecifier}, - hardware::{ - Hardware, - HardwareConnector, - HardwareEvent, - HardwareInternal, - HardwareReadCmd, - HardwareReading, - HardwareSpecializer, - HardwareSubscribeCmd, - HardwareUnsubscribeCmd, - HardwareWriteCmd, - }, - }, - util::async_manager, -}; use async_trait::async_trait; +use buttplug_core::{errors::ButtplugDeviceError, util::async_manager}; +use buttplug_server::device::hardware::{ + communication::HardwareSpecificError, + Hardware, + HardwareConnector, + HardwareEvent, + HardwareInternal, + HardwareReadCmd, + HardwareReading, + HardwareSpecializer, + HardwareSubscribeCmd, + HardwareUnsubscribeCmd, + HardwareWriteCmd, +}; +use buttplug_server_device_config::{ProtocolCommunicationSpecifier, SerialSpecifier, Endpoint}; use futures::future; use futures::{future::BoxFuture, FutureExt}; use serialport::{SerialPort, SerialPortInfo}; @@ -104,6 +99,8 @@ impl HardwareSpecializer for SerialPortHardwareSpecialzier { &self.port_info.port_name, &self.port_info.port_name, &[Endpoint::Rx, Endpoint::Tx], + &None, + false, Box::new(hardware_internal), ); Ok(hardware) @@ -229,7 +226,10 @@ impl SerialPortHardware { .await .expect("This will always be a Some value, we're just blocking for bringup") .map_err(|e| { - ButtplugDeviceError::DeviceSpecificError(HardwareSpecificError::SerialError(e.to_string())) + ButtplugDeviceError::DeviceSpecificError( + HardwareSpecificError::HardwareSpecificError("Serial".to_owned(), e.to_string()) + .to_string(), + ) })?; debug!("Serial port received from thread."); let (writer_sender, writer_receiver) = mpsc::channel(256); @@ -249,10 +249,10 @@ impl SerialPortHardware { serial_read_thread(read_port, reader_sender, read_token); connected_clone.store(false, Ordering::Relaxed); if event_stream_clone.receiver_count() != 0 { - if let Err(err) = event_stream_clone - .send(HardwareEvent::Disconnected( - format!("{:?}", &port_name_clone) - )) { + if let Err(err) = event_stream_clone.send(HardwareEvent::Disconnected(format!( + "{:?}", + &port_name_clone + ))) { error!( "Cannot send notification, device object disappeared: {:?}", err @@ -296,7 +296,7 @@ impl HardwareInternal for SerialPortHardware { fn disconnect(&self) -> BoxFuture<'static, Result<(), ButtplugDeviceError>> { let connected = self.connected.clone(); async move { - connected.store(false, Ordering::SeqCst); + connected.store(false, Ordering::Relaxed); Ok(()) } .boxed() @@ -327,7 +327,7 @@ impl HardwareInternal for SerialPortHardware { msg: &HardwareWriteCmd, ) -> BoxFuture<'static, Result<(), ButtplugDeviceError>> { let sender = self.port_sender.clone(); - let data = msg.data.clone(); + let data = msg.data().clone(); // TODO Should check endpoint validity async move { if sender.send(data).await.is_err() { diff --git a/crates/buttplug_server_hwmgr_udp/Cargo.toml b/crates/buttplug_server_hwmgr_udp/Cargo.toml new file mode 100644 index 000000000..768a25a1b --- /dev/null +++ b/crates/buttplug_server_hwmgr_udp/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "buttplug_server_hwmgr_udp" +version = "10.0.0" +authors = ["Millibyte, LLC , + ) -> Box { + Box::new(UdpCommunicationManager::new(sender)) + } +} + +pub struct UdpCommunicationManager { + sender: Sender +} + +impl UdpCommunicationManager { + pub fn new(sender: Sender) -> Self { + trace!("Udp socket created."); + Self { sender, } + } +} + +impl HardwareCommunicationManager for UdpCommunicationManager { + fn name(&self) -> &'static str { + "UdpCommunicationManager" + } + + fn start_scanning(&mut self) -> ButtplugResultFuture { + debug!("Udp scan starting"); + let sender_clone = self.sender.clone(); + async move { + // TODO: Look through confiuration to locate configured UDP + let specifiers = [ + UdpSpecifier::new("192.168.2.185", 8000) + ]; + for specifier in specifiers + { + if sender_clone.send(HardwareCommunicationManagerEvent::DeviceFound { + name: format!("UDP Device {}", specifier.to_string()), + address: specifier.to_string(), + creator: Box::new(UdpHardwareConnector::new( + specifier + )), + }) + .await + .is_err() + { + error!("Device manager disappeared, exiting"); + } + } + Ok(()) + }.boxed() + } + + fn stop_scanning(&mut self) -> ButtplugResultFuture { + debug!("Udp scan stopping"); + async move { Ok(()) }.boxed() + } + + fn can_scan(&self) -> bool { + true + } +} + diff --git a/crates/buttplug_server_hwmgr_udp/src/udp_hardware.rs b/crates/buttplug_server_hwmgr_udp/src/udp_hardware.rs new file mode 100644 index 000000000..c46c4140b --- /dev/null +++ b/crates/buttplug_server_hwmgr_udp/src/udp_hardware.rs @@ -0,0 +1,212 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2025 Nonpolynomial Labs LLC., Milibyte LLC. All rights reserved. +// +// Licensed under the BSD 3-Clause license. See LICENSE file in the project root +// for full license information. + +use async_trait::async_trait; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server::device::hardware::GenericHardwareSpecializer; +use buttplug_server::device::hardware::{ + communication::HardwareSpecificError, + Hardware, + HardwareConnector, + HardwareEvent, + HardwareInternal, + HardwareReadCmd, + HardwareReading, + HardwareSpecializer, + HardwareSubscribeCmd, + HardwareUnsubscribeCmd, + HardwareWriteCmd, +}; +use buttplug_server_device_config::{Endpoint, ProtocolCommunicationSpecifier, UdpSpecifier}; +use futures::future; +use futures::{future::BoxFuture, FutureExt}; +use std::{ + fmt::{self, Debug}, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, +}; +use tokio::sync::{broadcast, mpsc::{channel, Receiver, Sender}}; +use tokio::net::UdpSocket; + +pub struct UdpHardwareConnector { + specifier: UdpSpecifier, +} + +impl UdpHardwareConnector { + pub fn new(specifier: UdpSpecifier) -> Self { + Self { + specifier: specifier.clone(), + } + } +} + +impl Debug for UdpHardwareConnector { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("UdpHardwareConnector") + .finish() + } +} + +#[async_trait] +impl HardwareConnector for UdpHardwareConnector { + fn specifier(&self) -> ProtocolCommunicationSpecifier { + ProtocolCommunicationSpecifier::Udp(self.specifier.clone()) + } + + async fn connect(&mut self) -> Result, ButtplugDeviceError> { + let address = self.specifier.address().clone(); + let port = *self.specifier.port(); + let socket = Arc::new(UdpSocket::bind("0.0.0.0:0") + .await + .map_err(|e| { + ButtplugDeviceError::DeviceSpecificError( + HardwareSpecificError::HardwareSpecificError("UDP-bind".to_owned(), e.to_string()).to_string()) + })?); + socket.connect(format!("{}:{}", address.clone(), port)) + .await + .map_err(|e| { + ButtplugDeviceError::DeviceSpecificError( + HardwareSpecificError::HardwareSpecificError("UDP-connect".to_owned(), e.to_string()).to_string(), + ) + })?; + let hardware_internal = UdpHardware::new( + socket, + address, + port, + ); + let hardware = Hardware::new( + &format!("UDP ({})", self.specifier.to_string()).to_owned(), + &self.specifier.to_string(), + &[Endpoint::Rx, Endpoint::Tx], + &None, + false, + Box::new(hardware_internal), + ); + Ok(Box::new(GenericHardwareSpecializer::new(hardware))) + } +} + +async fn udp_write_thread(socket: Arc, receiver: Receiver>) { + let mut recv = receiver; + // Instead of waiting on a token here, we'll expect that we'll break on our + // channel going away. + while let Some(v) = recv.recv().await { + if let Err(err) = socket.send(&v).await { + warn!("Cannot write data to udp port, exiting thread: {}", err); + return; + } + } +} + +pub struct UdpHardware { + address: Arc, + port: Arc, + socket_sender: Sender>, + connected: Arc, + device_event_sender: broadcast::Sender, +} + +impl UdpHardware { + pub fn name(&self) -> String { + format!("{}:{}", self.address, self.port) + } + + pub fn new(socket: Arc, address: String, port: u16) -> Self { + let (outgoing_sender, outgoing_receiver) = channel(256); + let (device_event_sender, _) = broadcast::channel(256); + // If we've gotten this far, we can expect we have a socket definition. + let connected = Arc::new(AtomicBool::new(true)); + let write_socket = socket.clone(); + tokio::spawn(async move { + udp_write_thread(write_socket, outgoing_receiver).await; + }); + + Self { + address: Arc::new(address.to_owned()), + port: Arc::new(port), + socket_sender: outgoing_sender, + connected, + device_event_sender, + } + } +} + +impl HardwareInternal for UdpHardware { + fn event_stream(&self) -> broadcast::Receiver { + self.device_event_sender.subscribe() + } + + fn disconnect(&self) -> BoxFuture<'static, Result<(), ButtplugDeviceError>> { + let connected = self.connected.clone(); + async move { + connected.store(false, Ordering::Relaxed); + Ok(()) + } + .boxed() + } + + fn read_value( + &self, + _msg: &HardwareReadCmd, + ) -> BoxFuture<'static, Result> { + future::ready(Err(ButtplugDeviceError::UnhandledCommand( + "UDP does not support read".to_owned(), + ))) + .boxed() + } + + fn write_value( + &self, + msg: &HardwareWriteCmd, + ) -> BoxFuture<'static, Result<(), ButtplugDeviceError>> { + let sender = self.socket_sender.clone(); + let data = msg.data().clone(); + // TODO Should check endpoint validity + async move { + if let Err(e) = sender.send(data) + .await + .map_err(|e| { + ButtplugDeviceError::DeviceSpecificError( + HardwareSpecificError::HardwareSpecificError("UDP send-to-thread".to_owned(), e.to_string()).to_string(), + ) + }) + { + warn!("UDP write value: {}", e.to_string()); + } + + Ok(()) + } + .boxed() + } + + fn subscribe( + &self, + _msg: &HardwareSubscribeCmd, + ) -> BoxFuture<'static, Result<(), ButtplugDeviceError>> { + future::ready(Err(ButtplugDeviceError::UnhandledCommand( + "UDP does not support subscribe".to_owned(), + ))) + .boxed() + } + + fn unsubscribe( + &self, + _msg: &HardwareUnsubscribeCmd, + ) -> BoxFuture<'static, Result<(), ButtplugDeviceError>> { + future::ready(Err(ButtplugDeviceError::UnhandledCommand( + "UDP does not support unsubscribe".to_owned(), + ))) + .boxed() + } +} + +impl Drop for UdpHardware { + fn drop(&mut self) { + } +} diff --git a/crates/buttplug_server_hwmgr_websocket/Cargo.toml b/crates/buttplug_server_hwmgr_websocket/Cargo.toml new file mode 100644 index 000000000..b0eed555d --- /dev/null +++ b/crates/buttplug_server_hwmgr_websocket/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "buttplug_server_hwmgr_websocket" +version = "10.0.0" +authors = ["Nonpolynomial Labs, LLC "] +description = "Buttplug Intimate Hardware Control Library - Core Library" +license = "BSD-3-Clause" +homepage = "http://buttplug.io" +repository = "https://github.com/buttplugio/buttplug.git" +readme = "./README.md" +keywords = ["usb", "serial", "hardware", "bluetooth", "teledildonics"] +edition = "2024" +exclude = ["examples/**"] + +[lib] +name = "buttplug_server_hwmgr_websocket" +path = "src/lib.rs" +test = true +doctest = true +doc = true + + + +# Only build docs on one platform (linux) +[package.metadata.docs.rs] +targets = [] +# Features to pass to Cargo (default: []) +features = ["default", "unstable"] + +[dependencies] +buttplug_derive = "0.8.1" +buttplug_core = { path = "../buttplug_core" } +buttplug_server = { path = "../buttplug_server" } +buttplug_server_device_config = { path = "../buttplug_server_device_config" } +futures = "0.3.31" +futures-util = "0.3.31" +log = "0.4.27" +tokio = { version = "1.46.1", features = ["sync", "time"] } +async-trait = "0.1.88" +uuid = { version = "1.17.0", features = ["serde", "v4"] } +dashmap = { version = "6.1.0", features = ["serde"] } +tracing = "0.1.41" +thiserror = "2.0.12" +tokio-util = "0.7.15" +tokio-tungstenite = { version = "0.27.0", features = ["url"] } +getset = "0.1.6" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" diff --git a/buttplug/src/server/device/hardware/communication/websocket_server/mod.rs b/crates/buttplug_server_hwmgr_websocket/src/lib.rs similarity index 74% rename from buttplug/src/server/device/hardware/communication/websocket_server/mod.rs rename to crates/buttplug_server_hwmgr_websocket/src/lib.rs index 345d2c608..c7202694f 100644 --- a/buttplug/src/server/device/hardware/communication/websocket_server/mod.rs +++ b/crates/buttplug_server_hwmgr_websocket/src/lib.rs @@ -5,5 +5,11 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. +#[macro_use] +extern crate log; + pub mod websocket_server_comm_manager; pub mod websocket_server_hardware; + +pub use websocket_server_comm_manager::*; +pub use websocket_server_hardware::*; diff --git a/buttplug/src/server/device/hardware/communication/websocket_server/websocket_server_comm_manager.rs b/crates/buttplug_server_hwmgr_websocket/src/websocket_server_comm_manager.rs similarity index 93% rename from buttplug/src/server/device/hardware/communication/websocket_server/websocket_server_comm_manager.rs rename to crates/buttplug_server_hwmgr_websocket/src/websocket_server_comm_manager.rs index 7ad0286c6..a3d63a1db 100644 --- a/buttplug/src/server/device/hardware/communication/websocket_server/websocket_server_comm_manager.rs +++ b/crates/buttplug_server_hwmgr_websocket/src/websocket_server_comm_manager.rs @@ -6,19 +6,16 @@ // for full license information. use super::websocket_server_hardware::WebsocketServerHardwareConnector; -use crate::{ - core::ButtplugResultFuture, - server::device::hardware::communication::{ - HardwareCommunicationManager, - HardwareCommunicationManagerBuilder, - HardwareCommunicationManagerEvent, - }, - util::async_manager, +use buttplug_core::{util::async_manager, ButtplugResultFuture}; +use buttplug_server::device::hardware::communication::{ + HardwareCommunicationManager, + HardwareCommunicationManagerBuilder, + HardwareCommunicationManagerEvent, }; use futures::{FutureExt, StreamExt}; use getset::{CopyGetters, Getters}; use serde::{Deserialize, Serialize}; -use tokio::{net::TcpListener, sync::mpsc::Sender}; +use tokio::{net::TcpListener, select, sync::mpsc::Sender}; use tokio_util::sync::CancellationToken; // Packet format received from external devices. @@ -92,7 +89,7 @@ impl WebsocketServerDeviceCommunicationManager { "127.0.0.1" }; - let addr = format!("{}:{}", base_addr, port); + let addr = format!("{base_addr}:{port}"); debug!("Trying to listen on {}", addr); // Create the event loop and TCP listener we'll accept connections on. @@ -107,7 +104,7 @@ impl WebsocketServerDeviceCommunicationManager { debug!("Listening on: {}", addr); loop { select! { - listener_result = listener.accept().fuse() => { + listener_result = listener.accept() => { let stream = if let Ok((stream, _)) = listener_result { stream } else { @@ -162,7 +159,7 @@ impl WebsocketServerDeviceCommunicationManager { } }); }, - _ = child_token.cancelled().fuse() => { + _ = child_token.cancelled() => { info!("Task token cancelled, assuming websocket server comm manager shutdown."); break; } diff --git a/buttplug/src/server/device/hardware/communication/websocket_server/websocket_server_hardware.rs b/crates/buttplug_server_hwmgr_websocket/src/websocket_server_hardware.rs similarity index 90% rename from buttplug/src/server/device/hardware/communication/websocket_server/websocket_server_hardware.rs rename to crates/buttplug_server_hwmgr_websocket/src/websocket_server_hardware.rs index 8a10bca6c..089c9b986 100644 --- a/buttplug/src/server/device/hardware/communication/websocket_server/websocket_server_hardware.rs +++ b/crates/buttplug_server_hwmgr_websocket/src/websocket_server_hardware.rs @@ -6,27 +6,22 @@ // for full license information. use super::websocket_server_comm_manager::WebsocketServerDeviceCommManagerInitInfo; -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, WebsocketSpecifier}, - hardware::{ - GenericHardwareSpecializer, - Hardware, - HardwareConnector, - HardwareEvent, - HardwareInternal, - HardwareReadCmd, - HardwareReading, - HardwareSpecializer, - HardwareSubscribeCmd, - HardwareUnsubscribeCmd, - HardwareWriteCmd, - }, - }, - util::async_manager, -}; use async_trait::async_trait; +use buttplug_core::{errors::ButtplugDeviceError, util::async_manager}; +use buttplug_server::device::hardware::{ + GenericHardwareSpecializer, + Hardware, + HardwareConnector, + HardwareEvent, + HardwareInternal, + HardwareReadCmd, + HardwareReading, + HardwareSpecializer, + HardwareSubscribeCmd, + HardwareUnsubscribeCmd, + HardwareWriteCmd, +}; +use buttplug_server_device_config::{Endpoint, ProtocolCommunicationSpecifier, WebsocketSpecifier}; use futures::{ future::{self, BoxFuture}, FutureExt, @@ -43,6 +38,7 @@ use std::{ }; use tokio::{ net::TcpStream, + select, sync::{ broadcast, mpsc::{channel, Receiver, Sender}, @@ -68,7 +64,7 @@ async fn run_connection_loop( loop { select! { - _ = sleep(Duration::from_millis(10000)).fuse() => { + _ = sleep(Duration::from_millis(10000)) => { if pong_count == 0 { error!("No pongs received, considering connection closed."); break; @@ -82,7 +78,7 @@ async fn run_connection_loop( break; } } - ws_msg = request_receiver.recv().fuse() => { + ws_msg = request_receiver.recv() => { if let Some(binary_msg) = ws_msg { if websocket_server_sender .send(tokio_tungstenite::tungstenite::Message::Binary(binary_msg.into())) @@ -96,7 +92,7 @@ async fn run_connection_loop( break; } } - websocket_server_msg = websocket_server_receiver.next().fuse() => match websocket_server_msg { + websocket_server_msg = websocket_server_receiver.next() => match websocket_server_msg { Some(ws_data) => { match ws_data { Ok(msg) => { @@ -213,6 +209,8 @@ impl HardwareConnector for WebsocketServerHardwareConnector { self.info.identifier(), self.info.address(), &[Endpoint::Rx, Endpoint::Tx], + &None, + false, Box::new(hardware_internal), ); Ok(Box::new(GenericHardwareSpecializer::new(hardware))) @@ -256,7 +254,7 @@ impl HardwareInternal for WebsocketServerHardware { fn disconnect(&self) -> BoxFuture<'static, Result<(), ButtplugDeviceError>> { let connected = self.connected.clone(); async move { - connected.store(false, Ordering::SeqCst); + connected.store(false, Ordering::Relaxed); Ok(()) } .boxed() @@ -277,13 +275,12 @@ impl HardwareInternal for WebsocketServerHardware { msg: &HardwareWriteCmd, ) -> BoxFuture<'static, Result<(), ButtplugDeviceError>> { let sender = self.outgoing_sender.clone(); - let data = msg.data.clone(); + let data = msg.data().clone(); // TODO Should check endpoint validity async move { sender.send(data).await.map_err(|err| { ButtplugDeviceError::DeviceCommunicationError(format!( - "Could not write value to websocket device: {}", - err + "Could not write value to websocket device: {err}" )) }) } @@ -294,7 +291,7 @@ impl HardwareInternal for WebsocketServerHardware { &self, _msg: &HardwareSubscribeCmd, ) -> BoxFuture<'static, Result<(), ButtplugDeviceError>> { - if self.subscribed.load(Ordering::SeqCst) { + if self.subscribed.load(Ordering::Relaxed) { error!("Endpoint already subscribed somehow!"); return future::ready(Ok(())).boxed(); } @@ -305,7 +302,7 @@ impl HardwareInternal for WebsocketServerHardware { let subscribed = self.subscribed.clone(); let subscribed_token = self.subscribe_token.clone(); async move { - subscribed.store(true, Ordering::SeqCst); + subscribed.store(true, Ordering::Relaxed); let token = CancellationToken::new(); *(subscribed_token.lock().await) = Some(token.child_token()); async_manager::spawn(async move { @@ -342,11 +339,11 @@ impl HardwareInternal for WebsocketServerHardware { &self, _msg: &HardwareUnsubscribeCmd, ) -> BoxFuture<'static, Result<(), ButtplugDeviceError>> { - if self.subscribed.load(Ordering::SeqCst) { + if self.subscribed.load(Ordering::Relaxed) { let subscribed = self.subscribed.clone(); let subscribed_token = self.subscribe_token.clone(); async move { - subscribed.store(false, Ordering::SeqCst); + subscribed.store(false, Ordering::Relaxed); let token = (subscribed_token.lock().await) .take() .expect("If we were subscribed, we'll have a token."); diff --git a/crates/buttplug_server_hwmgr_xinput/Cargo.toml b/crates/buttplug_server_hwmgr_xinput/Cargo.toml new file mode 100644 index 000000000..51c98af40 --- /dev/null +++ b/crates/buttplug_server_hwmgr_xinput/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "buttplug_server_hwmgr_xinput" +version = "10.0.0" +authors = ["Nonpolynomial Labs, LLC "] +description = "Buttplug Intimate Hardware Control Library - Core Library" +license = "BSD-3-Clause" +homepage = "http://buttplug.io" +repository = "https://github.com/buttplugio/buttplug.git" +readme = "./README.md" +keywords = ["usb", "serial", "hardware", "bluetooth", "teledildonics"] +edition = "2024" +exclude = ["examples/**"] + +[lib] +name = "buttplug_server_hwmgr_xinput" +path = "src/lib.rs" +test = true +doctest = true +doc = true + + +[dependencies] +buttplug_derive = "0.8.1" +# buttplug_derive = { path = "../buttplug_derive" } +buttplug_core = { path = "../buttplug_core" } +buttplug_server = { path = "../buttplug_server" } +buttplug_server_device_config = { path = "../buttplug_server_device_config" } +futures = "0.3.31" +futures-util = "0.3.31" +log = "0.4.27" +tokio = { version = "1.46.1", features = ["sync", "time"] } +async-trait = "0.1.88" +uuid = { version = "1.17.0", features = ["serde", "v4"] } +dashmap = { version = "6.1.0", features = ["serde"] } +tracing = "0.1.41" +thiserror = "2.0.12" +rusty-xinput = "1.3.0" +strum_macros = "0.27.1" +strum = "0.27.1" +byteorder = "1.5.0" +tokio-util = "0.7.15" diff --git a/buttplug/src/server/device/hardware/communication/xinput/mod.rs b/crates/buttplug_server_hwmgr_xinput/src/lib.rs similarity index 66% rename from buttplug/src/server/device/hardware/communication/xinput/mod.rs rename to crates/buttplug_server_hwmgr_xinput/src/lib.rs index ad6002a98..db129ca86 100644 --- a/buttplug/src/server/device/hardware/communication/xinput/mod.rs +++ b/crates/buttplug_server_hwmgr_xinput/src/lib.rs @@ -5,9 +5,20 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. +#[cfg(target_os = "windows")] +#[macro_use] +extern crate log; + +#[cfg(target_os = "windows")] +#[macro_use] +extern crate strum_macros; + +#[cfg(target_os = "windows")] mod xinput_device_comm_manager; +#[cfg(target_os = "windows")] mod xinput_hardware; +#[cfg(target_os = "windows")] pub use xinput_device_comm_manager::{ XInputDeviceCommunicationManager, XInputDeviceCommunicationManagerBuilder, diff --git a/buttplug/src/server/device/hardware/communication/xinput/xinput_device_comm_manager.rs b/crates/buttplug_server_hwmgr_xinput/src/xinput_device_comm_manager.rs similarity index 90% rename from buttplug/src/server/device/hardware/communication/xinput/xinput_device_comm_manager.rs rename to crates/buttplug_server_hwmgr_xinput/src/xinput_device_comm_manager.rs index 844ec68a8..867a321e2 100644 --- a/buttplug/src/server/device/hardware/communication/xinput/xinput_device_comm_manager.rs +++ b/crates/buttplug_server_hwmgr_xinput/src/xinput_device_comm_manager.rs @@ -6,17 +6,15 @@ // for full license information. use super::xinput_hardware::XInputHardwareConnector; -use crate::{ - core::errors::ButtplugDeviceError, - server::device::hardware::communication::{ - HardwareCommunicationManager, - HardwareCommunicationManagerBuilder, - HardwareCommunicationManagerEvent, - TimedRetryCommunicationManager, - TimedRetryCommunicationManagerImpl, - }, -}; use async_trait::async_trait; +use buttplug_core::errors::ButtplugDeviceError; +use buttplug_server::device::hardware::communication::{ + HardwareCommunicationManager, + HardwareCommunicationManagerBuilder, + HardwareCommunicationManagerEvent, + TimedRetryCommunicationManager, + TimedRetryCommunicationManagerImpl, +}; use rusty_xinput::XInputHandle; use std::string::ToString; use tokio::sync::mpsc; diff --git a/buttplug/src/server/device/hardware/communication/xinput/xinput_hardware.rs b/crates/buttplug_server_hwmgr_xinput/src/xinput_hardware.rs similarity index 84% rename from buttplug/src/server/device/hardware/communication/xinput/xinput_hardware.rs rename to crates/buttplug_server_hwmgr_xinput/src/xinput_hardware.rs index e758e7968..5a8edddc0 100644 --- a/buttplug/src/server/device/hardware/communication/xinput/xinput_hardware.rs +++ b/crates/buttplug_server_hwmgr_xinput/src/xinput_hardware.rs @@ -6,28 +6,23 @@ // for full license information. use super::xinput_device_comm_manager::XInputControllerIndex; -use crate::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::hardware::communication::HardwareSpecificError, - server::device::{ - configuration::{ProtocolCommunicationSpecifier, XInputSpecifier}, - hardware::{ - GenericHardwareSpecializer, - Hardware, - HardwareConnector, - HardwareEvent, - HardwareInternal, - HardwareReadCmd, - HardwareReading, - HardwareSpecializer, - HardwareSubscribeCmd, - HardwareUnsubscribeCmd, - HardwareWriteCmd, - }, - }, - util::async_manager, -}; use async_trait::async_trait; +use buttplug_core::{errors::ButtplugDeviceError, util::async_manager}; +use buttplug_server::device::hardware::{ + communication::HardwareSpecificError, + GenericHardwareSpecializer, + Hardware, + HardwareConnector, + HardwareEvent, + HardwareInternal, + HardwareReadCmd, + HardwareReading, + HardwareSpecializer, + HardwareSubscribeCmd, + HardwareUnsubscribeCmd, + HardwareWriteCmd, +}; +use buttplug_server_device_config::{ProtocolCommunicationSpecifier, XInputSpecifier, Endpoint}; use byteorder::{LittleEndian, ReadBytesExt}; use futures::future::{self, BoxFuture, FutureExt}; use rusty_xinput::{XInputHandle, XInputUsageError}; @@ -96,6 +91,8 @@ impl HardwareConnector for XInputHardwareConnector { &self.index.to_string(), &create_address(self.index), &[Endpoint::Tx, Endpoint::Rx], + &None, + false, Box::new(hardware_internal), ); Ok(Box::new(GenericHardwareSpecializer::new(hardware))) @@ -147,7 +144,10 @@ impl HardwareInternal for XInputHardware { let battery = handle .get_gamepad_battery_information(index as u32) .map_err(|e| { - ButtplugDeviceError::from(HardwareSpecificError::XInputError(format!("{:?}", e))) + ButtplugDeviceError::from(ButtplugDeviceError::DeviceSpecificError( + HardwareSpecificError::HardwareSpecificError("Xinput".to_string(), format!("{e:?}")) + .to_string(), + )) })?; Ok(HardwareReading::new( Endpoint::Rx, @@ -163,7 +163,7 @@ impl HardwareInternal for XInputHardware { ) -> BoxFuture<'static, Result<(), ButtplugDeviceError>> { let handle = self.handle.clone(); let index = self.index; - let data = msg.data.clone(); + let data = msg.data().clone(); async move { let mut cursor = Cursor::new(data); let left_motor_speed = cursor @@ -175,7 +175,10 @@ impl HardwareInternal for XInputHardware { handle .set_state(index as u32, left_motor_speed, right_motor_speed) .map_err(|e: XInputUsageError| { - ButtplugDeviceError::from(HardwareSpecificError::XInputError(format!("{:?}", e))) + ButtplugDeviceError::from(ButtplugDeviceError::DeviceSpecificError( + HardwareSpecificError::HardwareSpecificError("Xinput".to_string(), format!("{e:?}")) + .to_string(), + )) }) } .boxed() diff --git a/crates/buttplug_tests/Cargo.toml b/crates/buttplug_tests/Cargo.toml new file mode 100644 index 000000000..29bfd4289 --- /dev/null +++ b/crates/buttplug_tests/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "buttplug_tests" +version = "10.0.0" +authors = ["Nonpolynomial Labs, LLC "] +description = "Buttplug Intimate Hardware Control Library - Core Library" +license = "BSD-3-Clause" +homepage = "http://buttplug.io" +repository = "https://github.com/buttplugio/buttplug.git" +readme = "./README.md" +keywords = ["usb", "serial", "hardware", "bluetooth", "teledildonics"] +edition = "2024" + +[dependencies] +buttplug_derive = "0.8.1" +buttplug_core = { path = "../buttplug_core" } +buttplug_client = { path = "../buttplug_client" } +buttplug_client_in_process = { path = "../buttplug_client_in_process", default-features = false} +buttplug_server = { path = "../buttplug_server" } +buttplug_server_device_config = { path = "../buttplug_server_device_config" } +log = "0.4.27" +tokio = { version = "1.46.1", features = ["macros"] } +uuid = "1.17.0" +futures = "0.3.31" +tracing = "0.1.41" +tracing-subscriber = "0.3.19" +tracing-futures = "0.2.5" +tokio-test = "0.4.4" +serde = "1.0.219" +async-trait = "0.1.88" +dashmap = "6.1.0" +thiserror = "2.0.12" +getset = "0.1.6" +jsonschema = { version = "0.30.0", default-features = false } +test-case = "3.3.1" +serde_yaml = "0.9.34" diff --git a/buttplug/tests/mod.rs b/crates/buttplug_tests/tests/mod.rs similarity index 53% rename from buttplug/tests/mod.rs rename to crates/buttplug_tests/tests/mod.rs index 91005ee20..b9d75f4e1 100644 --- a/buttplug/tests/mod.rs +++ b/crates/buttplug_tests/tests/mod.rs @@ -1,2 +1,4 @@ extern crate tracing; +#[macro_use] +extern crate log; pub mod util; diff --git a/buttplug/tests/test_client.rs b/crates/buttplug_tests/tests/test_client.rs similarity index 89% rename from buttplug/tests/test_client.rs rename to crates/buttplug_tests/tests/test_client.rs index 92951efc6..1ef753372 100644 --- a/buttplug/tests/test_client.rs +++ b/crates/buttplug_tests/tests/test_client.rs @@ -5,24 +5,24 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. +/* mod util; use util::{test_client, test_client_with_delayed_device_manager, test_client_with_device}; extern crate buttplug; extern crate tracing; use buttplug::{ - client::{ButtplugClient, ButtplugClientError, ButtplugClientEvent}, + client::{connector::ButtplugInProcessClientConnectorBuilder, ButtplugClient, ButtplugClientError, ButtplugClientEvent}, core::{ connector::{ ButtplugConnector, ButtplugConnectorError, ButtplugConnectorResultFuture, - ButtplugInProcessClientConnectorBuilder, }, errors::{ButtplugDeviceError, ButtplugError}, - message::{ButtplugClientMessageCurrent, ButtplugServerMessageCurrent}, + message::{ButtplugClientMessageCurrent, ButtplugClientMessageV4, ButtplugServerMessageCurrent, ButtplugServerMessageV4}, }, - server::ButtplugServerBuilder, + server::{message::{ButtplugClientMessageVariant, ButtplugServerMessageVariant}, ButtplugServerBuilder}, }; use futures::{future::BoxFuture, StreamExt}; @@ -32,12 +32,12 @@ use tokio::{sync::mpsc::Sender, time::sleep}; #[derive(Default)] struct ButtplugFailingConnector {} -impl ButtplugConnector +impl ButtplugConnector for ButtplugFailingConnector { fn connect( &mut self, - _: Sender, + _: Sender, ) -> BoxFuture<'static, Result<(), ButtplugConnectorError>> { ButtplugConnectorError::ConnectorNotConnected.into() } @@ -46,12 +46,12 @@ impl ButtplugConnector ButtplugConnectorResultFuture { + fn send(&self, _msg: ButtplugClientMessageVariant) -> ButtplugConnectorResultFuture { panic!("Should never be called") } } -#[cfg(feature = "server")] + #[tokio::test] async fn test_failing_connection() { let client = ButtplugClient::new("Test Client"); @@ -61,7 +61,7 @@ async fn test_failing_connection() { .is_err()); } -#[cfg(feature = "server")] + #[tokio::test] async fn test_disconnect_status() { let client = test_client().await; @@ -69,7 +69,7 @@ async fn test_disconnect_status() { assert!(!client.connected()); } -#[cfg(feature = "server")] + #[tokio::test] async fn test_double_disconnect() { let client = test_client().await; @@ -77,17 +77,16 @@ async fn test_double_disconnect() { assert!(client.disconnect().await.is_err()); } -#[cfg(feature = "server")] + #[tokio::test] async fn test_connect_init() { let client = test_client().await; assert_eq!(client.server_name(), Some("Buttplug Server".to_owned())); } -#[cfg(feature = "server")] + #[tokio::test] async fn test_client_connected_status() { - tracing_subscriber::fmt::init(); let client = test_client().await; client .disconnect() @@ -96,14 +95,14 @@ async fn test_client_connected_status() { assert!(!client.connected()); } -#[cfg(feature = "server")] + #[tokio::test] async fn test_start_scanning() { let (client, _) = test_client_with_device().await; assert!(client.start_scanning().await.is_ok()); } -#[cfg(feature = "server")] + #[tokio::test] #[ignore = "We may want to just call this Ok now?"] async fn test_stop_scanning_when_not_scanning() { @@ -120,7 +119,7 @@ async fn test_stop_scanning_when_not_scanning() { assert!(client.stop_scanning().await.is_err()); } -#[cfg(feature = "server")] + #[tokio::test] async fn test_start_scanning_when_already_scanning() { let client = test_client_with_delayed_device_manager().await; @@ -128,7 +127,7 @@ async fn test_start_scanning_when_already_scanning() { assert!(client.start_scanning().await.is_ok()); } -#[cfg(feature = "server")] + #[tokio::test] async fn test_successive_start_scanning() { let (client, _) = test_client_with_device().await; @@ -136,7 +135,7 @@ async fn test_successive_start_scanning() { assert!(client.start_scanning().await.is_ok()); } -#[cfg(feature = "server")] + #[tokio::test] async fn test_client_scanning_finished() { let (client, _) = test_client_with_device().await; @@ -148,7 +147,7 @@ async fn test_client_scanning_finished() { )); } -#[cfg(feature = "server")] + #[tokio::test] async fn test_client_ping() { let server = ButtplugServerBuilder::default() @@ -171,7 +170,7 @@ async fn test_client_ping() { /* // Tests both the stop all devices functionality, as well as both ends of the // command range for is_in_command_range message validation. -#[cfg(feature = "server")] + #[tokio::test] async fn test_stop_all_devices_and_device_command_range() { let (client, test_device) = test_client_with_device().await; @@ -227,3 +226,4 @@ async fn test_stop_all_devices_and_device_command_range() { // TODO Test receiving unmatched DeviceRemoved // TODO Test receiving Error when expecting Ok (i.e. StartScanning returns an error) // TODO Test receiving wrong message expecting Ok (i.e. StartScanning returns DeviceList) +*/ diff --git a/crates/buttplug_tests/tests/test_client_device.rs b/crates/buttplug_tests/tests/test_client_device.rs new file mode 100644 index 000000000..55ae079d6 --- /dev/null +++ b/crates/buttplug_tests/tests/test_client_device.rs @@ -0,0 +1,287 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 Nonpolynomial Labs LLC. All rights reserved. +// +// Licensed under the BSD 3-Clause license. See LICENSE file in the project root +// for full license information. + +mod util; +use buttplug_client::{ButtplugClientDeviceEvent, ButtplugClientError, ButtplugClientEvent}; +use buttplug_core::{ + errors::ButtplugError, + message::{OutputType, FeatureType}, + util::async_manager +}; +use buttplug_server_device_config::{load_protocol_configs, UserDeviceCustomization, DeviceDefinition, UserDeviceIdentifier, ServerDeviceFeature, ServerDeviceFeatureOutput, Endpoint}; +use buttplug_server::{ + device::{ + hardware::{HardwareCommand, HardwareWriteCmd}, + }, +}; +use futures::StreamExt; +use std::{collections::HashMap, sync::Arc, time::Duration}; +use tokio::time::sleep; +use util::test_device_manager::{check_test_recv_value, TestDeviceIdentifier}; +use util::{ + test_client_with_device, + test_client_with_device_and_custom_dcm, + test_device_manager::TestHardwareEvent, +}; +use uuid::Uuid; + +#[tokio::test] +async fn test_client_device_connected_status() { + let (client, device) = test_client_with_device().await; + + let mut event_stream = client.event_stream(); + client + .start_scanning() + .await + .expect("Test, assuming infallible."); + let mut client_device = None; + while let Some(msg) = event_stream.next().await { + if let ButtplugClientEvent::DeviceAdded(da) = msg { + client_device = Some(da); + break; + } + } + let test_device = client_device.expect("Test, assuming infallible."); + let mut device_event_stream = test_device.event_stream(); + assert!(test_device.connected()); + device + .sender + .send(TestHardwareEvent::Disconnect) + .await + .expect("Test, assuming infallible."); + while let Some(msg) = device_event_stream.next().await { + if let ButtplugClientDeviceEvent::DeviceRemoved = msg { + assert!(!test_device.connected()); + break; + } + } + client + .disconnect() + .await + .expect("Test, assuming infallible."); + assert!(!client.connected()); +} + + +#[tokio::test] +async fn test_client_device_client_disconnected_status() { + let (client, _) = test_client_with_device().await; + + let mut event_stream = client.event_stream(); + client + .start_scanning() + .await + .expect("Test, assuming infallible."); + let mut client_device = None; + while let Some(msg) = event_stream.next().await { + if let ButtplugClientEvent::DeviceAdded(da) = msg { + client_device = Some(da); + break; + } + } + let test_device = client_device.expect("Test, assuming infallible."); + let mut device_event_stream = test_device.event_stream(); + assert!(test_device.connected()); + client + .disconnect() + .await + .expect("Test, assuming infallible."); + while let Some(msg) = event_stream.next().await { + if let ButtplugClientEvent::ServerDisconnect = msg { + assert!(!client.connected()); + assert!(!test_device.connected()); + break; + } + } + while let Some(msg) = device_event_stream.next().await { + if let ButtplugClientDeviceEvent::DeviceRemoved = msg { + break; + } + } +} + + +#[tokio::test] +async fn test_client_device_connected_no_event_listener() { + let (client, device) = test_client_with_device().await; + + client + .start_scanning() + .await + .expect("Test, assuming infallible."); + sleep(Duration::from_millis(100)).await; + device + .sender + .send(TestHardwareEvent::Disconnect) + .await + .expect("Test, assuming infallible."); + sleep(Duration::from_millis(100)).await; + client + .disconnect() + .await + .expect("Test, assuming infallible."); + assert!(!client.connected()); + sleep(Duration::from_millis(100)).await; +} + + +#[tokio::test] +async fn test_client_device_invalid_command() { + use buttplug_core::errors::ButtplugDeviceError; + let (client, _) = test_client_with_device().await; + + let mut event_stream = client.event_stream(); + client + .start_scanning() + .await + .expect("Test, assuming infallible."); + let mut client_device = None; + while let Some(msg) = event_stream.next().await { + if let ButtplugClientEvent::DeviceAdded(da) = msg { + client_device = Some(da); + break; + } + } + let test_device = client_device.expect("Test, assuming infallible."); + + assert!(matches!( + test_device.vibrate(1000).await.unwrap_err(), + ButtplugClientError::ButtplugOutputCommandConversionError(_)) + ); +} + +/* +#[tokio::test] +async fn test_client_range_limits() { + let dcm = load_protocol_configs(&None, &None, false) + .expect("Test, assuming infallible.") + .finish() + .expect("Test, assuming infallible."); + + // Add a user config that configures the test device to only user the lower and upper half for the two vibrators + let identifier = UserDeviceIdentifier::new("range-test", "aneros", &Some("Massage Demo".into())); + let test_identifier = TestDeviceIdentifier::new("Massage Demo", Some("range-test".into())); + let mut feature_1_actuator = HashMap::new(); + feature_1_actuator.insert( + OutputType::Vibrate, + ServerDeviceFeatureOutput::new(&(0..=127), &(0..=64)), + ); + let mut feature_2_actuator = HashMap::new(); + feature_2_actuator.insert( + OutputType::Vibrate, + ServerDeviceFeatureOutput::new(&(0..=127), &(64..=127)), + ); + dcm + .add_user_device_definition( + &identifier, + &DeviceDefinition::new( + "Massage Demo", + &Uuid::new_v4(), + &None, + &None, + &[ + ServerDeviceFeature::new( + "Lower half", + &Uuid::new_v4(), + &None, + FeatureType::Vibrate, + &Some(feature_1_actuator), + &None, + &None, + ), + ServerDeviceFeature::new( + "Upper half", + &Uuid::new_v4(), + &None, + FeatureType::Vibrate, + &Some(feature_2_actuator), + &None, + &None, + ), + ], + &UserDeviceCustomization::default(), + ), + ) + .unwrap(); + + // Start the server & client + let (client, mut device) = test_client_with_device_and_custom_dcm(&test_identifier, dcm).await; + let mut event_stream = client.event_stream(); + assert!(client.start_scanning().await.is_ok()); + + while let Some(event) = event_stream.next().await { + if let ButtplugClientEvent::DeviceAdded(dev) = event { + // Vibrate at half strength + assert!(dev.vibrate(32).await.is_ok()); + + // Lower half + check_test_recv_value( + &Duration::from_millis(150), + &mut device, + HardwareCommand::Write(HardwareWriteCmd::new( + &[Uuid::nil()], + Endpoint::Tx, + vec![0xF1, 32], + false, + )), + ) + .await; + + // Upper half + check_test_recv_value( + &Duration::from_millis(150), + &mut device, + HardwareCommand::Write(HardwareWriteCmd::new( + &[Uuid::nil()], + Endpoint::Tx, + vec![0xF2, 96], + false, + )), + ) + .await; + + // Disable device + assert!(dev.vibrate(0).await.is_ok()); + + // Lower half + check_test_recv_value( + &Duration::from_millis(150), + &mut device, + HardwareCommand::Write(HardwareWriteCmd::new( + &[Uuid::nil()], + Endpoint::Tx, + vec![0xF1, 0], + false, + )), + ) + .await; + + // Upper half + check_test_recv_value( + &Duration::from_millis(150), + &mut device, + HardwareCommand::Write(HardwareWriteCmd::new( + &[Uuid::nil()], + Endpoint::Tx, + vec![0xF2, 0], + false, + )), + ) + .await; + break; + } + } + assert!(client.stop_all_devices().await.is_ok()); +} + + */ +// TODO Test invalid messages to device +// TODO Test invalid parameters in message +// TODO Test device invalidation across client connections (i.e. a device shouldn't be allowed to reconnect even if index is the same) +// TODO Test DeviceList being sent followed by repeat DeviceAdded +// TODO Test DeviceList being sent multiple times +// TODO Test sending device return for device that doesn't exist (in client) diff --git a/buttplug/tests/test_device_config.rs b/crates/buttplug_tests/tests/test_device_config.rs similarity index 96% rename from buttplug/tests/test_device_config.rs rename to crates/buttplug_tests/tests/test_device_config.rs index 616b4b154..bcf843783 100644 --- a/buttplug/tests/test_device_config.rs +++ b/crates/buttplug_tests/tests/test_device_config.rs @@ -6,9 +6,8 @@ // for full license information. mod util; -extern crate buttplug; -use buttplug::util::device_configuration::load_protocol_configs; +use buttplug_server_device_config::load_protocol_configs; use tokio_test::assert_ok; const BASE_CONFIG_JSON: &str = r#" @@ -60,7 +59,7 @@ const BASE_CONFIG_JSON: &str = r#" const BASE_VALID_VERSION_CONFIG_JSON: &str = r#" { "version": { - "major": 3, + "major": 4, "minor": 999 } } @@ -78,14 +77,14 @@ const BASE_INVALID_VERSION_CONFIG_JSON: &str = r#" const BASE_VALID_NULL_USER_CONFIG_JSON: &str = r#" { "version": { - "major": 3, + "major": 4, "minor": 999 }, "user-configs": {} } "#; -#[cfg(feature = "server")] + #[tokio::test] async fn test_valid_null_version_config() { assert_ok!(load_protocol_configs( @@ -95,7 +94,7 @@ async fn test_valid_null_version_config() { )); } -#[cfg(feature = "server")] + #[tokio::test] async fn test_valid_null_user_config() { assert_ok!(load_protocol_configs( @@ -105,7 +104,7 @@ async fn test_valid_null_user_config() { )); } -#[cfg(feature = "server")] + #[tokio::test] async fn test_invalid_null_version_config() { assert!(load_protocol_configs( @@ -116,14 +115,14 @@ async fn test_invalid_null_version_config() { .is_err()); } -#[cfg(feature = "server")] + #[tokio::test] #[ignore = "Still need to update for new message format"] async fn test_basic_device_config() { assert!(load_protocol_configs(&Some(BASE_CONFIG_JSON.to_owned()), &None, false).is_ok()); } -#[cfg(feature = "server")] + #[tokio::test] async fn test_valid_user_config() { let user_config_json = r#" @@ -195,7 +194,7 @@ async fn test_valid_user_config() { .is_err()); } -#[cfg(feature = "server")] + #[tokio::test] async fn test_invalid_step_range_device_config_wrong_range_length() { let user_config_json = r#" diff --git a/buttplug/tests/test_device_protocols.rs b/crates/buttplug_tests/tests/test_device_protocols.rs similarity index 53% rename from buttplug/tests/test_device_protocols.rs rename to crates/buttplug_tests/tests/test_device_protocols.rs index 104075006..4eb5547b5 100644 --- a/buttplug/tests/test_device_protocols.rs +++ b/crates/buttplug_tests/tests/test_device_protocols.rs @@ -18,404 +18,721 @@ async fn load_test_case(test_file: &str) -> DeviceTestCase { serde_yaml::from_str(&yaml_test_case).expect("Could not parse yaml for file.") } +//#[test_case("test_cowgirl_cone_protocol.yaml" ; "The Cowgirl Cone Protocol")] +#[test_case("test_activejoy_protocol.yaml" ; "ActiveJoy Protocol")] +#[test_case("test_adrienlastic_protocol.yaml" ; "Adrien Lastic Protocol")] +#[test_case("test_amorelie_joy_protocol.yaml" ; "Amorelie Joy Protocol")] #[test_case("test_aneros_protocol.yaml" ; "Aneros Protocol")] -#[test_case("test_ankni_protocol.yaml" ; "Ankni Protocol")] #[test_case("test_ankni_protocol_no_handshake.yaml" ; "Ankni Protocol - No Handshake")] +#[test_case("test_ankni_protocol.yaml" ; "Ankni Protocol")] +#[test_case("test_bananasome_protocol.yaml" ; "Bananasome Protocol")] #[test_case("test_cachito_protocol.yaml" ; "Cachito Protocol")] -#[test_case("test_fredorch_protocol.yaml" ; "Fredorch Protocol")] +#[test_case("test_cowgirl_protocol.yaml" ; "The Cowgirl Protocol")] +#[test_case("test_cupido_protocol.yaml" ; "Cupido Protocol")] +#[test_case("test_deepsire.yaml" ; "DeepSire Protocol")] +#[test_case("test_feelingso.yaml" ; "FeelingSo Protocol")] +#[test_case("test_fleshy_thrust_protocol.yaml" ; "Fleshy Thrust Sync Protocol")] +#[test_case("test_foreo_protocol.yaml" ; "Foreo Protocol")] +#[test_case("test_fox_protocol.yaml" ; "Fox Protocol")] +//#[test_case("test_fredorch_protocol.yaml" ; "Fredorch Protocol")] +#[test_case("test_galaku_nebula.yaml" ; "Galaku Pump Protocol - Nebula")] +#[test_case("test_galaku.yaml" ; "Galaku Protocol")] +#[test_case("test_hgod_protocol.yaml" ; "Hgod Protocol")] #[test_case("test_hismith_auxfun_box.yaml" ; "Hismith Mini Protocol - Auxfun Box")] -#[test_case("test_hismith_v4.yaml" ; "Hismith Mini Protocol - Hismith v4")] #[test_case("test_hismith_sinloli.yaml" ; "Hismith Mini Protocol - Sinloli")] #[test_case("test_hismith_thrusting_cup.yaml" ; "Hismith Protocol - Thrusting Cup")] +#[test_case("test_hismith_v4.yaml" ; "Hismith Mini Protocol - Hismith v4")] #[test_case("test_hismith_wildolo.yaml" ; "Hismith Protocol - Wildolo")] -#[test_case("test_lovense_single_vibrator.yaml" ; "Lovense Protocol - Single Vibrator Device")] +#[test_case("test_itoys_protocol.yaml" ; "iToys Protocol")] +#[test_case("test_itoys_twinklingstars.yaml" ; "iToys Protocol - Twinkling Stars")] +#[test_case("test_joyhub_moonhorn.yaml" ; "JoyHub Protocol - Moonhorn")] +#[test_case("test_joyhub_petalwish_compat.yaml" ; "JoyHub Protocol - Petalwish Compat")] +#[test_case("test_joyhub_petalwish.yaml" ; "JoyHub Protocol - Petalwish")] +#[test_case("test_joyhub_roselin.yaml" ; "JoyHub Protocol - RoseLin")] +#[test_case("test_kiiroo_prowand.yaml" ; "Kiiroo ProWand Protocol")] +#[test_case("test_kiiroo_spot.yaml" ; "Kiiroo Spot Protocol")] +#[test_case("test_lelo_f1sv1.yaml" ; "Lelo F1s V1 Protocol")] +#[test_case("test_lelo_f1sv2.yaml" ; "Lelo F1s V2 Protocol")] +#[test_case("test_lelo_idawave.yaml" ; "Lelo Harmony Protocol - Ida Wave")] +#[test_case("test_lelo_tianiharmony.yaml" ; "Lelo Harmony Protocol - Tiani Harmony")] +#[test_case("test_leten_protocol.yaml" ; "Leten Protocol")] +#[test_case("test_loob_protocol.yaml" ; "Joyroid Loob Protocol")] +#[test_case("test_lovehoney_desire_egg.yaml" ; "Lovehoney Desire Protocol - Love Egg")] +#[test_case("test_lovehoney_desire_prostate.yaml" ; "Lovehoney Desire Protocol - Prostate Vibe")] +#[test_case("test_lovense_battery_non_default.yaml" ; "Lovense Protocol - Lovense Battery (Non-Default Devices)")] +#[test_case("test_lovense_battery.yaml" ; "Lovense Protocol - Lovense Battery (Default Devices)")] +#[test_case("test_lovense_edge.yaml" ; "Lovense Protocol - Edge")] +#[test_case("test_lovense_flexer_fw2.yaml" ; "Lovense Protocol - Flexer FW2")] +//#[test_case("test_lovense_flexer_fw3.yaml" ; "Lovense Protocol - Flexer FW3")] #[test_case("test_lovense_max.yaml" ; "Lovense Protocol - Lovense Max (Vibrate/Constrict)")] #[test_case("test_lovense_nora.yaml" ; "Lovense Protocol - Lovense Nora (Vibrate/Rotate)")] -#[test_case("test_lovense_ridge.yaml" ; "Lovense Protocol - Lovense Ridge (Oscillate)")] -#[test_case("test_lovense_battery.yaml" ; "Lovense Protocol - Lovense Battery (Default Devices)")] -#[test_case("test_lovense_battery_non_default.yaml" ; "Lovense Protocol - Lovense Battery (Non-Default Devices)")] +//#[test_case("test_lovense_osci3.yaml" ; "Lovense Protocol - Osci3")] #[test_case("test_lovense_ridge_user_config.yaml" ; "Lovense Protocol - Lovense Ridge (User Config)")] -#[test_case("test_lovense_flexer_fw2.yaml" ; "Lovense Protocol - Flexer FW2")] -#[test_case("test_lovense_flexer_fw3.yaml" ; "Lovense Protocol - Flexer FW3")] -#[test_case("test_lovense_edge.yaml" ; "Lovense Protocol - Edge")] -#[test_case("test_lovense_osci3.yaml" ; "Lovense Protocol - Osci3")] -#[test_case("test_user_config_display_name.yaml" ; "User Config Display Name")] -#[test_case("test_satisfyer_single_vibrator.yaml" ; "Satisfyer Protocol - Single Vibrator")] -#[test_case("test_satisfyer_dual_vibrator.yaml" ; "Satisfyer Protocol - Dual Vibrator")] -#[test_case("test_mysteryvibe.yaml" ; "Mysteryvibe Protocol")] +#[test_case("test_lovense_ridge.yaml" ; "Lovense Protocol - Lovense Ridge (Oscillate)")] +#[test_case("test_lovense_single_vibrator.yaml" ; "Lovense Protocol - Single Vibrator Device")] +#[test_case("test_luvmazer_protocol.yaml" ; "Luvmazer Protocol")] +#[test_case("test_magic_motion_1_magic_cell.yaml" ; "MagicMotion Protocol 1 - Magic Cell")] +////#[test_case("test_magic_motion_2_eidolon.yaml" ; "MagicMotion Protocol 2 - Eidolon")] +#[test_case("test_magic_motion_2_equinox.yaml" ; "MagicMotion Protocol 2 - Equinox")] +#[test_case("test_magic_motion_3_krush.yaml" ; "MagicMotion Protocol 3 - Krush")] +#[test_case("test_magic_motion_4_bobi.yaml" ; "MagicMotion Protocol 4 - Bobi")] +#[test_case("test_magic_motion_4_nyx.yaml" ; "MagicMotion Protocol 4 - Nyx")] #[test_case("test_meese_protocol.yaml" ; "Meese Protocol")] +#[test_case("test_metaxsire_cali.yaml" ; "metaXsire Protocol - Cali")] +#[test_case("test_metaxsire_nolan.yaml" ; "metaXsire Protocol v2 - Nolan")] +#[test_case("test_metaxsire_olis.yaml" ; "metaXsire Protocol - Olis")] +#[test_case("test_metaxsire_rex.yaml" ; "metaXsire Protocol - Rex")] #[test_case("test_mizzzee_protocol.yaml" ; "Mizz Zee Protocol")] #[test_case("test_mizzzee_v2_protocol.yaml" ; "Mizz Zee v2 Protocol")] #[test_case("test_mizzzee_v3_protocol.yaml" ; "Mizz Zee v3 Protocol")] -#[test_case("test_vorze_ufo.yaml" ; "Vorze Protocol - UFO")] -#[test_case("test_vorze_ufo_tw.yaml" ; "Vorze Protocol - UFO TW")] -#[test_case("test_vorze_cyclone.yaml" ; "Vorze Protocol - Cyclone")] -#[test_case("test_wevibe_4plus.yaml" ; "WeVibe Protocol (Legacy) - 4 Plus")] -#[test_case("test_wevibe_pivot.yaml" ; "WeVibe Protocol (Legacy) - Pivot")] -#[test_case("test_wevibe_vector.yaml" ; "WeVibe Protocol (8bit) - Vector")] -#[test_case("test_wevibe_moxie.yaml" ; "WeVibe Protocol (8bit) - Moxie")] -#[test_case("test_wevibe_chorus.yaml" ; "WeVibe Protocol (Chorus) - Chorus")] +#[test_case("test_motorbunny_protocol.yaml" ; "Motorbunny Protocol")] +#[test_case("test_mysteryvibe.yaml" ; "Mysteryvibe Protocol")] +#[test_case("test_nexus_revo.yaml" ; "Nexus Revo Protocol")] #[test_case("test_nobra_protocol.yaml" ; "Nobra Protocol")] -#[test_case("test_lovehoney_desire_prostate.yaml" ; "Lovehoney Desire Protocol - Prostate Vibe")] -#[test_case("test_lovehoney_desire_egg.yaml" ; "Lovehoney Desire Protocol - Love Egg")] -#[test_case("test_wetoy_protocol.yaml" ; "WeToy Protocol")] +#[test_case("test_omobo_protocol.yaml" ; "Omobo Protocol")] #[test_case("test_pink_punch_protocol.yaml" ; "Pink Punch Protocol")] +#[test_case("test_sakuraneko_koikoi.yaml" ; "Sakuraneko Protocol - Koikoi")] #[test_case("test_sakuraneko_protocol.yaml" ; "Sakuraneko Protocol")] +#[test_case("test_satisfyer_triple_vibrator.yaml" ; "Satisfyer Protocol - Triple Vibrator")] +#[test_case("test_satisfyer_dual_vibrator.yaml" ; "Satisfyer Protocol - Dual Vibrator")] +#[test_case("test_satisfyer_single_vibrator.yaml" ; "Satisfyer Protocol - Single Vibrator")] +#[test_case("test_sensee_capsule.yaml" ; "Sensee Capsule Protocol")] +#[test_case("test_sensee_protocol.yaml" ; "Sensee Diandou Protocol - Rabbit")] +#[test_case("test_serveu_protocol.yaml" ; "ServeU")] +#[test_case("test_sexverse_heart_protocol.yaml" ; "Sexverse Heart Protocol")] +#[test_case("test_sexverse_lg389_protocol.yaml" ; "Sexverse LG389 Protocol")] +#[test_case("test_svakom_alex_v2.yaml" ; "Svakom Alex Neo 2")] +#[test_case("test_svakom_alex.yaml" ; "Svakom Alex Neo")] +#[test_case("test_svakom_barnard.yaml" ; "Svakom (Fantasy Cup) Barnard")] +#[test_case("test_svakom_cocopro.yaml" ; "Svakom Coco Pro")] +#[test_case("test_svakom_ella.yaml" ; "Svakom V1 Protocol - Ella")] +#[test_case("test_svakom_iker.yaml" ; "Svakom Iker")] +#[test_case("test_svakom_mora_neo.yaml" ; "Svakom Mora Neo")] +#[test_case("test_svakom_pulse.yaml" ; "Svakom Pulse Protocol - Pulse Lite Neo")] +#[test_case("test_svakom_sam2.yaml" ; "Svakom Sam Neo 2 Pro")] +#[test_case("test_svakom_theodore.yaml" ; "Svakom V3 Protocol - Theodore")] +#[test_case("test_svakom_vivianna.yaml" ; "Svakom V2 Protocol - Vivianna")] #[test_case("test_synchro_protocol.yaml" ; "Synchro Protocol")] -#[test_case("test_lelo_tianiharmony.yaml" ; "Lelo Harmony Protocol - Tiani Harmony")] +#[test_case("test_tcode_linear_and_vibrate.yaml" ; "TCode (Linear + Vibrate)")] +#[test_case("test_tryfun_blackhole_protocol.yaml" ; "TryFun Protocol - Black Hole Plus")] +#[test_case("test_tryfun_meta2_protocol.yaml" ; "TryFun Protocol - Meta 2")] +#[test_case("test_tryfun_protocol.yaml" ; "TryFun Protocol")] +#[test_case("test_tryfun_surge.yaml" ; "TryFun Protocol - Surge Pro")] +#[test_case("test_user_config_display_name.yaml" ; "User Config Display Name")] +#[test_case("test_vorze_cyclone.yaml" ; "Vorze Protocol - Cyclone")] +#[test_case("test_vorze_ufo_tw.yaml" ; "Vorze Protocol - UFO TW")] +#[test_case("test_vorze_ufo.yaml" ; "Vorze Protocol - UFO")] +#[test_case("test_vvd_vkini_protocol.yaml" ; "VVD Vikini (metaXsire v4) Protocol")] +#[test_case("test_wetoy_protocol.yaml" ; "WeToy Protocol")] +#[test_case("test_wevibe_4plus.yaml" ; "WeVibe Protocol (Legacy) - 4 Plus")] +#[test_case("test_wevibe_chorus.yaml" ; "WeVibe Protocol (Chorus) - Chorus")] +#[test_case("test_wevibe_moxie.yaml" ; "WeVibe Protocol (8bit) - Moxie")] +#[test_case("test_wevibe_pivot.yaml" ; "WeVibe Protocol (Legacy) - Pivot")] +#[test_case("test_wevibe_vector.yaml" ; "WeVibe Protocol (8bit) - Vector")] +#[test_case("test_xibao_protocol.yaml" ; "Xibao Protocol")] +#[test_case("test_xiuxiuda_protocol.yaml" ; "Xiuxiuda Protocol")] +#[test_case("test_xuanhuan_protocol.yaml" ; "Xuanhuan Protocol")] +#[tokio::test] +async fn test_device_protocols_embedded_v4(test_file: &str) { + //tracing_subscriber::fmt::init(); + util::device_test::client::client_v4::run_embedded_test_case(&load_test_case(test_file).await) + .await; +} + +//#[test_case("test_cowgirl_cone_protocol.yaml" ; "The Cowgirl Cone Protocol")] +#[test_case("test_activejoy_protocol.yaml" ; "ActiveJoy Protocol")] +#[test_case("test_adrienlastic_protocol.yaml" ; "Adrien Lastic Protocol")] +#[test_case("test_amorelie_joy_protocol.yaml" ; "Amorelie Joy Protocol")] +#[test_case("test_aneros_protocol.yaml" ; "Aneros Protocol")] +#[test_case("test_ankni_protocol_no_handshake.yaml" ; "Ankni Protocol - No Handshake")] +#[test_case("test_ankni_protocol.yaml" ; "Ankni Protocol")] +#[test_case("test_bananasome_protocol.yaml" ; "Bananasome Protocol")] +#[test_case("test_cachito_protocol.yaml" ; "Cachito Protocol")] +#[test_case("test_cowgirl_protocol.yaml" ; "The Cowgirl Protocol")] +#[test_case("test_cupido_protocol.yaml" ; "Cupido Protocol")] +#[test_case("test_deepsire.yaml" ; "DeepSire Protocol")] +#[test_case("test_feelingso.yaml" ; "FeelingSo Protocol")] +#[test_case("test_fleshy_thrust_protocol.yaml" ; "Fleshy Thrust Sync Protocol")] +#[test_case("test_foreo_protocol.yaml" ; "Foreo Protocol")] +#[test_case("test_fox_protocol.yaml" ; "Fox Protocol")] +//#[test_case("test_fredorch_protocol.yaml" ; "Fredorch Protocol")] +#[test_case("test_galaku_nebula.yaml" ; "Galaku Pump Protocol - Nebula")] +#[test_case("test_galaku.yaml" ; "Galaku Protocol")] +#[test_case("test_hgod_protocol.yaml" ; "Hgod Protocol")] +#[test_case("test_hismith_auxfun_box.yaml" ; "Hismith Mini Protocol - Auxfun Box")] +#[test_case("test_hismith_sinloli.yaml" ; "Hismith Mini Protocol - Sinloli")] +#[test_case("test_hismith_thrusting_cup.yaml" ; "Hismith Protocol - Thrusting Cup")] +#[test_case("test_hismith_v4.yaml" ; "Hismith Mini Protocol - Hismith v4")] +#[test_case("test_hismith_wildolo.yaml" ; "Hismith Protocol - Wildolo")] +#[test_case("test_itoys_protocol.yaml" ; "iToys Protocol")] +#[test_case("test_itoys_twinklingstars.yaml" ; "iToys Protocol - Twinkling Stars")] +#[test_case("test_joyhub_moonhorn.yaml" ; "JoyHub Protocol - Moonhorn")] +#[test_case("test_joyhub_petalwish_compat.yaml" ; "JoyHub Protocol - Petalwish Compat")] +#[test_case("test_joyhub_petalwish.yaml" ; "JoyHub Protocol - Petalwish")] +#[test_case("test_joyhub_roselin.yaml" ; "JoyHub Protocol - RoseLin")] +#[test_case("test_kiiroo_prowand.yaml" ; "Kiiroo ProWand Protocol")] +#[test_case("test_kiiroo_spot.yaml" ; "Kiiroo Spot Protocol")] #[test_case("test_lelo_f1sv1.yaml" ; "Lelo F1s V1 Protocol")] #[test_case("test_lelo_f1sv2.yaml" ; "Lelo F1s V2 Protocol")] #[test_case("test_lelo_idawave.yaml" ; "Lelo Harmony Protocol - Ida Wave")] +#[test_case("test_lelo_tianiharmony.yaml" ; "Lelo Harmony Protocol - Tiani Harmony")] +#[test_case("test_leten_protocol.yaml" ; "Leten Protocol")] +#[test_case("test_loob_protocol.yaml" ; "Joyroid Loob Protocol")] +#[test_case("test_lovehoney_desire_egg.yaml" ; "Lovehoney Desire Protocol - Love Egg")] +#[test_case("test_lovehoney_desire_prostate.yaml" ; "Lovehoney Desire Protocol - Prostate Vibe")] +#[test_case("test_lovense_battery_non_default.yaml" ; "Lovense Protocol - Lovense Battery (Non-Default Devices)")] +#[test_case("test_lovense_battery.yaml" ; "Lovense Protocol - Lovense Battery (Default Devices)")] +#[test_case("test_lovense_edge.yaml" ; "Lovense Protocol - Edge")] +#[test_case("test_lovense_flexer_fw2.yaml" ; "Lovense Protocol - Flexer FW2")] +//#[test_case("test_lovense_flexer_fw3.yaml" ; "Lovense Protocol - Flexer FW3")] +#[test_case("test_lovense_max.yaml" ; "Lovense Protocol - Lovense Max (Vibrate/Constrict)")] +#[test_case("test_lovense_nora.yaml" ; "Lovense Protocol - Lovense Nora (Vibrate/Rotate)")] +//#[test_case("test_lovense_osci3.yaml" ; "Lovense Protocol - Osci3")] +#[test_case("test_lovense_ridge_user_config.yaml" ; "Lovense Protocol - Lovense Ridge (User Config)")] +#[test_case("test_lovense_ridge.yaml" ; "Lovense Protocol - Lovense Ridge (Oscillate)")] +#[test_case("test_lovense_single_vibrator.yaml" ; "Lovense Protocol - Single Vibrator Device")] +#[test_case("test_luvmazer_protocol.yaml" ; "Luvmazer Protocol")] #[test_case("test_magic_motion_1_magic_cell.yaml" ; "MagicMotion Protocol 1 - Magic Cell")] -#[test_case("test_magic_motion_2_eidolon.yaml" ; "MagicMotion Protocol 2 - Eidolon")] +////#[test_case("test_magic_motion_2_eidolon.yaml" ; "MagicMotion Protocol 2 - Eidolon")] #[test_case("test_magic_motion_2_equinox.yaml" ; "MagicMotion Protocol 2 - Equinox")] #[test_case("test_magic_motion_3_krush.yaml" ; "MagicMotion Protocol 3 - Krush")] #[test_case("test_magic_motion_4_bobi.yaml" ; "MagicMotion Protocol 4 - Bobi")] #[test_case("test_magic_motion_4_nyx.yaml" ; "MagicMotion Protocol 4 - Nyx")] -#[test_case("test_hgod_protocol.yaml" ; "Hgod Protocol")] -#[test_case("test_tryfun_protocol.yaml" ; "TryFun Protocol")] -#[test_case("test_tryfun_surge.yaml" ; "TryFun Protocol - Surge Pro")] -#[test_case("test_tryfun_meta2_protocol.yaml" ; "TryFun Protocol - Meta 2")] -#[test_case("test_tryfun_blackhole_protocol.yaml" ; "TryFun Protocol - Black Hole Plus")] -#[test_case("test_metaxsire_rex.yaml" ; "metaXsire Protocol - Rex")] -#[test_case("test_metaxsire_olis.yaml" ; "metaXsire Protocol - Olis")] +#[test_case("test_meese_protocol.yaml" ; "Meese Protocol")] #[test_case("test_metaxsire_cali.yaml" ; "metaXsire Protocol - Cali")] #[test_case("test_metaxsire_nolan.yaml" ; "metaXsire Protocol v2 - Nolan")] -#[test_case("test_sexverse_lg389_protocol.yaml" ; "Sexverse LG389 Protocol")] -#[test_case("test_cowgirl_protocol.yaml" ; "The Cowgirl Protocol")] -#[test_case("test_galaku_nebula.yaml" ; "Galaku Pump Protocol - Nebula")] -#[test_case("test_galaku.yaml" ; "Galaku Protocol")] -#[test_case("test_xibao_protocol.yaml" ; "Xibao Protocol")] -#[test_case("test_sensee_protocol.yaml" ; "Sensee Diandou Protocol - Rabbit")] +#[test_case("test_metaxsire_olis.yaml" ; "metaXsire Protocol - Olis")] +#[test_case("test_metaxsire_rex.yaml" ; "metaXsire Protocol - Rex")] +#[test_case("test_mizzzee_protocol.yaml" ; "Mizz Zee Protocol")] +#[test_case("test_mizzzee_v2_protocol.yaml" ; "Mizz Zee v2 Protocol")] +#[test_case("test_mizzzee_v3_protocol.yaml" ; "Mizz Zee v3 Protocol")] +#[test_case("test_motorbunny_protocol.yaml" ; "Motorbunny Protocol")] +#[test_case("test_mysteryvibe.yaml" ; "Mysteryvibe Protocol")] +#[test_case("test_nexus_revo.yaml" ; "Nexus Revo Protocol")] +#[test_case("test_nobra_protocol.yaml" ; "Nobra Protocol")] +#[test_case("test_omobo_protocol.yaml" ; "Omobo Protocol")] +#[test_case("test_pink_punch_protocol.yaml" ; "Pink Punch Protocol")] +#[test_case("test_sakuraneko_koikoi.yaml" ; "Sakuraneko Protocol - Koikoi")] +#[test_case("test_sakuraneko_protocol.yaml" ; "Sakuraneko Protocol")] +#[test_case("test_satisfyer_triple_vibrator.yaml" ; "Satisfyer Protocol - Triple Vibrator")] +#[test_case("test_satisfyer_dual_vibrator.yaml" ; "Satisfyer Protocol - Dual Vibrator")] +#[test_case("test_satisfyer_single_vibrator.yaml" ; "Satisfyer Protocol - Single Vibrator")] #[test_case("test_sensee_capsule.yaml" ; "Sensee Capsule Protocol")] -#[test_case("test_svakom_pulse.yaml" ; "Svakom Pulse Protocol - Pulse Lite Neo")] -#[test_case("test_svakom_ella.yaml" ; "Svakom V1 Protocol - Ella")] -#[test_case("test_svakom_vivianna.yaml" ; "Svakom V2 Protocol - Vivianna")] -#[test_case("test_svakom_theodore.yaml" ; "Svakom V3 Protocol - Theodore")] -#[test_case("test_svakom_alex.yaml" ; "Svakom Alex Neo")] +#[test_case("test_sensee_protocol.yaml" ; "Sensee Diandou Protocol - Rabbit")] +#[test_case("test_serveu_protocol.yaml" ; "ServeU")] +#[test_case("test_sexverse_heart_protocol.yaml" ; "Sexverse Heart Protocol")] +#[test_case("test_sexverse_lg389_protocol.yaml" ; "Sexverse LG389 Protocol")] #[test_case("test_svakom_alex_v2.yaml" ; "Svakom Alex Neo 2")] -#[test_case("test_svakom_iker.yaml" ; "Svakom Iker")] -#[test_case("test_svakom_cocopro.yaml" ; "Svakom Coco Pro")] +#[test_case("test_svakom_alex.yaml" ; "Svakom Alex Neo")] #[test_case("test_svakom_barnard.yaml" ; "Svakom (Fantasy Cup) Barnard")] +#[test_case("test_svakom_cocopro.yaml" ; "Svakom Coco Pro")] +#[test_case("test_svakom_ella.yaml" ; "Svakom V1 Protocol - Ella")] +#[test_case("test_svakom_iker.yaml" ; "Svakom Iker")] #[test_case("test_svakom_mora_neo.yaml" ; "Svakom Mora Neo")] -#[test_case("test_fox_protocol.yaml" ; "Fox Protocol")] -#[test_case("test_sakuraneko_koikoi.yaml" ; "Sakuraneko Protocol - Koikoi")] +#[test_case("test_svakom_pulse.yaml" ; "Svakom Pulse Protocol - Pulse Lite Neo")] +#[test_case("test_svakom_sam2.yaml" ; "Svakom Sam Neo 2 Pro")] +#[test_case("test_svakom_theodore.yaml" ; "Svakom V3 Protocol - Theodore")] +#[test_case("test_svakom_vivianna.yaml" ; "Svakom V2 Protocol - Vivianna")] +#[test_case("test_synchro_protocol.yaml" ; "Synchro Protocol")] +#[test_case("test_tcode_linear_and_vibrate.yaml" ; "TCode (Linear + Vibrate)")] +#[test_case("test_tryfun_blackhole_protocol.yaml" ; "TryFun Protocol - Black Hole Plus")] +#[test_case("test_tryfun_meta2_protocol.yaml" ; "TryFun Protocol - Meta 2")] +#[test_case("test_tryfun_protocol.yaml" ; "TryFun Protocol")] +#[test_case("test_tryfun_surge.yaml" ; "TryFun Protocol - Surge Pro")] +#[test_case("test_user_config_display_name.yaml" ; "User Config Display Name")] +#[test_case("test_vorze_cyclone.yaml" ; "Vorze Protocol - Cyclone")] +#[test_case("test_vorze_ufo_tw.yaml" ; "Vorze Protocol - UFO TW")] +#[test_case("test_vorze_ufo.yaml" ; "Vorze Protocol - UFO")] +#[test_case("test_vvd_vkini_protocol.yaml" ; "VVD Vikini (metaXsire v4) Protocol")] +#[test_case("test_wetoy_protocol.yaml" ; "WeToy Protocol")] +#[test_case("test_wevibe_4plus.yaml" ; "WeVibe Protocol (Legacy) - 4 Plus")] +#[test_case("test_wevibe_chorus.yaml" ; "WeVibe Protocol (Chorus) - Chorus")] +#[test_case("test_wevibe_moxie.yaml" ; "WeVibe Protocol (8bit) - Moxie")] +#[test_case("test_wevibe_pivot.yaml" ; "WeVibe Protocol (Legacy) - Pivot")] +#[test_case("test_wevibe_vector.yaml" ; "WeVibe Protocol (8bit) - Vector")] +#[test_case("test_xibao_protocol.yaml" ; "Xibao Protocol")] #[test_case("test_xiuxiuda_protocol.yaml" ; "Xiuxiuda Protocol")] -#[test_case("test_longlosttouch_protocol.yaml" ; "LongLostTouch Protocol")] +#[test_case("test_xuanhuan_protocol.yaml" ; "Xuanhuan Protocol")] +#[tokio::test] +async fn test_device_protocols_json_v4(test_file: &str) { + //tracing_subscriber::fmt::init(); + util::device_test::client::client_v4::run_json_test_case(&load_test_case(test_file).await).await; +} + +//#[test_case("test_cowgirl_cone_protocol.yaml" ; "The Cowgirl Cone Protocol")] +#[test_case("test_activejoy_protocol.yaml" ; "ActiveJoy Protocol")] #[test_case("test_adrienlastic_protocol.yaml" ; "Adrien Lastic Protocol")] +#[test_case("test_amorelie_joy_protocol.yaml" ; "Amorelie Joy Protocol")] +#[test_case("test_aneros_protocol.yaml" ; "Aneros Protocol")] +#[test_case("test_ankni_protocol_no_handshake.yaml" ; "Ankni Protocol - No Handshake")] +#[test_case("test_ankni_protocol.yaml" ; "Ankni Protocol")] +#[test_case("test_bananasome_protocol.yaml" ; "Bananasome Protocol")] +#[test_case("test_cachito_protocol.yaml" ; "Cachito Protocol")] +#[test_case("test_cowgirl_protocol.yaml" ; "The Cowgirl Protocol")] +#[test_case("test_cupido_protocol.yaml" ; "Cupido Protocol")] +#[test_case("test_deepsire.yaml" ; "DeepSire Protocol")] +#[test_case("test_feelingso.yaml" ; "FeelingSo Protocol")] +#[test_case("test_fleshy_thrust_protocol.yaml" ; "Fleshy Thrust Sync Protocol")] #[test_case("test_foreo_protocol.yaml" ; "Foreo Protocol")] -#[test_case("test_loob_protocol.yaml" ; "Joyroid Loob Protocol")] -#[test_case("test_joyhub_petalwish.yaml" ; "JoyHub Protocol - Petalwish")] +#[test_case("test_fox_protocol.yaml" ; "Fox Protocol")] +//#[test_case("test_fredorch_protocol.yaml" ; "Fredorch Protocol")] +#[test_case("test_galaku_nebula.yaml" ; "Galaku Pump Protocol - Nebula")] +#[test_case("test_galaku.yaml" ; "Galaku Protocol")] +#[test_case("test_hgod_protocol.yaml" ; "Hgod Protocol")] +#[test_case("test_hismith_auxfun_box.yaml" ; "Hismith Mini Protocol - Auxfun Box")] +#[test_case("test_hismith_sinloli.yaml" ; "Hismith Mini Protocol - Sinloli")] +#[test_case("test_hismith_thrusting_cup.yaml" ; "Hismith Protocol - Thrusting Cup")] +#[test_case("test_hismith_v4.yaml" ; "Hismith Mini Protocol - Hismith v4")] +#[test_case("test_hismith_wildolo.yaml" ; "Hismith Protocol - Wildolo")] +#[test_case("test_itoys_protocol.yaml" ; "iToys Protocol")] +#[test_case("test_itoys_twinklingstars.yaml" ; "iToys Protocol - Twinkling Stars")] +#[test_case("test_joyhub_moonhorn.yaml" ; "JoyHub Protocol - Moonhorn")] #[test_case("test_joyhub_petalwish_compat.yaml" ; "JoyHub Protocol - Petalwish Compat")] +#[test_case("test_joyhub_petalwish.yaml" ; "JoyHub Protocol - Petalwish")] #[test_case("test_joyhub_roselin.yaml" ; "JoyHub Protocol - RoseLin")] -#[test_case("test_joyhub_moonhorn.yaml" ; "JoyHub Protocol - Moonhorn")] -#[test_case("test_itoys_protocol.yaml" ; "iToys Protocol")] -#[test_case("test_leten_protocol.yaml" ; "Leten Protocol")] -#[test_case("test_motorbunny_protocol.yaml" ; "Motorbunny Protocol")] -#[test_case("test_activejoy_protocol.yaml" ; "ActiveJoy Protocol")] -#[test_case("test_cupido_protocol.yaml" ; "Cupido Protocol")] -//#[test_case("test_cowgirl_cone_protocol.yaml" ; "The Cowgirl Cone Protocol")] -#[test_case("test_amorelie_joy_protocol.yaml" ; "Amorelie Joy Protocol")] -#[test_case("test_svakom_sam2.yaml" ; "Svakom Sam Neo 2 Pro")] -#[test_case("test_feelingso.yaml" ; "FeelingSo Protocol")] -#[test_case("test_deepsire.yaml" ; "DeepSire Protocol")] -#[test_case("test_xuanhuan_protocol.yaml" ; "Xuanhuan Protocol")] -#[test_case("test_tcode_linear_and_vibrate.yaml" ; "TCode (Linear + Vibrate)")] -#[test_case("test_serveu_protocol.yaml" ; "ServeU")] #[test_case("test_kiiroo_prowand.yaml" ; "Kiiroo ProWand Protocol")] #[test_case("test_kiiroo_spot.yaml" ; "Kiiroo Spot Protocol")] -#[test_case("test_fleshy_thrust_protocol.yaml" ; "Fleshy Thrust Sync Protocol")] -#[test_case("test_nexus_revo.yaml" ; "Nexus Revo Protocol")] +#[test_case("test_lelo_f1sv1.yaml" ; "Lelo F1s V1 Protocol")] +#[test_case("test_lelo_f1sv2.yaml" ; "Lelo F1s V2 Protocol")] +#[test_case("test_lelo_idawave.yaml" ; "Lelo Harmony Protocol - Ida Wave")] +#[test_case("test_lelo_tianiharmony.yaml" ; "Lelo Harmony Protocol - Tiani Harmony")] +#[test_case("test_leten_protocol.yaml" ; "Leten Protocol")] +#[test_case("test_loob_protocol.yaml" ; "Joyroid Loob Protocol")] +#[test_case("test_lovehoney_desire_egg.yaml" ; "Lovehoney Desire Protocol - Love Egg")] +#[test_case("test_lovehoney_desire_prostate.yaml" ; "Lovehoney Desire Protocol - Prostate Vibe")] +#[test_case("test_lovense_battery_non_default.yaml" ; "Lovense Protocol - Lovense Battery (Non-Default Devices)")] +#[test_case("test_lovense_battery.yaml" ; "Lovense Protocol - Lovense Battery (Default Devices)")] +#[test_case("test_lovense_edge.yaml" ; "Lovense Protocol - Edge")] +#[test_case("test_lovense_flexer_fw2.yaml" ; "Lovense Protocol - Flexer FW2")] +//#[test_case("test_lovense_flexer_fw3.yaml" ; "Lovense Protocol - Flexer FW3")] +#[test_case("test_lovense_max.yaml" ; "Lovense Protocol - Lovense Max (Vibrate/Constrict)")] +#[test_case("test_lovense_nora.yaml" ; "Lovense Protocol - Lovense Nora (Vibrate/Rotate)")] +//#[test_case("test_lovense_osci3.yaml" ; "Lovense Protocol - Osci3")] +#[test_case("test_lovense_ridge_user_config.yaml" ; "Lovense Protocol - Lovense Ridge (User Config)")] +#[test_case("test_lovense_ridge.yaml" ; "Lovense Protocol - Lovense Ridge (Oscillate)")] +#[test_case("test_lovense_single_vibrator.yaml" ; "Lovense Protocol - Single Vibrator Device")] #[test_case("test_luvmazer_protocol.yaml" ; "Luvmazer Protocol")] -#[test_case("test_bananasome_protocol.yaml" ; "Bananasome Protocol")] +#[test_case("test_magic_motion_1_magic_cell.yaml" ; "MagicMotion Protocol 1 - Magic Cell")] +////#[test_case("test_magic_motion_2_eidolon.yaml" ; "MagicMotion Protocol 2 - Eidolon")] +#[test_case("test_magic_motion_2_equinox.yaml" ; "MagicMotion Protocol 2 - Equinox")] +#[test_case("test_magic_motion_3_krush.yaml" ; "MagicMotion Protocol 3 - Krush")] +#[test_case("test_magic_motion_4_bobi.yaml" ; "MagicMotion Protocol 4 - Bobi")] +#[test_case("test_magic_motion_4_nyx.yaml" ; "MagicMotion Protocol 4 - Nyx")] +#[test_case("test_meese_protocol.yaml" ; "Meese Protocol")] +#[test_case("test_metaxsire_cali.yaml" ; "metaXsire Protocol - Cali")] +#[test_case("test_metaxsire_nolan.yaml" ; "metaXsire Protocol v2 - Nolan")] +#[test_case("test_metaxsire_olis.yaml" ; "metaXsire Protocol - Olis")] +#[test_case("test_metaxsire_rex.yaml" ; "metaXsire Protocol - Rex")] +#[test_case("test_mizzzee_protocol.yaml" ; "Mizz Zee Protocol")] +#[test_case("test_mizzzee_v2_protocol.yaml" ; "Mizz Zee v2 Protocol")] +#[test_case("test_mizzzee_v3_protocol.yaml" ; "Mizz Zee v3 Protocol")] +#[test_case("test_motorbunny_protocol.yaml" ; "Motorbunny Protocol")] +#[test_case("test_mysteryvibe.yaml" ; "Mysteryvibe Protocol")] +#[test_case("test_nexus_revo.yaml" ; "Nexus Revo Protocol")] +#[test_case("test_nobra_protocol.yaml" ; "Nobra Protocol")] #[test_case("test_omobo_protocol.yaml" ; "Omobo Protocol")] +#[test_case("test_pink_punch_protocol.yaml" ; "Pink Punch Protocol")] +#[test_case("test_sakuraneko_koikoi.yaml" ; "Sakuraneko Protocol - Koikoi")] +#[test_case("test_sakuraneko_protocol.yaml" ; "Sakuraneko Protocol")] +#[test_case("test_satisfyer_triple_vibrator.yaml" ; "Satisfyer Protocol - Triple Vibrator")] +#[test_case("test_satisfyer_dual_vibrator.yaml" ; "Satisfyer Protocol - Dual Vibrator")] +#[test_case("test_satisfyer_single_vibrator.yaml" ; "Satisfyer Protocol - Single Vibrator")] +#[test_case("test_sensee_capsule.yaml" ; "Sensee Capsule Protocol")] +#[test_case("test_sensee_protocol.yaml" ; "Sensee Diandou Protocol - Rabbit")] +#[test_case("test_serveu_protocol.yaml" ; "ServeU")] +#[test_case("test_sexverse_heart_protocol.yaml" ; "Sexverse Heart Protocol")] +#[test_case("test_sexverse_lg389_protocol.yaml" ; "Sexverse LG389 Protocol")] +#[test_case("test_svakom_alex_v2.yaml" ; "Svakom Alex Neo 2")] +#[test_case("test_svakom_alex.yaml" ; "Svakom Alex Neo")] +#[test_case("test_svakom_barnard.yaml" ; "Svakom (Fantasy Cup) Barnard")] +#[test_case("test_svakom_cocopro.yaml" ; "Svakom Coco Pro")] +#[test_case("test_svakom_ella.yaml" ; "Svakom V1 Protocol - Ella")] +#[test_case("test_svakom_iker.yaml" ; "Svakom Iker")] +#[test_case("test_svakom_mora_neo.yaml" ; "Svakom Mora Neo")] +#[test_case("test_svakom_pulse.yaml" ; "Svakom Pulse Protocol - Pulse Lite Neo")] +#[test_case("test_svakom_sam2.yaml" ; "Svakom Sam Neo 2 Pro")] +#[test_case("test_svakom_theodore.yaml" ; "Svakom V3 Protocol - Theodore")] +#[test_case("test_svakom_vivianna.yaml" ; "Svakom V2 Protocol - Vivianna")] +#[test_case("test_synchro_protocol.yaml" ; "Synchro Protocol")] +#[test_case("test_tcode_linear_and_vibrate.yaml" ; "TCode (Linear + Vibrate)")] +#[test_case("test_tryfun_blackhole_protocol.yaml" ; "TryFun Protocol - Black Hole Plus")] +#[test_case("test_tryfun_meta2_protocol.yaml" ; "TryFun Protocol - Meta 2")] +#[test_case("test_tryfun_protocol.yaml" ; "TryFun Protocol")] +#[test_case("test_tryfun_surge.yaml" ; "TryFun Protocol - Surge Pro")] +#[test_case("test_user_config_display_name.yaml" ; "User Config Display Name")] +#[test_case("test_vorze_cyclone.yaml" ; "Vorze Protocol - Cyclone")] +#[test_case("test_vorze_ufo_tw.yaml" ; "Vorze Protocol - UFO TW")] +#[test_case("test_vorze_ufo.yaml" ; "Vorze Protocol - UFO")] +#[test_case("test_vvd_vkini_protocol.yaml" ; "VVD Vikini (metaXsire v4) Protocol")] +#[test_case("test_wetoy_protocol.yaml" ; "WeToy Protocol")] +#[test_case("test_wevibe_4plus.yaml" ; "WeVibe Protocol (Legacy) - 4 Plus")] +#[test_case("test_wevibe_chorus.yaml" ; "WeVibe Protocol (Chorus) - Chorus")] +#[test_case("test_wevibe_moxie.yaml" ; "WeVibe Protocol (8bit) - Moxie")] +#[test_case("test_wevibe_pivot.yaml" ; "WeVibe Protocol (Legacy) - Pivot")] +#[test_case("test_wevibe_vector.yaml" ; "WeVibe Protocol (8bit) - Vector")] +#[test_case("test_xibao_protocol.yaml" ; "Xibao Protocol")] +#[test_case("test_xiuxiuda_protocol.yaml" ; "Xiuxiuda Protocol")] +#[test_case("test_xuanhuan_protocol.yaml" ; "Xuanhuan Protocol")] #[tokio::test] async fn test_device_protocols_embedded_v3(test_file: &str) { //tracing_subscriber::fmt::init(); - //error!("RUNNING TEST CASE"); util::device_test::client::client_v3::run_embedded_test_case(&load_test_case(test_file).await) .await; } +//#[test_case("test_cowgirl_cone_protocol.yaml" ; "The Cowgirl Cone Protocol")] +#[test_case("test_activejoy_protocol.yaml" ; "ActiveJoy Protocol")] +#[test_case("test_adrienlastic_protocol.yaml" ; "Adrien Lastic Protocol")] +#[test_case("test_amorelie_joy_protocol.yaml" ; "Amorelie Joy Protocol")] #[test_case("test_aneros_protocol.yaml" ; "Aneros Protocol")] -#[test_case("test_ankni_protocol.yaml" ; "Ankni Protocol")] #[test_case("test_ankni_protocol_no_handshake.yaml" ; "Ankni Protocol - No Handshake")] +#[test_case("test_ankni_protocol.yaml" ; "Ankni Protocol")] +#[test_case("test_bananasome_protocol.yaml" ; "Bananasome Protocol")] #[test_case("test_cachito_protocol.yaml" ; "Cachito Protocol")] -#[test_case("test_fredorch_protocol.yaml" ; "Fredorch Protocol")] +#[test_case("test_cowgirl_protocol.yaml" ; "The Cowgirl Protocol")] +#[test_case("test_cupido_protocol.yaml" ; "Cupido Protocol")] +#[test_case("test_deepsire.yaml" ; "DeepSire Protocol")] +#[test_case("test_feelingso.yaml" ; "FeelingSo Protocol")] +#[test_case("test_fleshy_thrust_protocol.yaml" ; "Fleshy Thrust Sync Protocol")] +#[test_case("test_foreo_protocol.yaml" ; "Foreo Protocol")] +#[test_case("test_fox_protocol.yaml" ; "Fox Protocol")] +//#[test_case("test_fredorch_protocol.yaml" ; "Fredorch Protocol")] +#[test_case("test_galaku_nebula.yaml" ; "Galaku Pump Protocol - Nebula")] +#[test_case("test_galaku.yaml" ; "Galaku Protocol")] +#[test_case("test_hgod_protocol.yaml" ; "Hgod Protocol")] #[test_case("test_hismith_auxfun_box.yaml" ; "Hismith Mini Protocol - Auxfun Box")] -#[test_case("test_hismith_v4.yaml" ; "Hismith Mini Protocol - Hismith v4")] #[test_case("test_hismith_sinloli.yaml" ; "Hismith Mini Protocol - Sinloli")] #[test_case("test_hismith_thrusting_cup.yaml" ; "Hismith Protocol - Thrusting Cup")] +#[test_case("test_hismith_v4.yaml" ; "Hismith Mini Protocol - Hismith v4")] #[test_case("test_hismith_wildolo.yaml" ; "Hismith Protocol - Wildolo")] -#[test_case("test_lovense_single_vibrator.yaml" ; "Lovense Protocol - Single Vibrator Device")] +#[test_case("test_itoys_protocol.yaml" ; "iToys Protocol")] +#[test_case("test_itoys_twinklingstars.yaml" ; "iToys Protocol - Twinkling Stars")] +#[test_case("test_joyhub_moonhorn.yaml" ; "JoyHub Protocol - Moonhorn")] +#[test_case("test_joyhub_petalwish_compat.yaml" ; "JoyHub Protocol - Petalwish Compat")] +#[test_case("test_joyhub_petalwish.yaml" ; "JoyHub Protocol - Petalwish")] +#[test_case("test_joyhub_roselin.yaml" ; "JoyHub Protocol - RoseLin")] +#[test_case("test_kiiroo_prowand.yaml" ; "Kiiroo ProWand Protocol")] +#[test_case("test_kiiroo_spot.yaml" ; "Kiiroo Spot Protocol")] +#[test_case("test_lelo_f1sv1.yaml" ; "Lelo F1s V1 Protocol")] +#[test_case("test_lelo_f1sv2.yaml" ; "Lelo F1s V2 Protocol")] +#[test_case("test_lelo_idawave.yaml" ; "Lelo Harmony Protocol - Ida Wave")] +#[test_case("test_lelo_tianiharmony.yaml" ; "Lelo Harmony Protocol - Tiani Harmony")] +#[test_case("test_leten_protocol.yaml" ; "Leten Protocol")] +#[test_case("test_loob_protocol.yaml" ; "Joyroid Loob Protocol")] +#[test_case("test_lovehoney_desire_egg.yaml" ; "Lovehoney Desire Protocol - Love Egg")] +#[test_case("test_lovehoney_desire_prostate.yaml" ; "Lovehoney Desire Protocol - Prostate Vibe")] +#[test_case("test_lovense_battery_non_default.yaml" ; "Lovense Protocol - Lovense Battery (Non-Default Devices)")] +#[test_case("test_lovense_battery.yaml" ; "Lovense Protocol - Lovense Battery (Default Devices)")] +#[test_case("test_lovense_edge.yaml" ; "Lovense Protocol - Edge")] +#[test_case("test_lovense_flexer_fw2.yaml" ; "Lovense Protocol - Flexer FW2")] +//#[test_case("test_lovense_flexer_fw3.yaml" ; "Lovense Protocol - Flexer FW3")] #[test_case("test_lovense_max.yaml" ; "Lovense Protocol - Lovense Max (Vibrate/Constrict)")] #[test_case("test_lovense_nora.yaml" ; "Lovense Protocol - Lovense Nora (Vibrate/Rotate)")] -#[test_case("test_lovense_ridge.yaml" ; "Lovense Protocol - Lovense Ridge (Oscillate)")] -#[test_case("test_lovense_battery.yaml" ; "Lovense Protocol - Lovense Battery (Default Devices)")] -#[test_case("test_lovense_battery_non_default.yaml" ; "Lovense Protocol - Lovense Battery (Non-Default Devices)")] +//#[test_case("test_lovense_osci3.yaml" ; "Lovense Protocol - Osci3")] #[test_case("test_lovense_ridge_user_config.yaml" ; "Lovense Protocol - Lovense Ridge (User Config)")] -#[test_case("test_lovense_flexer_fw2.yaml" ; "Lovense Protocol - Flexer FW2")] -#[test_case("test_lovense_flexer_fw3.yaml" ; "Lovense Protocol - Flexer FW3")] -#[test_case("test_lovense_edge.yaml" ; "Lovense Protocol - Edge")] -#[test_case("test_lovense_osci3.yaml" ; "Lovense Protocol - Osci3")] -#[test_case("test_user_config_display_name.yaml" ; "User Config Display Name")] -#[test_case("test_satisfyer_single_vibrator.yaml" ; "Satisfyer Protocol - Single Vibrator")] -#[test_case("test_satisfyer_dual_vibrator.yaml" ; "Satisfyer Protocol - Dual Vibrator")] -#[test_case("test_satisfyer_triple_vibrator.yaml" ; "Satisfyer Protocol - Triple Vibrator")] -#[test_case("test_mysteryvibe.yaml" ; "Mysteryvibe Protocol")] +#[test_case("test_lovense_ridge.yaml" ; "Lovense Protocol - Lovense Ridge (Oscillate)")] +#[test_case("test_lovense_single_vibrator.yaml" ; "Lovense Protocol - Single Vibrator Device")] +#[test_case("test_luvmazer_protocol.yaml" ; "Luvmazer Protocol")] +#[test_case("test_magic_motion_1_magic_cell.yaml" ; "MagicMotion Protocol 1 - Magic Cell")] +////#[test_case("test_magic_motion_2_eidolon.yaml" ; "MagicMotion Protocol 2 - Eidolon")] +#[test_case("test_magic_motion_2_equinox.yaml" ; "MagicMotion Protocol 2 - Equinox")] +#[test_case("test_magic_motion_3_krush.yaml" ; "MagicMotion Protocol 3 - Krush")] +#[test_case("test_magic_motion_4_bobi.yaml" ; "MagicMotion Protocol 4 - Bobi")] +#[test_case("test_magic_motion_4_nyx.yaml" ; "MagicMotion Protocol 4 - Nyx")] #[test_case("test_meese_protocol.yaml" ; "Meese Protocol")] +#[test_case("test_metaxsire_cali.yaml" ; "metaXsire Protocol - Cali")] +#[test_case("test_metaxsire_nolan.yaml" ; "metaXsire Protocol v2 - Nolan")] +#[test_case("test_metaxsire_olis.yaml" ; "metaXsire Protocol - Olis")] +#[test_case("test_metaxsire_rex.yaml" ; "metaXsire Protocol - Rex")] #[test_case("test_mizzzee_protocol.yaml" ; "Mizz Zee Protocol")] #[test_case("test_mizzzee_v2_protocol.yaml" ; "Mizz Zee v2 Protocol")] #[test_case("test_mizzzee_v3_protocol.yaml" ; "Mizz Zee v3 Protocol")] -#[test_case("test_vorze_ufo.yaml" ; "Vorze Protocol - UFO")] -#[test_case("test_vorze_ufo_tw.yaml" ; "Vorze Protocol - UFO TW")] -#[test_case("test_vorze_cyclone.yaml" ; "Vorze Protocol - Cyclone")] -#[test_case("test_wevibe_4plus.yaml" ; "WeVibe Protocol (Legacy) - 4 Plus")] -#[test_case("test_wevibe_pivot.yaml" ; "WeVibe Protocol (Legacy) - Pivot")] -#[test_case("test_wevibe_vector.yaml" ; "WeVibe Protocol (8bit) - Vector")] -#[test_case("test_wevibe_moxie.yaml" ; "WeVibe Protocol (8bit) - Moxie")] -#[test_case("test_wevibe_chorus.yaml" ; "WeVibe Protocol (Chorus) - Chorus")] +#[test_case("test_motorbunny_protocol.yaml" ; "Motorbunny Protocol")] +#[test_case("test_mysteryvibe.yaml" ; "Mysteryvibe Protocol")] +#[test_case("test_nexus_revo.yaml" ; "Nexus Revo Protocol")] #[test_case("test_nobra_protocol.yaml" ; "Nobra Protocol")] -#[test_case("test_lovehoney_desire_prostate.yaml" ; "Lovehoney Desire Protocol - Prostate Vibe")] -#[test_case("test_lovehoney_desire_egg.yaml" ; "Lovehoney Desire Protocol - Love Egg")] -#[test_case("test_wetoy_protocol.yaml" ; "WeToy Protocol")] +#[test_case("test_omobo_protocol.yaml" ; "Omobo Protocol")] #[test_case("test_pink_punch_protocol.yaml" ; "Pink Punch Protocol")] +#[test_case("test_sakuraneko_koikoi.yaml" ; "Sakuraneko Protocol - Koikoi")] #[test_case("test_sakuraneko_protocol.yaml" ; "Sakuraneko Protocol")] +#[test_case("test_satisfyer_triple_vibrator.yaml" ; "Satisfyer Protocol - Triple Vibrator")] +#[test_case("test_satisfyer_dual_vibrator.yaml" ; "Satisfyer Protocol - Dual Vibrator")] +#[test_case("test_satisfyer_single_vibrator.yaml" ; "Satisfyer Protocol - Single Vibrator")] +#[test_case("test_sensee_capsule.yaml" ; "Sensee Capsule Protocol")] +#[test_case("test_sensee_protocol.yaml" ; "Sensee Diandou Protocol - Rabbit")] +#[test_case("test_serveu_protocol.yaml" ; "ServeU")] +#[test_case("test_sexverse_heart_protocol.yaml" ; "Sexverse Heart Protocol")] +#[test_case("test_sexverse_lg389_protocol.yaml" ; "Sexverse LG389 Protocol")] +#[test_case("test_svakom_alex_v2.yaml" ; "Svakom Alex Neo 2")] +#[test_case("test_svakom_alex.yaml" ; "Svakom Alex Neo")] +#[test_case("test_svakom_barnard.yaml" ; "Svakom (Fantasy Cup) Barnard")] +#[test_case("test_svakom_cocopro.yaml" ; "Svakom Coco Pro")] +#[test_case("test_svakom_ella.yaml" ; "Svakom V1 Protocol - Ella")] +#[test_case("test_svakom_iker.yaml" ; "Svakom Iker")] +#[test_case("test_svakom_mora_neo.yaml" ; "Svakom Mora Neo")] +#[test_case("test_svakom_pulse.yaml" ; "Svakom Pulse Protocol - Pulse Lite Neo")] +#[test_case("test_svakom_sam2.yaml" ; "Svakom Sam Neo 2 Pro")] +#[test_case("test_svakom_theodore.yaml" ; "Svakom V3 Protocol - Theodore")] +#[test_case("test_svakom_vivianna.yaml" ; "Svakom V2 Protocol - Vivianna")] #[test_case("test_synchro_protocol.yaml" ; "Synchro Protocol")] -#[test_case("test_lelo_tianiharmony.yaml" ; "Lelo Harmony Protocol - Tiani Harmony")] -#[test_case("test_lelo_f1sv1.yaml" ; "Lelo F1s V1 Protocol")] -#[test_case("test_lelo_f1sv2.yaml" ; "Lelo F1s V2 Protocol")] -#[test_case("test_lelo_idawave.yaml" ; "Lelo Harmony Protocol - Ida Wave")] -#[test_case("test_magic_motion_1_magic_cell.yaml" ; "MagicMotion Protocol 1 - Magic Cell")] -#[test_case("test_magic_motion_2_eidolon.yaml" ; "MagicMotion Protocol 2 - Eidolon")] -#[test_case("test_magic_motion_2_equinox.yaml" ; "MagicMotion Protocol 2 - Equinox")] -#[test_case("test_magic_motion_3_krush.yaml" ; "MagicMotion Protocol 3 - Krush")] -#[test_case("test_magic_motion_4_bobi.yaml" ; "MagicMotion Protocol 4 - Bobi")] -#[test_case("test_magic_motion_4_nyx.yaml" ; "MagicMotion Protocol 4 - Nyx")] -#[test_case("test_hgod_protocol.yaml" ; "Hgod Protocol")] +#[test_case("test_tcode_linear_and_vibrate.yaml" ; "TCode (Linear + Vibrate)")] +#[test_case("test_tryfun_blackhole_protocol.yaml" ; "TryFun Protocol - Black Hole Plus")] +#[test_case("test_tryfun_meta2_protocol.yaml" ; "TryFun Protocol - Meta 2")] #[test_case("test_tryfun_protocol.yaml" ; "TryFun Protocol")] #[test_case("test_tryfun_surge.yaml" ; "TryFun Protocol - Surge Pro")] -#[test_case("test_tryfun_meta2_protocol.yaml" ; "TryFun Protocol - Meta 2")] -#[test_case("test_tryfun_blackhole_protocol.yaml" ; "TryFun Protocol - Black Hole Plus")] -#[test_case("test_metaxsire_rex.yaml" ; "metaXsire Protocol - Rex")] -#[test_case("test_metaxsire_olis.yaml" ; "metaXsire Protocol - Olis")] -#[test_case("test_metaxsire_cali.yaml" ; "metaXsire Protocol - Cali")] -#[test_case("test_metaxsire_nolan.yaml" ; "metaXsire Protocol v2 - Nolan")] -#[test_case("test_sexverse_lg389_protocol.yaml" ; "Sexverse LG389 Protocol")] -#[test_case("test_cowgirl_protocol.yaml" ; "The Cowgirl Protocol")] -#[test_case("test_galaku_nebula.yaml" ; "Galaku Pump Protocol - Nebula")] -#[test_case("test_galaku.yaml" ; "Galaku Protocol")] +#[test_case("test_user_config_display_name.yaml" ; "User Config Display Name")] +#[test_case("test_vorze_cyclone.yaml" ; "Vorze Protocol - Cyclone")] +#[test_case("test_vorze_ufo_tw.yaml" ; "Vorze Protocol - UFO TW")] +#[test_case("test_vorze_ufo.yaml" ; "Vorze Protocol - UFO")] +#[test_case("test_vvd_vkini_protocol.yaml" ; "VVD Vikini (metaXsire v4) Protocol")] +#[test_case("test_wetoy_protocol.yaml" ; "WeToy Protocol")] +#[test_case("test_wevibe_4plus.yaml" ; "WeVibe Protocol (Legacy) - 4 Plus")] +#[test_case("test_wevibe_chorus.yaml" ; "WeVibe Protocol (Chorus) - Chorus")] +#[test_case("test_wevibe_moxie.yaml" ; "WeVibe Protocol (8bit) - Moxie")] +#[test_case("test_wevibe_pivot.yaml" ; "WeVibe Protocol (Legacy) - Pivot")] +#[test_case("test_wevibe_vector.yaml" ; "WeVibe Protocol (8bit) - Vector")] #[test_case("test_xibao_protocol.yaml" ; "Xibao Protocol")] -#[test_case("test_sensee_protocol.yaml" ; "Sensee Diandou Protocol - Rabbit")] -#[test_case("test_sensee_capsule.yaml" ; "Sensee Capsule Protocol")] -#[test_case("test_svakom_pulse.yaml" ; "Svakom Pulse Protocol - Pulse Lite Neo")] -#[test_case("test_svakom_ella.yaml" ; "Svakom V1 Protocol - Ella")] -#[test_case("test_svakom_vivianna.yaml" ; "Svakom V2 Protocol - Vivianna")] -#[test_case("test_svakom_theodore.yaml" ; "Svakom V3 Protocol - Theodore")] -#[test_case("test_svakom_alex.yaml" ; "Svakom Alex Neo")] -#[test_case("test_svakom_alex_v2.yaml" ; "Svakom Alex Neo 2")] -#[test_case("test_svakom_barnard.yaml" ; "Svakom (Fantasy Cup) Barnard")] -#[test_case("test_svakom_mora_neo.yaml" ; "Svakom Mora Neo")] -#[test_case("test_svakom_iker.yaml" ; "Svakom Iker")] -#[test_case("test_svakom_cocopro.yaml" ; "Svakom Coco Pro")] -#[test_case("test_fox_protocol.yaml" ; "Fox Protocol")] -#[test_case("test_sakuraneko_koikoi.yaml" ; "Sakuraneko Protocol - Koikoi")] #[test_case("test_xiuxiuda_protocol.yaml" ; "Xiuxiuda Protocol")] -#[test_case("test_adrienlastic_protocol.yaml" ; "Adrien Lastic Protocol")] -#[test_case("test_foreo_protocol.yaml" ; "Foreo Protocol")] -#[test_case("test_loob_protocol.yaml" ; "Joyroid Loob Protocol")] -#[test_case("test_joyhub_petalwish.yaml" ; "JoyHub Protocol - Petalwish")] -#[test_case("test_joyhub_petalwish_compat.yaml" ; "JoyHub Protocol - Petalwish Compat")] -#[test_case("test_joyhub_moonhorn.yaml" ; "JoyHub Protocol - Moonhorn")] -#[test_case("test_joyhub_roselin.yaml" ; "JoyHub Protocol - RoseLin")] -#[test_case("test_itoys_protocol.yaml" ; "iToys Protocol")] -#[test_case("test_leten_protocol.yaml" ; "Leten Protocol")] -#[test_case("test_motorbunny_protocol.yaml" ; "Motorbunny Protocol")] -#[test_case("test_activejoy_protocol.yaml" ; "ActiveJoy Protocol")] -#[test_case("test_cupido_protocol.yaml" ; "Cupido Protocol")] -//#[test_case("test_cowgirl_cone_protocol.yaml" ; "The Cowgirl Cone Protocol")] -#[test_case("test_amorelie_joy_protocol.yaml" ; "Amorelie Joy Protocol")] -#[test_case("test_svakom_sam2.yaml" ; "Svakom Sam Neo 2 Pro")] -#[test_case("test_feelingso.yaml" ; "FeelingSo Protocol")] -#[test_case("test_deepsire.yaml" ; "DeepSire Protocol")] #[test_case("test_xuanhuan_protocol.yaml" ; "Xuanhuan Protocol")] -#[test_case("test_tcode_linear_and_vibrate.yaml" ; "TCode (Linear + Vibrate)")] -#[test_case("test_serveu_protocol.yaml" ; "ServeU")] -#[test_case("test_kiiroo_prowand.yaml" ; "Kiiroo ProWand Protocol")] -#[test_case("test_kiiroo_spot.yaml" ; "Kiiroo Spot Protocol")] -#[test_case("test_fleshy_thrust_protocol.yaml" ; "Fleshy Thrust Sync Protocol")] -#[test_case("test_nexus_revo.yaml" ; "Nexus Revo Protocol")] -#[test_case("test_luvmazer_protocol.yaml" ; "Luvmazer Protocol")] -#[test_case("test_bananasome_protocol.yaml" ; "Bananasome Protocol")] -#[test_case("test_omobo_protocol.yaml" ; "Omobo Protocol")] #[tokio::test] async fn test_device_protocols_json_v3(test_file: &str) { //tracing_subscriber::fmt::init(); util::device_test::client::client_v3::run_json_test_case(&load_test_case(test_file).await).await; } + +/* +//#[test_case("test_cowgirl_cone_protocol.yaml" ; "The Cowgirl Cone Protocol")] +#[test_case("test_activejoy_protocol.yaml" ; "ActiveJoy Protocol")] +#[test_case("test_adrienlastic_protocol.yaml" ; "Adrien Lastic Protocol")] +#[test_case("test_amorelie_joy_protocol.yaml" ; "Amorelie Joy Protocol")] #[test_case("test_aneros_protocol.yaml" ; "Aneros Protocol")] -#[test_case("test_ankni_protocol.yaml" ; "Ankni Protocol")] #[test_case("test_ankni_protocol_no_handshake.yaml" ; "Ankni Protocol - No Handshake")] +#[test_case("test_ankni_protocol.yaml" ; "Ankni Protocol")] +#[test_case("test_bananasome_protocol.yaml" ; "Bananasome Protocol")] #[test_case("test_cachito_protocol.yaml" ; "Cachito Protocol")] -#[test_case("test_fredorch_protocol.yaml" ; "Fredorch Protocol")] +#[test_case("test_cowgirl_protocol.yaml" ; "The Cowgirl Protocol")] +#[test_case("test_cupido_protocol.yaml" ; "Cupido Protocol")] +#[test_case("test_deepsire.yaml" ; "DeepSire Protocol")] +#[test_case("test_feelingso.yaml" ; "FeelingSo Protocol")] +#[test_case("test_fleshy_thrust_protocol.yaml" ; "Fleshy Thrust Sync Protocol")] +#[test_case("test_foreo_protocol.yaml" ; "Foreo Protocol")] +#[test_case("test_fox_protocol.yaml" ; "Fox Protocol")] +//#[test_case("test_fredorch_protocol.yaml" ; "Fredorch Protocol")] +#[test_case("test_galaku_nebula.yaml" ; "Galaku Pump Protocol - Nebula")] +#[test_case("test_galaku.yaml" ; "Galaku Protocol")] +#[test_case("test_hgod_protocol.yaml" ; "Hgod Protocol")] +#[test_case("test_hismith_auxfun_box.yaml" ; "Hismith Mini Protocol - Auxfun Box")] +#[test_case("test_hismith_sinloli.yaml" ; "Hismith Mini Protocol - Sinloli")] +#[test_case("test_hismith_thrusting_cup.yaml" ; "Hismith Protocol - Thrusting Cup")] +#[test_case("test_hismith_v4.yaml" ; "Hismith Mini Protocol - Hismith v4")] #[test_case("test_hismith_wildolo.yaml" ; "Hismith Protocol - Wildolo")] -#[test_case("test_lovense_single_vibrator.yaml" ; "Lovense Protocol - Single Vibrator Device")] -#[test_case("test_lovense_nora.yaml" ; "Lovense Protocol - Lovense Nora (Vibrate/Rotate)")] -#[test_case("test_lovense_battery.yaml" ; "Lovense Protocol - Lovense Battery (Default Devices)")] +#[test_case("test_itoys_protocol.yaml" ; "iToys Protocol")] +//#[test_case("test_joyhub_moonhorn.yaml" ; "JoyHub Protocol - Moonhorn")] +//#[test_case("test_joyhub_petalwish_compat.yaml" ; "JoyHub Protocol - Petalwish Compat")] +//#[test_case("test_joyhub_petalwish.yaml" ; "JoyHub Protocol - Petalwish")] +//#[test_case("test_joyhub_roselin.yaml" ; "JoyHub Protocol - RoseLin")] +//#[test_case("test_kiiroo_prowand.yaml" ; "Kiiroo ProWand Protocol")] +//#[test_case("test_kiiroo_spot.yaml" ; "Kiiroo Spot Protocol")] +//#[test_case("test_lelo_f1sv1.yaml" ; "Lelo F1s V1 Protocol")] +//#[test_case("test_lelo_f1sv2.yaml" ; "Lelo F1s V2 Protocol")] +#[test_case("test_lelo_idawave.yaml" ; "Lelo Harmony Protocol - Ida Wave")] +#[test_case("test_lelo_tianiharmony.yaml" ; "Lelo Harmony Protocol - Tiani Harmony")] +#[test_case("test_leten_protocol.yaml" ; "Leten Protocol")] +//#[test_case("test_longlosttouch_protocol.yaml" ; "LongLostTouch Protocol")] +#[test_case("test_loob_protocol.yaml" ; "Joyroid Loob Protocol")] +//#[test_case("test_lovehoney_desire_egg.yaml" ; "Lovehoney Desire Protocol - Love Egg")] +//#[test_case("test_lovehoney_desire_prostate.yaml" ; "Lovehoney Desire Protocol - Prostate Vibe")] #[test_case("test_lovense_battery_non_default.yaml" ; "Lovense Protocol - Lovense Battery (Non-Default Devices)")] +#[test_case("test_lovense_battery.yaml" ; "Lovense Protocol - Lovense Battery (Default Devices)")] +//#[test_case("test_lovense_edge.yaml" ; "Lovense Protocol - Edge")] #[test_case("test_lovense_flexer_fw2.yaml" ; "Lovense Protocol - Flexer FW2")] -#[test_case("test_lovense_edge.yaml" ; "Lovense Protocol - Edge")] -#[test_case("test_lovense_osci3.yaml" ; "Lovense Protocol - Osci3")] -#[test_case("test_satisfyer_single_vibrator.yaml" ; "Satisfyer Protocol - Single Vibrator")] -#[test_case("test_satisfyer_dual_vibrator.yaml" ; "Satisfyer Protocol - Dual Vibrator")] -#[test_case("test_satisfyer_triple_vibrator.yaml" ; "Satisfyer Protocol - Triple Vibrator")] -#[test_case("test_mysteryvibe.yaml" ; "Mysteryvibe Protocol")] +//#[test_case("test_lovense_flexer_fw3.yaml" ; "Lovense Protocol - Flexer FW3")] +#[test_case("test_lovense_max.yaml" ; "Lovense Protocol - Lovense Max (Vibrate/Constrict)")] +//#[test_case("test_lovense_nora.yaml" ; "Lovense Protocol - Lovense Nora (Vibrate/Rotate)")] +//#[test_case("test_lovense_osci3.yaml" ; "Lovense Protocol - Osci3")] +//#[test_case("test_lovense_ridge_user_config.yaml" ; "Lovense Protocol - Lovense Ridge (User Config)")] +//#[test_case("test_lovense_ridge.yaml" ; "Lovense Protocol - Lovense Ridge (Oscillate)")] +#[test_case("test_lovense_single_vibrator.yaml" ; "Lovense Protocol - Single Vibrator Device")] +#[test_case("test_luvmazer_protocol.yaml" ; "Luvmazer Protocol")] +#[test_case("test_magic_motion_1_magic_cell.yaml" ; "MagicMotion Protocol 1 - Magic Cell")] +////#[test_case("test_magic_motion_2_eidolon.yaml" ; "MagicMotion Protocol 2 - Eidolon")] +#[test_case("test_magic_motion_2_equinox.yaml" ; "MagicMotion Protocol 2 - Equinox")] +#[test_case("test_magic_motion_3_krush.yaml" ; "MagicMotion Protocol 3 - Krush")] +//#[test_case("test_magic_motion_4_bobi.yaml" ; "MagicMotion Protocol 4 - Bobi")] +//#[test_case("test_magic_motion_4_nyx.yaml" ; "MagicMotion Protocol 4 - Nyx")] #[test_case("test_meese_protocol.yaml" ; "Meese Protocol")] +//#[test_case("test_metaxsire_cali.yaml" ; "metaXsire Protocol - Cali")] +//#[test_case("test_metaxsire_nolan.yaml" ; "metaXsire Protocol v2 - Nolan")] +//#[test_case("test_metaxsire_olis.yaml" ; "metaXsire Protocol - Olis")] +//#[test_case("test_metaxsire_rex.yaml" ; "metaXsire Protocol - Rex")] #[test_case("test_mizzzee_protocol.yaml" ; "Mizz Zee Protocol")] #[test_case("test_mizzzee_v2_protocol.yaml" ; "Mizz Zee v2 Protocol")] -#[test_case("test_mizzzee_v3_protocol.yaml" ; "Mizz Zee v3 Protocol")] -#[test_case("test_vorze_ufo.yaml" ; "Vorze Protocol - UFO")] -#[test_case("test_vorze_ufo_tw.yaml" ; "Vorze Protocol - UFO TW")] -#[test_case("test_vorze_cyclone.yaml" ; "Vorze Protocol - Cyclone")] -#[test_case("test_wevibe_4plus.yaml" ; "WeVibe Protocol (Legacy) - 4 Plus")] -#[test_case("test_wevibe_pivot.yaml" ; "WeVibe Protocol (Legacy) - Pivot")] -#[test_case("test_wevibe_vector.yaml" ; "WeVibe Protocol (8bit) - Vector")] -#[test_case("test_wevibe_moxie.yaml" ; "WeVibe Protocol (8bit) - Moxie")] -#[test_case("test_wevibe_chorus.yaml" ; "WeVibe Protocol (Chorus) - Chorus")] +//#[test_case("test_mizzzee_v3_protocol.yaml" ; "Mizz Zee v3 Protocol")] +#[test_case("test_motorbunny_protocol.yaml" ; "Motorbunny Protocol")] +//#[test_case("test_mysteryvibe.yaml" ; "Mysteryvibe Protocol")] +#[test_case("test_nexus_revo.yaml" ; "Nexus Revo Protocol")] #[test_case("test_nobra_protocol.yaml" ; "Nobra Protocol")] -#[test_case("test_lovehoney_desire_prostate.yaml" ; "Lovehoney Desire Protocol - Prostate Vibe")] -#[test_case("test_lovehoney_desire_egg.yaml" ; "Lovehoney Desire Protocol - Love Egg")] -#[test_case("test_wetoy_protocol.yaml" ; "WeToy Protocol")] +#[test_case("test_omobo_protocol.yaml" ; "Omobo Protocol")] #[test_case("test_pink_punch_protocol.yaml" ; "Pink Punch Protocol")] +#[test_case("test_sakuraneko_koikoi.yaml" ; "Sakuraneko Protocol - Koikoi")] #[test_case("test_sakuraneko_protocol.yaml" ; "Sakuraneko Protocol")] -#[test_case("test_synchro_protocol.yaml" ; "Synchro Protocol")] -#[test_case("test_lelo_tianiharmony.yaml" ; "Lelo Harmony Protocol - Tiani Harmony")] -#[test_case("test_lelo_f1sv1.yaml" ; "Lelo F1s V1 Protocol")] -#[test_case("test_lelo_f1sv2.yaml" ; "Lelo F1s V2 Protocol")] -#[test_case("test_magic_motion_1_magic_cell.yaml" ; "MagicMotion Protocol 1 - Magic Cell")] -#[test_case("test_magic_motion_2_eidolon.yaml" ; "MagicMotion Protocol 2 - Eidolon")] -#[test_case("test_magic_motion_2_equinox.yaml" ; "MagicMotion Protocol 2 - Equinox")] -#[test_case("test_magic_motion_3_krush.yaml" ; "MagicMotion Protocol 3 - Krush")] -#[test_case("test_magic_motion_4_bobi.yaml" ; "MagicMotion Protocol 4 - Bobi")] -#[test_case("test_magic_motion_4_nyx.yaml" ; "MagicMotion Protocol 4 - Nyx")] -#[test_case("test_hgod_protocol.yaml" ; "Hgod Protocol")] -#[test_case("test_metaxsire_rex.yaml" ; "metaXsire Protocol - Rex")] +//#[test_case("test_satisfyer_dual_vibrator.yaml" ; "Satisfyer Protocol - Dual Vibrator")] +//#[test_case("test_satisfyer_single_vibrator.yaml" ; "Satisfyer Protocol - Single Vibrator")] +//#[test_case("test_sensee_capsule.yaml" ; "Sensee Capsule Protocol")] #[test_case("test_sensee_protocol.yaml" ; "Sensee Diandou Protocol - Rabbit")] -#[test_case("test_svakom_pulse.yaml" ; "Svakom Pulse Protocol - Pulse Lite Neo")] -#[test_case("test_svakom_ella.yaml" ; "Svakom V1 Protocol - Ella")] -#[test_case("test_svakom_vivianna.yaml" ; "Svakom V2 Protocol - Vivianna")] -#[test_case("test_svakom_alex.yaml" ; "Svakom Alex Neo")] +#[test_case("test_serveu_protocol.yaml" ; "ServeU")] +//#[test_case("test_sexverse_lg389_protocol.yaml" ; "Sexverse LG389 Protocol")] #[test_case("test_svakom_alex_v2.yaml" ; "Svakom Alex Neo 2")] -#[test_case("test_svakom_iker.yaml" ; "Svakom Iker")] -#[test_case("test_svakom_cocopro.yaml" ; "Svakom Coco Pro")] -#[test_case("test_fox_protocol.yaml" ; "Fox Protocol")] +#[test_case("test_svakom_alex.yaml" ; "Svakom Alex Neo")] +//#[test_case("test_svakom_barnard.yaml" ; "Svakom (Fantasy Cup) Barnard")] +//#[test_case("test_svakom_cocopro.yaml" ; "Svakom Coco Pro")] +//#[test_case("test_svakom_ella.yaml" ; "Svakom V1 Protocol - Ella")] +//#[test_case("test_svakom_iker.yaml" ; "Svakom Iker")] +//#[test_case("test_svakom_mora_neo.yaml" ; "Svakom Mora Neo")] +//#[test_case("test_svakom_pulse.yaml" ; "Svakom Pulse Protocol - Pulse Lite Neo")] +//#[test_case("test_svakom_sam2.yaml" ; "Svakom Sam Neo 2 Pro")] +//#[test_case("test_svakom_theodore.yaml" ; "Svakom V3 Protocol - Theodore")] +//#[test_case("test_svakom_vivianna.yaml" ; "Svakom V2 Protocol - Vivianna")] +//#[test_case("test_synchro_protocol.yaml" ; "Synchro Protocol")] +#[test_case("test_tcode_linear_and_vibrate.yaml" ; "TCode (Linear + Vibrate)")] +#[test_case("test_tryfun_blackhole_protocol.yaml" ; "TryFun Protocol - Black Hole Plus")] +#[test_case("test_tryfun_meta2_protocol.yaml" ; "TryFun Protocol - Meta 2")] +//#[test_case("test_tryfun_protocol.yaml" ; "TryFun Protocol")] +//#[test_case("test_tryfun_surge.yaml" ; "TryFun Protocol - Surge Pro")] +//#[test_case("test_user_config_display_name.yaml" ; "User Config Display Name")] +//#[test_case("test_vorze_cyclone.yaml" ; "Vorze Protocol - Cyclone")] +//#[test_case("test_vorze_ufo_tw.yaml" ; "Vorze Protocol - UFO TW")] +//#[test_case("test_vorze_ufo.yaml" ; "Vorze Protocol - UFO")] +#[test_case("test_wetoy_protocol.yaml" ; "WeToy Protocol")] +#[test_case("test_wevibe_4plus.yaml" ; "WeVibe Protocol (Legacy) - 4 Plus")] +#[test_case("test_wevibe_chorus.yaml" ; "WeVibe Protocol (Chorus) - Chorus")] +#[test_case("test_wevibe_moxie.yaml" ; "WeVibe Protocol (8bit) - Moxie")] +#[test_case("test_wevibe_pivot.yaml" ; "WeVibe Protocol (Legacy) - Pivot")] +#[test_case("test_wevibe_vector.yaml" ; "WeVibe Protocol (8bit) - Vector")] +#[test_case("test_xibao_protocol.yaml" ; "Xibao Protocol")] #[test_case("test_xiuxiuda_protocol.yaml" ; "Xiuxiuda Protocol")] -#[test_case("test_adrienlastic_protocol.yaml" ; "Adrien Lastic Protocol")] -#[test_case("test_foreo_protocol.yaml" ; "Foreo Protocol")] -#[test_case("test_loob_protocol.yaml" ; "Joyroid Loob Protocol")] -#[test_case("test_joyhub_petalwish_compat.yaml" ; "JoyHub Protocol - Petalwish Compat")] -#[test_case("test_itoys_protocol.yaml" ; "iToys Protocol")] -#[test_case("test_leten_protocol.yaml" ; "Leten Protocol")] -#[test_case("test_motorbunny_protocol.yaml" ; "Motorbunny Protocol")] -#[test_case("test_activejoy_protocol.yaml" ; "ActiveJoy Protocol")] -#[test_case("test_tryfun_surge.yaml" ; "TryFun Protocol - Surge Pro")] -#[test_case("test_cupido_protocol.yaml" ; "Cupido Protocol")] -//#[test_case("test_cowgirl_cone_protocol.yaml" ; "The Cowgirl Cone Protocol")] -#[test_case("test_amorelie_joy_protocol.yaml" ; "Amorelie Joy Protocol")] -#[test_case("test_deepsire.yaml" ; "DeepSire Protocol")] #[test_case("test_xuanhuan_protocol.yaml" ; "Xuanhuan Protocol")] -#[test_case("test_tcode_linear_and_vibrate.yaml" ; "TCode (Linear + Vibrate)")] -#[test_case("test_serveu_protocol.yaml" ; "ServeU")] -#[test_case("test_kiiroo_prowand.yaml" ; "Kiiroo ProWand Protocol")] -#[test_case("test_kiiroo_spot.yaml" ; "Kiiroo Spot Protocol")] -#[test_case("test_fleshy_thrust_protocol.yaml" ; "Fleshy Thrust Sync Protocol")] -#[test_case("test_nexus_revo.yaml" ; "Nexus Revo Protocol")] -#[test_case("test_omobo_protocol.yaml" ; "Omobo Protocol")] #[tokio::test] async fn test_device_protocols_embedded_v2(test_file: &str) { + tracing_subscriber::fmt::init(); util::device_test::client::client_v2::run_embedded_test_case(&load_test_case(test_file).await) .await; } +//#[test_case("test_cowgirl_cone_protocol.yaml" ; "The Cowgirl Cone Protocol")] +#[test_case("test_activejoy_protocol.yaml" ; "ActiveJoy Protocol")] +#[test_case("test_adrienlastic_protocol.yaml" ; "Adrien Lastic Protocol")] +#[test_case("test_amorelie_joy_protocol.yaml" ; "Amorelie Joy Protocol")] #[test_case("test_aneros_protocol.yaml" ; "Aneros Protocol")] -#[test_case("test_ankni_protocol.yaml" ; "Ankni Protocol")] #[test_case("test_ankni_protocol_no_handshake.yaml" ; "Ankni Protocol - No Handshake")] +#[test_case("test_ankni_protocol.yaml" ; "Ankni Protocol")] +#[test_case("test_bananasome_protocol.yaml" ; "Bananasome Protocol")] #[test_case("test_cachito_protocol.yaml" ; "Cachito Protocol")] -#[test_case("test_fredorch_protocol.yaml" ; "Fredorch Protocol")] +#[test_case("test_cowgirl_protocol.yaml" ; "The Cowgirl Protocol")] +#[test_case("test_cupido_protocol.yaml" ; "Cupido Protocol")] +#[test_case("test_deepsire.yaml" ; "DeepSire Protocol")] +#[test_case("test_feelingso.yaml" ; "FeelingSo Protocol")] +#[test_case("test_fleshy_thrust_protocol.yaml" ; "Fleshy Thrust Sync Protocol")] +#[test_case("test_foreo_protocol.yaml" ; "Foreo Protocol")] +#[test_case("test_fox_protocol.yaml" ; "Fox Protocol")] +//#[test_case("test_fredorch_protocol.yaml" ; "Fredorch Protocol")] +#[test_case("test_galaku_nebula.yaml" ; "Galaku Pump Protocol - Nebula")] +#[test_case("test_galaku.yaml" ; "Galaku Protocol")] +#[test_case("test_hgod_protocol.yaml" ; "Hgod Protocol")] +#[test_case("test_hismith_auxfun_box.yaml" ; "Hismith Mini Protocol - Auxfun Box")] +#[test_case("test_hismith_sinloli.yaml" ; "Hismith Mini Protocol - Sinloli")] +#[test_case("test_hismith_thrusting_cup.yaml" ; "Hismith Protocol - Thrusting Cup")] +#[test_case("test_hismith_v4.yaml" ; "Hismith Mini Protocol - Hismith v4")] #[test_case("test_hismith_wildolo.yaml" ; "Hismith Protocol - Wildolo")] -#[test_case("test_lovense_single_vibrator.yaml" ; "Lovense Protocol - Single Vibrator Device")] -#[test_case("test_lovense_nora.yaml" ; "Lovense Protocol - Lovense Nora (Vibrate/Rotate)")] -#[test_case("test_lovense_battery.yaml" ; "Lovense Protocol - Lovense Battery (Default Devices)")] +#[test_case("test_itoys_protocol.yaml" ; "iToys Protocol")] +//#[test_case("test_joyhub_moonhorn.yaml" ; "JoyHub Protocol - Moonhorn")] +//#[test_case("test_joyhub_petalwish_compat.yaml" ; "JoyHub Protocol - Petalwish Compat")] +//#[test_case("test_joyhub_petalwish.yaml" ; "JoyHub Protocol - Petalwish")] +//#[test_case("test_joyhub_roselin.yaml" ; "JoyHub Protocol - RoseLin")] +//#[test_case("test_kiiroo_prowand.yaml" ; "Kiiroo ProWand Protocol")] +//#[test_case("test_kiiroo_spot.yaml" ; "Kiiroo Spot Protocol")] +//#[test_case("test_lelo_f1sv1.yaml" ; "Lelo F1s V1 Protocol")] +//#[test_case("test_lelo_f1sv2.yaml" ; "Lelo F1s V2 Protocol")] +#[test_case("test_lelo_idawave.yaml" ; "Lelo Harmony Protocol - Ida Wave")] +#[test_case("test_lelo_tianiharmony.yaml" ; "Lelo Harmony Protocol - Tiani Harmony")] +#[test_case("test_leten_protocol.yaml" ; "Leten Protocol")] +//#[test_case("test_longlosttouch_protocol.yaml" ; "LongLostTouch Protocol")] +#[test_case("test_loob_protocol.yaml" ; "Joyroid Loob Protocol")] +//#[test_case("test_lovehoney_desire_egg.yaml" ; "Lovehoney Desire Protocol - Love Egg")] +//#[test_case("test_lovehoney_desire_prostate.yaml" ; "Lovehoney Desire Protocol - Prostate Vibe")] #[test_case("test_lovense_battery_non_default.yaml" ; "Lovense Protocol - Lovense Battery (Non-Default Devices)")] -#[test_case("test_lovense_edge.yaml" ; "Lovense Protocol - Edge")] -#[test_case("test_lovense_osci3.yaml" ; "Lovense Protocol - Osci3")] -#[test_case("test_satisfyer_single_vibrator.yaml" ; "Satisfyer Protocol - Single Vibrator")] -#[test_case("test_satisfyer_dual_vibrator.yaml" ; "Satisfyer Protocol - Dual Vibrator")] -#[test_case("test_satisfyer_triple_vibrator.yaml" ; "Satisfyer Protocol - Triple Vibrator")] -#[test_case("test_mysteryvibe.yaml" ; "Mysteryvibe Protocol")] +#[test_case("test_lovense_battery.yaml" ; "Lovense Protocol - Lovense Battery (Default Devices)")] +//#[test_case("test_lovense_edge.yaml" ; "Lovense Protocol - Edge")] +#[test_case("test_lovense_flexer_fw2.yaml" ; "Lovense Protocol - Flexer FW2")] +//#[test_case("test_lovense_flexer_fw3.yaml" ; "Lovense Protocol - Flexer FW3")] +#[test_case("test_lovense_max.yaml" ; "Lovense Protocol - Lovense Max (Vibrate/Constrict)")] +//#[test_case("test_lovense_nora.yaml" ; "Lovense Protocol - Lovense Nora (Vibrate/Rotate)")] +//#[test_case("test_lovense_osci3.yaml" ; "Lovense Protocol - Osci3")] +//#[test_case("test_lovense_ridge_user_config.yaml" ; "Lovense Protocol - Lovense Ridge (User Config)")] +//#[test_case("test_lovense_ridge.yaml" ; "Lovense Protocol - Lovense Ridge (Oscillate)")] +#[test_case("test_lovense_single_vibrator.yaml" ; "Lovense Protocol - Single Vibrator Device")] +#[test_case("test_luvmazer_protocol.yaml" ; "Luvmazer Protocol")] +#[test_case("test_magic_motion_1_magic_cell.yaml" ; "MagicMotion Protocol 1 - Magic Cell")] +////#[test_case("test_magic_motion_2_eidolon.yaml" ; "MagicMotion Protocol 2 - Eidolon")] +#[test_case("test_magic_motion_2_equinox.yaml" ; "MagicMotion Protocol 2 - Equinox")] +#[test_case("test_magic_motion_3_krush.yaml" ; "MagicMotion Protocol 3 - Krush")] +//#[test_case("test_magic_motion_4_bobi.yaml" ; "MagicMotion Protocol 4 - Bobi")] +//#[test_case("test_magic_motion_4_nyx.yaml" ; "MagicMotion Protocol 4 - Nyx")] #[test_case("test_meese_protocol.yaml" ; "Meese Protocol")] +//#[test_case("test_metaxsire_cali.yaml" ; "metaXsire Protocol - Cali")] +//#[test_case("test_metaxsire_nolan.yaml" ; "metaXsire Protocol v2 - Nolan")] +//#[test_case("test_metaxsire_olis.yaml" ; "metaXsire Protocol - Olis")] +//#[test_case("test_metaxsire_rex.yaml" ; "metaXsire Protocol - Rex")] #[test_case("test_mizzzee_protocol.yaml" ; "Mizz Zee Protocol")] #[test_case("test_mizzzee_v2_protocol.yaml" ; "Mizz Zee v2 Protocol")] -#[test_case("test_mizzzee_v3_protocol.yaml" ; "Mizz Zee v3 Protocol")] -#[test_case("test_vorze_ufo.yaml" ; "Vorze Protocol - UFO")] -#[test_case("test_vorze_ufo_tw.yaml" ; "Vorze Protocol - UFO TW")] -#[test_case("test_vorze_cyclone.yaml" ; "Vorze Protocol - Cyclone")] -#[test_case("test_wevibe_4plus.yaml" ; "WeVibe Protocol (Legacy) - 4 Plus")] -#[test_case("test_wevibe_pivot.yaml" ; "WeVibe Protocol (Legacy) - Pivot")] -#[test_case("test_wevibe_vector.yaml" ; "WeVibe Protocol (8bit) - Vector")] -#[test_case("test_wevibe_moxie.yaml" ; "WeVibe Protocol (8bit) - Moxie")] -#[test_case("test_wevibe_chorus.yaml" ; "WeVibe Protocol (Chorus) - Chorus")] +//#[test_case("test_mizzzee_v3_protocol.yaml" ; "Mizz Zee v3 Protocol")] +#[test_case("test_motorbunny_protocol.yaml" ; "Motorbunny Protocol")] +//#[test_case("test_mysteryvibe.yaml" ; "Mysteryvibe Protocol")] +#[test_case("test_nexus_revo.yaml" ; "Nexus Revo Protocol")] #[test_case("test_nobra_protocol.yaml" ; "Nobra Protocol")] -#[test_case("test_lovehoney_desire_prostate.yaml" ; "Lovehoney Desire Protocol - Prostate Vibe")] -#[test_case("test_lovehoney_desire_egg.yaml" ; "Lovehoney Desire Protocol - Love Egg")] -#[test_case("test_wetoy_protocol.yaml" ; "WeToy Protocol")] +#[test_case("test_omobo_protocol.yaml" ; "Omobo Protocol")] #[test_case("test_pink_punch_protocol.yaml" ; "Pink Punch Protocol")] +#[test_case("test_sakuraneko_koikoi.yaml" ; "Sakuraneko Protocol - Koikoi")] #[test_case("test_sakuraneko_protocol.yaml" ; "Sakuraneko Protocol")] -#[test_case("test_synchro_protocol.yaml" ; "Synchro Protocol")] -#[test_case("test_lelo_tianiharmony.yaml" ; "Lelo Harmony Protocol - Tiani Harmony")] -#[test_case("test_lelo_f1sv1.yaml" ; "Lelo F1s V1 Protocol")] -#[test_case("test_lelo_f1sv2.yaml" ; "Lelo F1s V2 Protocol")] -#[test_case("test_magic_motion_1_magic_cell.yaml" ; "MagicMotion Protocol 1 - Magic Cell")] -#[test_case("test_magic_motion_2_eidolon.yaml" ; "MagicMotion Protocol 2 - Eidolon")] -#[test_case("test_magic_motion_2_equinox.yaml" ; "MagicMotion Protocol 2 - Equinox")] -#[test_case("test_magic_motion_3_krush.yaml" ; "MagicMotion Protocol 3 - Krush")] -#[test_case("test_magic_motion_4_bobi.yaml" ; "MagicMotion Protocol 4 - Bobi")] -#[test_case("test_magic_motion_4_nyx.yaml" ; "MagicMotion Protocol 4 - Nyx")] -#[test_case("test_hgod_protocol.yaml" ; "Hgod Protocol")] -#[test_case("test_metaxsire_rex.yaml" ; "metaXsire Protocol - Rex")] +//#[test_case("test_satisfyer_dual_vibrator.yaml" ; "Satisfyer Protocol - Dual Vibrator")] +//#[test_case("test_satisfyer_single_vibrator.yaml" ; "Satisfyer Protocol - Single Vibrator")] +//#[test_case("test_sensee_capsule.yaml" ; "Sensee Capsule Protocol")] #[test_case("test_sensee_protocol.yaml" ; "Sensee Diandou Protocol - Rabbit")] -#[test_case("test_svakom_pulse.yaml" ; "Svakom Pulse Protocol - Pulse Lite Neo")] -#[test_case("test_svakom_ella.yaml" ; "Svakom V1 Protocol - Ella")] -#[test_case("test_svakom_vivianna.yaml" ; "Svakom V2 Protocol - Vivianna")] -#[test_case("test_svakom_alex.yaml" ; "Svakom Alex Neo")] +#[test_case("test_serveu_protocol.yaml" ; "ServeU")] +//#[test_case("test_sexverse_lg389_protocol.yaml" ; "Sexverse LG389 Protocol")] #[test_case("test_svakom_alex_v2.yaml" ; "Svakom Alex Neo 2")] -#[test_case("test_svakom_iker.yaml" ; "Svakom Iker")] -#[test_case("test_svakom_cocopro.yaml" ; "Svakom Coco Pro")] -#[test_case("test_fox_protocol.yaml" ; "Fox Protocol")] +#[test_case("test_svakom_alex.yaml" ; "Svakom Alex Neo")] +//#[test_case("test_svakom_barnard.yaml" ; "Svakom (Fantasy Cup) Barnard")] +//#[test_case("test_svakom_cocopro.yaml" ; "Svakom Coco Pro")] +//#[test_case("test_svakom_ella.yaml" ; "Svakom V1 Protocol - Ella")] +//#[test_case("test_svakom_iker.yaml" ; "Svakom Iker")] +//#[test_case("test_svakom_mora_neo.yaml" ; "Svakom Mora Neo")] +//#[test_case("test_svakom_pulse.yaml" ; "Svakom Pulse Protocol - Pulse Lite Neo")] +//#[test_case("test_svakom_sam2.yaml" ; "Svakom Sam Neo 2 Pro")] +//#[test_case("test_svakom_theodore.yaml" ; "Svakom V3 Protocol - Theodore")] +//#[test_case("test_svakom_vivianna.yaml" ; "Svakom V2 Protocol - Vivianna")] +//#[test_case("test_synchro_protocol.yaml" ; "Synchro Protocol")] +#[test_case("test_tcode_linear_and_vibrate.yaml" ; "TCode (Linear + Vibrate)")] +#[test_case("test_tryfun_blackhole_protocol.yaml" ; "TryFun Protocol - Black Hole Plus")] +#[test_case("test_tryfun_meta2_protocol.yaml" ; "TryFun Protocol - Meta 2")] +//#[test_case("test_tryfun_protocol.yaml" ; "TryFun Protocol")] +//#[test_case("test_tryfun_surge.yaml" ; "TryFun Protocol - Surge Pro")] +//#[test_case("test_user_config_display_name.yaml" ; "User Config Display Name")] +//#[test_case("test_vorze_cyclone.yaml" ; "Vorze Protocol - Cyclone")] +//#[test_case("test_vorze_ufo_tw.yaml" ; "Vorze Protocol - UFO TW")] +//#[test_case("test_vorze_ufo.yaml" ; "Vorze Protocol - UFO")] +#[test_case("test_wetoy_protocol.yaml" ; "WeToy Protocol")] +#[test_case("test_wevibe_4plus.yaml" ; "WeVibe Protocol (Legacy) - 4 Plus")] +#[test_case("test_wevibe_chorus.yaml" ; "WeVibe Protocol (Chorus) - Chorus")] +#[test_case("test_wevibe_moxie.yaml" ; "WeVibe Protocol (8bit) - Moxie")] +#[test_case("test_wevibe_pivot.yaml" ; "WeVibe Protocol (Legacy) - Pivot")] +#[test_case("test_wevibe_vector.yaml" ; "WeVibe Protocol (8bit) - Vector")] +#[test_case("test_xibao_protocol.yaml" ; "Xibao Protocol")] #[test_case("test_xiuxiuda_protocol.yaml" ; "Xiuxiuda Protocol")] -#[test_case("test_adrienlastic_protocol.yaml" ; "Adrien Lastic Protocol")] -#[test_case("test_foreo_protocol.yaml" ; "Foreo Protocol")] -#[test_case("test_loob_protocol.yaml" ; "Joyroid Loob Protocol")] -#[test_case("test_joyhub_petalwish_compat.yaml" ; "JoyHub Protocol - Petalwish Compat")] -#[test_case("test_itoys_protocol.yaml" ; "iToys Protocol")] -#[test_case("test_leten_protocol.yaml" ; "Leten Protocol")] -#[test_case("test_motorbunny_protocol.yaml" ; "Motorbunny Protocol")] -#[test_case("test_activejoy_protocol.yaml" ; "ActiveJoy Protocol")] -#[test_case("test_tryfun_surge.yaml" ; "TryFun Protocol - Surge Pro")] -#[test_case("test_cupido_protocol.yaml" ; "Cupido Protocol")] -//#[test_case("test_cowgirl_cone_protocol.yaml" ; "The Cowgirl Cone Protocol")] -#[test_case("test_amorelie_joy_protocol.yaml" ; "Amorelie Joy Protocol")] -#[test_case("test_deepsire.yaml" ; "DeepSire Protocol")] #[test_case("test_xuanhuan_protocol.yaml" ; "Xuanhuan Protocol")] -#[test_case("test_tcode_linear_and_vibrate.yaml" ; "TCode (Linear + Vibrate)")] -#[test_case("test_serveu_protocol.yaml" ; "ServeU")] -#[test_case("test_kiiroo_prowand.yaml" ; "Kiiroo ProWand Protocol")] -#[test_case("test_kiiroo_spot.yaml" ; "Kiiroo Spot Protocol")] -#[test_case("test_fleshy_thrust_protocol.yaml" ; "Fleshy Thrust Sync Protocol")] -#[test_case("test_nexus_revo.yaml" ; "Nexus Revo Protocol")] -#[test_case("test_omobo_protocol.yaml" ; "Omobo Protocol")] #[tokio::test] async fn test_device_protocols_json_v2(test_file: &str) { util::device_test::client::client_v2::run_json_test_case(&load_test_case(test_file).await).await; } +*/ \ No newline at end of file diff --git a/buttplug/tests/test_message_downgrades.rs b/crates/buttplug_tests/tests/test_message_downgrades.rs similarity index 87% rename from buttplug/tests/test_message_downgrades.rs rename to crates/buttplug_tests/tests/test_message_downgrades.rs index c0b29e301..c222f7766 100644 --- a/buttplug/tests/test_message_downgrades.rs +++ b/crates/buttplug_tests/tests/test_message_downgrades.rs @@ -5,33 +5,28 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -extern crate buttplug; mod util; +use std::time::Duration; + +use buttplug_server_device_config::Endpoint; pub use util::test_device_manager::check_test_recv_value; -use buttplug::{ - core::message::{ - self, - serializer::{ - ButtplugMessageSerializer, - ButtplugSerializedMessage, - ButtplugServerJSONSerializer, - }, - Endpoint, - }, - server::{ +use buttplug_core::message::{ + serializer::{ButtplugMessageSerializer, ButtplugSerializedMessage}, + StartScanningV0, + }; +use buttplug_server::{ device::hardware::{HardwareCommand, HardwareWriteCmd}, + message::{serializer::ButtplugServerJSONSerializer, ButtplugClientMessageVariant}, ButtplugServerBuilder, - ButtplugServerDowngradeWrapper, - }, }; use futures::{pin_mut, StreamExt}; use util::test_server_with_device; +use uuid::Uuid; #[tokio::test] async fn test_version0_connection() { - let server = - ButtplugServerDowngradeWrapper::new(ButtplugServerBuilder::default().finish().unwrap()); + let server = ButtplugServerBuilder::default().finish().unwrap(); let serializer = ButtplugServerJSONSerializer::default(); let rsi = r#"[{"RequestServerInfo":{"Id": 1, "ClientName": "Test Client"}}]"#; let output = serializer @@ -50,8 +45,7 @@ async fn test_version0_connection() { #[tokio::test] async fn test_version2_connection() { - let server = - ButtplugServerDowngradeWrapper::new(ButtplugServerBuilder::default().finish().unwrap()); + let server = ButtplugServerBuilder::default().finish().unwrap(); let serializer = ButtplugServerJSONSerializer::default(); let rsi = r#"[{"RequestServerInfo":{"Id": 1, "ClientName": "Test Client", "MessageVersion": 2}}]"#; @@ -71,8 +65,8 @@ async fn test_version2_connection() { #[tokio::test] async fn test_version0_device_added_device_list() { - let (server, _) = test_server_with_device("Massage Demo", false); - let recv = server.client_version_event_stream(); + let (server, _) = test_server_with_device("Massage Demo"); + let recv = server.event_stream(); pin_mut!(recv); let serializer = ButtplugServerJSONSerializer::default(); let rsi = r#"[{"RequestServerInfo":{"Id": 1, "ClientName": "Test Client"}}]"#; @@ -91,8 +85,8 @@ async fn test_version0_device_added_device_list() { ); // Skip JSON parsing here, we aren't converting versions. let reply = server - .parse_message(message::ButtplugClientMessageVariant::V0( - message::StartScanningV0::default().into(), + .parse_message(ButtplugClientMessageVariant::V0( + StartScanningV0::default().into(), )) .await; assert!(reply.is_ok(), "Should get back ok: {:?}", reply); @@ -121,8 +115,9 @@ async fn test_version0_device_added_device_list() { #[tokio::test] async fn test_version0_singlemotorvibratecmd() { - let (server, mut device) = test_server_with_device("Massage Demo", false); - let recv = server.client_version_event_stream(); + tracing_subscriber::fmt::init(); + let (server, mut device) = test_server_with_device("Massage Demo"); + let recv = server.event_stream(); pin_mut!(recv); let serializer = ButtplugServerJSONSerializer::default(); let rsi = r#"[{"RequestServerInfo":{"Id": 1, "ClientName": "Test Client"}}]"#; @@ -141,8 +136,8 @@ async fn test_version0_singlemotorvibratecmd() { ); // Skip JSON parsing here, we aren't converting versions. let reply = server - .parse_message(message::ButtplugClientMessageVariant::V0( - message::StartScanningV0::default().into(), + .parse_message(ButtplugClientMessageVariant::V0( + StartScanningV0::default().into(), )) .await; assert!(reply.is_ok(), "Should get back ok: {:?}", reply); @@ -172,15 +167,22 @@ async fn test_version0_singlemotorvibratecmd() { r#"[{"Ok":{"Id":2}}]"#.to_owned().into() ); check_test_recv_value( + &Duration::from_millis(150), &mut device, - HardwareCommand::Write(HardwareWriteCmd::new(Endpoint::Tx, vec![0xF1, 64], false)), - ); + HardwareCommand::Write(HardwareWriteCmd::new( + &[Uuid::nil()], + Endpoint::Tx, + vec![0xF1, 64], + false, + )), + ) + .await; } #[tokio::test] async fn test_version1_singlemotorvibratecmd() { - let (server, mut device) = test_server_with_device("Massage Demo", false); - let recv = server.client_version_event_stream(); + let (server, mut device) = test_server_with_device("Massage Demo"); + let recv = server.event_stream(); pin_mut!(recv); let serializer = ButtplugServerJSONSerializer::default(); let rsi = @@ -200,8 +202,8 @@ async fn test_version1_singlemotorvibratecmd() { ); // Skip JSON parsing here, we aren't converting versions. let reply = server - .parse_message(message::ButtplugClientMessageVariant::V1( - message::StartScanningV0::default().into(), + .parse_message(ButtplugClientMessageVariant::V1( + StartScanningV0::default().into(), )) .await; assert!(reply.is_ok(), "Should get back ok: {:?}", reply); @@ -241,15 +243,22 @@ async fn test_version1_singlemotorvibratecmd() { r#"[{"Ok":{"Id":2}}]"#.to_owned().into() ); check_test_recv_value( + &Duration::from_millis(150), &mut device, - HardwareCommand::Write(HardwareWriteCmd::new(Endpoint::Tx, vec![0xF1, 64], false)), - ); + HardwareCommand::Write(HardwareWriteCmd::new( + &[Uuid::nil()], + Endpoint::Tx, + vec![0xF1, 64], + false, + )), + ) + .await; } #[tokio::test] -async fn test_version0_oscilatoronly() { - let (server, mut _device) = test_server_with_device("Xone", false); - let recv = server.client_version_event_stream(); +async fn test_version0_oscillatoronly() { + let (server, mut _device) = test_server_with_device("Xone"); + let recv = server.event_stream(); pin_mut!(recv); let serializer = ButtplugServerJSONSerializer::default(); let rsi = r#"[{"RequestServerInfo":{"Id": 1, "ClientName": "Test Client"}}]"#; @@ -268,8 +277,8 @@ async fn test_version0_oscilatoronly() { ); // Skip JSON parsing here, we aren't converting versions. let reply = server - .parse_message(message::ButtplugClientMessageVariant::V0( - message::StartScanningV0::default().into(), + .parse_message(ButtplugClientMessageVariant::V0( + StartScanningV0::default().into(), )) .await; assert!(reply.is_ok(), "Should get back ok: {:?}", reply); @@ -295,8 +304,8 @@ async fn test_version0_oscilatoronly() { #[tokio::test] async fn test_version1_oscilatoronly() { - let (server, mut _device) = test_server_with_device("Xone", false); - let recv = server.client_version_event_stream(); + let (server, mut _device) = test_server_with_device("Xone"); + let recv = server.event_stream(); pin_mut!(recv); let serializer = ButtplugServerJSONSerializer::default(); let rsi = @@ -316,8 +325,8 @@ async fn test_version1_oscilatoronly() { ); // Skip JSON parsing here, we aren't converting versions. let reply = server - .parse_message(message::ButtplugClientMessageVariant::V1( - message::StartScanningV0::default().into(), + .parse_message(ButtplugClientMessageVariant::V1( + StartScanningV0::default().into(), )) .await; assert!(reply.is_ok(), "Should get back ok: {:?}", reply); diff --git a/buttplug/tests/test_serializers.rs b/crates/buttplug_tests/tests/test_serializers.rs similarity index 73% rename from buttplug/tests/test_serializers.rs rename to crates/buttplug_tests/tests/test_serializers.rs index 666d16bd8..65e33ca6e 100644 --- a/buttplug/tests/test_serializers.rs +++ b/crates/buttplug_tests/tests/test_serializers.rs @@ -7,28 +7,32 @@ mod util; -use buttplug::{ - client::ButtplugClientError, - core::{ +use buttplug_client::ButtplugClientError; +use buttplug_core::{ connector::transport::ButtplugTransportIncomingMessage, errors::{ButtplugError, ButtplugUnknownError}, message::{ - self, serializer::ButtplugSerializedMessage, - ButtplugClientMessageV3, - ButtplugClientMessageVariant, + ButtplugClientMessageV4, ButtplugMessage, - ButtplugServerMessageV3, - ButtplugServerMessageVariant, + ButtplugServerMessageV4, + ErrorV0, + BUTTPLUG_CURRENT_API_MAJOR_VERSION, }, - }, util::async_manager, + }; +use buttplug_server::message::{ + ButtplugClientMessageVariant, + ButtplugServerMessageVariant, + DeviceListV3, + ServerInfoV2, }; use std::sync::Arc; use tokio::sync::Notify; use util::channel_transport::ChannelClientTestHelper; #[tokio::test] +#[ignore = "Needs update to v4"] async fn test_garbled_client_rsi_response() { let helper = Arc::new(ChannelClientTestHelper::new()); let helper_clone = helper.clone(); @@ -51,16 +55,11 @@ async fn test_garbled_client_rsi_response() { .await; helper .send_client_incoming(ButtplugServerMessageVariant::V3( - message::ServerInfoV2::new( - "test server", - message::BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION, - 0, - ) - .into(), + ServerInfoV2::new("test server", BUTTPLUG_CURRENT_API_MAJOR_VERSION, 0).into(), )) .await; let _ = helper.recv_outgoing().await; - let mut dl = message::DeviceListV3::new(vec![]); + let mut dl = DeviceListV3::new(vec![]); dl.set_id(2); helper .send_client_incoming(ButtplugServerMessageVariant::V3(dl.into())) @@ -76,20 +75,20 @@ async fn test_serialized_error_relay() { async_manager::spawn(async move { assert!(matches!( helper_clone.next_client_message().await, - ButtplugClientMessageVariant::V3(ButtplugClientMessageV3::StartScanning(..)) - )); - let mut error_msg = ButtplugServerMessageV3::Error(message::ErrorV0::from( - ButtplugError::from(ButtplugUnknownError::NoDeviceCommManagers), + ButtplugClientMessageVariant::V4(ButtplugClientMessageV4::StartScanning(..)) )); + let mut error_msg = ButtplugServerMessageV4::Error(ErrorV0::from(ButtplugError::from( + ButtplugUnknownError::NoDeviceCommManagers, + ))); error_msg.set_id(3); helper_clone - .send_client_incoming(ButtplugServerMessageVariant::V3(error_msg)) + .send_client_incoming(ButtplugServerMessageVariant::V4(error_msg)) .await; }); assert!(matches!( helper.client().start_scanning().await.unwrap_err(), ButtplugClientError::ButtplugError(ButtplugError::ButtplugUnknownError( - buttplug::core::errors::ButtplugUnknownError::NoDeviceCommManagers + buttplug_core::errors::ButtplugUnknownError::NoDeviceCommManagers )) )); } diff --git a/buttplug/tests/test_server.rs b/crates/buttplug_tests/tests/test_server.rs similarity index 65% rename from buttplug/tests/test_server.rs rename to crates/buttplug_tests/tests/test_server.rs index c64139fef..a5d1cde97 100644 --- a/buttplug/tests/test_server.rs +++ b/crates/buttplug_tests/tests/test_server.rs @@ -6,6 +6,7 @@ // for full license information. mod util; +use buttplug_server_device_config::Endpoint; use util::test_server; pub use util::{ create_test_dcm, @@ -18,50 +19,70 @@ pub use util::{ test_server_with_device, }; -use buttplug::{ - core::{ +use buttplug_core::{ errors::{ButtplugDeviceError, ButtplugError, ButtplugHandshakeError}, message::{ - self, + OutputCmdV4, + OutputCommand, + OutputValue, + ButtplugClientMessageV4, ButtplugMessageSpecVersion, - ButtplugServerMessageV2, - ButtplugServerMessageV3, ButtplugServerMessageV4, - ButtplugServerMessageVariant, - Endpoint, - BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION, + ErrorCode, + PingV0, + RequestServerInfoV4, + ServerInfoV4, + StartScanningV0, + BUTTPLUG_CURRENT_API_MAJOR_VERSION, + BUTTPLUG_CURRENT_API_MINOR_VERSION, }, - }, - server::{ + }; +use buttplug_server::{ device::{ hardware::{HardwareCommand, HardwareWriteCmd}, ServerDeviceManagerBuilder, }, + message::{ + checked_output_cmd::CheckedOutputCmdV4, + spec_enums::ButtplugCheckedClientMessageV4, + ButtplugClientMessageV3, + ButtplugClientMessageVariant, + ButtplugServerMessageV2, + ButtplugServerMessageV3, + ButtplugServerMessageVariant, + RequestServerInfoV1, + ServerInfoV2, + }, + ButtplugServer, ButtplugServerBuilder, - ButtplugServerDowngradeWrapper, - }, }; use futures::{pin_mut, Stream, StreamExt}; use std::time::Duration; use tokio::time::sleep; +use uuid::Uuid; async fn setup_test_server( - msg_union: message::ButtplugClientMessageV3, + msg_union: ButtplugClientMessageV4, ) -> ( - ButtplugServerDowngradeWrapper, + ButtplugServer, impl Stream, ) { - let server = ButtplugServerDowngradeWrapper::new(test_server(false)); - let recv = server.client_version_event_stream(); + let server = test_server(); + let recv = server.event_stream(); // assert_eq!(server.server_name, "Test Server"); match server .parse_message(msg_union.into()) .await .expect("Test, assuming infallible.") { - ButtplugServerMessageVariant::V3(ButtplugServerMessageV3::ServerInfo(s)) => assert_eq!( + ButtplugServerMessageVariant::V4(ButtplugServerMessageV4::ServerInfo(s)) => assert_eq!( s, - message::ServerInfoV2::new("Buttplug Server", ButtplugMessageSpecVersion::Version3, 0) + ServerInfoV4::new( + "Buttplug Server", + ButtplugMessageSpecVersion::Version4, + 0, + 0 + ) ), _ => panic!("Should've received ok"), } @@ -70,18 +91,22 @@ async fn setup_test_server( #[tokio::test] async fn test_server_handshake() { - let msg = - message::RequestServerInfoV1::new("Test Client", ButtplugMessageSpecVersion::Version3).into(); + let msg = RequestServerInfoV4::new( + "Test Client", + BUTTPLUG_CURRENT_API_MAJOR_VERSION, + BUTTPLUG_CURRENT_API_MINOR_VERSION, + ) + .into(); let (server, _recv) = setup_test_server(msg).await; assert!(server.connected()); } #[tokio::test] async fn test_server_handshake_not_done_first_v4() { - let msg = message::ButtplugClientMessageV4::Ping(message::PingV0::default().into()); - let server = test_server(false); + let msg = ButtplugCheckedClientMessageV4::Ping(PingV0::default().into()); + let server = test_server(); // assert_eq!(server.server_name, "Test Server"); - let result = server.parse_message(msg).await; + let result = server.parse_checked_message(msg).await; assert!(result.is_err()); assert!(matches!( result.unwrap_err().original_error(), @@ -92,8 +117,8 @@ async fn test_server_handshake_not_done_first_v4() { #[tokio::test] async fn test_server_handshake_not_done_first_v3() { - let msg = message::ButtplugClientMessageV3::Ping(message::PingV0::default().into()); - let server = ButtplugServerDowngradeWrapper::new(test_server(false)); + let msg = ButtplugClientMessageV3::Ping(PingV0::default().into()); + let server = test_server(); // assert_eq!(server.server_name, "Test Server"); let result = server.parse_message(msg.try_into().unwrap()).await; assert!(result.is_err()); @@ -110,10 +135,10 @@ async fn test_server_handshake_not_done_first_v3() { #[tokio::test] async fn test_client_version_older_than_server() { - let msg = message::ButtplugClientMessageVariant::V2( - message::RequestServerInfoV1::new("Test Client", ButtplugMessageSpecVersion::Version2).into(), + let msg = ButtplugClientMessageVariant::V2( + RequestServerInfoV1::new("Test Client", ButtplugMessageSpecVersion::Version2).into(), ); - let server = ButtplugServerDowngradeWrapper::new(test_server(false)); + let server = test_server(); // assert_eq!(server.server_name, "Test Server"); match server .parse_message(msg) @@ -122,7 +147,7 @@ async fn test_client_version_older_than_server() { { ButtplugServerMessageVariant::V2(ButtplugServerMessageV2::ServerInfo(s)) => assert_eq!( s, - message::ServerInfoV2::new("Buttplug Server", ButtplugMessageSpecVersion::Version2, 0) + ServerInfoV2::new("Buttplug Server", ButtplugMessageSpecVersion::Version2, 0) ), _ => panic!("Should've received ok"), } @@ -131,9 +156,9 @@ async fn test_client_version_older_than_server() { #[tokio::test] #[ignore = "Needs to be rewritten to send in via the JSON parser, otherwise we're type bound due to the enum and can't fail"] async fn test_server_version_older_than_client() { - let server = ButtplugServerDowngradeWrapper::new(test_server(false)); - let msg = message::ButtplugClientMessageVariant::V2( - message::RequestServerInfoV1::new("Test Client", ButtplugMessageSpecVersion::Version2).into(), + let server = test_server(); + let msg = ButtplugClientMessageVariant::V2( + RequestServerInfoV1::new("Test Client", ButtplugMessageSpecVersion::Version2).into(), ); assert!( server.parse_message(msg).await.is_err(), @@ -149,10 +174,14 @@ async fn test_ping_timeout() { .expect("Test, assuming infallible."); let recv = server.event_stream(); pin_mut!(recv); - let msg = message::RequestServerInfoV1::new("Test Client", BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION); + let msg = RequestServerInfoV4::new( + "Test Client", + BUTTPLUG_CURRENT_API_MAJOR_VERSION, + BUTTPLUG_CURRENT_API_MINOR_VERSION, + ); sleep(Duration::from_millis(150)).await; let reply = server - .parse_message(message::ButtplugClientMessageV4::RequestServerInfo(msg)) + .parse_checked_message(ButtplugCheckedClientMessageV4::RequestServerInfo(msg)) .await; assert!( reply.is_ok(), @@ -160,9 +189,9 @@ async fn test_ping_timeout() { reply ); sleep(Duration::from_millis(300)).await; - let pingmsg = message::PingV0::default(); + let pingmsg = PingV0::default(); let result = server - .parse_message(message::ButtplugClientMessageV4::Ping(pingmsg.into())) + .parse_checked_message(ButtplugCheckedClientMessageV4::Ping(pingmsg.into())) .await; let err = result.unwrap_err(); if !matches!(err.original_error(), ButtplugError::ButtplugPingError(_)) { @@ -170,8 +199,8 @@ async fn test_ping_timeout() { } // Check that we got an event back about the ping out. let msg = recv.next().await.expect("Test, assuming infallible."); - if let ButtplugServerMessageV4::Error(e) = msg { - if message::ErrorCode::ErrorPing != e.error_code() { + if let ButtplugServerMessageVariant::V4(ButtplugServerMessageV4::Error(e)) = msg { + if ErrorCode::ErrorPing != e.error_code() { panic!("Didn't get a ping error"); } } else { @@ -184,7 +213,7 @@ async fn test_device_stop_on_ping_timeout() { let mut builder = TestDeviceCommunicationManagerBuilder::default(); let mut device = builder.add_test_device(&TestDeviceIdentifier::new("Massage Demo", None)); - let dm_builder = ServerDeviceManagerBuilder::new(create_test_dcm(false)) + let dm_builder = ServerDeviceManagerBuilder::new(create_test_dcm()) .comm_manager(builder) .finish() .unwrap(); @@ -193,17 +222,21 @@ async fn test_device_stop_on_ping_timeout() { server_builder.max_ping_time(100); let server = server_builder.finish().unwrap(); - let recv = server.event_stream(); + let recv = server.server_version_event_stream(); pin_mut!(recv); - let msg = message::RequestServerInfoV1::new("Test Client", BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION); + let msg = RequestServerInfoV4::new( + "Test Client", + BUTTPLUG_CURRENT_API_MAJOR_VERSION, + BUTTPLUG_CURRENT_API_MINOR_VERSION, + ); let mut reply = server - .parse_message(message::ButtplugClientMessageV4::from(msg)) + .parse_checked_message(ButtplugCheckedClientMessageV4::from(msg)) .await; assert!(reply.is_ok()); reply = server - .parse_message(message::ButtplugClientMessageV4::from( - message::StartScanningV0::default(), + .parse_checked_message(ButtplugCheckedClientMessageV4::from( + StartScanningV0::default(), )) .await; assert!(reply.is_ok()); @@ -212,7 +245,8 @@ async fn test_device_stop_on_ping_timeout() { while let Some(msg) = recv.next().await { if let ButtplugServerMessageV4::ScanningFinished(_) = msg { continue; - } else if let ButtplugServerMessageV4::DeviceAdded(da) = msg { + } else if let ButtplugServerMessageV4::DeviceList(list) = msg { + let da = &list.devices()[&0]; assert_eq!(da.device_name(), "Aneros Vivi"); device_index = da.device_index(); break; @@ -223,24 +257,31 @@ async fn test_device_stop_on_ping_timeout() { ); } } + server - .parse_message(message::ButtplugClientMessageV4::from( - message::ScalarCmdV4::new( + .parse_checked_message(ButtplugCheckedClientMessageV4::from( + CheckedOutputCmdV4::new( + 0, device_index, - vec![message::ScalarSubcommandV4::new( - 0, - 0.5, - message::ActuatorType::Vibrate, - )], + 0, + "f50a528b-b023-40f0-9906-df037443950a".try_into().unwrap(), + OutputCommand::Vibrate(OutputValue::new(64)), ), )) .await .expect("Test, assuming infallible."); check_test_recv_value( + &Duration::from_millis(150), &mut device, - HardwareCommand::Write(HardwareWriteCmd::new(Endpoint::Tx, vec![0xF1, 64], false)), - ); + HardwareCommand::Write(HardwareWriteCmd::new( + &[Uuid::nil()], + Endpoint::Tx, + vec![0xF1, 64], + false, + )), + ) + .await; /* // Wait out the ping, we should get a stop message. let mut i = 0u32; @@ -259,15 +300,19 @@ async fn test_device_stop_on_ping_timeout() { #[tokio::test] async fn test_repeated_handshake() { - let msg = message::RequestServerInfoV1::new("Test Client", ButtplugMessageSpecVersion::Version3); + let msg = RequestServerInfoV4::new( + "Test Client", + BUTTPLUG_CURRENT_API_MAJOR_VERSION, + BUTTPLUG_CURRENT_API_MINOR_VERSION, + ); let (server, _recv) = setup_test_server((msg.clone()).into()).await; assert!(server.connected()); let err = server - .parse_message(message::ButtplugClientMessageVariant::V3(msg.into())) + .parse_message(ButtplugClientMessageVariant::V4(msg.into())) .await .unwrap_err(); - if let ButtplugServerMessageVariant::V3(ButtplugServerMessageV3::Error(e)) = err { + if let ButtplugServerMessageVariant::V4(ButtplugServerMessageV4::Error(e)) = err { assert!(matches!( e.original_error(), ButtplugError::ButtplugHandshakeError(ButtplugHandshakeError::HandshakeAlreadyHappened) @@ -279,15 +324,19 @@ async fn test_repeated_handshake() { #[tokio::test] async fn test_invalid_device_index() { - let msg = message::RequestServerInfoV1::new("Test Client", BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION); + let msg = RequestServerInfoV4::new( + "Test Client", + BUTTPLUG_CURRENT_API_MAJOR_VERSION, + BUTTPLUG_CURRENT_API_MINOR_VERSION, + ); let (server, _) = setup_test_server(msg.into()).await; let err = server - .parse_message(message::ButtplugClientMessageVariant::V3( - message::VibrateCmdV1::new(10, vec![]).into(), + .parse_message(ButtplugClientMessageVariant::V4( + OutputCmdV4::new(10, 0, OutputCommand::Vibrate(OutputValue::new(0))).into(), )) .await .unwrap_err(); - if let ButtplugServerMessageVariant::V3(ButtplugServerMessageV3::Error(e)) = err { + if let ButtplugServerMessageVariant::V4(ButtplugServerMessageV4::Error(e)) = err { assert!(matches!( e.original_error(), ButtplugError::ButtplugDeviceError(ButtplugDeviceError::DeviceNotAvailable(_)) @@ -303,19 +352,23 @@ async fn test_device_index_generation() { let mut _device1 = builder.add_test_device(&TestDeviceIdentifier::new("Massage Demo", None)); let mut _device2 = builder.add_test_device(&TestDeviceIdentifier::new("Massage Demo", None)); - let server = test_server_with_comm_manager(builder, false); + let server = test_server_with_comm_manager(builder); - let recv = server.event_stream(); + let recv = server.server_version_event_stream(); pin_mut!(recv); assert!(server - .parse_message( - message::RequestServerInfoV1::new("Test Client", BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION) - .into() + .parse_checked_message( + RequestServerInfoV4::new( + "Test Client", + BUTTPLUG_CURRENT_API_MAJOR_VERSION, + BUTTPLUG_CURRENT_API_MINOR_VERSION + ) + .into() ) .await .is_ok()); assert!(server - .parse_message(message::StartScanningV0::default().into()) + .parse_checked_message(StartScanningV0::default().into()) .await .is_ok()); // Check that we got an event back about a new device. @@ -323,7 +376,8 @@ async fn test_device_index_generation() { while let Some(msg) = recv.next().await { if let ButtplugServerMessageV4::ScanningFinished(_) = msg { continue; - } else if let ButtplugServerMessageV4::DeviceAdded(da) = msg { + } else if let ButtplugServerMessageV4::DeviceList(list) = msg { + let da = &list.devices()[&0]; assert_eq!(da.device_name(), "Aneros Vivi"); // Devices aren't guaranteed to be added in any specific order, the // scheduler will do whatever it wants. So check boundaries instead of @@ -349,19 +403,23 @@ async fn test_server_scanning_finished() { let mut _device1 = builder.add_test_device(&TestDeviceIdentifier::new("Massage Demo", None)); let mut _device2 = builder.add_test_device(&TestDeviceIdentifier::new("Massage Demo", None)); - let server = test_server_with_comm_manager(builder, false); + let server = test_server_with_comm_manager(builder); - let recv = server.event_stream(); + let recv = server.server_version_event_stream(); pin_mut!(recv); assert!(server - .parse_message( - message::RequestServerInfoV1::new("Test Client", BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION) - .into() + .parse_checked_message( + RequestServerInfoV4::new( + "Test Client", + BUTTPLUG_CURRENT_API_MAJOR_VERSION, + BUTTPLUG_CURRENT_API_MINOR_VERSION + ) + .into() ) .await .is_ok()); assert!(server - .parse_message(message::StartScanningV0::default().into()) + .parse_checked_message(StartScanningV0::default().into()) .await .is_ok()); // Check that we got an event back about a new device. diff --git a/crates/buttplug_tests/tests/test_server_device.rs b/crates/buttplug_tests/tests/test_server_device.rs new file mode 100644 index 000000000..943e59d53 --- /dev/null +++ b/crates/buttplug_tests/tests/test_server_device.rs @@ -0,0 +1,129 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2024 Nonpolynomial Labs LLC. All rights reserved. +// +// Licensed under the BSD 3-Clause license. See LICENSE file in the project root +// for full license information. + +mod util; +use buttplug_core::{ + message::{ + ButtplugServerMessageV4, + RequestServerInfoV4, + StartScanningV0, + BUTTPLUG_CURRENT_API_MAJOR_VERSION, + BUTTPLUG_CURRENT_API_MINOR_VERSION, + }, + }; +use buttplug_server::message::{ + ButtplugClientMessageVariant, + ButtplugServerMessageVariant, +}; + +use futures::{pin_mut, StreamExt}; +pub use util::test_device_manager::TestDeviceCommunicationManagerBuilder; +use util::test_server_with_device; + +// Test devices that have protocols that support movements not all devices do. +// For instance, the Onyx+ is part of a protocol that supports vibration, but +// the device itself does not. +#[tokio::test] +#[ignore = "Need to figure out what exposure we're testing here"] +async fn test_capabilities_exposure() { + tracing_subscriber::fmt::init(); + // Hold the channel but don't do anything with it. + let (server, _channel) = test_server_with_device("Onyx+"); + let recv = server.event_stream(); + pin_mut!(recv); + + server + .parse_message(ButtplugClientMessageVariant::V4( + RequestServerInfoV4::new( + "Test Client", + BUTTPLUG_CURRENT_API_MAJOR_VERSION, + BUTTPLUG_CURRENT_API_MINOR_VERSION, + ) + .into(), + )) + .await + .expect("Test, assuming infallible."); + server + .parse_message(ButtplugClientMessageVariant::V4( + StartScanningV0::default().into(), + )) + .await + .expect("Test, assuming infallible."); + while let Some(msg) = recv.next().await { + if let ButtplugServerMessageVariant::V4(ButtplugServerMessageV4::DeviceList(_list)) = msg { + // TODO Figure out what we're actually testing here?! + //assert!(device.device_features().iter().any(|x| x.actuator().)); + //assert!(device.device_messages().linear_cmd().is_some()); + return; + } + } +} + +/* +#[cfg(target_os = "windows")] +#[ignore = "Has weird timeout issues"] +#[tokio::test] +async fn test_repeated_address_additions() { + let mut server_builder = ButtplugServerBuilder::default(); + let builder = TestDeviceCommunicationManagerBuilder::default(); + let helper = builder.helper(); + server_builder.comm_manager(builder); + let server = server_builder.finish().unwrap(); + let recv = server.event_stream(); + pin_mut!(recv); + helper + .add_ble_device_with_address("Massage Demo", "SameAddress") + .await; + helper + .add_ble_device_with_address("Massage Demo", "SameAddress") + .await; + assert!(server + .parse_message( + messages::RequestServerInfo::new("Test Client", BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION) + .into() + ) + .await + .is_ok()); + assert!(server + .parse_message(messages::StartScanning::default().into()) + .await + .is_ok()); + let mut device_index = None; + let mut device_removed_called = true; + while let Some(msg) = recv.next().await { + match msg { + ButtplugServerScanningFinished(_) => continue, + ButtplugServerDeviceAdded(da) => { + assert_eq!(da.device_name(), "Aneros Vivi"); + if device_index.is_none() { + device_index = Some(da.device_index()); + } else { + assert!(device_removed_called); + assert_eq!( + da.device_index(), + device_index.expect("Test, assuming infallible.") + ); + return; + } + } + ButtplugServerDeviceRemoved(dr) => { + assert_eq!( + dr.device_index(), + device_index.expect("Test, assuming infallible.") + ); + device_removed_called = true; + } + _ => { + panic!( + "Returned message was not a DeviceAdded message or timed out: {:?}", + msg + ); + } + } + } +} +*/ diff --git a/buttplug/tests/test_websocket_connectors.rs b/crates/buttplug_tests/tests/test_websocket_connectors.rs similarity index 100% rename from buttplug/tests/test_websocket_connectors.rs rename to crates/buttplug_tests/tests/test_websocket_connectors.rs diff --git a/buttplug/tests/test_websocket_device_comm_manager.rs b/crates/buttplug_tests/tests/test_websocket_device_comm_manager.rs similarity index 94% rename from buttplug/tests/test_websocket_device_comm_manager.rs rename to crates/buttplug_tests/tests/test_websocket_device_comm_manager.rs index 94e061426..b6b4d5deb 100644 --- a/buttplug/tests/test_websocket_device_comm_manager.rs +++ b/crates/buttplug_tests/tests/test_websocket_device_comm_manager.rs @@ -11,8 +11,8 @@ mod util; mod test { use buttplug::{ + client::connector::ButtplugInProcessClientConnectorBuilder, client::ButtplugClient, - core::connector::ButtplugInProcessClientConnectorBuilder, server::device::hardware::communication::websocket_server::websocket_server_comm_manager::WebsocketServerDeviceCommunicationManagerBuilder, }; @@ -23,7 +23,6 @@ mod test { WebsocketServerDeviceCommunicationManagerBuilder::default() .server_port(51283) .listen_on_all_interfaces(true), - false, ); let connector = ButtplugInProcessClientConnectorBuilder::default() .server(server) diff --git a/buttplug/tests/util/channel_transport.rs b/crates/buttplug_tests/tests/util/channel_transport.rs similarity index 88% rename from buttplug/tests/util/channel_transport.rs rename to crates/buttplug_tests/tests/util/channel_transport.rs index 16121cf3b..e117c837f 100644 --- a/buttplug/tests/util/channel_transport.rs +++ b/crates/buttplug_tests/tests/util/channel_transport.rs @@ -8,45 +8,50 @@ #![allow(dead_code)] use crate::util::ButtplugTestServer; -use buttplug::{ - client::{ButtplugClient, ButtplugClientError}, - core::{ +use buttplug_client::{ + connector::ButtplugRemoteClientConnector, + serializer::ButtplugClientJSONSerializer, + ButtplugClient, + ButtplugClientError, + }; +use buttplug_core::{ connector::{ transport::{ButtplugConnectorTransport, ButtplugTransportIncomingMessage}, ButtplugConnectorError, - ButtplugRemoteClientConnector, - ButtplugRemoteServerConnector, }, message::{ - self, - serializer::{ - ButtplugClientJSONSerializer, - ButtplugMessageSerializer, - ButtplugSerializedMessage, - ButtplugServerJSONSerializer, - }, - ButtplugClientMessageCurrent, - ButtplugClientMessageV3, - ButtplugClientMessageVariant, + serializer::{ButtplugMessageSerializer, ButtplugSerializedMessage}, + ButtplugClientMessageV4, ButtplugMessage, + DeviceListV4, + RequestServerInfoV4, + ServerInfoV4, + BUTTPLUG_CURRENT_API_MAJOR_VERSION, + BUTTPLUG_CURRENT_API_MINOR_VERSION, + }, + util::async_manager, + + }; +use buttplug_server::{ + connector::ButtplugRemoteServerConnector, + message::{ + serializer::ButtplugServerJSONSerializer, + ButtplugClientMessageVariant, ButtplugServerMessageVariant, - BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION, }, - }, - util::async_manager, }; use futures::{ future::{self, BoxFuture}, select, FutureExt, }; +use log::*; use std::sync::Arc; use tokio::sync::{ mpsc::{channel, Receiver, Sender}, Mutex, Notify, }; -use tracing::*; struct ChannelTransport { outside_receiver: Arc>>>, @@ -143,9 +148,10 @@ impl ChannelClientTestHelper { outgoing_sender, ))))); let client_serializer = ButtplugClientJSONSerializer::default(); - let rsi_setup_msg = client_serializer.serialize(&[message::RequestServerInfoV1::new( + let rsi_setup_msg = client_serializer.serialize(&[RequestServerInfoV4::new( "Test client", - BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION, + BUTTPLUG_CURRENT_API_MAJOR_VERSION, + BUTTPLUG_CURRENT_API_MINOR_VERSION, ) .into()]); let server_serializer = ButtplugServerJSONSerializer::default(); @@ -195,28 +201,23 @@ impl ChannelClientTestHelper { // Wait for RequestServerInfo message assert!(matches!( self.next_client_message().await, - ButtplugClientMessageVariant::V3(ButtplugClientMessageV3::RequestServerInfo(..)) + ButtplugClientMessageVariant::V4(ButtplugClientMessageV4::RequestServerInfo(..)) )); // Just assume we get an RSI message self - .send_client_incoming(ButtplugServerMessageVariant::V3( - message::ServerInfoV2::new( - "test server", - message::BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION, - 0, - ) - .into(), + .send_client_incoming(ButtplugServerMessageVariant::V4( + ServerInfoV4::new("test server", BUTTPLUG_CURRENT_API_MAJOR_VERSION, 0, 0).into(), )) .await; // Wait for RequestDeviceList message. assert!(matches!( self.next_client_message().await, - ButtplugClientMessageVariant::V3(ButtplugClientMessageV3::RequestDeviceList(..)) + ButtplugClientMessageVariant::V4(ButtplugClientMessageV4::RequestDeviceList(..)) )); - let mut dl = message::DeviceListV3::new(vec![]); + let mut dl = DeviceListV4::new(vec![]); dl.set_id(2); self - .send_client_incoming(ButtplugServerMessageVariant::V3(dl.into())) + .send_client_incoming(ButtplugServerMessageVariant::V4(dl.into())) .await; finish_notifier.notified().await; } @@ -260,7 +261,7 @@ impl ChannelClientTestHelper { .await; } - pub async fn send_server_incoming(&self, msg: ButtplugClientMessageCurrent) { + pub async fn send_server_incoming(&self, msg: ButtplugClientMessageV4) { self .send_incoming(ButtplugTransportIncomingMessage::Message( self.client_serializer.serialize(&[msg]), @@ -340,7 +341,7 @@ impl ChannelServerTestHelper { .await; } - pub async fn send_server_incoming(&self, msg: ButtplugClientMessageCurrent) { + pub async fn send_server_incoming(&self, msg: ButtplugClientMessageV4) { self .send_incoming(ButtplugTransportIncomingMessage::Message( self.client_serializer.serialize(&[msg]), diff --git a/buttplug/tests/util/delay_device_communication_manager.rs b/crates/buttplug_tests/tests/util/delay_device_communication_manager.rs similarity index 88% rename from buttplug/tests/util/delay_device_communication_manager.rs rename to crates/buttplug_tests/tests/util/delay_device_communication_manager.rs index 00687f949..dff858ec0 100644 --- a/buttplug/tests/util/delay_device_communication_manager.rs +++ b/crates/buttplug_tests/tests/util/delay_device_communication_manager.rs @@ -5,13 +5,11 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use buttplug::{ - core::ButtplugResultFuture, - server::device::hardware::communication::{ +use buttplug_core::ButtplugResultFuture; +use buttplug_server::device::hardware::communication::{ HardwareCommunicationManager, HardwareCommunicationManagerBuilder, HardwareCommunicationManagerEvent, - }, }; use futures::FutureExt; use std::sync::{ @@ -54,7 +52,7 @@ impl HardwareCommunicationManager for DelayDeviceCommunicationManager { fn start_scanning(&mut self) -> ButtplugResultFuture { let is_scanning = self.is_scanning.clone(); async move { - is_scanning.store(true, Ordering::SeqCst); + is_scanning.store(true, Ordering::Relaxed); Ok(()) } .boxed() @@ -64,7 +62,7 @@ impl HardwareCommunicationManager for DelayDeviceCommunicationManager { let is_scanning = self.is_scanning.clone(); let sender = self.sender.clone(); async move { - is_scanning.store(false, Ordering::SeqCst); + is_scanning.store(false, Ordering::Relaxed); sender .send(HardwareCommunicationManagerEvent::ScanningFinished) .await @@ -75,7 +73,7 @@ impl HardwareCommunicationManager for DelayDeviceCommunicationManager { } fn scanning_status(&self) -> bool { - self.is_scanning.load(Ordering::SeqCst) + self.is_scanning.load(Ordering::Relaxed) } fn can_scan(&self) -> bool { diff --git a/buttplug/tests/util/device_test/client/client_v0/mod.rs b/crates/buttplug_tests/tests/util/device_test/client/client_v0/mod.rs similarity index 100% rename from buttplug/tests/util/device_test/client/client_v0/mod.rs rename to crates/buttplug_tests/tests/util/device_test/client/client_v0/mod.rs diff --git a/buttplug/tests/util/device_test/client/client_v1/mod.rs b/crates/buttplug_tests/tests/util/device_test/client/client_v1/mod.rs similarity index 100% rename from buttplug/tests/util/device_test/client/client_v1/mod.rs rename to crates/buttplug_tests/tests/util/device_test/client/client_v1/mod.rs diff --git a/buttplug/tests/util/device_test/client/client_v2/client.rs b/crates/buttplug_tests/tests/util/device_test/client/client_v2/client.rs similarity index 98% rename from buttplug/tests/util/device_test/client/client_v2/client.rs rename to crates/buttplug_tests/tests/util/device_test/client/client_v2/client.rs index 367d69a98..82f091826 100644 --- a/buttplug/tests/util/device_test/client/client_v2/client.rs +++ b/crates/buttplug_tests/tests/util/device_test/client/client_v2/client.rs @@ -9,22 +9,18 @@ use super::client_event_loop::{ButtplugClientEventLoop, ButtplugClientRequest}; use super::device::ButtplugClientDevice; -use buttplug::{ - core::{ +use buttplug_server::message::{RequestServerInfoV1, ButtplugClientMessageV2, ButtplugServerMessageV2}; +use buttplug_core::{ connector::{ButtplugConnector, ButtplugConnectorError, ButtplugConnectorFuture}, errors::{ButtplugError, ButtplugHandshakeError}, message::{ - ButtplugClientMessageV2, ButtplugMessageSpecVersion, - ButtplugServerMessageV2, PingV0, RequestDeviceListV0, - RequestServerInfoV1, StartScanningV0, StopAllDevicesV0, StopScanningV0, }, - }, util::{ async_manager, future::{ButtplugFuture, ButtplugFutureStateShared}, @@ -36,13 +32,14 @@ use futures::{ future::{self, BoxFuture}, Stream, }; +use log::*; use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, }; use thiserror::Error; use tokio::sync::{broadcast, mpsc, Mutex}; -use tracing::*; +use tracing::{span, Level, Span}; use tracing_futures::Instrument; /// Result type used for public APIs. @@ -252,7 +249,7 @@ impl ButtplugClient { // Don't set ourselves as connected until after ServerInfo has been // received. This means we avoid possible races with the RequestServerInfo // handshake. - self.connected.store(true, Ordering::SeqCst); + self.connected.store(true, Ordering::Relaxed); // Get currently connected devices. The event loop will // handle sending the message and getting the return, and @@ -276,7 +273,7 @@ impl ButtplugClient { /// Returns true if client is currently connected. pub fn connected(&self) -> bool { - self.connected.load(Ordering::SeqCst) + self.connected.load(Ordering::Relaxed) } /// Disconnects from server, if connected. @@ -298,7 +295,7 @@ impl ButtplugClient { let connected = self.connected.clone(); Box::pin(async move { send_fut.await?; - connected.store(false, Ordering::SeqCst); + connected.store(false, Ordering::Relaxed); Ok(()) }) } diff --git a/buttplug/tests/util/device_test/client/client_v2/client_event_loop.rs b/crates/buttplug_tests/tests/util/device_test/client/client_v2/client_event_loop.rs similarity index 95% rename from buttplug/tests/util/device_test/client/client_v2/client_event_loop.rs rename to crates/buttplug_tests/tests/util/device_test/client/client_v2/client_event_loop.rs index f2a2ca4db..ee3a6b4d6 100644 --- a/buttplug/tests/util/device_test/client/client_v2/client_event_loop.rs +++ b/crates/buttplug_tests/tests/util/device_test/client/client_v2/client_event_loop.rs @@ -12,25 +12,24 @@ use super::{ client_message_sorter::ClientMessageSorter, device::{ButtplugClientDevice, ButtplugClientDeviceEvent}, }; -use buttplug::core::{ - connector::{ButtplugConnector, ButtplugConnectorStateShared}, - errors::{ButtplugDeviceError, ButtplugError}, - message::{ +use buttplug_core::{ + connector::{ButtplugConnector, ButtplugConnectorStateShared}, + errors::{ButtplugDeviceError, ButtplugError}, + message::ButtplugMessageValidator, + }; +use buttplug_server::message::{ ButtplugClientMessageV2, - ButtplugDeviceMessage, - ButtplugMessageValidator, ButtplugServerMessageV2, DeviceListV2, DeviceMessageInfoV2, - }, }; use dashmap::DashMap; +use log::*; use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, }; use tokio::sync::{broadcast, mpsc}; -use tracing::*; /// Enum used for communication from the client to the event loop. #[derive(Clone)] @@ -239,16 +238,6 @@ where trace!("Scanning finished event received, forwarding to client."); self.send_client_event(ButtplugClientEvent::ScanningFinished); } - ButtplugServerMessageV2::RawReading(msg) => { - let device_idx = msg.device_index(); - if let Some(device) = self.device_map.get(&device_idx) { - device - .value() - .queue_event(ButtplugClientDeviceEvent::Message( - ButtplugServerMessageV2::RawReading(msg), - )); - } - } ButtplugServerMessageV2::Error(e) => { self.send_client_event(ButtplugClientEvent::Error(e.into())); } @@ -325,7 +314,7 @@ where client = self.from_client_receiver.recv() => match client { Err(_) => { info!("Client disconnected, exiting loop."); - self.connected_status.store(false, Ordering::SeqCst); + self.connected_status.store(false, Ordering::Relaxed); self.device_map.iter().for_each(|val| val.value().set_client_connected(false)); self.send_client_event(ButtplugClientEvent::ServerDisconnect); return; diff --git a/buttplug/tests/util/device_test/client/client_v2/client_message_sorter.rs b/crates/buttplug_tests/tests/util/device_test/client/client_v2/client_message_sorter.rs similarity index 95% rename from buttplug/tests/util/device_test/client/client_v2/client_message_sorter.rs rename to crates/buttplug_tests/tests/util/device_test/client/client_v2/client_message_sorter.rs index d7d423bcc..ecc85a275 100644 --- a/buttplug/tests/util/device_test/client/client_v2/client_message_sorter.rs +++ b/crates/buttplug_tests/tests/util/device_test/client/client_v2/client_message_sorter.rs @@ -12,13 +12,14 @@ use super::client::{ ButtplugClientMessageFuturePair, ButtplugServerMessageStateShared, }; -use buttplug::core::message::{ButtplugMessage, ButtplugMessageValidator, ButtplugServerMessageV2}; +use buttplug_core::message::{ButtplugMessage, ButtplugMessageValidator}; +use buttplug_server::message::ButtplugServerMessageV2; use dashmap::DashMap; +use log::*; use std::sync::{ atomic::{AtomicU32, Ordering}, Arc, }; -use tracing::*; /// Message sorting and pairing for remote client connectors. /// @@ -73,11 +74,11 @@ impl ClientMessageSorter { /// Given a message and its related future, set the message's `id`, and match that id with the /// future to be resolved when we get a response back. pub fn register_future(&self, msg_fut: &mut ButtplugClientMessageFuturePair) { - let id = self.current_id.load(Ordering::SeqCst); + let id = self.current_id.load(Ordering::Relaxed); trace!("Setting message id to {}", id); msg_fut.msg.set_id(id); self.future_map.insert(id, msg_fut.waker.clone()); - self.current_id.store(id + 1, Ordering::SeqCst); + self.current_id.store(id + 1, Ordering::Relaxed); } /// Given a response message from the server, resolve related future if we have one. diff --git a/buttplug/tests/util/device_test/client/client_v2/device.rs b/crates/buttplug_tests/tests/util/device_test/client/client_v2/device.rs similarity index 78% rename from buttplug/tests/util/device_test/client/client_v2/device.rs rename to crates/buttplug_tests/tests/util/device_test/client/client_v2/device.rs index 713596110..de252fd9d 100644 --- a/buttplug/tests/util/device_test/client/client_v2/device.rs +++ b/crates/buttplug_tests/tests/util/device_test/client/client_v2/device.rs @@ -16,37 +16,29 @@ use super::{ }, client_event_loop::ButtplugClientRequest, }; -use buttplug::{ - core::{ +use buttplug_core::{ connector::ButtplugConnectorError, errors::{ButtplugDeviceError, ButtplugError, ButtplugMessageError}, - message::{ - BatteryLevelCmdV2, - ButtplugClientMessageV2, - ButtplugDeviceMessageType, - ButtplugMessage, - ButtplugServerMessageV2, - ClientDeviceMessageAttributesV2, - DeviceMessageInfoV2, - Endpoint, - LinearCmdV1, - RSSILevelCmdV2, - RawReadCmdV2, - RawSubscribeCmdV2, - RawUnsubscribeCmdV2, - RawWriteCmdV2, - RotateCmdV1, - RotationSubcommandV1, - StopDeviceCmdV0, - VectorSubcommandV1, - VibrateCmdV1, - VibrateSubcommandV1, - }, - }, - util::stream::convert_broadcast_receiver_to_stream, + message::{ButtplugMessage, StopDeviceCmdV0}, + util::stream::convert_broadcast_receiver_to_stream, +}; +use buttplug_server::message::{ + BatteryLevelCmdV2, + ButtplugClientMessageV2, + ButtplugDeviceMessageNameV2, + ButtplugServerMessageV2, + ClientDeviceMessageAttributesV2, + DeviceMessageInfoV2, + LinearCmdV1, + RotateCmdV1, + RotationSubcommandV1, + VectorSubcommandV1, + VibrateCmdV1, + VibrateSubcommandV1, }; use futures::{future, Stream}; use getset::Getters; +use log::*; use std::{ collections::HashMap, fmt, @@ -56,7 +48,6 @@ use std::{ }, }; use tokio::sync::broadcast; -use tracing::*; use tracing_futures::Instrument; /// Enum for messages going to a [ButtplugClientDevice] instance. @@ -209,7 +200,7 @@ impl ButtplugClientDevice { } pub fn connected(&self) -> bool { - self.device_connected.load(Ordering::SeqCst) + self.device_connected.load(Ordering::Relaxed) } /// Sends a message through the owning @@ -228,10 +219,10 @@ impl ButtplugClientDevice { let device_name = self.name.clone(); Box::pin( async move { - if !client_connected.load(Ordering::SeqCst) { + if !client_connected.load(Ordering::Relaxed) { error!("Client not connected, cannot run device command"); return Err(ButtplugConnectorError::ConnectorNotConnected.into()); - } else if !device_connected.load(Ordering::SeqCst) { + } else if !device_connected.load(Ordering::Relaxed) { error!("Device not connected, cannot run device command"); return Err( ButtplugError::from(ButtplugDeviceError::DeviceNotConnected(device_name)).into(), @@ -293,10 +284,13 @@ impl ButtplugClientDevice { /// Commands device to vibrate, assuming it has the features to do so. pub fn vibrate(&self, speed_cmd: VibrateCommand) -> ButtplugClientResultFuture { let vibrator_count: u32 = if let Some(features) = self.message_attributes.vibrate_cmd() { - *features.feature_count() + features.feature_count() } else { return self.create_boxed_future_client_error( - ButtplugDeviceError::MessageNotSupported(ButtplugDeviceMessageType::VibrateCmd).into(), + ButtplugDeviceError::MessageNotSupported( + ButtplugDeviceMessageNameV2::VibrateCmd.to_string(), + ) + .into(), ); }; let mut speed_vec: Vec; @@ -344,10 +338,13 @@ impl ButtplugClientDevice { /// Commands device to move linearly, assuming it has the features to do so. pub fn linear(&self, linear_cmd: LinearCommand) -> ButtplugClientResultFuture { let linear_count: u32 = if let Some(features) = self.message_attributes.linear_cmd() { - *features.feature_count() + features.feature_count() } else { return self.create_boxed_future_client_error( - ButtplugDeviceError::MessageNotSupported(ButtplugDeviceMessageType::LinearCmd).into(), + ButtplugDeviceError::MessageNotSupported( + ButtplugDeviceMessageNameV2::LinearCmd.to_string(), + ) + .into(), ); }; let mut linear_vec: Vec; @@ -393,10 +390,13 @@ impl ButtplugClientDevice { /// Commands device to rotate, assuming it has the features to do so. pub fn rotate(&self, rotate_cmd: RotateCommand) -> ButtplugClientResultFuture { let rotate_count: u32 = if let Some(features) = self.message_attributes.rotate_cmd() { - *features.feature_count() + features.feature_count() } else { return self.create_boxed_future_client_error( - ButtplugDeviceError::MessageNotSupported(ButtplugDeviceMessageType::RotateCmd).into(), + ButtplugDeviceError::MessageNotSupported( + ButtplugDeviceMessageNameV2::RotateCmd.to_string(), + ) + .into(), ); }; let mut rotate_vec: Vec; @@ -442,7 +442,10 @@ impl ButtplugClientDevice { pub fn battery_level(&self) -> ButtplugClientResultFuture { if self.message_attributes.battery_level_cmd().is_none() { return self.create_boxed_future_client_error( - ButtplugDeviceError::MessageNotSupported(ButtplugDeviceMessageType::BatteryLevelCmd).into(), + ButtplugDeviceError::MessageNotSupported( + ButtplugDeviceMessageNameV2::BatteryLevelCmd.to_string(), + ) + .into(), ); } let msg = ButtplugClientMessageV2::BatteryLevelCmd(BatteryLevelCmdV2::new(self.index)); @@ -462,105 +465,6 @@ impl ButtplugClientDevice { }) } - pub fn rssi_level(&self) -> ButtplugClientResultFuture { - if self.message_attributes.rssi_level_cmd().is_none() { - return self.create_boxed_future_client_error( - ButtplugDeviceError::MessageNotSupported(ButtplugDeviceMessageType::RSSILevelCmd).into(), - ); - } - let msg = ButtplugClientMessageV2::RSSILevelCmd(RSSILevelCmdV2::new(self.index)); - let send_fut = self.send_message(msg); - Box::pin(async move { - match send_fut.await? { - ButtplugServerMessageV2::RSSILevelReading(reading) => Ok(reading.rssi_level()), - ButtplugServerMessageV2::Error(err) => Err(ButtplugError::from(err).into()), - msg => Err( - ButtplugError::from(ButtplugMessageError::UnexpectedMessageType(format!( - "{:?}", - msg - ))) - .into(), - ), - } - }) - } - - pub fn raw_write( - &self, - endpoint: Endpoint, - data: Vec, - write_with_response: bool, - ) -> ButtplugClientResultFuture { - if self.message_attributes.raw_write_cmd().is_none() { - return self.create_boxed_future_client_error( - ButtplugDeviceError::MessageNotSupported(ButtplugDeviceMessageType::RawWriteCmd).into(), - ); - } - let msg = ButtplugClientMessageV2::RawWriteCmd(RawWriteCmdV2::new( - self.index, - endpoint, - &data, - write_with_response, - )); - self.send_message_expect_ok(msg) - } - - pub fn raw_read( - &self, - endpoint: Endpoint, - expected_length: u32, - timeout: u32, - ) -> ButtplugClientResultFuture> { - if self.message_attributes.raw_read_cmd().is_none() { - return self.create_boxed_future_client_error( - ButtplugDeviceError::MessageNotSupported(ButtplugDeviceMessageType::RawReadCmd).into(), - ); - } - let msg = ButtplugClientMessageV2::RawReadCmd(RawReadCmdV2::new( - self.index, - endpoint, - expected_length, - timeout, - )); - let send_fut = self.send_message(msg); - Box::pin(async move { - match send_fut.await? { - ButtplugServerMessageV2::RawReading(reading) => Ok(reading.data().clone()), - ButtplugServerMessageV2::Error(err) => Err(ButtplugError::from(err).into()), - msg => Err( - ButtplugError::from(ButtplugMessageError::UnexpectedMessageType(format!( - "{:?}", - msg - ))) - .into(), - ), - } - }) - } - - pub fn raw_subscribe(&self, endpoint: Endpoint) -> ButtplugClientResultFuture { - if self.message_attributes.raw_subscribe_cmd().is_none() { - return self.create_boxed_future_client_error( - ButtplugDeviceError::MessageNotSupported(ButtplugDeviceMessageType::RawSubscribeCmd).into(), - ); - } - let msg = - ButtplugClientMessageV2::RawSubscribeCmd(RawSubscribeCmdV2::new(self.index, endpoint)); - self.send_message_expect_ok(msg) - } - - pub fn raw_unsubscribe(&self, endpoint: Endpoint) -> ButtplugClientResultFuture { - if self.message_attributes.raw_subscribe_cmd().is_none() { - return self.create_boxed_future_client_error( - ButtplugDeviceError::MessageNotSupported(ButtplugDeviceMessageType::RawUnsubscribeCmd) - .into(), - ); - } - let msg = - ButtplugClientMessageV2::RawUnsubscribeCmd(RawUnsubscribeCmdV2::new(self.index, endpoint)); - self.send_message_expect_ok(msg) - } - /// Commands device to stop all movement. pub fn stop(&self) -> ButtplugClientResultFuture { // All devices accept StopDeviceCmd @@ -572,11 +476,11 @@ impl ButtplugClientDevice { } pub(super) fn set_device_connected(&self, connected: bool) { - self.device_connected.store(connected, Ordering::SeqCst); + self.device_connected.store(connected, Ordering::Relaxed); } pub(super) fn set_client_connected(&self, connected: bool) { - self.client_connected.store(connected, Ordering::SeqCst); + self.client_connected.store(connected, Ordering::Relaxed); } pub(super) fn queue_event(&self, event: ButtplugClientDeviceEvent) { diff --git a/buttplug/tests/util/device_test/client/client_v2/in_process_connector.rs b/crates/buttplug_tests/tests/util/device_test/client/client_v2/in_process_connector.rs similarity index 87% rename from buttplug/tests/util/device_test/client/client_v2/in_process_connector.rs rename to crates/buttplug_tests/tests/util/device_test/client/client_v2/in_process_connector.rs index 7f81dcedb..245025931 100644 --- a/buttplug/tests/util/device_test/client/client_v2/in_process_connector.rs +++ b/crates/buttplug_tests/tests/util/device_test/client/client_v2/in_process_connector.rs @@ -7,26 +7,27 @@ //! In-process communication between clients and servers -use buttplug::{ - core::{ +use buttplug_core::{ connector::{ButtplugConnector, ButtplugConnectorError, ButtplugConnectorResultFuture}, errors::{ButtplugError, ButtplugMessageError}, + util::async_manager, + }; +use buttplug_server::{ message::{ButtplugClientMessageV2, ButtplugServerMessageV2, ButtplugServerMessageVariant}, - }, - server::{ButtplugServer, ButtplugServerBuilder, ButtplugServerDowngradeWrapper}, - util::async_manager, + ButtplugServer, + ButtplugServerBuilder, }; use futures::{ future::{self, BoxFuture, FutureExt}, pin_mut, StreamExt, }; +use log::*; use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, }; use tokio::sync::mpsc::{channel, Sender}; -use tracing::*; use tracing_futures::Instrument; #[derive(Default)] @@ -66,23 +67,23 @@ impl ButtplugInProcessClientConnectorBuilder { /// develop (and we highly recommend developing that way), and also an easy way to get users up and /// running as quickly as possible, we recommend also including some sort of IPC Connector in order /// for your application to connect to newer servers when they come out. -#[cfg(feature = "server")] + pub struct ButtplugInProcessClientConnector { /// Internal server object for the embedded connector. - server: Arc, + server: Arc, server_outbound_sender: Sender, connected: Arc, } -#[cfg(feature = "server")] + impl Default for ButtplugInProcessClientConnector { fn default() -> Self { ButtplugInProcessClientConnectorBuilder::default().finish() } } -#[cfg(feature = "server")] -impl<'a> ButtplugInProcessClientConnector { + +impl ButtplugInProcessClientConnector { /// Creates a new in-process connector, with a server instance. /// /// Sets up a server, using the basic [ButtplugServer] construction arguments. @@ -93,19 +94,17 @@ impl<'a> ButtplugInProcessClientConnector { let (server_outbound_sender, _) = channel(256); Self { server_outbound_sender, - server: Arc::new(ButtplugServerDowngradeWrapper::new(server.unwrap_or_else( - || { - ButtplugServerBuilder::default() - .finish() - .expect("Default server builder should always work.") - }, - ))), + server: Arc::new(server.unwrap_or_else(|| { + ButtplugServerBuilder::default() + .finish() + .expect("Default server builder should always work.") + })), connected: Arc::new(AtomicBool::new(false)), } } } -#[cfg(feature = "server")] + impl ButtplugConnector for ButtplugInProcessClientConnector { @@ -113,11 +112,11 @@ impl ButtplugConnector &mut self, message_sender: Sender, ) -> BoxFuture<'static, Result<(), ButtplugConnectorError>> { - if !self.connected.load(Ordering::SeqCst) { + if !self.connected.load(Ordering::Relaxed) { let connected = self.connected.clone(); let send = message_sender.clone(); self.server_outbound_sender = message_sender; - let server_recv = self.server.client_version_event_stream(); + let server_recv = self.server.event_stream(); async move { async_manager::spawn(async move { info!("Starting In Process Client Connector Event Sender Loop"); @@ -138,7 +137,7 @@ impl ButtplugConnector } info!("Stopping In Process Client Connector Event Sender Loop, due to channel receiver being dropped."); }.instrument(tracing::info_span!("InProcessClientConnectorEventSenderLoop"))); - connected.store(true, Ordering::SeqCst); + connected.store(true, Ordering::Relaxed); Ok(()) }.boxed() } else { @@ -147,8 +146,8 @@ impl ButtplugConnector } fn disconnect(&self) -> ButtplugConnectorResultFuture { - if self.connected.load(Ordering::SeqCst) { - self.connected.store(false, Ordering::SeqCst); + if self.connected.load(Ordering::Relaxed) { + self.connected.store(false, Ordering::Relaxed); future::ready(Ok(())).boxed() } else { ButtplugConnectorError::ConnectorNotConnected.into() @@ -156,7 +155,7 @@ impl ButtplugConnector } fn send(&self, msg: ButtplugClientMessageV2) -> ButtplugConnectorResultFuture { - if !self.connected.load(Ordering::SeqCst) { + if !self.connected.load(Ordering::Relaxed) { return ButtplugConnectorError::ConnectorNotConnected.into(); } let input = msg.into(); diff --git a/buttplug/tests/util/device_test/client/client_v2/mod.rs b/crates/buttplug_tests/tests/util/device_test/client/client_v2/mod.rs similarity index 95% rename from buttplug/tests/util/device_test/client/client_v2/mod.rs rename to crates/buttplug_tests/tests/util/device_test/client/client_v2/mod.rs index a990f637b..d624525b0 100644 --- a/buttplug/tests/util/device_test/client/client_v2/mod.rs +++ b/crates/buttplug_tests/tests/util/device_test/client/client_v2/mod.rs @@ -9,10 +9,10 @@ use crate::util::{ ButtplugTestServer, TestDeviceChannelHost, }; -use buttplug::{ - server::{device::ServerDeviceManagerBuilder, ButtplugServer, ButtplugServerBuilder}, - util::{async_manager, device_configuration::load_protocol_configs}, -}; +use buttplug_server::{device::ServerDeviceManagerBuilder, ButtplugServer, ButtplugServerBuilder}; +use buttplug_core::util::async_manager; +use buttplug_server_device_config::load_protocol_configs; + use client::{ButtplugClient, ButtplugClientEvent}; use device::{ButtplugClientDevice, LinearCommand, RotateCommand, VibrateCommand}; use in_process_connector::ButtplugInProcessClientConnectorBuilder; @@ -25,15 +25,13 @@ use super::super::{ TestCommand, }; use futures::StreamExt; +use log::*; use std::{sync::Arc, time::Duration}; -use tracing::*; async fn run_test_client_command(command: &TestClientCommand, device: &Arc) { use TestClientCommand::*; match command { - Scalar(_) => { - panic!("Can't run scalar tests on V2!"); - } + Scalar(_) => {} Vibrate(msg) => { device .vibrate(VibrateCommand::SpeedMap( @@ -211,7 +209,7 @@ pub async fn run_test_case( for command in commands { tokio::select! { _ = tokio::time::sleep(Duration::from_millis(500)) => { - panic!("Timeout while waiting for device output!") + panic!("Timeout while waiting for device init output!") } event = device_receiver.recv() => { info!("Got event {:?}", event); @@ -287,7 +285,7 @@ pub async fn run_test_case( for command in commands { tokio::select! { _ = tokio::time::sleep(Duration::from_millis(500)) => { - panic!("Timeout while waiting for device output!") + panic!("Timeout while waiting for device command output!") } event = device_receiver.recv() => { if let Some(command_event) = event { diff --git a/buttplug/src/client/mod.rs b/crates/buttplug_tests/tests/util/device_test/client/client_v3/client.rs similarity index 94% rename from buttplug/src/client/mod.rs rename to crates/buttplug_tests/tests/util/device_test/client/client_v3/client.rs index 5af9ebaa2..52f6ff569 100644 --- a/buttplug/src/client/mod.rs +++ b/crates/buttplug_tests/tests/util/device_test/client/client_v3/client.rs @@ -5,47 +5,31 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -//! Communications API for accessing Buttplug Servers -pub mod client_event_loop; -pub mod client_message_sorter; -pub mod device; - -use crate::{ - core::{ +use super::client_event_loop::{ButtplugClientEventLoop, ButtplugClientRequest}; +pub use super::device::ButtplugClientDevice; +use buttplug_core::{ connector::{ButtplugConnector, ButtplugConnectorError, ButtplugConnectorFuture}, errors::{ButtplugError, ButtplugHandshakeError}, message::{ - ButtplugClientMessageV3, - ButtplugServerMessageV3, + ButtplugMessageSpecVersion, PingV0, RequestDeviceListV0, - RequestServerInfoV1, StartScanningV0, StopAllDevicesV0, StopScanningV0, - BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION, - }, - }, - util::{ + }, util::{ async_manager, future::{ButtplugFuture, ButtplugFutureStateShared}, stream::convert_broadcast_receiver_to_stream, }, }; -use client_event_loop::{ButtplugClientEventLoop, ButtplugClientRequest}; +use buttplug_server::message::{ButtplugClientMessageV3, ButtplugServerMessageV3, RequestServerInfoV1}; use dashmap::DashMap; -pub use device::{ - ButtplugClientDevice, - ButtplugClientDeviceEvent, - LinearCommand, - RotateCommand, - ScalarCommand, - ScalarValueCommand, -}; use futures::{ future::{self, BoxFuture, FutureExt}, Stream, }; +use log::*; use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, @@ -60,7 +44,7 @@ use tracing_futures::Instrument; /// [ButtplugConnectorError]) and an issue within Buttplug (as a /// [ButtplugError]). type ButtplugClientResult = Result; -type ButtplugClientResultFuture = BoxFuture<'static, ButtplugClientResult>; +pub type ButtplugClientResultFuture = BoxFuture<'static, ButtplugClientResult>; /// Result type used for passing server responses. pub type ButtplugServerMessageResult = ButtplugClientResult; @@ -85,8 +69,8 @@ pub(crate) type ButtplugServerMessageFuture = ButtplugFuture bool { - self.connected.load(Ordering::SeqCst) + self.connected.load(Ordering::Relaxed) } /// Disconnects from server, if connected. @@ -390,7 +374,7 @@ impl ButtplugClient { let send_fut = self.message_sender.send_message_to_event_loop(msg); let connected = self.connected.clone(); async move { - connected.store(false, Ordering::SeqCst); + connected.store(false, Ordering::Relaxed); send_fut.await?; Ok(()) } diff --git a/buttplug/src/client/client_event_loop.rs b/crates/buttplug_tests/tests/util/device_test/client/client_v3/client_event_loop.rs similarity index 93% rename from buttplug/src/client/client_event_loop.rs rename to crates/buttplug_tests/tests/util/device_test/client/client_v3/client_event_loop.rs index 6b6bc6d5a..a4e2bb1f8 100644 --- a/buttplug/src/client/client_event_loop.rs +++ b/crates/buttplug_tests/tests/util/device_test/client/client_v3/client_event_loop.rs @@ -8,31 +8,33 @@ //! Implementation of internal Buttplug Client event loop. use super::{ + client::{ButtplugClientMessageFuturePair, ButtplugClientMessageSender}, client_message_sorter::ClientMessageSorter, device::{ButtplugClientDevice, ButtplugClientDeviceEvent}, ButtplugClientEvent, - ButtplugClientMessageFuturePair, - ButtplugClientMessageSender, }; -use crate::core::{ - connector::{ButtplugConnector, ButtplugConnectorStateShared}, - errors::{ButtplugDeviceError, ButtplugError}, - message::{ +use buttplug_core::{ + connector::{ButtplugConnector, ButtplugConnectorStateShared}, + errors::{ButtplugDeviceError, ButtplugError}, + message::{ButtplugDeviceMessage, ButtplugMessageValidator}, + }; +use buttplug_server::message::{ ButtplugClientMessageV3, - ButtplugDeviceMessage, - ButtplugMessageValidator, ButtplugServerMessageV3, DeviceListV3, DeviceMessageInfoV3, - }, }; use dashmap::DashMap; use futures::FutureExt; +use log::*; use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, }; -use tokio::sync::{broadcast, mpsc}; +use tokio::{ + select, + sync::{broadcast, mpsc}, +}; /// Enum used for communication from the client to the event loop. #[derive(Clone)] @@ -241,16 +243,6 @@ where trace!("Scanning finished event received, forwarding to client."); self.send_client_event(ButtplugClientEvent::ScanningFinished); } - ButtplugServerMessageV3::RawReading(msg) => { - let device_idx = msg.device_index(); - if let Some(device) = self.device_map.get(&device_idx) { - device - .value() - .queue_event(ButtplugClientDeviceEvent::Message( - ButtplugServerMessageV3::from(msg), - )); - } - } ButtplugServerMessageV3::SensorReading(msg) => { let device_idx = msg.device_index(); if let Some(device) = self.device_map.get(&device_idx) { @@ -280,8 +272,11 @@ where trace!("Sending message to connector: {:?}", msg_fut.msg); self.sorter.register_future(&mut msg_fut); - if self.connector.send(msg_fut.msg).await.is_err() { - error!("Sending message failed, connector most likely no longer connected."); + if let Err(e) = self.connector.send(msg_fut.msg).await { + error!( + "Sending message failed, connector most likely no longer connected: {:?}", + e + ); } } @@ -355,7 +350,7 @@ where device_indexes .iter() .for_each(|k| self.disconnect_device(*k)); - self.connected_status.store(false, Ordering::SeqCst); + self.connected_status.store(false, Ordering::Relaxed); self.send_client_event(ButtplugClientEvent::ServerDisconnect); debug!("Exiting client event loop."); diff --git a/buttplug/src/client/client_message_sorter.rs b/crates/buttplug_tests/tests/util/device_test/client/client_v3/client_message_sorter.rs similarity index 95% rename from buttplug/src/client/client_message_sorter.rs rename to crates/buttplug_tests/tests/util/device_test/client/client_v3/client_message_sorter.rs index 2eb882c91..899e0f91b 100644 --- a/buttplug/src/client/client_message_sorter.rs +++ b/crates/buttplug_tests/tests/util/device_test/client/client_v3/client_message_sorter.rs @@ -7,15 +7,16 @@ //! Handling of remote message pairing and future resolution. -use crate::{ - client::{ - ButtplugClientError, - ButtplugClientMessageFuturePair, - ButtplugServerMessageStateShared, - }, - core::message::{ButtplugMessage, ButtplugMessageValidator, ButtplugServerMessageV3}, +use super::client::{ + ButtplugClientError, + ButtplugClientMessageFuturePair, + ButtplugServerMessageStateShared, }; +use buttplug_core::message::{ButtplugMessage, ButtplugMessageValidator}; +use buttplug_server::message::ButtplugServerMessageV3; + use dashmap::DashMap; +use log::*; use std::sync::{ atomic::{AtomicU32, Ordering}, Arc, diff --git a/buttplug/src/core/connector/in_process_connector.rs b/crates/buttplug_tests/tests/util/device_test/client/client_v3/connector/in_process_connector.rs similarity index 85% rename from buttplug/src/core/connector/in_process_connector.rs rename to crates/buttplug_tests/tests/util/device_test/client/client_v3/connector/in_process_connector.rs index 845a1d6bd..4f45bc56b 100644 --- a/buttplug/src/core/connector/in_process_connector.rs +++ b/crates/buttplug_tests/tests/util/device_test/client/client_v3/connector/in_process_connector.rs @@ -7,25 +7,26 @@ //! In-process communication between clients and servers -use crate::{ - core::{ +use buttplug_core::{ connector::{ButtplugConnector, ButtplugConnectorError, ButtplugConnectorResultFuture}, errors::{ButtplugError, ButtplugMessageError}, + util::async_manager, + + }; +use buttplug_server::{ message::{ButtplugClientMessageV3, ButtplugServerMessageV3, ButtplugServerMessageVariant}, - }, - server::{ButtplugServer, ButtplugServerDowngradeWrapper}, - util::async_manager, + ButtplugServer, + ButtplugServerBuilder, }; use futures::{ future::{self, BoxFuture, FutureExt}, + pin_mut, StreamExt, }; -use std::{ - convert::TryInto, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, +use log::info; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, }; use tokio::sync::mpsc::{channel, Sender}; use tracing_futures::Instrument; @@ -70,7 +71,7 @@ impl ButtplugInProcessClientConnectorBuilder { #[derive(Clone)] pub struct ButtplugInProcessClientConnector { /// Internal server object for the embedded connector. - server: Arc, + server: Arc, server_outbound_sender: Sender, connected: Arc, } @@ -81,7 +82,7 @@ impl Default for ButtplugInProcessClientConnector { } } -impl<'a> ButtplugInProcessClientConnector { +impl ButtplugInProcessClientConnector { /// Creates a new in-process connector, with a server instance. /// /// Sets up a server, using the basic [ButtplugServer] construction arguments. @@ -92,13 +93,11 @@ impl<'a> ButtplugInProcessClientConnector { let (server_outbound_sender, _) = channel(256); Self { server_outbound_sender, - server: Arc::new(ButtplugServerDowngradeWrapper::new( - server.unwrap(), /*.unwrap_or_else(|| { - ButtplugServerBuilder::default() - .finish() - .expect("Default server builder should always work.") - })*/ - )), + server: Arc::new(server.unwrap_or_else(|| { + ButtplugServerBuilder::default() + .finish() + .expect("Default server builder should always work.") + })), connected: Arc::new(AtomicBool::new(false)), } } @@ -111,11 +110,11 @@ impl ButtplugConnector &mut self, message_sender: Sender, ) -> BoxFuture<'static, Result<(), ButtplugConnectorError>> { - if !self.connected.load(Ordering::SeqCst) { + if !self.connected.load(Ordering::Relaxed) { let connected = self.connected.clone(); let send = message_sender.clone(); self.server_outbound_sender = message_sender; - let server_recv = self.server.client_version_event_stream(); + let server_recv = self.server.event_stream(); async move { async_manager::spawn(async move { info!("Starting In Process Client Connector Event Sender Loop"); @@ -134,7 +133,7 @@ impl ButtplugConnector } info!("Stopping In Process Client Connector Event Sender Loop, due to channel receiver being dropped."); }.instrument(tracing::info_span!("InProcessClientConnectorEventSenderLoop"))); - connected.store(true, Ordering::SeqCst); + connected.store(true, Ordering::Relaxed); Ok(()) }.boxed() } else { @@ -143,8 +142,8 @@ impl ButtplugConnector } fn disconnect(&self) -> ButtplugConnectorResultFuture { - if self.connected.load(Ordering::SeqCst) { - self.connected.store(false, Ordering::SeqCst); + if self.connected.load(Ordering::Relaxed) { + self.connected.store(false, Ordering::Relaxed); future::ready(Ok(())).boxed() } else { ButtplugConnectorError::ConnectorNotConnected.into() @@ -152,12 +151,10 @@ impl ButtplugConnector } fn send(&self, msg: ButtplugClientMessageV3) -> ButtplugConnectorResultFuture { - if !self.connected.load(Ordering::SeqCst) { + if !self.connected.load(Ordering::Relaxed) { return ButtplugConnectorError::ConnectorNotConnected.into(); } - let input = msg - .try_into() - .expect("This is in-process so message conversions will always work."); + let input = msg.into(); let output_fut = self.server.parse_message(input); let sender = self.server_outbound_sender.clone(); async move { diff --git a/crates/buttplug_tests/tests/util/device_test/client/client_v3/connector/mod.rs b/crates/buttplug_tests/tests/util/device_test/client/client_v3/connector/mod.rs new file mode 100644 index 000000000..93e1b9b89 --- /dev/null +++ b/crates/buttplug_tests/tests/util/device_test/client/client_v3/connector/mod.rs @@ -0,0 +1,20 @@ +mod in_process_connector; +pub use in_process_connector::{ + ButtplugInProcessClientConnectorBuilder, +}; + + +use buttplug_client::serializer::ButtplugClientJSONSerializer; +use buttplug_core::connector::ButtplugRemoteConnector; +use buttplug_server::message::{ButtplugClientMessageV3, ButtplugServerMessageV3}; + + +pub type ButtplugRemoteClientConnector< + TransportType, + SerializerType = ButtplugClientJSONSerializer, +> = ButtplugRemoteConnector< + TransportType, + SerializerType, + ButtplugClientMessageV3, + ButtplugServerMessageV3, +>; diff --git a/buttplug/src/client/device.rs b/crates/buttplug_tests/tests/util/device_test/client/client_v3/device.rs similarity index 80% rename from buttplug/src/client/device.rs rename to crates/buttplug_tests/tests/util/device_test/client/client_v3/device.rs index 018629859..93a960fe3 100644 --- a/buttplug/src/client/device.rs +++ b/crates/buttplug_tests/tests/util/device_test/client/client_v3/device.rs @@ -7,44 +7,35 @@ //! Representation and management of devices connected to the server. -use super::{ +use super::client::{ create_boxed_future_client_error, ButtplugClientMessageSender, ButtplugClientResultFuture, }; -use crate::{ - core::{ +use buttplug_core::{ errors::{ButtplugDeviceError, ButtplugError, ButtplugMessageError}, - message::{ - ActuatorType, - ButtplugClientMessageV3, - ButtplugDeviceMessageType, - ButtplugServerMessageV3, - ClientDeviceMessageAttributesV3, - ClientGenericDeviceMessageAttributesV3, - DeviceMessageInfoV3, - Endpoint, - LinearCmdV1, - RawReadCmdV2, - RawSubscribeCmdV2, - RawUnsubscribeCmdV2, - RawWriteCmdV2, - RotateCmdV1, - RotationSubcommandV1, - ScalarCmdV3, - ScalarSubcommandV3, - SensorReadCmdV3, - SensorSubscribeCmdV3, - SensorType, - SensorUnsubscribeCmdV3, - StopDeviceCmdV0, - VectorSubcommandV1, - }, - }, - util::stream::convert_broadcast_receiver_to_stream, + message::{OutputType, InputType, StopDeviceCmdV0}, + util::stream::convert_broadcast_receiver_to_stream, +}; +use buttplug_server::message::{ + ButtplugDeviceMessageNameV3, + ButtplugServerMessageV3, + ClientDeviceMessageAttributesV3, + ClientGenericDeviceMessageAttributesV3, + DeviceMessageInfoV3, + LinearCmdV1, + RotateCmdV1, + RotationSubcommandV1, + ScalarCmdV3, + ScalarSubcommandV3, + SensorReadCmdV3, + SensorSubscribeCmdV3, + SensorUnsubscribeCmdV3, + VectorSubcommandV1, }; use futures::{FutureExt, Stream}; use getset::{CopyGetters, Getters}; +use log::*; use std::{ collections::HashMap, fmt, @@ -76,15 +67,15 @@ pub enum ButtplugClientDeviceEvent { /// a device. Units are in absolute speed values (0.0-1.0). pub enum ScalarCommand { /// Sets all vibration features of a device to the same speed. - Scalar((f64, ActuatorType)), + Scalar((f64, OutputType)), /// Sets vibration features to speed based on the index of the speed in the /// vec (i.e. motor 0 is set to `SpeedVec[0]`, motor 1 is set to /// `SpeedVec[1]`, etc...) - ScalarVec(Vec<(f64, ActuatorType)>), + ScalarVec(Vec<(f64, OutputType)>), /// Sets vibration features indicated by index to requested speed. For /// instance, if the map has an entry of (1, 0.5), it will set motor 1 to a /// speed of 0.5. - ScalarMap(HashMap), + ScalarMap(HashMap), } /// Convenience enum for forming [VibrateCmd] commands. @@ -233,7 +224,7 @@ impl ButtplugClientDevice { } pub fn connected(&self) -> bool { - self.device_connected.load(Ordering::SeqCst) + self.device_connected.load(Ordering::Relaxed) } pub fn event_stream(&self) -> Box + Send + Unpin> { @@ -244,7 +235,7 @@ impl ButtplugClientDevice { fn scalar_value_attributes( &self, - actuator: &ActuatorType, + actuator: &OutputType, ) -> Vec { if let Some(attrs) = self.message_attributes.scalar_cmd() { attrs @@ -280,7 +271,7 @@ impl ButtplugClientDevice { fn scalar_from_value_command( &self, value_cmd: &ScalarValueCommand, - actuator: &ActuatorType, + actuator: &OutputType, attrs: &Vec, ) -> ButtplugClientResultFuture { if attrs.is_empty() { @@ -308,7 +299,7 @@ impl ButtplugClientDevice { ButtplugDeviceError::DeviceFeatureCountMismatch(scalar_count, map.len() as u32).into(), ); } - scalar_vec = Vec::with_capacity(map.len() as usize); + scalar_vec = Vec::with_capacity(map.len()); for (idx, speed) in map { if *idx >= scalar_count { return create_boxed_future_client_error( @@ -328,7 +319,7 @@ impl ButtplugClientDevice { ButtplugDeviceError::DeviceFeatureCountMismatch(scalar_count, vec.len() as u32).into(), ); } - scalar_vec = Vec::with_capacity(vec.len() as usize); + scalar_vec = Vec::with_capacity(vec.len()); for (i, v) in vec.iter().enumerate() { scalar_vec.push(ScalarSubcommandV3::new(*attrs[i].index(), *v, *actuator)); } @@ -339,27 +330,27 @@ impl ButtplugClientDevice { } pub fn vibrate_attributes(&self) -> Vec { - self.scalar_value_attributes(&ActuatorType::Vibrate) + self.scalar_value_attributes(&OutputType::Vibrate) } /// Commands device to vibrate, assuming it has the features to do so. pub fn vibrate(&self, speed_cmd: &ScalarValueCommand) -> ButtplugClientResultFuture { self.scalar_from_value_command( speed_cmd, - &ActuatorType::Vibrate, + &OutputType::Vibrate, &self.vibrate_attributes(), ) } pub fn oscillate_attributes(&self) -> Vec { - self.scalar_value_attributes(&ActuatorType::Oscillate) + self.scalar_value_attributes(&OutputType::Oscillate) } /// Commands device to vibrate, assuming it has the features to do so. pub fn oscillate(&self, speed_cmd: &ScalarValueCommand) -> ButtplugClientResultFuture { self.scalar_from_value_command( speed_cmd, - &ActuatorType::Oscillate, + &OutputType::Oscillate, &self.oscillate_attributes(), ) } @@ -367,7 +358,10 @@ impl ButtplugClientDevice { pub fn scalar(&self, scalar_cmd: &ScalarCommand) -> ButtplugClientResultFuture { if self.message_attributes.scalar_cmd().is_none() { return create_boxed_future_client_error( - ButtplugDeviceError::MessageNotSupported(ButtplugDeviceMessageType::VibrateCmd).into(), + ButtplugDeviceError::MessageNotSupported( + ButtplugDeviceMessageNameV3::ScalarCmd.to_string(), + ) + .into(), ); } @@ -392,7 +386,7 @@ impl ButtplugClientDevice { ButtplugDeviceError::DeviceFeatureCountMismatch(scalar_count, map.len() as u32).into(), ); } - scalar_vec = Vec::with_capacity(map.len() as usize); + scalar_vec = Vec::with_capacity(map.len()); for (idx, (scalar, actuator)) in map { if *idx >= scalar_count { return create_boxed_future_client_error( @@ -408,7 +402,7 @@ impl ButtplugClientDevice { ButtplugDeviceError::DeviceFeatureCountMismatch(scalar_count, vec.len() as u32).into(), ); } - scalar_vec = Vec::with_capacity(vec.len() as usize); + scalar_vec = Vec::with_capacity(vec.len()); for (i, (scalar, actuator)) in vec.iter().enumerate() { scalar_vec.push(ScalarSubcommandV3::new(i as u32, *scalar, *actuator)); } @@ -430,7 +424,10 @@ impl ButtplugClientDevice { pub fn linear(&self, linear_cmd: &LinearCommand) -> ButtplugClientResultFuture { if self.message_attributes.linear_cmd().is_none() { return create_boxed_future_client_error( - ButtplugDeviceError::MessageNotSupported(ButtplugDeviceMessageType::LinearCmd).into(), + ButtplugDeviceError::MessageNotSupported( + ButtplugDeviceMessageNameV3::LinearCmd.to_string(), + ) + .into(), ); } @@ -450,7 +447,7 @@ impl ButtplugClientDevice { ButtplugDeviceError::DeviceFeatureCountMismatch(linear_count, map.len() as u32).into(), ); } - linear_vec = Vec::with_capacity(map.len() as usize); + linear_vec = Vec::with_capacity(map.len()); for (idx, (dur, pos)) in map { if *idx >= linear_count { return create_boxed_future_client_error( @@ -466,7 +463,7 @@ impl ButtplugClientDevice { ButtplugDeviceError::DeviceFeatureCountMismatch(linear_count, vec.len() as u32).into(), ); } - linear_vec = Vec::with_capacity(vec.len() as usize); + linear_vec = Vec::with_capacity(vec.len()); for (i, v) in vec.iter().enumerate() { linear_vec.push(VectorSubcommandV1::new(i as u32, v.0, v.1)); } @@ -488,7 +485,10 @@ impl ButtplugClientDevice { pub fn rotate(&self, rotate_cmd: &RotateCommand) -> ButtplugClientResultFuture { if self.message_attributes.rotate_cmd().is_none() { return create_boxed_future_client_error( - ButtplugDeviceError::MessageNotSupported(ButtplugDeviceMessageType::RotateCmd).into(), + ButtplugDeviceError::MessageNotSupported( + ButtplugDeviceMessageNameV3::RotateCmd.to_string(), + ) + .into(), ); } @@ -508,7 +508,7 @@ impl ButtplugClientDevice { ButtplugDeviceError::DeviceFeatureCountMismatch(rotate_count, map.len() as u32).into(), ); } - rotate_vec = Vec::with_capacity(map.len() as usize); + rotate_vec = Vec::with_capacity(map.len()); for (idx, (speed, clockwise)) in map { if *idx > rotate_count - 1 { return create_boxed_future_client_error( @@ -524,7 +524,7 @@ impl ButtplugClientDevice { ButtplugDeviceError::DeviceFeatureCountMismatch(rotate_count, vec.len() as u32).into(), ); } - rotate_vec = Vec::with_capacity(vec.len() as usize); + rotate_vec = Vec::with_capacity(vec.len()); for (i, v) in vec.iter().enumerate() { rotate_vec.push(RotationSubcommandV1::new(i as u32, v.0, v.1)); } @@ -537,12 +537,14 @@ impl ButtplugClientDevice { pub fn subscribe_sensor( &self, sensor_index: u32, - sensor_type: SensorType, + sensor_type: InputType, ) -> ButtplugClientResultFuture { if self.message_attributes.sensor_subscribe_cmd().is_none() { return create_boxed_future_client_error( - ButtplugDeviceError::MessageNotSupported(ButtplugDeviceMessageType::SensorSubscribeCmd) - .into(), + ButtplugDeviceError::MessageNotSupported( + ButtplugDeviceMessageNameV3::SensorSubscribeCmd.to_string(), + ) + .into(), ); } let msg = SensorSubscribeCmdV3::new(self.index, sensor_index, sensor_type).into(); @@ -552,22 +554,27 @@ impl ButtplugClientDevice { pub fn unsubscribe_sensor( &self, sensor_index: u32, - sensor_type: SensorType, + sensor_type: InputType, ) -> ButtplugClientResultFuture { if self.message_attributes.sensor_subscribe_cmd().is_none() { return create_boxed_future_client_error( - ButtplugDeviceError::MessageNotSupported(ButtplugDeviceMessageType::SensorSubscribeCmd) - .into(), + ButtplugDeviceError::MessageNotSupported( + ButtplugDeviceMessageNameV3::SensorSubscribeCmd.to_string(), + ) + .into(), ); } let msg = SensorUnsubscribeCmdV3::new(self.index, sensor_index, sensor_type).into(); self.event_loop_sender.send_message_expect_ok(msg) } - fn read_single_sensor(&self, sensor_type: &SensorType) -> ButtplugClientResultFuture> { + fn read_single_sensor(&self, sensor_type: &InputType) -> ButtplugClientResultFuture> { if self.message_attributes.sensor_read_cmd().is_none() { return create_boxed_future_client_error( - ButtplugDeviceError::MessageNotSupported(ButtplugDeviceMessageType::SensorReadCmd).into(), + ButtplugDeviceError::MessageNotSupported( + ButtplugDeviceMessageNameV3::SensorReadCmd.to_string(), + ) + .into(), ); } let sensor_indexes: Vec = self @@ -602,7 +609,7 @@ impl ButtplugClientDevice { .boxed() } - fn has_sensor_read(&self, sensor_type: SensorType) -> bool { + fn has_sensor_read(&self, sensor_type: InputType) -> bool { if let Some(sensor_attrs) = self.message_attributes.sensor_read_cmd() { sensor_attrs.iter().any(|x| *x.sensor_type() == sensor_type) } else { @@ -611,11 +618,11 @@ impl ButtplugClientDevice { } pub fn has_battery_level(&self) -> bool { - self.has_sensor_read(SensorType::Battery) + self.has_sensor_read(InputType::Battery) } pub fn battery_level(&self) -> ButtplugClientResultFuture { - let send_fut = self.read_single_sensor(&SensorType::Battery); + let send_fut = self.read_single_sensor(&InputType::Battery); Box::pin(async move { let data = send_fut.await?; let battery_level = data[0]; @@ -624,93 +631,17 @@ impl ButtplugClientDevice { } pub fn has_rssi_level(&self) -> bool { - self.has_sensor_read(SensorType::RSSI) + self.has_sensor_read(InputType::Rssi) } pub fn rssi_level(&self) -> ButtplugClientResultFuture { - let send_fut = self.read_single_sensor(&SensorType::RSSI); + let send_fut = self.read_single_sensor(&InputType::Rssi); Box::pin(async move { let data = send_fut.await?; Ok(data[0]) }) } - pub fn raw_write( - &self, - endpoint: Endpoint, - data: &[u8], - write_with_response: bool, - ) -> ButtplugClientResultFuture { - if self.message_attributes.raw_write_cmd().is_none() { - return create_boxed_future_client_error( - ButtplugDeviceError::MessageNotSupported(ButtplugDeviceMessageType::RawWriteCmd).into(), - ); - } - let msg = ButtplugClientMessageV3::RawWriteCmd(RawWriteCmdV2::new( - self.index, - endpoint, - data, - write_with_response, - )); - self.event_loop_sender.send_message_expect_ok(msg) - } - - pub fn raw_read( - &self, - endpoint: Endpoint, - expected_length: u32, - timeout: u32, - ) -> ButtplugClientResultFuture> { - if self.message_attributes.raw_read_cmd().is_none() { - return create_boxed_future_client_error( - ButtplugDeviceError::MessageNotSupported(ButtplugDeviceMessageType::RawReadCmd).into(), - ); - } - let msg = ButtplugClientMessageV3::RawReadCmd(RawReadCmdV2::new( - self.index, - endpoint, - expected_length, - timeout, - )); - let send_fut = self.event_loop_sender.send_message(msg); - async move { - match send_fut.await? { - ButtplugServerMessageV3::RawReading(reading) => Ok(reading.data().clone()), - ButtplugServerMessageV3::Error(err) => Err(ButtplugError::from(err).into()), - msg => Err( - ButtplugError::from(ButtplugMessageError::UnexpectedMessageType(format!( - "{:?}", - msg - ))) - .into(), - ), - } - } - .boxed() - } - - pub fn raw_subscribe(&self, endpoint: Endpoint) -> ButtplugClientResultFuture { - if self.message_attributes.raw_subscribe_cmd().is_none() { - return create_boxed_future_client_error( - ButtplugDeviceError::MessageNotSupported(ButtplugDeviceMessageType::RawSubscribeCmd).into(), - ); - } - let msg = - ButtplugClientMessageV3::RawSubscribeCmd(RawSubscribeCmdV2::new(self.index, endpoint)); - self.event_loop_sender.send_message_expect_ok(msg) - } - - pub fn raw_unsubscribe(&self, endpoint: Endpoint) -> ButtplugClientResultFuture { - if self.message_attributes.raw_subscribe_cmd().is_none() { - return create_boxed_future_client_error( - ButtplugDeviceError::MessageNotSupported(ButtplugDeviceMessageType::RawSubscribeCmd).into(), - ); - } - let msg = - ButtplugClientMessageV3::RawUnsubscribeCmd(RawUnsubscribeCmdV2::new(self.index, endpoint)); - self.event_loop_sender.send_message_expect_ok(msg) - } - /// Commands device to stop all movement. pub fn stop(&self) -> ButtplugClientResultFuture { // All devices accept StopDeviceCmd @@ -720,11 +651,11 @@ impl ButtplugClientDevice { } pub(super) fn set_device_connected(&self, connected: bool) { - self.device_connected.store(connected, Ordering::SeqCst); + self.device_connected.store(connected, Ordering::Relaxed); } pub(super) fn set_client_connected(&self, connected: bool) { - self.client_connected.store(connected, Ordering::SeqCst); + self.client_connected.store(connected, Ordering::Relaxed); } pub(super) fn queue_event(&self, event: ButtplugClientDeviceEvent) { diff --git a/buttplug/tests/util/device_test/client/client_v3/mod.rs b/crates/buttplug_tests/tests/util/device_test/client/client_v3/mod.rs similarity index 93% rename from buttplug/tests/util/device_test/client/client_v3/mod.rs rename to crates/buttplug_tests/tests/util/device_test/client/client_v3/mod.rs index d548b380d..d880d755c 100644 --- a/buttplug/tests/util/device_test/client/client_v3/mod.rs +++ b/crates/buttplug_tests/tests/util/device_test/client/client_v3/mod.rs @@ -1,22 +1,22 @@ +pub mod client; +pub mod client_event_loop; +pub mod client_message_sorter; +pub mod connector; +pub mod device; +pub mod serializer; + use crate::util::{ - device_test::connector::build_channel_connector, + device_test::connector::build_channel_connector_v3, ButtplugTestServer, TestDeviceChannelHost, }; -use buttplug::{ - client::{ - ButtplugClient, - ButtplugClientDevice, - ButtplugClientEvent, - LinearCommand, - RotateCommand, - ScalarCommand, - ScalarValueCommand, - }, - core::connector::ButtplugInProcessClientConnectorBuilder, - server::{device::ServerDeviceManagerBuilder, ButtplugServer, ButtplugServerBuilder}, - util::{async_manager, device_configuration::load_protocol_configs}, -}; +use client::{ButtplugClient, ButtplugClientDevice, ButtplugClientEvent}; +use crate::util::device_test::client::client_v3::connector::ButtplugInProcessClientConnectorBuilder; +use device::{LinearCommand, RotateCommand, ScalarCommand, ScalarValueCommand}; + +use buttplug_server::{device::ServerDeviceManagerBuilder, ButtplugServer, ButtplugServerBuilder}; +use buttplug_core::util::async_manager; +use buttplug_server_device_config::load_protocol_configs; use tokio::sync::Notify; use super::super::{ @@ -26,8 +26,8 @@ use super::super::{ TestCommand, }; use futures::StreamExt; +use log::*; use std::{sync::Arc, time::Duration}; -use tracing::*; async fn run_test_client_command(command: &TestClientCommand, device: &Arc) { use TestClientCommand::*; @@ -170,7 +170,7 @@ pub async fn run_embedded_test_case(test_case: &DeviceTestCase) { pub async fn run_json_test_case(test_case: &DeviceTestCase) { let notify = Arc::new(Notify::default()); - let (client_connector, server_connector) = build_channel_connector(¬ify); + let (client_connector, server_connector) = build_channel_connector_v3(¬ify); let (server, device_channels) = build_server(test_case); let remote_server = ButtplugTestServer::new(server); diff --git a/crates/buttplug_tests/tests/util/device_test/client/client_v3/serializer/mod.rs b/crates/buttplug_tests/tests/util/device_test/client/client_v3/serializer/mod.rs new file mode 100644 index 000000000..1741d3918 --- /dev/null +++ b/crates/buttplug_tests/tests/util/device_test/client/client_v3/serializer/mod.rs @@ -0,0 +1,71 @@ +use buttplug_core::message::{ + serializer::{ + json_serializer::{create_message_validator, deserialize_to_message, vec_to_protocol_json}, + ButtplugMessageSerializer, + ButtplugSerializedMessage, + ButtplugSerializerError, + }, + ButtplugMessage, + ButtplugMessageFinalizer, + }; +use buttplug_server::message::{ButtplugClientMessageV3, ButtplugServerMessageV3}; + +use jsonschema::Validator; +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; + +pub struct ButtplugClientJSONSerializerImpl { + validator: Validator, +} + +impl Default for ButtplugClientJSONSerializerImpl { + fn default() -> Self { + Self { + validator: create_message_validator(), + } + } +} + +impl ButtplugClientJSONSerializerImpl { + pub fn deserialize( + &self, + msg: &ButtplugSerializedMessage, + ) -> Result, ButtplugSerializerError> + where + T: serde::de::DeserializeOwned + ButtplugMessageFinalizer + Clone + Debug, + { + if let ButtplugSerializedMessage::Text(text_msg) = msg { + deserialize_to_message::(Some(&self.validator), text_msg) + } else { + Err(ButtplugSerializerError::BinaryDeserializationError) + } + } + + pub fn serialize(&self, msg: &[T]) -> ButtplugSerializedMessage + where + T: ButtplugMessage + Serialize + Deserialize<'static>, + { + ButtplugSerializedMessage::Text(vec_to_protocol_json(msg)) + } +} + +#[derive(Default)] +pub struct ButtplugClientJSONSerializer { + serializer_impl: ButtplugClientJSONSerializerImpl, +} + +impl ButtplugMessageSerializer for ButtplugClientJSONSerializer { + type Inbound = ButtplugServerMessageV3; + type Outbound = ButtplugClientMessageV3; + + fn deserialize( + &self, + msg: &ButtplugSerializedMessage, + ) -> Result, ButtplugSerializerError> { + self.serializer_impl.deserialize(msg) + } + + fn serialize(&self, msg: &[Self::Outbound]) -> ButtplugSerializedMessage { + self.serializer_impl.serialize(msg) + } +} 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 new file mode 100644 index 000000000..0e97a87d6 --- /dev/null +++ b/crates/buttplug_tests/tests/util/device_test/client/client_v4/mod.rs @@ -0,0 +1,371 @@ +use crate::util::{ + device_test::connector::build_channel_connector, + ButtplugTestServer, + TestDeviceChannelHost, +}; +use buttplug_client::{ + device::{ClientDeviceFeature, ClientDeviceOutputCommand}, + ButtplugClient, + ButtplugClientDevice, + ButtplugClientEvent, + }; +use buttplug_client_in_process::ButtplugInProcessClientConnectorBuilder; +use buttplug_server_device_config::load_protocol_configs; +use buttplug_core::{message::{OutputType, FeatureType}, util::{async_manager,}}; +use buttplug_server::{device::ServerDeviceManagerBuilder, ButtplugServer, ButtplugServerBuilder}; +use tokio::sync::Notify; + +use super::super::{ + super::TestDeviceCommunicationManagerBuilder, + DeviceTestCase, + TestClientCommand, + TestCommand, +}; +use futures::StreamExt; +use log::*; +use std::{sync::Arc, time::Duration}; + +fn from_type_and_value(output_type: OutputType, value: f64) -> ClientDeviceOutputCommand { + match output_type { + OutputType::Constrict => ClientDeviceOutputCommand::ConstrictFloat(value), + OutputType::Heater => ClientDeviceOutputCommand::HeaterFloat(value), + OutputType::Led => ClientDeviceOutputCommand::LedFloat(value), + OutputType::Oscillate => ClientDeviceOutputCommand::OscillateFloat(value), + OutputType::Position => ClientDeviceOutputCommand::PositionFloat(value), + OutputType::Rotate => ClientDeviceOutputCommand::RotateFloat(value), + OutputType::Spray => ClientDeviceOutputCommand::SprayFloat(value), + OutputType::Vibrate => ClientDeviceOutputCommand::VibrateFloat(value), + _ => panic!("Value not translatable, test cannot run") + } +} + +async fn run_test_client_command(command: &TestClientCommand, device: &Arc) { + use TestClientCommand::*; + match command { + Scalar(msg) => { + let fut_vec: Vec<_> = msg + .iter() + .map(|cmd| { + let f = device.device_features()[&cmd.index()].clone(); + f.send_command(&from_type_and_value(cmd.actuator_type(), cmd.scalar())) + }) + .collect(); + futures::future::try_join_all(fut_vec).await.unwrap(); + } + Vibrate(msg) => { + let fut_vec: Vec<_> = msg + .iter() + .map(|cmd| { + let vibe_features: Vec<&ClientDeviceFeature> = device + .device_features() + .iter() + .filter(|f| f.1.feature().feature_type() == FeatureType::Vibrate) + .map(|(_, x)| x) + .collect(); + let f = vibe_features[cmd.index() as usize].clone(); + f.send_command(&from_type_and_value(OutputType::Vibrate, cmd.speed())) + }) + .collect(); + futures::future::try_join_all(fut_vec).await.unwrap(); + } + Stop => { + device.stop().await.expect("Stop failed"); + } + Rotate(msg) => { + let fut_vec: Vec<_> = msg + .iter() + .map(|cmd| { + let rotate_features: Vec<&ClientDeviceFeature> = device + .device_features() + .iter() + .filter(|f| f.1.feature().feature_type() == FeatureType::RotateWithDirection) + .map(|(_, x)| x) + .collect(); + let f = rotate_features[cmd.index() as usize].clone(); + f.rotate_with_direction( + (cmd.speed() + * f + .feature() + .output() + .as_ref() + .unwrap() + .get(&OutputType::RotateWithDirection) + .unwrap() + .step_count() as f64) + .ceil() as u32, + cmd.clockwise(), + ) + }) + .collect(); + futures::future::try_join_all(fut_vec).await.unwrap(); + } + Linear(msg) => { + let fut_vec: Vec<_> = msg + .iter() + .map(|cmd| { + let f = device.device_features()[&cmd.index()].clone(); + f.position_with_duration( + (cmd.position() + * f + .feature() + .output() + .as_ref() + .unwrap() + .get(&OutputType::PositionWithDuration) + .unwrap() + .step_count() as f64) + .ceil() as u32, + cmd.duration(), + ) + }) + .collect(); + futures::future::try_join_all(fut_vec).await.unwrap(); + } + Battery { + expected_power, + run_async, + } => { + if *run_async { + // This is a special case specifically for lovense, since they read their battery off of + // their notification endpoint. This is a mess but it does the job. + let device = device.clone(); + let expected_power = *expected_power; + async_manager::spawn(async move { + let battery_level = device.battery_level().await.unwrap() as f64 / 100f64; + assert_eq!(battery_level, expected_power); + }); + } else { + assert_eq!( + device.battery_level().await.unwrap() as f64 / 100f64, + *expected_power + ); + } + } + _ => { + panic!( + "Tried to run unhandled TestClientCommand type {:?}", + command + ); + } + } +} + +fn build_server(test_case: &DeviceTestCase) -> (ButtplugServer, Vec) { + let base_cfg = if let Some(device_config_file) = &test_case.device_config_file { + let config_file_path = std::path::Path::new( + &std::env::var("CARGO_MANIFEST_DIR").expect("Should have manifest path"), + ) + .join("tests") + .join("util") + .join("device_test") + .join("device_test_case") + .join("config") + .join(device_config_file); + + Some(std::fs::read_to_string(config_file_path).expect("Should be able to load config")) + } else { + None + }; + let user_cfg = if let Some(user_device_config_file) = &test_case.user_device_config_file { + let config_file_path = std::path::Path::new( + &std::env::var("CARGO_MANIFEST_DIR").expect("Should have manifest path"), + ) + .join("tests") + .join("util") + .join("device_test") + .join("device_test_case") + .join("config") + .join(user_device_config_file); + Some(std::fs::read_to_string(config_file_path).expect("Should be able to load config")) + } else { + None + }; + + let dcm = load_protocol_configs(&base_cfg, &user_cfg, false) + .unwrap() + .finish() + .unwrap(); + // Create our TestDeviceManager with the device identifier we want to create + let mut builder = TestDeviceCommunicationManagerBuilder::default(); + let mut device_channels = vec![]; + for device in &test_case.devices { + info!("identifier: {:?}", device.identifier); + device_channels.push(builder.add_test_device(&device.identifier)); + } + let dm = ServerDeviceManagerBuilder::new(dcm) + .comm_manager(builder) + .finish() + .unwrap(); + + ( + ButtplugServerBuilder::new(dm) + .finish() + .expect("Should always build"), + device_channels, + ) +} + +pub async fn run_embedded_test_case(test_case: &DeviceTestCase) { + let (server, device_channels) = build_server(test_case); + // Connect client + let client = ButtplugClient::new("Test Client"); + let mut in_process_connector_builder = ButtplugInProcessClientConnectorBuilder::default(); + in_process_connector_builder.server(server); + client + .connect(in_process_connector_builder.finish()) + .await + .expect("Test client couldn't connect to embedded process"); + run_test_case(client, device_channels, test_case).await; +} + +pub async fn run_json_test_case(test_case: &DeviceTestCase) { + let notify = Arc::new(Notify::default()); + + let (client_connector, server_connector) = build_channel_connector(¬ify); + + let (server, device_channels) = build_server(test_case); + let remote_server = ButtplugTestServer::new(server); + async_manager::spawn(async move { + remote_server + .start(server_connector) + .await + .expect("Should always succeed"); + }); + + // Connect client + let client = ButtplugClient::new("Test Client"); + client + .connect(client_connector) + .await + .expect("Test client couldn't connect to embedded process"); + run_test_case(client, device_channels, test_case).await; +} + +pub async fn run_test_case( + client: ButtplugClient, + mut device_channels: Vec, + test_case: &DeviceTestCase, +) { + let mut event_stream = client.event_stream(); + + client + .start_scanning() + .await + .expect("Scanning should work."); + + if let Some(device_init) = &test_case.device_init { + // Parse send message into client calls, receives into response checks + for command in device_init { + match command { + TestCommand::Messages { + device_index: _, + messages: _, + } => { + panic!("Shouldn't have messages during initialization"); + } + TestCommand::Commands { + device_index, + commands, + } => { + let device_receiver = &mut device_channels[*device_index as usize].receiver; + for command in commands { + tokio::select! { + _ = tokio::time::sleep(Duration::from_millis(500)) => { + panic!("Timeout while waiting for device output!") + } + event = device_receiver.recv() => { + info!("Got event {:?}", event); + if let Some(command_event) = event { + assert_eq!(command_event, *command); + } else { + panic!("Should not drop device command receiver"); + } + } + } + } + } + TestCommand::Events { + device_index, + events, + } => { + let device_sender = &device_channels[*device_index as usize].sender; + for event in events { + device_sender.send(event.clone()).await.unwrap(); + } + } + } + } + } + + // Scan for devices, wait 'til we get all of the ones we're expecting. Also check names at this + // point. + loop { + tokio::select! { + _ = tokio::time::sleep(Duration::from_millis(300)) => { + panic!("Timeout while waiting for device scan return!") + } + event = event_stream.next() => { + if let Some(ButtplugClientEvent::DeviceAdded(device_added)) = event { + // Compare expected device name + if let Some(expected_name) = &test_case.devices[device_added.index() as usize].expected_name { + assert_eq!(*expected_name, *device_added.name()); + } + if let Some(expected_display_name) = &test_case.devices[device_added.index() as usize].expected_display_name { + assert_eq!(Some(expected_display_name.clone()), *device_added.display_name()); + } + if client.devices().len() == test_case.devices.len() { + break; + } + } else if event.is_none() { + panic!("Should not have dropped event stream!"); + } else { + debug!("Ignoring client message while waiting for devices: {:?}", event); + } + } + } + } + + // Parse send message into client calls, receives into response checks + for command in &test_case.device_commands { + match command { + TestCommand::Messages { + device_index, + messages, + } => { + let device = &client.devices()[*device_index as usize]; + for message in messages { + run_test_client_command(message, device).await; + } + } + TestCommand::Commands { + device_index, + commands, + } => { + let device_receiver = &mut device_channels[*device_index as usize].receiver; + for command in commands { + tokio::select! { + _ = tokio::time::sleep(Duration::from_millis(500)) => { + panic!("Timeout while waiting for device output!") + } + event = device_receiver.recv() => { + if let Some(command_event) = event { + assert_eq!(command_event, *command); + } else { + panic!("Should not drop device command receiver"); + } + } + } + } + } + TestCommand::Events { + device_index, + events, + } => { + let device_sender = &device_channels[*device_index as usize].sender; + for event in events { + device_sender.send(event.clone()).await.unwrap(); + } + } + } + } +} diff --git a/buttplug/tests/util/device_test/client/mod.rs b/crates/buttplug_tests/tests/util/device_test/client/mod.rs similarity index 80% rename from buttplug/tests/util/device_test/client/mod.rs rename to crates/buttplug_tests/tests/util/device_test/client/mod.rs index 9b2c50263..88edc87aa 100644 --- a/buttplug/tests/util/device_test/client/mod.rs +++ b/crates/buttplug_tests/tests/util/device_test/client/mod.rs @@ -2,3 +2,4 @@ pub mod client_v0; pub mod client_v1; pub mod client_v2; pub mod client_v3; +pub mod client_v4; diff --git a/buttplug/tests/util/device_test/connector/channel_transport.rs b/crates/buttplug_tests/tests/util/device_test/connector/channel_transport.rs similarity index 98% rename from buttplug/tests/util/device_test/connector/channel_transport.rs rename to crates/buttplug_tests/tests/util/device_test/connector/channel_transport.rs index db6eede88..f035b4fbe 100644 --- a/buttplug/tests/util/device_test/connector/channel_transport.rs +++ b/crates/buttplug_tests/tests/util/device_test/connector/channel_transport.rs @@ -1,12 +1,10 @@ -use buttplug::{ - core::{ +use buttplug_core::{ connector::{ transport::{ButtplugConnectorTransport, ButtplugTransportIncomingMessage}, ButtplugConnectorError, ButtplugConnectorResultFuture, }, message::serializer::ButtplugSerializedMessage, - }, util::async_manager, }; use futures::{future::BoxFuture, FutureExt}; diff --git a/buttplug/tests/util/device_test/connector/mod.rs b/crates/buttplug_tests/tests/util/device_test/connector/mod.rs similarity index 58% rename from buttplug/tests/util/device_test/connector/mod.rs rename to crates/buttplug_tests/tests/util/device_test/connector/mod.rs index 594510904..cbd0d2a5c 100644 --- a/buttplug/tests/util/device_test/connector/mod.rs +++ b/crates/buttplug_tests/tests/util/device_test/connector/mod.rs @@ -1,32 +1,56 @@ pub mod channel_transport; -use buttplug::core::{ - connector::{ - ButtplugRemoteClientConnector, - ButtplugRemoteConnector, - ButtplugRemoteServerConnector, - }, - message::{ - serializer::{ - ButtplugClientJSONSerializer, - ButtplugClientJSONSerializerImpl, +use buttplug_client::{ + connector::ButtplugRemoteClientConnector, + serializer::{ButtplugClientJSONSerializer, ButtplugClientJSONSerializerImpl}, + }; +use buttplug_core::{ + connector::ButtplugRemoteConnector, + message::serializer::{ ButtplugMessageSerializer, ButtplugSerializedMessage, ButtplugSerializerError, - ButtplugServerJSONSerializer, }, - ButtplugClientMessageV0, - ButtplugClientMessageV1, - ButtplugClientMessageV2, - ButtplugServerMessageV0, - ButtplugServerMessageV1, - ButtplugServerMessageV2, - }, + }; +use buttplug_server::{ + connector::ButtplugRemoteServerConnector, + message::{ + serializer::ButtplugServerJSONSerializer, + ButtplugClientMessageV0, + ButtplugClientMessageV1, + ButtplugClientMessageV2, + ButtplugClientMessageV3, + ButtplugServerMessageV0, + ButtplugServerMessageV1, + ButtplugServerMessageV2, + ButtplugServerMessageV3, + }, }; use std::sync::Arc; use tokio::sync::{mpsc, Notify}; use self::channel_transport::ChannelTransport; +#[derive(Default)] +pub struct ButtplugClientJSONSerializerV3 { + serializer_impl: ButtplugClientJSONSerializerImpl, +} + +impl ButtplugMessageSerializer for ButtplugClientJSONSerializerV3 { + type Inbound = ButtplugServerMessageV3; + type Outbound = ButtplugClientMessageV3; + + fn deserialize( + &self, + msg: &ButtplugSerializedMessage, + ) -> Result, ButtplugSerializerError> { + self.serializer_impl.deserialize(msg) + } + + fn serialize(&self, msg: &[Self::Outbound]) -> ButtplugSerializedMessage { + self.serializer_impl.serialize(msg) + } +} + #[derive(Default)] pub struct ButtplugClientJSONSerializerV2 { serializer_impl: ButtplugClientJSONSerializerImpl, @@ -51,6 +75,13 @@ impl ButtplugMessageSerializer for ButtplugClientJSONSerializerV2 { pub type ChannelClientConnectorCurrent = ButtplugRemoteClientConnector; +pub type ChannelClientConnectorV3 = ButtplugRemoteConnector< + channel_transport::ChannelTransport, + ButtplugClientJSONSerializerV3, + ButtplugClientMessageV3, + ButtplugServerMessageV3, +>; + pub type ChannelClientConnectorV2 = ButtplugRemoteConnector< channel_transport::ChannelTransport, ButtplugClientJSONSerializerV2, @@ -94,6 +125,25 @@ pub fn build_channel_connector( (client_connector, server_connector) } +pub fn build_channel_connector_v3( + notify: &Arc, +) -> (ChannelClientConnectorV3, ChannelServerConnector) { + let (server_sender, server_receiver) = mpsc::channel(256); + let (client_sender, client_receiver) = mpsc::channel(256); + + let client_connector = ChannelClientConnectorV3::new(ChannelTransport::new( + notify, + server_sender, + client_receiver, + )); + let server_connector = ChannelServerConnector::new(ChannelTransport::new( + notify, + client_sender, + server_receiver, + )); + (client_connector, server_connector) +} + pub fn build_channel_connector_v2( notify: &Arc, ) -> (ChannelClientConnectorV2, ChannelServerConnector) { diff --git a/crates/buttplug_tests/tests/util/device_test/device_test_case/config/lovense_ridge_user_config.json b/crates/buttplug_tests/tests/util/device_test/device_test_case/config/lovense_ridge_user_config.json new file mode 100644 index 000000000..64ef89014 --- /dev/null +++ b/crates/buttplug_tests/tests/util/device_test/device_test_case/config/lovense_ridge_user_config.json @@ -0,0 +1,43 @@ +{ + "version": { + "major": 4, + "minor": 999 + }, + "user-configs": { + "devices": [ + { + "identifier": { + "address": "UserConfigTest", + "protocol": "lovense", + "identifier": "F" + }, + "config": { + "name": "Lovense Sex Machine", + "id": "c8633234-07a4-4ad9-961d-a4d777b32be8", + "base-id": "9b52eca4-0e49-426e-a543-2ef735cd803a", + "features": [ + { + "description": "Fucking Machine Oscillation Speed", + "output": { + "Oscillate": { + "step-limit": [ + 0, + 10 + ] + } + }, + "base-id": "0ab80cc0-7a82-4cb6-ba4f-0f18ddb2911f", + "id": "56d94863-b321-428b-8b68-bac0197556e2" + } + ], + "user-config": { + "allow": false, + "deny": false, + "index": 0, + "display-name": "Lovense Name Test" + } + } + } + ] + } +} \ No newline at end of file diff --git a/crates/buttplug_tests/tests/util/device_test/device_test_case/config/lovense_ridge_user_config_invalid_range.json b/crates/buttplug_tests/tests/util/device_test/device_test_case/config/lovense_ridge_user_config_invalid_range.json new file mode 100644 index 000000000..31c6e0e96 --- /dev/null +++ b/crates/buttplug_tests/tests/util/device_test/device_test_case/config/lovense_ridge_user_config_invalid_range.json @@ -0,0 +1,66 @@ +{ + "version": { + "major": 4, + "minor": 999 + }, + "user-configs": { + "devices": [ + { + "identifier": { + "address": "UserConfigTest", + "protocol": "lovense", + "identifier": "F" + }, + "config": { + "name": "Lovense Ridge", + "id": "c8633234-07a4-4ad9-961d-a4d777b32be8", + "base-id": "c8633234-07a4-4ad9-961d-a4d777b32be7", + "features": [ + { + "feature-type": "Oscillate", + "description": "Fucking Machine Oscillation Speed", + "output": { + "Oscillate": { + "step-range": [ + 0, + 30 + ], + "step-limit": [ + 0, + 30 + ] + } + }, + "base-id": "56d94863-b321-428b-8b68-bac0197556e1", + "id": "56d94863-b321-428b-8b68-bac0197556e2" + }, + { + "feature-type": "Battery", + "description": "Battery Level", + "input": { + "Battery": { + "value-range": [ + [ + 0, + 100 + ] + ], + "input-commands": [ + "Read" + ] + } + }, + "base-id": "b9899daa-7755-4ebb-88b4-13122d12745e", + "id": "b9899daa-7755-4ebb-88b4-13122d12745f" + } + ], + "user-config": { + "allow": false, + "deny": false, + "index": 0 + } + } + } + ] + } +} \ No newline at end of file diff --git a/crates/buttplug_tests/tests/util/device_test/device_test_case/config/tcode_linear_and_vibrate_user_config.json b/crates/buttplug_tests/tests/util/device_test/device_test_case/config/tcode_linear_and_vibrate_user_config.json new file mode 100644 index 000000000..d548552fc --- /dev/null +++ b/crates/buttplug_tests/tests/util/device_test/device_test_case/config/tcode_linear_and_vibrate_user_config.json @@ -0,0 +1,109 @@ +{ + "version": { + "major": 4, + "minor": 999 + }, + "user-configs": { + "protocols": { + "tcode-v03": { + "communication": [ + { + "btle": { + "names": [ + "tcode-v03-linear-vibrate" + ], + "services": { + "0000eea0-0000-1000-8000-00805f9b34fb": { + "tx": "0000ee01-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "identifier": [ + "tcode-v03-linear-vibrate" + ], + "name": "TCode v0.3 (Single Linear Axis + Single Vibe)", + "id": "e4384a37-fd37-4b9e-8464-a510dd8410a7", + "features": [ + { + "description": "", + "feature-type": "PositionWithDuration", + "output": { + "PositionWithDuration": { + "step-range": [ + 0, + 100 + ] + } + }, + "id": "c5e6384e-399b-4c2b-9791-eb48abaf3bf7" + }, + { + "description": "", + "feature-type": "Vibrate", + "output": { + "Vibrate": { + "step-range": [ + 0, + 99 + ] + } + }, + "id": "e4384a37-fd37-4b9e-8464-a510dd8410a7" + } + ] + } + ] + } + }, + "devices": [ + { + "identifier": { + "protocol": "tcode-v03", + "identifier": "tcode-v03-linear-vibrate", + "address": "COM7" + }, + "config": { + "id": "6d3f0643-1933-4df6-85ff-5675bf1a3dd4", + "base-id": "e4384a37-fd37-4b9e-8464-a510dd8410a7", + "features": [ + { + "output": { + "PositionWithDuration": { + "step-limit": [ + 0, + 100 + ] + } + }, + "base-id": "c5e6384e-399b-4c2b-9791-eb48abaf3bf7", + "id": "a9e532ff-f72f-4c2f-9ae6-02a6ddcb2d74" + }, + { + "description": "", + "feature-type": "Vibrate", + "output": { + "Vibrate": { + "step-limit": [ + 0, + 99 + ] + } + }, + "base-id": "e4384a37-fd37-4b9e-8464-a510dd8410a7", + "id": "10deb5c9-9d44-4dab-abf3-500b7df51e9f" + } + ], + "user-config": { + "allow": false, + "deny": false, + "index": 0 + } + } + } + ] + } +} \ No newline at end of file diff --git a/buttplug/tests/util/device_test/device_test_case/test_activejoy_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_activejoy_protocol.yaml similarity index 84% rename from buttplug/tests/util/device_test/device_test_case/test_activejoy_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_activejoy_protocol.yaml index 3a358e95a..587a549d1 100644 --- a/buttplug/tests/util/device_test/device_test_case/test_activejoy_protocol.yaml +++ b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_activejoy_protocol.yaml @@ -13,6 +13,7 @@ device_commands: device_index: 0 commands: - !Write + feature_id: ab40e5e5-ef04-47f4-aecb-8aca26f3c0ec endpoint: tx data: [0xb0, 0x01, 0x00, 0x00, 0x01, 0x80] write_with_response: false @@ -26,6 +27,7 @@ device_commands: device_index: 0 commands: - !Write + feature_id: ab40e5e5-ef04-47f4-aecb-8aca26f3c0ec endpoint: tx data: [0xb0, 0x01, 0x00, 0x00, 0x01, 0xc0] write_with_response: false @@ -37,6 +39,7 @@ device_commands: device_index: 0 commands: - !Write + feature_id: ab40e5e5-ef04-47f4-aecb-8aca26f3c0ec endpoint: tx data: [0xb0, 0x01, 0x00, 0x00, 0x00, 0x00] write_with_response: false diff --git a/buttplug/tests/util/device_test/device_test_case/test_adrienlastic_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_adrienlastic_protocol.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_adrienlastic_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_adrienlastic_protocol.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_amorelie_joy_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_amorelie_joy_protocol.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_amorelie_joy_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_amorelie_joy_protocol.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_aneros_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_aneros_protocol.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_aneros_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_aneros_protocol.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_ankni_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_ankni_protocol.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_ankni_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_ankni_protocol.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_ankni_protocol_no_handshake.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_ankni_protocol_no_handshake.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_ankni_protocol_no_handshake.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_ankni_protocol_no_handshake.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_bananasome_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_bananasome_protocol.yaml similarity index 85% rename from buttplug/tests/util/device_test/device_test_case/test_bananasome_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_bananasome_protocol.yaml index 328809bc7..d66376090 100644 --- a/buttplug/tests/util/device_test/device_test_case/test_bananasome_protocol.yaml +++ b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_bananasome_protocol.yaml @@ -13,6 +13,7 @@ device_commands: device_index: 0 commands: - !Write + feature_id: "a0a2e5f8-3692-4f6b-8add-043513ed86f6" endpoint: tx data: [0xa0, 0x03, 0x00, 0x80, 0x00] write_with_response: false @@ -26,6 +27,7 @@ device_commands: device_index: 0 commands: - !Write + feature_id: "a0a2e5f8-3692-4f6b-8add-043513ed86f6" endpoint: tx data: [0xa0, 0x03, 0x00, 0x80, 0x80] write_with_response: false @@ -46,6 +48,7 @@ device_commands: device_index: 0 commands: - !Write + feature_id: "a0a2e5f8-3692-4f6b-8add-043513ed86f6" endpoint: tx data: [0xa0, 0x03, 0xc0, 0xc0, 0x40] write_with_response: false @@ -57,6 +60,7 @@ device_commands: device_index: 0 commands: - !Write + feature_id: "a0a2e5f8-3692-4f6b-8add-043513ed86f6" endpoint: tx data: [0xa0, 0x03, 0x00, 0x00, 0x00] write_with_response: false diff --git a/buttplug/tests/util/device_test/device_test_case/test_cachito_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_cachito_protocol.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_cachito_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_cachito_protocol.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_cowgirl_cone_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_cowgirl_cone_protocol.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_cowgirl_cone_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_cowgirl_cone_protocol.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_cowgirl_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_cowgirl_protocol.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_cowgirl_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_cowgirl_protocol.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_cupido_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_cupido_protocol.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_cupido_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_cupido_protocol.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_deepsire.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_deepsire.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_deepsire.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_deepsire.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_feelingso.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_feelingso.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_feelingso.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_feelingso.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_fleshy_thrust_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_fleshy_thrust_protocol.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_fleshy_thrust_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_fleshy_thrust_protocol.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_foreo_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_foreo_protocol.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_foreo_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_foreo_protocol.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_fox_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_fox_protocol.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_fox_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_fox_protocol.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_fredorch_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_fredorch_protocol.yaml similarity index 98% rename from buttplug/tests/util/device_test/device_test_case/test_fredorch_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_fredorch_protocol.yaml index 0b386184f..956b51b44 100644 --- a/buttplug/tests/util/device_test/device_test_case/test_fredorch_protocol.yaml +++ b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_fredorch_protocol.yaml @@ -83,7 +83,6 @@ device_init: device_commands: # Commands # - # TODO How do we send FleshlightLaunchFW12Cmd?! - !Messages device_index: 0 messages: diff --git a/buttplug/tests/util/device_test/device_test_case/test_galaku.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_galaku.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_galaku.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_galaku.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_galaku_nebula.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_galaku_nebula.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_galaku_nebula.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_galaku_nebula.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_hgod_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_hgod_protocol.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_hgod_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_hgod_protocol.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_hismith_auxfun_box.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_hismith_auxfun_box.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_hismith_auxfun_box.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_hismith_auxfun_box.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_hismith_sinloli.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_hismith_sinloli.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_hismith_sinloli.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_hismith_sinloli.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_hismith_thrusting_cup.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_hismith_thrusting_cup.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_hismith_thrusting_cup.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_hismith_thrusting_cup.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_hismith_v4.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_hismith_v4.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_hismith_v4.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_hismith_v4.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_hismith_wildolo.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_hismith_wildolo.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_hismith_wildolo.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_hismith_wildolo.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_itoys_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_itoys_protocol.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_itoys_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_itoys_protocol.yaml diff --git a/crates/buttplug_tests/tests/util/device_test/device_test_case/test_itoys_twinklingstars.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_itoys_twinklingstars.yaml new file mode 100644 index 000000000..a3a7993ea --- /dev/null +++ b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_itoys_twinklingstars.yaml @@ -0,0 +1,61 @@ +devices: + - identifier: + name: "SML-2310-SZ-B" + expected_name: "iToys Twinkling Stars" +device_commands: + # Commands + - !Messages + device_index: 0 + messages: + - !Vibrate + - Index: 0 + Speed: 0.5 + - !Commands + device_index: 0 + commands: + - !Write + endpoint: tx + data: [0xa0, 0x01, 0x00, 0x00, 0x02, 0xff] + write_with_response: false + - !Messages + device_index: 0 + messages: + - !Vibrate + - Index: 0 + Speed: 0.1 + - !Commands + device_index: 0 + commands: + - !Write + endpoint: tx + data: [0xa0, 0x01, 0x00, 0x00, 0x01, 0xff] + write_with_response: false + - !Messages + device_index: 0 + messages: + - !Scalar + - Index: 1 + Scalar: 0.9 + ActuatorType: Oscillate + - !Commands + device_index: 0 + commands: + - !Write + endpoint: tx + data: [0xa0, 0x06, 0x01, 0x00, 0x01, 0xe6] + write_with_response: false + - !Messages + device_index: 0 + messages: + - !Stop + - !Commands + device_index: 0 + commands: + - !Write + endpoint: tx + data: [0xa0, 0x01, 0x00, 0x00, 0x00, 0xff] + write_with_response: false + - !Write + endpoint: tx + data: [0xa0, 0x06, 0x00, 0x00, 0x00, 0x00] + write_with_response: false diff --git a/crates/buttplug_tests/tests/util/device_test/device_test_case/test_jejoue_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_jejoue_protocol.yaml new file mode 100644 index 000000000..c74c0c541 --- /dev/null +++ b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_jejoue_protocol.yaml @@ -0,0 +1,52 @@ +devices: + - identifier: + name: "Je Joue" + expected_name: "Je Joue Device" +device_commands: + # Set vibration to 50% + - !Messages + device_index: 0 + messages: + - !Vibrate + - Index: 0 + Speed: 0.5 + # Verify hardware message for 50% speed + - !Commands + device_index: 0 + commands: + - !Write + feature_id: d3dd2bf5-b029-4bc1-9466-39f82c2e3258 + endpoint: tx + data: [2, 3] # pattern=2 (vibe1), speed=127 (50%) + write_with_response: false + # Set vibration to 100% + - !Messages + device_index: 0 + messages: + - !Vibrate + - Index: 0 + Speed: 1.0 + # Verify hardware message for 100% speed + - !Commands + device_index: 0 + commands: + - !Write + feature_id: d3dd2bf5-b029-4bc1-9466-39f82c2e3258 + endpoint: tx + data: [2, 5] # pattern=2 (vibe1), speed=255 (100%) + write_with_response: false + # Stop the device + - !Messages + device_index: 0 + messages: + - !Stop # Verify hardware message for stop + + + - !Commands + device_index: 0 + commands: + - !Write + feature_id: d3dd2bf5-b029-4bc1-9466-39f82c2e3258 + endpoint: tx + data: [1, 0] # pattern=1 (both), speed=0 (stop) + write_with_response: false diff --git a/buttplug/tests/util/device_test/device_test_case/test_joyhub_moonhorn.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_joyhub_moonhorn.yaml similarity index 93% rename from buttplug/tests/util/device_test/device_test_case/test_joyhub_moonhorn.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_joyhub_moonhorn.yaml index b4541ec65..fa79e0761 100644 --- a/buttplug/tests/util/device_test/device_test_case/test_joyhub_moonhorn.yaml +++ b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_joyhub_moonhorn.yaml @@ -17,10 +17,10 @@ device_commands: endpoint: tx data: [0xa0, 0x03, 0x80, 0x00, 0x00, 0x00, 0xaa] write_with_response: false - - !Write - endpoint: tx - data: [0xa0, 0x0d, 0x00, 0x00, 0x00, 0xff] # First 0 is free - write_with_response: false +# - !Write +# endpoint: tx +# data: [0xa0, 0x0d, 0x00, 0x00, 0x00, 0xff] # First 0 is free +# write_with_response: false - !Messages device_index: 0 messages: diff --git a/buttplug/tests/util/device_test/device_test_case/test_joyhub_petalwish.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_joyhub_petalwish.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_joyhub_petalwish.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_joyhub_petalwish.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_joyhub_petalwish_compat.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_joyhub_petalwish_compat.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_joyhub_petalwish_compat.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_joyhub_petalwish_compat.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_joyhub_roselin.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_joyhub_roselin.yaml similarity index 91% rename from buttplug/tests/util/device_test/device_test_case/test_joyhub_roselin.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_joyhub_roselin.yaml index db9cf8bea..d648aa214 100644 --- a/buttplug/tests/util/device_test/device_test_case/test_joyhub_roselin.yaml +++ b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_joyhub_roselin.yaml @@ -17,10 +17,10 @@ device_commands: endpoint: tx data: [0xa0, 0x03, 0x80, 0x00, 0x00, 0x00, 0xaa] write_with_response: false - - !Write - endpoint: tx - data: [0xa0, 0x07, 0x00, 0x00, 0x00, 0xff] - write_with_response: false +# - !Write +# endpoint: tx +# data: [0xa0, 0x07, 0x00, 0x00, 0x00, 0xff] +# write_with_response: false - !Messages device_index: 0 messages: diff --git a/buttplug/tests/util/device_test/device_test_case/test_kiiroo_prowand.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_kiiroo_prowand.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_kiiroo_prowand.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_kiiroo_prowand.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_kiiroo_spot.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_kiiroo_spot.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_kiiroo_spot.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_kiiroo_spot.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_lelo_f1sv1.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_lelo_f1sv1.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_lelo_f1sv1.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_lelo_f1sv1.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_lelo_f1sv2.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_lelo_f1sv2.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_lelo_f1sv2.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_lelo_f1sv2.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_lelo_idawave.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_lelo_idawave.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_lelo_idawave.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_lelo_idawave.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_lelo_tianiharmony.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_lelo_tianiharmony.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_lelo_tianiharmony.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_lelo_tianiharmony.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_leten_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_leten_protocol.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_leten_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_leten_protocol.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_loob_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_loob_protocol.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_loob_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_loob_protocol.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_lovehoney_desire_egg.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_lovehoney_desire_egg.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_lovehoney_desire_egg.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_lovehoney_desire_egg.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_lovehoney_desire_prostate.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_lovehoney_desire_prostate.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_lovehoney_desire_prostate.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_lovehoney_desire_prostate.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_lovense_battery.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_lovense_battery.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_lovense_battery.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_lovense_battery.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_lovense_battery_non_default.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_lovense_battery_non_default.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_lovense_battery_non_default.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_lovense_battery_non_default.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_lovense_edge.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_lovense_edge.yaml similarity index 77% rename from buttplug/tests/util/device_test/device_test_case/test_lovense_edge.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_lovense_edge.yaml index e6ccbd076..87822ccb4 100644 --- a/buttplug/tests/util/device_test/device_test_case/test_lovense_edge.yaml +++ b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_lovense_edge.yaml @@ -49,9 +49,14 @@ device_commands: commands: - !Write endpoint: tx - # "Vibrate:10;" - data: [86, 105, 98, 114, 97, 116, 101, 58, 49, 48, 59] + # "Vibrate1:10;" + data: [86, 105, 98, 114, 97, 116, 101, 49, 58, 49, 48, 59] write_with_response: false + - !Write + endpoint: tx + # "Vibrate2:10;" + data: [86, 105, 98, 114, 97, 116, 101, 50, 58, 49, 48, 59] + write_with_response: false - !Messages device_index: 0 messages: @@ -78,5 +83,10 @@ device_commands: - !Write endpoint: tx # "Vibrate:0;" - data: [86, 105, 98, 114, 97, 116, 101, 58, 48, 59] + data: [86, 105, 98, 114, 97, 116, 101, 49, 58, 48, 59] write_with_response: false + - !Write + endpoint: tx + # "Vibrate:0;" + data: [86, 105, 98, 114, 97, 116, 101, 50, 58, 48, 59] + write_with_response: false diff --git a/buttplug/tests/util/device_test/device_test_case/test_lovense_flexer_fw2.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_lovense_flexer_fw2.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_lovense_flexer_fw2.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_lovense_flexer_fw2.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_lovense_flexer_fw3.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_lovense_flexer_fw3.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_lovense_flexer_fw3.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_lovense_flexer_fw3.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_lovense_max.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_lovense_max.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_lovense_max.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_lovense_max.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_lovense_nora.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_lovense_nora.yaml similarity index 89% rename from buttplug/tests/util/device_test/device_test_case/test_lovense_nora.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_lovense_nora.yaml index 9322fe4ec..4ad071b73 100644 --- a/buttplug/tests/util/device_test/device_test_case/test_lovense_nora.yaml +++ b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_lovense_nora.yaml @@ -47,12 +47,16 @@ device_commands: endpoint: tx # "Vibrate:0;" data: [86, 105, 98, 114, 97, 116, 101, 58, 48, 59] - write_with_response: false + write_with_response: false - !Write endpoint: tx # "Rotate:0;" data: [82, 111, 116, 97, 116, 101, 58, 48, 59] write_with_response: false + - !Write + endpoint: tx + data: [82, 111, 116, 97, 116, 101, 67, 104, 97, 110, 103, 101, 59] + write_with_response: false - !Messages device_index: 0 messages: diff --git a/buttplug/tests/util/device_test/device_test_case/test_lovense_osci3.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_lovense_osci3.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_lovense_osci3.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_lovense_osci3.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_lovense_ridge.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_lovense_ridge.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_lovense_ridge.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_lovense_ridge.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_lovense_ridge_user_config.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_lovense_ridge_user_config.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_lovense_ridge_user_config.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_lovense_ridge_user_config.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_lovense_single_vibrator.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_lovense_single_vibrator.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_lovense_single_vibrator.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_lovense_single_vibrator.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_luvmazer_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_luvmazer_protocol.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_luvmazer_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_luvmazer_protocol.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_magic_motion_1_magic_cell.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_magic_motion_1_magic_cell.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_magic_motion_1_magic_cell.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_magic_motion_1_magic_cell.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_magic_motion_2_eidolon.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_magic_motion_2_eidolon.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_magic_motion_2_eidolon.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_magic_motion_2_eidolon.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_magic_motion_2_equinox.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_magic_motion_2_equinox.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_magic_motion_2_equinox.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_magic_motion_2_equinox.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_magic_motion_3_krush.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_magic_motion_3_krush.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_magic_motion_3_krush.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_magic_motion_3_krush.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_magic_motion_4_bobi.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_magic_motion_4_bobi.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_magic_motion_4_bobi.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_magic_motion_4_bobi.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_magic_motion_4_nyx.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_magic_motion_4_nyx.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_magic_motion_4_nyx.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_magic_motion_4_nyx.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_meese_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_meese_protocol.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_meese_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_meese_protocol.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_metaxsire_cali.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_metaxsire_cali.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_metaxsire_cali.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_metaxsire_cali.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_metaxsire_nolan.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_metaxsire_nolan.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_metaxsire_nolan.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_metaxsire_nolan.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_metaxsire_olis.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_metaxsire_olis.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_metaxsire_olis.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_metaxsire_olis.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_metaxsire_rex.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_metaxsire_rex.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_metaxsire_rex.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_metaxsire_rex.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_mizzzee_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_mizzzee_protocol.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_mizzzee_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_mizzzee_protocol.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_mizzzee_v2_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_mizzzee_v2_protocol.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_mizzzee_v2_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_mizzzee_v2_protocol.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_mizzzee_v3_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_mizzzee_v3_protocol.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_mizzzee_v3_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_mizzzee_v3_protocol.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_motorbunny_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_motorbunny_protocol.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_motorbunny_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_motorbunny_protocol.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_mysteryvibe.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_mysteryvibe.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_mysteryvibe.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_mysteryvibe.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_nexus_revo.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_nexus_revo.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_nexus_revo.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_nexus_revo.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_nobra_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_nobra_protocol.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_nobra_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_nobra_protocol.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_omobo_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_omobo_protocol.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_omobo_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_omobo_protocol.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_pink_punch_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_pink_punch_protocol.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_pink_punch_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_pink_punch_protocol.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_sakuraneko_koikoi.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_sakuraneko_koikoi.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_sakuraneko_koikoi.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_sakuraneko_koikoi.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_sakuraneko_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_sakuraneko_protocol.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_sakuraneko_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_sakuraneko_protocol.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_satisfyer_dual_vibrator.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_satisfyer_dual_vibrator.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_satisfyer_dual_vibrator.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_satisfyer_dual_vibrator.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_satisfyer_single_vibrator.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_satisfyer_single_vibrator.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_satisfyer_single_vibrator.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_satisfyer_single_vibrator.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_satisfyer_triple_vibrator.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_satisfyer_triple_vibrator.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_satisfyer_triple_vibrator.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_satisfyer_triple_vibrator.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_sensee_capsule.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_sensee_capsule.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_sensee_capsule.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_sensee_capsule.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_sensee_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_sensee_protocol.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_sensee_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_sensee_protocol.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_serveu_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_serveu_protocol.yaml similarity index 73% rename from buttplug/tests/util/device_test/device_test_case/test_serveu_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_serveu_protocol.yaml index a5b2dc236..9410b8ccb 100644 --- a/buttplug/tests/util/device_test/device_test_case/test_serveu_protocol.yaml +++ b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_serveu_protocol.yaml @@ -51,17 +51,18 @@ device_commands: data: [0x01, 100, 250] write_with_response: false # Test same position/stop - - !Messages - device_index: 0 - messages: - - !Linear - - Index: 0 - Position: 1.0 - Duration: 10 - - !Commands - device_index: 0 - commands: - - !Write - endpoint: tx - data: [0x01, 100, 0] - write_with_response: false + # NOTE: we now throw away repeat commands. Should we not for this? + # - !Messages + # device_index: 0 + # messages: + # - !Linear + # - Index: 0 + # Position: 1.0 + # Duration: 10 + # - !Commands + # device_index: 0 + # commands: + # - !Write + # endpoint: tx + # data: [0x01, 100, 0] + # write_with_response: false diff --git a/crates/buttplug_tests/tests/util/device_test/device_test_case/test_sexverse_heart_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_sexverse_heart_protocol.yaml new file mode 100644 index 000000000..3c23a81be --- /dev/null +++ b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_sexverse_heart_protocol.yaml @@ -0,0 +1,43 @@ +devices: + - identifier: + name: "CBW02" + expected_name: "Sexverse Heart" +device_commands: + - !Messages + device_index: 0 + messages: + - !Vibrate + - Index: 0 + Speed: 0.5 + - !Commands + device_index: 0 + commands: + - !Write + endpoint: tx + data: [0xaa, 0x03, 0x03, 0x32, 0x00, 0x00, 0x00] + write_with_response: false + - !Messages + device_index: 0 + messages: + - !Scalar + - Index: 0 + Scalar: 0.75 + ActuatorType: Vibrate + - !Commands + device_index: 0 + commands: + - !Write + endpoint: tx + data: [0xaa, 0x03, 0x03, 0x4B, 0x00, 0x00, 0x00] + write_with_response: false + - !Messages + device_index: 0 + messages: + - !Stop + - !Commands + device_index: 0 + commands: + - !Write + endpoint: tx + data: [0xaa, 0x03, 0x03, 0x00, 0x00, 0x00, 0x00] + write_with_response: false diff --git a/buttplug/tests/util/device_test/device_test_case/test_sexverse_lg389_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_sexverse_lg389_protocol.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_sexverse_lg389_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_sexverse_lg389_protocol.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_svakom_alex.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_svakom_alex.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_svakom_alex.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_svakom_alex.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_svakom_alex_v2.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_svakom_alex_v2.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_svakom_alex_v2.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_svakom_alex_v2.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_svakom_barnard.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_svakom_barnard.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_svakom_barnard.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_svakom_barnard.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_svakom_cocopro.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_svakom_cocopro.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_svakom_cocopro.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_svakom_cocopro.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_svakom_ella.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_svakom_ella.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_svakom_ella.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_svakom_ella.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_svakom_iker.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_svakom_iker.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_svakom_iker.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_svakom_iker.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_svakom_mora_neo.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_svakom_mora_neo.yaml similarity index 94% rename from buttplug/tests/util/device_test/device_test_case/test_svakom_mora_neo.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_svakom_mora_neo.yaml index eaf7fb983..03f27d8fa 100644 --- a/buttplug/tests/util/device_test/device_test_case/test_svakom_mora_neo.yaml +++ b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_svakom_mora_neo.yaml @@ -16,10 +16,6 @@ device_commands: endpoint: tx data: [0x55, 0x03, 0x01, 0x00, 0x01, 0x05] write_with_response: false - - !Write - endpoint: tx - data: [0x55, 0x09, 0x00, 0x00, 0x00, 0x00] - write_with_response: false - !Messages device_index: 0 messages: diff --git a/buttplug/tests/util/device_test/device_test_case/test_svakom_pulse.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_svakom_pulse.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_svakom_pulse.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_svakom_pulse.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_svakom_sam2.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_svakom_sam2.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_svakom_sam2.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_svakom_sam2.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_svakom_theodore.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_svakom_theodore.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_svakom_theodore.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_svakom_theodore.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_svakom_vivianna.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_svakom_vivianna.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_svakom_vivianna.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_svakom_vivianna.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_synchro_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_synchro_protocol.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_synchro_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_synchro_protocol.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_tcode_linear_and_vibrate.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_tcode_linear_and_vibrate.yaml similarity index 84% rename from buttplug/tests/util/device_test/device_test_case/test_tcode_linear_and_vibrate.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_tcode_linear_and_vibrate.yaml index 56d665952..b6354abb4 100644 --- a/buttplug/tests/util/device_test/device_test_case/test_tcode_linear_and_vibrate.yaml +++ b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_tcode_linear_and_vibrate.yaml @@ -1,7 +1,7 @@ user_device_config_file: "tcode_linear_and_vibrate_user_config.json" devices: - identifier: - name: "tcode-v03" + name: "tcode-v03-linear-vibrate" address: "COM7" expected_name: "TCode v0.3 (Single Linear Axis + Single Vibe)" device_commands: @@ -17,7 +17,7 @@ device_commands: commands: - !Write endpoint: tx - data: [76, 48, 53, 48, 73, 50, 48, 48, 10] + data: [76, 48, 53, 49, 73, 50, 48, 48, 10] write_with_response: false - !Messages device_index: 0 @@ -30,7 +30,7 @@ device_commands: commands: - !Write endpoint: tx - data: [86, 48, 57, 57, 10] + data: [86, 49, 57, 57, 10] write_with_response: false - !Messages device_index: 0 @@ -41,5 +41,5 @@ device_commands: commands: - !Write endpoint: tx - data: [86, 48, 48, 48, 10] + data: [86, 49, 48, 48, 10] write_with_response: false diff --git a/buttplug/tests/util/device_test/device_test_case/test_tryfun_blackhole_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_tryfun_blackhole_protocol.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_tryfun_blackhole_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_tryfun_blackhole_protocol.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_tryfun_meta2_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_tryfun_meta2_protocol.yaml similarity index 97% rename from buttplug/tests/util/device_test/device_test_case/test_tryfun_meta2_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_tryfun_meta2_protocol.yaml index a60bcbfa9..d28338169 100644 --- a/buttplug/tests/util/device_test/device_test_case/test_tryfun_meta2_protocol.yaml +++ b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_tryfun_meta2_protocol.yaml @@ -112,5 +112,5 @@ device_commands: write_with_response: false - !Write endpoint: tx - data: [0x09, 0x02, 0x00, 0x05, 0x21, 0x5, 0x0e, 0x00, 0xcc] + data: [0x09, 0x02, 0x00, 0x05, 0x21, 0x5, 0x0e, 0xff, 0xcd] write_with_response: false \ No newline at end of file diff --git a/buttplug/tests/util/device_test/device_test_case/test_tryfun_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_tryfun_protocol.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_tryfun_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_tryfun_protocol.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_tryfun_surge.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_tryfun_surge.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_tryfun_surge.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_tryfun_surge.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_user_config_display_name.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_user_config_display_name.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_user_config_display_name.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_user_config_display_name.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_vorze_cyclone.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_vorze_cyclone.yaml similarity index 96% rename from buttplug/tests/util/device_test/device_test_case/test_vorze_cyclone.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_vorze_cyclone.yaml index bb41e86a5..db8d5725a 100644 --- a/buttplug/tests/util/device_test/device_test_case/test_vorze_cyclone.yaml +++ b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_vorze_cyclone.yaml @@ -41,5 +41,5 @@ device_commands: commands: - !Write endpoint: tx - data: [0x01, 0x01, 0x00] + data: [0x01, 0x01, 0x80] write_with_response: true diff --git a/buttplug/tests/util/device_test/device_test_case/test_vorze_ufo.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_vorze_ufo.yaml similarity index 96% rename from buttplug/tests/util/device_test/device_test_case/test_vorze_ufo.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_vorze_ufo.yaml index 4fd53f69d..2d873c1ca 100644 --- a/buttplug/tests/util/device_test/device_test_case/test_vorze_ufo.yaml +++ b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_vorze_ufo.yaml @@ -41,5 +41,5 @@ device_commands: commands: - !Write endpoint: tx - data: [0x02, 0x01, 0x00] + data: [0x02, 0x01, 0x80] write_with_response: true diff --git a/buttplug/tests/util/device_test/device_test_case/test_vorze_ufo_tw.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_vorze_ufo_tw.yaml similarity index 93% rename from buttplug/tests/util/device_test/device_test_case/test_vorze_ufo_tw.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_vorze_ufo_tw.yaml index 0d032772c..0f5c3b867 100644 --- a/buttplug/tests/util/device_test/device_test_case/test_vorze_ufo_tw.yaml +++ b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_vorze_ufo_tw.yaml @@ -16,7 +16,7 @@ device_commands: commands: - !Write endpoint: tx - data: [0x05, 0xB2, 0x00] + data: [0x05, 0xB2, 0x80] write_with_response: true - !Messages device_index: 0 @@ -30,7 +30,7 @@ device_commands: commands: - !Write endpoint: tx - data: [0x05, 0x63, 0x00] + data: [0x05, 0x63, 0x80] write_with_response: true - !Messages device_index: 0 @@ -72,5 +72,5 @@ device_commands: commands: - !Write endpoint: tx - data: [0x05, 0x00, 0x00] + data: [0x05, 0x80, 0x80] write_with_response: true diff --git a/buttplug/tests/util/device_test/device_test_case/test_longlosttouch_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_vvd_vkini_protocol.yaml similarity index 50% rename from buttplug/tests/util/device_test/device_test_case/test_longlosttouch_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_vvd_vkini_protocol.yaml index c70dbfd7d..65b3dc7db 100644 --- a/buttplug/tests/util/device_test/device_test_case/test_longlosttouch_protocol.yaml +++ b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_vvd_vkini_protocol.yaml @@ -1,50 +1,34 @@ devices: - identifier: - name: "RS-KNW" - expected_name: "Long Lost Touch Possible Kiss" + name: "HJ2024N01" + expected_name: "VVD Vkini" device_commands: - # Commands - !Messages device_index: 0 messages: - - !Scalar + - !Vibrate - Index: 0 - Scalar: 0.5 - ActuatorType: Vibrate - - !Commands - device_index: 0 - commands: - - !Write - endpoint: tx - data: [0xaa, 0x02, 0x01, 0x00, 0x00, 0x32] - write_with_response: true - - !Messages - device_index: 0 - messages: - - !Scalar - - Index: 1 - Scalar: 1 - ActuatorType: Oscillate + Speed: 0.5 - !Commands device_index: 0 commands: - !Write endpoint: tx - data: [0xaa, 0x02, 0x02, 0x00, 0x00, 0x64] + data: [0xbb, 0x01, 0x32, 0x66] write_with_response: true - !Messages device_index: 0 messages: - !Scalar - Index: 0 - Scalar: 1 + Scalar: 0.75 ActuatorType: Vibrate - !Commands device_index: 0 commands: - !Write endpoint: tx - data: [0xaa, 0x02, 0x00, 0x00, 0x00, 0x64] + data: [0xbb, 0x01, 0x4B, 0x66] write_with_response: true - !Messages device_index: 0 @@ -55,5 +39,5 @@ device_commands: commands: - !Write endpoint: tx - data: [0xaa, 0x02, 0x00, 0x00, 0x00, 0x00] + data: [0xbb, 0x01, 0x00, 0x66] write_with_response: true diff --git a/buttplug/tests/util/device_test/device_test_case/test_wetoy_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_wetoy_protocol.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_wetoy_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_wetoy_protocol.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_wevibe_4plus.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_wevibe_4plus.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_wevibe_4plus.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_wevibe_4plus.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_wevibe_chorus.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_wevibe_chorus.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_wevibe_chorus.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_wevibe_chorus.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_wevibe_moxie.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_wevibe_moxie.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_wevibe_moxie.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_wevibe_moxie.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_wevibe_pivot.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_wevibe_pivot.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_wevibe_pivot.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_wevibe_pivot.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_wevibe_vector.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_wevibe_vector.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_wevibe_vector.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_wevibe_vector.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_xibao_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_xibao_protocol.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_xibao_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_xibao_protocol.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_xiuxiuda_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_xiuxiuda_protocol.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_xiuxiuda_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_xiuxiuda_protocol.yaml diff --git a/buttplug/tests/util/device_test/device_test_case/test_xuanhuan_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_xuanhuan_protocol.yaml similarity index 100% rename from buttplug/tests/util/device_test/device_test_case/test_xuanhuan_protocol.yaml rename to crates/buttplug_tests/tests/util/device_test/device_test_case/test_xuanhuan_protocol.yaml diff --git a/buttplug/tests/util/device_test/mod.rs b/crates/buttplug_tests/tests/util/device_test/mod.rs similarity index 90% rename from buttplug/tests/util/device_test/mod.rs rename to crates/buttplug_tests/tests/util/device_test/mod.rs index f160eeace..d612222d3 100644 --- a/buttplug/tests/util/device_test/mod.rs +++ b/crates/buttplug_tests/tests/util/device_test/mod.rs @@ -3,14 +3,12 @@ pub mod client; pub mod connector; use super::{TestDeviceIdentifier, TestHardwareEvent}; -use buttplug::{ - core::message::{ +use buttplug_server::device::hardware::HardwareCommand; +use buttplug_server::message::{ RotationSubcommandV1, ScalarSubcommandV3, VectorSubcommandV1, VibrateSubcommandV1, - }, - server::device::hardware::HardwareCommand, }; use serde::{Deserialize, Serialize}; @@ -21,7 +19,7 @@ struct TestDevice { expected_display_name: Option, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] enum TestCommand { Messages { device_index: u32, diff --git a/buttplug/tests/util/mod.rs b/crates/buttplug_tests/tests/util/mod.rs similarity index 82% rename from buttplug/tests/util/mod.rs rename to crates/buttplug_tests/tests/util/mod.rs index 7835b8ad8..65fbdcc59 100644 --- a/buttplug/tests/util/mod.rs +++ b/crates/buttplug_tests/tests/util/mod.rs @@ -12,20 +12,16 @@ pub mod device_test; pub mod test_device_manager; pub use delay_device_communication_manager::DelayDeviceCommunicationManagerBuilder; pub mod channel_transport; -use buttplug::{ - client::ButtplugClient, - core::connector::ButtplugInProcessClientConnectorBuilder, - server::{ +use buttplug_client::ButtplugClient; +use buttplug_client_in_process::ButtplugInProcessClientConnectorBuilder; +use buttplug_server_device_config::{load_protocol_configs, DeviceConfigurationManager}; +use buttplug_server::{ device::{ - configuration::DeviceConfigurationManager, hardware::communication::HardwareCommunicationManagerBuilder, ServerDeviceManagerBuilder, }, ButtplugServer, ButtplugServerBuilder, - ButtplugServerDowngradeWrapper, - }, - util::device_configuration::load_protocol_configs, }; pub use test_device_manager::{ TestDeviceChannelHost, @@ -35,10 +31,9 @@ pub use test_device_manager::{ use crate::util::test_device_manager::TestDeviceIdentifier; -pub fn create_test_dcm(allow_raw_messages: bool) -> DeviceConfigurationManager { +pub fn create_test_dcm() -> DeviceConfigurationManager { load_protocol_configs(&None, &None, false) .expect("If this fails, the whole library goes with it.") - .allow_raw_messages(allow_raw_messages) .finish() .expect("If this fails, the whole library goes with it.") } @@ -49,9 +44,9 @@ pub fn setup_logging() { } #[allow(dead_code)] -pub fn test_server(allow_raw_messages: bool) -> ButtplugServer { +pub fn test_server() -> ButtplugServer { ButtplugServerBuilder::new( - ServerDeviceManagerBuilder::new(create_test_dcm(allow_raw_messages)) + ServerDeviceManagerBuilder::new(create_test_dcm()) .finish() .unwrap(), ) @@ -62,7 +57,7 @@ pub fn test_server(allow_raw_messages: bool) -> ButtplugServer { #[allow(dead_code)] pub async fn test_client() -> ButtplugClient { let connector = ButtplugInProcessClientConnectorBuilder::default() - .server(test_server(false)) + .server(test_server()) .finish(); let client = ButtplugClient::new("Test Client"); @@ -80,7 +75,7 @@ pub async fn test_client_with_device() -> (ButtplugClient, TestDeviceChannelHost let mut builder = TestDeviceCommunicationManagerBuilder::default(); let device = builder.add_test_device(&TestDeviceIdentifier::new("Massage Demo", None)); - let mut dm_builder = ServerDeviceManagerBuilder::new(create_test_dcm(false)); + let mut dm_builder = ServerDeviceManagerBuilder::new(create_test_dcm()); dm_builder.comm_manager(builder); let server_builder = ButtplugServerBuilder::new(dm_builder.finish().unwrap()); @@ -130,7 +125,7 @@ pub async fn test_client_with_device_and_custom_dcm( pub async fn test_client_with_delayed_device_manager() -> ButtplugClient { let builder = DelayDeviceCommunicationManagerBuilder::default(); - let mut dm_builder = ServerDeviceManagerBuilder::new(create_test_dcm(false)); + let mut dm_builder = ServerDeviceManagerBuilder::new(create_test_dcm()); dm_builder.comm_manager(builder); let server_builder = ButtplugServerBuilder::new(dm_builder.finish().unwrap()); @@ -150,11 +145,11 @@ pub async fn test_client_with_delayed_device_manager() -> ButtplugClient { } #[allow(dead_code)] -pub fn test_server_with_comm_manager(dcm: T, allow_raw_message: bool) -> ButtplugServer +pub fn test_server_with_comm_manager(dcm: T) -> ButtplugServer where T: HardwareCommunicationManagerBuilder + 'static, { - let mut dm_builder = ServerDeviceManagerBuilder::new(create_test_dcm(allow_raw_message)); + let mut dm_builder = ServerDeviceManagerBuilder::new(create_test_dcm()); dm_builder.comm_manager(dcm); ButtplugServerBuilder::new(dm_builder.finish().unwrap()) @@ -165,13 +160,12 @@ where #[allow(dead_code)] pub fn test_server_with_device( device_type: &str, - allow_raw_message: bool, -) -> (ButtplugServerDowngradeWrapper, TestDeviceChannelHost) { +) -> (ButtplugServer, TestDeviceChannelHost) { let mut builder = TestDeviceCommunicationManagerBuilder::default(); let device = builder.add_test_device(&TestDeviceIdentifier::new(device_type, None)); ( - ButtplugServerDowngradeWrapper::new(test_server_with_comm_manager(builder, allow_raw_message)), + test_server_with_comm_manager(builder), device, ) } @@ -179,13 +173,12 @@ pub fn test_server_with_device( #[allow(dead_code)] pub fn test_server_v4_with_device( device_type: &str, - allow_raw_message: bool, ) -> (ButtplugServer, TestDeviceChannelHost) { let mut builder = TestDeviceCommunicationManagerBuilder::default(); let device = builder.add_test_device(&TestDeviceIdentifier::new(device_type, None)); ( - test_server_with_comm_manager(builder, allow_raw_message), + test_server_with_comm_manager(builder), device, ) } diff --git a/buttplug/tests/util/test_device_manager/mod.rs b/crates/buttplug_tests/tests/util/test_device_manager/mod.rs similarity index 54% rename from buttplug/tests/util/test_device_manager/mod.rs rename to crates/buttplug_tests/tests/util/test_device_manager/mod.rs index cf3ad8c50..f7c8199f1 100644 --- a/buttplug/tests/util/test_device_manager/mod.rs +++ b/crates/buttplug_tests/tests/util/test_device_manager/mod.rs @@ -6,34 +6,30 @@ // for full license information. mod test_device; -#[cfg(feature = "server")] + mod test_device_comm_manager; -use buttplug::{ - server::device::hardware::HardwareCommand, - util::stream::{iffy_is_empty_check, recv_now}, -}; -use std::sync::{Arc, Mutex}; +use buttplug_server::device::hardware::HardwareCommand; +use std::time::Duration; pub use test_device::{TestDevice, TestDeviceChannelHost, TestHardwareEvent}; -#[cfg(feature = "server")] + pub use test_device_comm_manager::{ //new_bluetoothle_test_device, TestDeviceCommunicationManagerBuilder, TestDeviceIdentifier, }; -use tokio::sync::mpsc::Receiver; #[allow(dead_code)] -pub fn check_test_recv_value(receiver: &mut TestDeviceChannelHost, command: HardwareCommand) { +pub async fn check_test_recv_value( + timeout: &Duration, + receiver: &mut TestDeviceChannelHost, + command: HardwareCommand, +) { assert_eq!( - recv_now(&mut receiver.receiver) + tokio::time::timeout(*timeout, receiver.receiver.recv()) + .await .expect("No messages received") .expect("Test"), command ); } - -#[allow(dead_code)] -pub fn check_test_recv_empty(receiver: &Arc>>) -> bool { - iffy_is_empty_check(&mut receiver.lock().expect("Test")) -} diff --git a/buttplug/tests/util/test_device_manager/test_device.rs b/crates/buttplug_tests/tests/util/test_device_manager/test_device.rs similarity index 94% rename from buttplug/tests/util/test_device_manager/test_device.rs rename to crates/buttplug_tests/tests/util/test_device_manager/test_device.rs index 72ef67890..329c01809 100644 --- a/buttplug/tests/util/test_device_manager/test_device.rs +++ b/crates/buttplug_tests/tests/util/test_device_manager/test_device.rs @@ -5,10 +5,9 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use buttplug::{ - core::{errors::ButtplugDeviceError, message::Endpoint}, - server::device::{ - configuration::ProtocolCommunicationSpecifier, +use buttplug_core::{errors::ButtplugDeviceError, util::async_manager}; +use buttplug_server_device_config::{Endpoint, ProtocolCommunicationSpecifier}; +use buttplug_server::device::{ hardware::{ Hardware, HardwareCommand, @@ -21,9 +20,7 @@ use buttplug::{ HardwareSubscribeCmd, HardwareUnsubscribeCmd, HardwareWriteCmd, - }, }, - util::async_manager, }; use async_trait::async_trait; @@ -33,7 +30,7 @@ use serde::{Deserialize, Serialize}; use std::{ collections::{HashSet, VecDeque}, fmt::{self, Debug}, - sync::Arc, + sync::Arc, time::Duration, }; use tokio::sync::{broadcast, mpsc, Mutex}; @@ -123,6 +120,9 @@ impl HardwareSpecializer for TestHardwareSpecializer { &device.name(), &device.address(), &endpoints, + // Add slight delay for protocols with multiple messages. + &Some(Duration::from_millis(1)), + false, Box::new(device), ); Ok(hardware) @@ -236,7 +236,7 @@ impl TestDevice { ) -> BoxFuture<'static, Result<(), ButtplugDeviceError>> { let sender = self.test_device_channel.clone(); async move { - sender.send(data_command).await.expect("Test"); + sender.send(data_command).await.unwrap(); Ok(()) } .boxed() @@ -299,7 +299,7 @@ impl HardwareInternal for TestDevice { msg: &HardwareWriteCmd, ) -> BoxFuture<'static, Result<(), ButtplugDeviceError>> { if !self.endpoints.contains(&msg.endpoint()) { - return future::ready(Err(ButtplugDeviceError::InvalidEndpoint(msg.endpoint()))).boxed(); + return future::ready(Err(ButtplugDeviceError::InvalidEndpoint(msg.endpoint().to_string()))).boxed(); } self.send_command(msg.clone().into()) } @@ -309,7 +309,7 @@ impl HardwareInternal for TestDevice { msg: &HardwareSubscribeCmd, ) -> BoxFuture<'static, Result<(), ButtplugDeviceError>> { if !self.endpoints.contains(&msg.endpoint()) { - return future::ready(Err(ButtplugDeviceError::InvalidEndpoint(msg.endpoint()))).boxed(); + return future::ready(Err(ButtplugDeviceError::InvalidEndpoint(msg.endpoint().to_string()))).boxed(); } self.subscribed_endpoints.insert(msg.endpoint()); self.send_command((*msg).into()) @@ -320,7 +320,7 @@ impl HardwareInternal for TestDevice { msg: &HardwareUnsubscribeCmd, ) -> BoxFuture<'static, Result<(), ButtplugDeviceError>> { if !self.endpoints.contains(&msg.endpoint()) { - return future::ready(Err(ButtplugDeviceError::InvalidEndpoint(msg.endpoint()))).boxed(); + return future::ready(Err(ButtplugDeviceError::InvalidEndpoint(msg.endpoint().to_string()))).boxed(); } self.subscribed_endpoints.remove(&msg.endpoint()); self.send_command((*msg).into()) diff --git a/buttplug/tests/util/test_device_manager/test_device_comm_manager.rs b/crates/buttplug_tests/tests/util/test_device_manager/test_device_comm_manager.rs similarity index 93% rename from buttplug/tests/util/test_device_manager/test_device_comm_manager.rs rename to crates/buttplug_tests/tests/util/test_device_manager/test_device_comm_manager.rs index d1a562ac5..87454024c 100644 --- a/buttplug/tests/util/test_device_manager/test_device_comm_manager.rs +++ b/crates/buttplug_tests/tests/util/test_device_manager/test_device_comm_manager.rs @@ -14,16 +14,15 @@ use super::{ }, TestDevice, }; -use buttplug::{ - core::ButtplugResultFuture, - server::device::configuration::{BluetoothLESpecifier, ProtocolCommunicationSpecifier}, - server::device::hardware::communication::{ +use buttplug_core::ButtplugResultFuture; +use buttplug_server_device_config::{BluetoothLESpecifier, ProtocolCommunicationSpecifier}; +use buttplug_server::device::hardware::communication::{ HardwareCommunicationManager, HardwareCommunicationManagerBuilder, HardwareCommunicationManagerEvent, - }, }; use futures::future::{self, FutureExt}; +use log::*; use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, @@ -34,7 +33,6 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; use tokio::sync::mpsc::Sender; -use tracing::*; pub fn generate_address() -> String { info!("Generating random address for test device"); @@ -160,14 +158,14 @@ impl HardwareCommunicationManager for TestDeviceCommunicationManager { let device_sender = self.device_sender.clone(); let is_scanning = self.is_scanning.clone(); async move { - is_scanning.store(true, Ordering::SeqCst); + is_scanning.store(true, Ordering::Relaxed); for event in events { if device_sender.send(event).await.is_err() { error!("Device channel no longer open."); } } // TODO Should should use - is_scanning.store(false, Ordering::SeqCst); + is_scanning.store(false, Ordering::Relaxed); if device_sender .send(HardwareCommunicationManagerEvent::ScanningFinished) .await @@ -191,6 +189,6 @@ impl HardwareCommunicationManager for TestDeviceCommunicationManager { } fn scanning_status(&self) -> bool { - self.is_scanning.load(Ordering::SeqCst) + self.is_scanning.load(Ordering::Relaxed) } } diff --git a/buttplug/tests/util/test_server.rs b/crates/buttplug_tests/tests/util/test_server.rs similarity index 87% rename from buttplug/tests/util/test_server.rs rename to crates/buttplug_tests/tests/util/test_server.rs index 1358e30fa..37baab66b 100644 --- a/buttplug/tests/util/test_server.rs +++ b/crates/buttplug_tests/tests/util/test_server.rs @@ -5,26 +5,23 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use buttplug::{ - core::{ +use buttplug_core::{ connector::ButtplugConnector, errors::ButtplugError, - message::{ - self, - ButtplugClientMessageVariant, - ButtplugMessage, - ButtplugMessageValidator, - ButtplugServerMessageVariant, - }, - }, - server::{ButtplugServer, ButtplugServerBuilder, ButtplugServerDowngradeWrapper}, - util::async_manager, + message::{ButtplugMessage, ButtplugMessageValidator, ErrorV0}, + util::async_manager, + + }; +use buttplug_server::{ + message::{ButtplugClientMessageVariant, ButtplugServerMessageVariant}, + ButtplugServer, + ButtplugServerBuilder, }; use futures::{future::Future, pin_mut, select, FutureExt, StreamExt}; +use log::*; use std::sync::Arc; use thiserror::Error; use tokio::sync::{mpsc, Notify}; -use tracing::*; #[derive(Error, Debug)] pub enum ButtplugServerConnectorError { @@ -33,12 +30,12 @@ pub enum ButtplugServerConnectorError { } pub struct ButtplugTestServer { - server: Arc, + server: Arc, disconnect_notifier: Arc, } async fn run_server( - server: Arc, + server: Arc, connector: ConnectorType, mut connector_receiver: mpsc::Receiver, disconnect_notifier: Arc, @@ -48,7 +45,7 @@ async fn run_server( { info!("Starting remote server loop"); let shared_connector = Arc::new(connector); - let server_receiver = server.client_version_event_stream(); + let server_receiver = server.event_stream(); pin_mut!(server_receiver); loop { select! { @@ -64,7 +61,7 @@ async fn run_server( async_manager::spawn(async move { if let Err(e) = client_message.is_valid() { error!("Message not valid: {:?} - Error: {}", client_message, e); - let mut err_msg = message::ErrorV0::from(ButtplugError::from(e)); + let mut err_msg = ErrorV0::from(ButtplugError::from(e)); err_msg.set_id(client_message.id()); let _ = connector_clone.send(ButtplugServerMessageVariant::V3(err_msg.into())).await; return; @@ -120,7 +117,7 @@ impl Default for ButtplugTestServer { impl ButtplugTestServer { pub fn new(server: ButtplugServer) -> Self { Self { - server: Arc::new(ButtplugServerDowngradeWrapper::new(server)), + server: Arc::new(server), disconnect_notifier: Arc::new(Notify::new()), } } @@ -159,7 +156,7 @@ impl ButtplugTestServer { } #[allow(dead_code)] - pub async fn shutdown(&self) -> Result<(), ButtplugError> { + pub async fn shutdown(self) -> Result<(), ButtplugError> { self.server.shutdown().await?; Ok(()) } diff --git a/crates/buttplug_transport_websocket_tungstenite/Cargo.toml b/crates/buttplug_transport_websocket_tungstenite/Cargo.toml new file mode 100644 index 000000000..035653666 --- /dev/null +++ b/crates/buttplug_transport_websocket_tungstenite/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "buttplug_transport_websocket_tungstenite" +version = "10.0.0" +authors = ["Nonpolynomial Labs, LLC "] +description = "Buttplug Intimate Hardware Control Library - Server Device Config Library" +license = "BSD-3-Clause" +homepage = "http://buttplug.io" +repository = "https://github.com/buttplugio/buttplug.git" +readme = "./README.md" +keywords = ["usb", "serial", "hardware", "bluetooth", "teledildonics"] +edition = "2024" +exclude = ["examples/**"] + +[lib] +name = "buttplug_transport_websocket_tungstenite" +path = "src/lib.rs" +test = true +doctest = true +doc = true + + +[dependencies] +buttplug_derive = "0.8.1" +buttplug_core = { path = "../buttplug_core" } +# buttplug_derive = { path = "../buttplug_derive" } +futures = "0.3.31" +futures-util = "0.3.31" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" +serde_repr = "0.1.20" +thiserror = "2.0.12" +displaydoc = "0.2.5" +dashmap = { version = "6.1.0", features = ["serde"] } +log = "0.4.27" +getset = "0.1.6" +jsonschema = { version = "0.30.0", default-features = false } +uuid = { version = "1.17.0", features = ["serde", "v4"] } +tokio-tungstenite = { version = "0.27.0", features = ["rustls-tls-webpki-roots", "url"]} +rustls = { version = "0.23.29", default-features = false, features = ["ring"]} +tokio = { version = "1.46.1", features = ["sync", "macros", "io-util"] } +tracing = "0.1.41" +url = "2.5.4" diff --git a/buttplug/src/core/connector/transport/websocket/mod.rs b/crates/buttplug_transport_websocket_tungstenite/src/lib.rs similarity index 95% rename from buttplug/src/core/connector/transport/websocket/mod.rs rename to crates/buttplug_transport_websocket_tungstenite/src/lib.rs index d9c9b8a01..e8b29df0b 100644 --- a/buttplug/src/core/connector/transport/websocket/mod.rs +++ b/crates/buttplug_transport_websocket_tungstenite/src/lib.rs @@ -7,6 +7,9 @@ //! Websocket connector for client/server communication +#[macro_use] +extern crate log; + pub mod websocket_client; pub mod websocket_server; diff --git a/buttplug/src/core/connector/transport/websocket/websocket_client.rs b/crates/buttplug_transport_websocket_tungstenite/src/websocket_client.rs similarity index 93% rename from buttplug/src/core/connector/transport/websocket/websocket_client.rs rename to crates/buttplug_transport_websocket_tungstenite/src/websocket_client.rs index befded5fd..33874e9e0 100644 --- a/buttplug/src/core/connector/transport/websocket/websocket_client.rs +++ b/crates/buttplug_transport_websocket_tungstenite/src/websocket_client.rs @@ -7,19 +7,17 @@ //! Handling of websockets using async-tungstenite -use crate::{ - core::{ - connector::{ - transport::{ - ButtplugConnectorTransport, - ButtplugConnectorTransportSpecificError, - ButtplugTransportIncomingMessage, - }, - ButtplugConnectorError, - ButtplugConnectorResultFuture, +use buttplug_core::{ + connector::{ + transport::{ + ButtplugConnectorTransport, + ButtplugConnectorTransportSpecificError, + ButtplugTransportIncomingMessage, }, - message::serializer::ButtplugSerializedMessage, + ButtplugConnectorError, + ButtplugConnectorResultFuture, }, + message::serializer::ButtplugSerializedMessage, util::async_manager, }; use futures::{future::BoxFuture, FutureExt, SinkExt, StreamExt}; @@ -29,9 +27,12 @@ use rustls::{ SignatureScheme, }; use std::sync::Arc; -use tokio::sync::{ - mpsc::{Receiver, Sender}, - Notify, +use tokio::{ + select, + sync::{ + mpsc::{Receiver, Sender}, + Notify, + }, }; use tokio_tungstenite::{ connect_async, @@ -186,7 +187,7 @@ impl ButtplugConnectorTransport for ButtplugWebsocketClientTransport { async move { loop { select! { - msg = outgoing_receiver.recv().fuse() => { + msg = outgoing_receiver.recv() => { if let Some(msg) = msg { let out_msg = match msg { ButtplugSerializedMessage::Text(text) => Message::Text(text.into()), @@ -207,7 +208,7 @@ impl ButtplugConnectorTransport for ButtplugWebsocketClientTransport { return; } }, - response = reader.next().fuse() => { + response = reader.next() => { trace!("Websocket receiving: {:?}", response); if response.is_none() { info!("Connector holding websocket dropped, returning"); @@ -267,7 +268,7 @@ impl ButtplugConnectorTransport for ButtplugWebsocketClientTransport { } } } - _ = disconnect_notifier.notified().fuse() => { + _ = disconnect_notifier.notified() => { // If we can't close, just print the error to the logs but // still break out of the loop. // @@ -291,9 +292,10 @@ impl ButtplugConnectorTransport for ButtplugWebsocketClientTransport { ); Ok(()) } - Err(websocket_error) => Err(ButtplugConnectorError::TransportSpecificError( - ButtplugConnectorTransportSpecificError::TungsteniteError(websocket_error), - )), + Err(err) => Err(ButtplugConnectorError::TransportSpecificError( + ButtplugConnectorTransportSpecificError::GenericNetworkError(err.to_string()) + ) +), } } .boxed() diff --git a/buttplug/src/core/connector/transport/websocket/websocket_server.rs b/crates/buttplug_transport_websocket_tungstenite/src/websocket_server.rs similarity index 95% rename from buttplug/src/core/connector/transport/websocket/websocket_server.rs rename to crates/buttplug_transport_websocket_tungstenite/src/websocket_server.rs index 7e7dcb073..61f2e4247 100644 --- a/buttplug/src/core/connector/transport/websocket/websocket_server.rs +++ b/crates/buttplug_transport_websocket_tungstenite/src/websocket_server.rs @@ -5,25 +5,24 @@ // Licensed under the BSD 3-Clause license. See LICENSE file in the project root // for full license information. -use crate::{ - core::{ - connector::{ - transport::{ - ButtplugConnectorTransport, - ButtplugConnectorTransportSpecificError, - ButtplugTransportIncomingMessage, - }, - ButtplugConnectorError, - ButtplugConnectorResultFuture, +use buttplug_core::{ + connector::{ + transport::{ + ButtplugConnectorTransport, + ButtplugConnectorTransportSpecificError, + ButtplugTransportIncomingMessage, }, - message::serializer::ButtplugSerializedMessage, + ButtplugConnectorError, + ButtplugConnectorResultFuture, }, + message::serializer::ButtplugSerializedMessage, util::async_manager, }; use futures::{future::BoxFuture, FutureExt, SinkExt, StreamExt}; use std::{sync::Arc, time::Duration}; use tokio::{ net::{TcpListener, TcpStream}, + select, sync::{ mpsc::{Receiver, Sender}, Notify, @@ -224,7 +223,7 @@ impl ButtplugConnectorTransport for ButtplugWebsocketServerTransport { debug!("Websocket: Socket bound."); let listener = try_socket.map_err(|e| { ButtplugConnectorError::TransportSpecificError( - ButtplugConnectorTransportSpecificError::GenericNetworkError(format!("{:?}", e)), + ButtplugConnectorTransportSpecificError::GenericNetworkError(format!("{e:?}")), ) })?; debug!("Websocket: Listening on: {}", addr); @@ -235,7 +234,7 @@ impl ButtplugConnectorTransport for ButtplugWebsocketServerTransport { .map_err(|err| { error!("Websocket server accept error: {:?}", err); ButtplugConnectorError::TransportSpecificError( - ButtplugConnectorTransportSpecificError::TungsteniteError(err), + ButtplugConnectorTransportSpecificError::GenericNetworkError(format!("{err:?}")), ) })?; async_manager::spawn(async move { diff --git a/crates/examples/.gitignore b/crates/examples/.gitignore new file mode 100644 index 000000000..eb5a316cb --- /dev/null +++ b/crates/examples/.gitignore @@ -0,0 +1 @@ +target diff --git a/crates/examples/Cargo.toml b/crates/examples/Cargo.toml new file mode 100644 index 000000000..10c288717 --- /dev/null +++ b/crates/examples/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "buttplug-examples" +version = "0.1.0" +authors = ["Melody Horn ", "Kyle Machulis "] +edition = "2024" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +buttplug_core = { path = "../buttplug_core" } +buttplug_client = { path = "../buttplug_client" } +buttplug_client_in_process = { path = "../buttplug_client_in_process" } +buttplug_server = { path = "../buttplug_server" } +buttplug_server_device_config = { path = "../buttplug_server_device_config" } +buttplug_server_hwmgr_btleplug = { path = "../buttplug_server_hwmgr_btleplug" } +buttplug_transport_websocket_tungstenite = { path = "../buttplug_transport_websocket_tungstenite" } +# buttplug = { path = "../../../buttplug/buttplug" } +anyhow = "1.0.98" +tracing-subscriber = "0.3.19" +futures = "0.3.31" +strum = "0.27.1" +tokio = { version = "1.46.1", features = ["io-std", "io-util", "rt-multi-thread", "macros"] } diff --git a/crates/examples/rustfmt.toml b/crates/examples/rustfmt.toml new file mode 100644 index 000000000..bcb01bef4 --- /dev/null +++ b/crates/examples/rustfmt.toml @@ -0,0 +1,3 @@ +tab_spaces = 2 +empty_item_single_line = false +imports_layout = "HorizontalVertical" \ No newline at end of file diff --git a/crates/examples/src/bin/async.rs b/crates/examples/src/bin/async.rs new file mode 100644 index 000000000..88a407c6b --- /dev/null +++ b/crates/examples/src/bin/async.rs @@ -0,0 +1,42 @@ +use buttplug_client::{connector::ButtplugRemoteClientConnector, serializer::ButtplugClientJSONSerializer, ButtplugClient, ButtplugClientEvent}; +use buttplug_transport_websocket_tungstenite::ButtplugWebsocketClientTransport; +use futures::StreamExt; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // In Rust, anything that will block is awaited. For instance, if we're going + // to connect to a remote server, that might take some time due to the network + // connection quality, or other issues. To deal with that, we use async/await. + // + // For now, you can ignore the API calls here, since we're just talking about + // how our API works in general. Setting up a connection is discussed more in + // the Connecting section of this document. + let connector = ButtplugRemoteClientConnector::< + ButtplugWebsocketClientTransport, + ButtplugClientJSONSerializer, + >::new(ButtplugWebsocketClientTransport::new_insecure_connector( + "ws://127.0.0.1:12345", + )); + + // For Request/Response messages, we'll use our Connect API. Connecting to a + // server requires the client and server to send information back and forth, + // so we'll await that while those (possibly somewhat slow, depending on if + // network is being used and other factors) transfers happen. + let client = ButtplugClient::new("Example Client"); + client + .connect(connector) + .await + .expect("Can't connect to Buttplug Server, exiting!"); + + let mut event_stream = client.event_stream(); + // As an example of event messages, we'll assume the server might + // send the client notifications about new devices that it has found. + // The client will let us know about this via events. + while let Some(event) = event_stream.next().await { + if let ButtplugClientEvent::DeviceAdded(device) = event { + println!("Device {} connected", device.name()); + } + } + + Ok(()) +} diff --git a/crates/examples/src/bin/connection.rs b/crates/examples/src/bin/connection.rs new file mode 100644 index 000000000..762805ea7 --- /dev/null +++ b/crates/examples/src/bin/connection.rs @@ -0,0 +1,73 @@ +use buttplug_client::{connector::ButtplugRemoteClientConnector, serializer::ButtplugClientJSONSerializer, ButtplugClient, ButtplugClientError}; + +use buttplug_core::errors::ButtplugError; +use buttplug_transport_websocket_tungstenite::ButtplugWebsocketClientTransport; +use tokio::io::{self, AsyncBufReadExt, BufReader}; + +async fn wait_for_input() { + BufReader::new(io::stdin()) + .lines() + .next_line() + .await + .unwrap(); +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // After you've created a connector, the connection looks the same no + // matter what, though the errors thrown may be different. + let connector = ButtplugRemoteClientConnector::< + ButtplugWebsocketClientTransport, + ButtplugClientJSONSerializer, + >::new(ButtplugWebsocketClientTransport::new_insecure_connector( + "ws://127.0.0.1:12345", + )); + + // Now we connect. If anything goes wrong here, we'll get an Err with either + // + // - A ButtplugClientConnectionError if there's a problem with + // the Connector, like the network address being wrong, server not + // being up, etc. + // - A ButtplugHandshakeError if there is a client/server version + // mismatch. + let client = ButtplugClient::new("Example Client"); + if let Err(e) = client.connect(connector).await { + match e { + ButtplugClientError::ButtplugConnectorError(error) => { + // If our connection failed, because the server wasn't turned on, + // SSL/TLS wasn't turned off, etc, we'll just print and exit + // here. + println!("Can't connect, exiting! Message: {}", error); + wait_for_input().await; + return Ok(()); + } + ButtplugClientError::ButtplugError(error) => match error { + ButtplugError::ButtplugHandshakeError(error) => { + // This means our client is newer than our server, and we need to + // upgrade the server we're connecting to. + println!("Handshake issue, exiting! Message: {}", error); + wait_for_input().await; + return Ok(()); + } + error => { + println!("Unexpected error type! {}", error); + wait_for_input().await; + return Ok(()); + } + }, + _ => { + // None of the other errors are valid in this instance. + } + } + }; + + // We're connected, yay! + println!("Connected! Check Server for Client Name."); + + wait_for_input().await; + + // And now we disconnect as usual + client.disconnect().await?; + + Ok(()) +} diff --git a/crates/examples/src/bin/device_control.rs b/crates/examples/src/bin/device_control.rs new file mode 100644 index 000000000..27516c35b --- /dev/null +++ b/crates/examples/src/bin/device_control.rs @@ -0,0 +1,115 @@ +use std::collections::HashMap; + +use buttplug_client::{connector::ButtplugRemoteClientConnector, serializer::ButtplugClientJSONSerializer, ButtplugClient, ButtplugClientError}; + +use buttplug_core::message::OutputType; +use buttplug_transport_websocket_tungstenite::ButtplugWebsocketClientTransport; +use strum::IntoEnumIterator; +use tokio::io::{self, AsyncBufReadExt, BufReader}; + +async fn wait_for_input() { + BufReader::new(io::stdin()) + .lines() + .next_line() + .await + .unwrap(); +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let connector = ButtplugRemoteClientConnector::< + ButtplugWebsocketClientTransport, + ButtplugClientJSONSerializer, + >::new(ButtplugWebsocketClientTransport::new_insecure_connector( + "ws://127.0.0.1:12345", + )); + + let client = ButtplugClient::new("Example Client"); + client.connect(connector).await?; + + println!("Connected!"); + + // You usually shouldn't run Start/Stop scanning back-to-back like + // this, but with TestDevice we know our device will be found when we + // call StartScanning, so we can get away with it. + client.start_scanning().await?; + client.stop_scanning().await?; + println!("Client currently knows about these devices:"); + for device in client.devices() { + println!("- {}", device.name()); + } + wait_for_input().await; + + for device in client.devices() { + println!("{} supports these outputs:", device.name()); + for output_type in OutputType::iter() { + for (_, feature) in device.device_features() { + for (output, _) in feature.feature().output().as_ref().unwrap_or(&HashMap::new()) { + if output_type == *output { + println!("- {}", output); + } + } + } + } + } + + println!("Sending commands"); + + // Now that we know the message types for our connected device, we + // can send a message over! Seeing as we want to stick with the + // modern generic messages, we'll go with VibrateCmd. + // + // There's a couple of ways to send this message. + let test_client_device = &client.devices()[0]; + + // We can use the convenience functions on ButtplugClientDevice to + // send the message. This version sets all of the motors on a + // vibrating device to the same speed. + test_client_device + .vibrate(10) + .await?; + + // If we wanted to just set one motor on and the other off, we could + // try this version that uses an array. It'll throw an exception if + // the array isn't the same size as the number of motors available as + // denoted by FeatureCount, though. + // + // You can get the vibrator count using the following code, though we + // know it's 2 so we don't really have to use it. + let vibrator_count = test_client_device + .vibrate_features() + .len(); + + println!( + "{} has {} vibrators.", + test_client_device.name(), + vibrator_count, + ); + + // Just set all of the vibrators to full speed. + if vibrator_count > 0 { + test_client_device + .vibrate(10) + .await?; + } else { + println!("Device does not have > 1 vibrators, not running multiple vibrator test."); + } + + wait_for_input().await; + println!("Disconnecting"); + // And now we disconnect as usual. + client.disconnect().await?; + println!("Trying error"); + // If we try to send a command to a device after the client has + // disconnected, we'll get an exception thrown. + let vibrate_result = test_client_device + .vibrate(30) + .await; + if let Err(ButtplugClientError::ButtplugConnectorError(error)) = vibrate_result { + println!("Tried to send after disconnection! Error: "); + println!("{}", error); + } + wait_for_input().await; + + Ok(()) +} diff --git a/crates/examples/src/bin/device_enumeration.rs b/crates/examples/src/bin/device_enumeration.rs new file mode 100644 index 000000000..36d85f421 --- /dev/null +++ b/crates/examples/src/bin/device_enumeration.rs @@ -0,0 +1,74 @@ +use buttplug_client::{connector::ButtplugRemoteClientConnector, serializer::ButtplugClientJSONSerializer, ButtplugClient, ButtplugClientEvent}; +use buttplug_transport_websocket_tungstenite::ButtplugWebsocketClientTransport; +use futures::StreamExt; +use tokio::io::{self, AsyncBufReadExt, BufReader}; + +async fn wait_for_input() { + BufReader::new(io::stdin()) + .lines() + .next_line() + .await + .unwrap(); +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Usual embedded connector setup. We'll assume the server found all + // of the subtype managers for us (the default features include all of them). + //let client = in_process_client("Example Client", false).await; + // To create a Websocket Connector, you need the websocket address and some generics fuckery. + let connector = ButtplugRemoteClientConnector::< + ButtplugWebsocketClientTransport, + ButtplugClientJSONSerializer, + >::new(ButtplugWebsocketClientTransport::new_insecure_connector( + "ws://127.0.0.1:12345", + )); + let client = ButtplugClient::new("Example Client"); + client.connect(connector).await?; + let mut events = client.event_stream(); + + // Set up our DeviceAdded/DeviceRemoved/ScanningFinished event handlers before connecting. + tokio::spawn(async move { + while let Some(event) = events.next().await { + match event { + ButtplugClientEvent::DeviceAdded(device) => { + println!("Device {} Connected!", device.name()); + } + ButtplugClientEvent::DeviceRemoved(info) => { + println!("Device {} Removed!", info.name()); + } + ButtplugClientEvent::ScanningFinished => { + println!("Device scanning is finished!"); + } + _ => {} + } + } + }); + + // We're connected, yay! + println!("Connected!"); + + // Now we can start scanning for devices, and any time a device is + // found, we should see the device name printed out. + client.start_scanning().await?; + wait_for_input().await; + + // Some Subtype Managers will scan until we still them to stop, so + // let's stop them now. + client.stop_scanning().await?; + wait_for_input().await; + + // Since we've scanned, the client holds information about devices it + // knows about for us. These devices can be accessed with the Devices + // getter on the client. + println!("Client currently knows about these devices:"); + for device in client.devices() { + println!("- {}", device.name()); + } + wait_for_input().await; + + // And now we disconnect as usual. + client.disconnect().await?; + + Ok(()) +} diff --git a/crates/examples/src/bin/embedded_connector.rs b/crates/examples/src/bin/embedded_connector.rs new file mode 100644 index 000000000..a396ea668 --- /dev/null +++ b/crates/examples/src/bin/embedded_connector.rs @@ -0,0 +1,37 @@ +use buttplug_client::ButtplugClient; +use buttplug_server::{device::ServerDeviceManagerBuilder, ButtplugServerBuilder}; +use buttplug_server_device_config::DeviceConfigurationManagerBuilder; +use buttplug_server_hwmgr_btleplug::BtlePlugCommunicationManagerBuilder; +use buttplug_client_in_process::{ButtplugInProcessClientConnectorBuilder, in_process_client}; + +#[allow(dead_code)] +async fn main_the_hard_way() -> anyhow::Result<()> { + let dcm = DeviceConfigurationManagerBuilder::default() + .finish() + .unwrap(); + + let mut device_manager_builder = ServerDeviceManagerBuilder::new(dcm); + device_manager_builder.comm_manager(BtlePlugCommunicationManagerBuilder::default()); + + // This is how we add Bluetooth manually. (We could also do this with any other communication manager.) + + let server = ButtplugServerBuilder::new(device_manager_builder.finish().unwrap()).finish().unwrap(); + + // First off, we'll set up our Embedded Connector. + let connector = ButtplugInProcessClientConnectorBuilder::default() + .server(server) + .finish(); + + let client = ButtplugClient::new("Example Client"); + client.connect(connector).await?; + + Ok(()) +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // This is the easy way, it sets up an embedded server with everything set up automatically + let _client = in_process_client("Example Client").await; + + Ok(()) +} diff --git a/crates/examples/src/bin/errors.rs b/crates/examples/src/bin/errors.rs new file mode 100644 index 000000000..2b51f4b41 --- /dev/null +++ b/crates/examples/src/bin/errors.rs @@ -0,0 +1,21 @@ +use buttplug_client::ButtplugClientError; +use buttplug_core::errors::ButtplugError; + +#[allow(dead_code)] +fn handle_error(error: ButtplugClientError) { + match error { + ButtplugClientError::ButtplugConnectorError(_details) => {} + ButtplugClientError::ButtplugError(error) => match error { + ButtplugError::ButtplugHandshakeError(_details) => {} + ButtplugError::ButtplugDeviceError(_details) => {} + ButtplugError::ButtplugMessageError(_details) => {} + ButtplugError::ButtplugPingError(_details) => {} + ButtplugError::ButtplugUnknownError(_details) => {} + }, + ButtplugClientError::ButtplugOutputCommandConversionError(_details) => {} + } +} + +fn main() { + // nothing to do here +} diff --git a/crates/examples/src/bin/external_connector.rs b/crates/examples/src/bin/external_connector.rs new file mode 100644 index 000000000..3156e153a --- /dev/null +++ b/crates/examples/src/bin/external_connector.rs @@ -0,0 +1,17 @@ +use buttplug_client::{connector::ButtplugRemoteClientConnector, serializer::ButtplugClientJSONSerializer, ButtplugClient, ButtplugClientEvent}; +use buttplug_transport_websocket_tungstenite::ButtplugWebsocketClientTransport; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // To create a Websocket Connector, you need the websocket address and some generics fuckery. + let connector = ButtplugRemoteClientConnector::< + ButtplugWebsocketClientTransport, + ButtplugClientJSONSerializer, + >::new(ButtplugWebsocketClientTransport::new_insecure_connector( + "ws://127.0.0.1:12345", + )); + let client = ButtplugClient::new("Example Client"); + client.connect(connector).await?; + + Ok(()) +} diff --git a/crates/examples/src/bin/logging.rs b/crates/examples/src/bin/logging.rs new file mode 100644 index 000000000..804b36600 --- /dev/null +++ b/crates/examples/src/bin/logging.rs @@ -0,0 +1,14 @@ +use buttplug_client_in_process::in_process_client; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Run this to turn on the environment logger. Running this more than once will panic. + tracing_subscriber::fmt::init(); + + // Now when you connect here, if you've set the RUST_LOG environment variable + // (set it to "Info" or "Debug"), you should see messages about connection + // setup. + let _client = in_process_client("Example Client").await; + + Ok(()) +} diff --git a/crates/intiface_engine/.github/workflows/cache_version b/crates/intiface_engine/.github/workflows/cache_version new file mode 100644 index 000000000..e440e5c84 --- /dev/null +++ b/crates/intiface_engine/.github/workflows/cache_version @@ -0,0 +1 @@ +3 \ No newline at end of file diff --git a/crates/intiface_engine/.github/workflows/rust.yml b/crates/intiface_engine/.github/workflows/rust.yml new file mode 100644 index 000000000..be7cc906e --- /dev/null +++ b/crates/intiface_engine/.github/workflows/rust.yml @@ -0,0 +1,154 @@ +name: Intiface Engine Build + +on: + push: + branches: + - main + - dev + - ci + +concurrency: + group: ${{ github.head_ref || github.ref }} + cancel-in-progress: true + +jobs: + build-stable: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - uses: actions/checkout@v2 + - name: Fix ~/.cargo directory permissions + if: startsWith(matrix.os, 'ubuntu') || startsWith(matrix.os, 'macos') + run: sudo chown -R $(whoami):$(id -ng) ~/.cargo/ + - name: Update package list + if: startsWith(matrix.os, 'ubuntu') + run: sudo apt-get -y update + - name: Install required packages + if: startsWith(matrix.os, 'ubuntu') + run: sudo apt-get -y install libudev-dev libusb-1.0-0-dev libdbus-1-dev + - name: Cache cargo registry + uses: actions/cache@v1 + with: + path: ~/.cargo/registry + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('.github/workflows/cache_version') }} + - name: Cache cargo build + uses: actions/cache@v1 + with: + path: target + key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('.github/workflows/cache_version') }} + - name: Rust toolchain fetch + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + override: true + components: rustfmt, clippy + - name: Formatting check + continue-on-error: true + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + - name: Build Release + run: cargo build --release + - name: Copy executable (Linux, MacOS) + if: startsWith(matrix.os, 'ubuntu') || startsWith(matrix.os, 'macos') + run: | + mkdir ci-output-release + cp target/release/intiface-engine ci-output-release/intiface-engine + - name: Copy executable (Windows) + if: startsWith(matrix.os, 'windows') + run: | + mkdir ci-output-release + copy target\release\intiface-engine.exe ci-output-release\intiface-engine.exe + - name: Upload artifacts (release) + uses: actions/upload-artifact@v4 + with: + name: intiface-engine-${{ runner.os }}-release + path: ci-output-release + build-v4: + runs-on: windows-latest + steps: + - uses: actions/checkout@v2 + - name: Cache cargo registry + uses: actions/cache@v1 + with: + path: ~/.cargo/registry + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('.github/workflows/cache_version') }} + - name: Cache cargo build + uses: actions/cache@v1 + with: + path: target + key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('.github/workflows/cache_version') }} + - name: Rust toolchain fetch + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + override: true + components: rustfmt, clippy + - name: Formatting check + continue-on-error: true + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + - name: Build Release + run: cargo build --release + - name: Copy executable (Windows) + run: | + mkdir ci-output-release + copy target\release\intiface-engine.exe ci-output-release\intiface-engine.exe + - name: Upload artifacts (release) + uses: actions/upload-artifact@v4 + with: + name: intiface-engine-${{ runner.os }}-unstable-v4-release + path: ci-output-release + release: + name: Release artifacts + needs: + - build-stable + - build-v4 + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v') + steps: + - uses: actions/checkout@v2 + - name: Download Artifact (Linux) + uses: actions/download-artifact@v4 + with: + name: intiface-engine-Linux-release + - name: Download Artifact (Windows) + uses: actions/download-artifact@v4 + with: + name: intiface-engine-Windows-release + - name: Download Artifact (Windows) (v4 Unstable) + uses: actions/download-artifact@v4 + with: + name: intiface-engine-Windows-unstable-v4-release + - name: Download Artifact (MacOS) + uses: actions/download-artifact@v4 + with: + name: intiface-engine-macOS-release + - name: Zip executables + # This follows the naming convention from C# and JS. Use -j to junk the + # directory structure. + run: | + zip -j intiface-engine-linux-x64-Release.zip intiface-engine-Linux-release/intiface-engine README.md CHANGELOG.md + zip -j intiface-engine-win-x64-Release.zip intiface-engine-Windows-release/intiface-engine.exe README.md CHANGELOG.md + zip -j intiface-engine-win-x64-unstable-v4-Release.zip intiface-engine-Windows-unstable-v4-release/intiface-engine.exe README.md CHANGELOG.md + zip -j intiface-engine-macos-x64-Release.zip intiface-engine-macOS-release/intiface-engine README.md CHANGELOG.md Info.plist + - name: Release + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') + with: + files: | + intiface-engine-linux-x64-Release.zip + intiface-engine-win-x64-Release.zip + intiface-engine-win-x64-unstable-v4-Release.zip + intiface-engine-macos-x64-Release.zip + README.md + CHANGELOG.md + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/crates/intiface_engine/.gitignore b/crates/intiface_engine/.gitignore new file mode 100644 index 000000000..793b1186b --- /dev/null +++ b/crates/intiface_engine/.gitignore @@ -0,0 +1,4 @@ +/target + +# IDE specific files +/.idea \ No newline at end of file diff --git a/crates/intiface_engine/CHANGELOG.md b/crates/intiface_engine/CHANGELOG.md new file mode 100644 index 000000000..136ce545d --- /dev/null +++ b/crates/intiface_engine/CHANGELOG.md @@ -0,0 +1,766 @@ +# Intiface Engine v3.0.8 (2025/04/20) + +- Update to Buttplug v9.0.8 + - Lots of new device support + - Bug fixes for serial ports, device limits + +# Intiface Engine v3.0.7 (2024/12/23) + +## Features + +- Update to Buttplug v9.0.7 + - Lots of new device support + - Lovense devices with changed names now still connect + +# Intiface Engine v3.0.6 (2024/12/23) + +## Features + +- Update to Buttplug v9.0.6 + - Lovense Gush 2/Osci 3 device support + +# Intiface Engine v3.0.5 (2024/12/21) + +## Features + +- Update to Buttplug v9.0.5 + - Many devices additions + +# Intiface Engine v3.0.4 (2024/10/06) + +## Features + +- Update to Buttplug v9.0.4 + - Lovense Solace Pro linear movement support + - Lovense Solace (non-pro) fixes + - Some device additions + +# Intiface Engine v3.0.3 (2024/09/29) + +## Features + +- Update to Buttplug v9.0.2 + - Lots of device additions, look at the Buttplug changelog for more info + +# Intiface Engine v3.0.2 (2024/09/02) + +## Bugfixes + +- Update to Buttplug v9.0.1 + - Fixes bug with messages IDs sometimes not getting set + +# Intiface Engine v3.0.1 (2024/09/01) + +## Features + +- Update to Buttplug v9.0.0 + - Starting the Message Spec v4 development line + - There is now a "allow-unstable-v4-connections" feature that will allow for testing throughout + v4 development. Will be removed when Buttplug v10/Message Spec v4 is released. + - Lots of device support for like 10 different brands. It's been more than 3 months! +- Rebuild server backdoor system to just be another Buttplug Server instead of exposing Device + Manager + - The thing I said I'd never do! + +## Bugfixes + +- Automatically prepend ws:// to repeater addresses if they don't have it already. + +# Intiface Engine v3.0.0 (2024/05/12) + +## Breaking Changes + +- Device Config File Compatibility + - This update moves to Buttplug v8, which changes our config file capabilities and adds extra API + calls for config file updates. + +## Features + +- Update to Buttplug v8.0.0 + - Rewrite of the device config system + - Lots of device support for JoyHub, Svakom, LoveDistance, etc... + - Some backward compat bugfixes + +# Intiface Engine v2.0.4 (2024/04/20) + +## Features + +- Update to Buttplug v7.1.16 + - Lots of device support for JoyHub, Kiiroo, Lioness + - Fix Lovense Solace issues + +# Intiface Engine v2.0.3 (2024/03/17) + +## Features + +- Update to Buttplug v7.1.15 + - Fix panics that can happen on shutdown in lovense dongle + +# Intiface Engine v2.0.2 (2024/03/16) + +## Features + +- Update to Buttplug v7.1.14 + - Added more device support (see Buttplug CHANGELOG) + +# Intiface Engine v2.0.1 (2024/01/27) + +## Features + +- Update to Buttplug v7.1.13 + - Added more device support (see Buttplug CHANGELOG) + +# Intiface Engine v2.0.0 (2024/01/21) + +## Breaking Changes + +- Removed sentry/crash reporting + - This is now a library AND a CLI. If someone is using the CLI, they're using it in their own + setup they can wrap it in whatever crash reporter they want. Moving crash reporting up to Intiface Central. +- Removed logging for library instances + - Intiface was originally built as a CLI and meant to be run only as such. Now that it's a CLI and + a library, we need to let applications handle their own logging. The CLI build still has logging features, but library now just exposes a log/tracing interface. +- Removed Websocket Frontend + - This was used when we were letting other programs run the CLI. Now that there's a library mode, + we expect applications to just attach directly. This makes things more secure overall, and if users want it back, they can implement their own frontend using the trait. +- All above changes will mostly be reflected externally in either missing CLI arguments, or updates + to the EngineOptions struct. +- The v2 line may be fairly short, as the engine will once again have a major revision once Buttplug + moves to its new spec and therefore new major revision. + +## Features + +- Update to Buttplug v7.1.12 + - Massive number of hardware support updates/bugfixes, just go look at the CHANGELOG + - Fixes bugs with streaming JSON +- Moved to tokio-tungstenite + - Matched move made by Buttplug +- Implemented repeater mode (Basic websocket proxy) + - Mostly needed for reflecting desktop browser apps to phone control + +# Intiface Engine v1.4.10 (2023/11/18) + +## Bugfixes + +- Update to Buttplug v7.1.11 + - Fixed btleplug compilation issue on macOS + +# Intiface Engine v1.4.9 (2023/11/18) + +## Features + +- Update to Buttplug v7.1.10 + - Fixes issues with invalid bluetooth names on Android + +# Intiface Engine v1.4.8 (2023/11/16) + +## Features + +- Update to Buttplug v7.1.9 + - Added Lovense Solace, OhMiBod Foxy, Chill support + +# Intiface Engine v1.4.7 (2023/11/04) + +## Features + +- Allow logging to use environment variables for setup over command line prefs +- Update to Buttplug v7.1.8 + - Add lovense device support + - Fix some device support issues + +# Intiface Engine v1.4.6 (2023/10/19) + +## Features + +- Update to Buttplug v7.1.7 + - Fixes memory leak in mDNS handling + - Defaults to device keepalive being on when compiling for iOS + +# Intiface Engine v1.4.5 (2023/10/08) + +## Features + +- Update to Buttplug v7.1.6 + - Fixes Lovense Dongle support + - Added Foreo device support + +# Intiface Engine v1.4.4 (2023/10/05) + +## Bugfixes + +- Make mDNS actually work in all cases (but it's still considered experimental) +- Fix compilation issues for android + +# Intiface Engine v1.4.3 (2023/10/04) + +## Features + +- Update to Buttplug v7.1.5 + - Lots of device additions, HID device manager for Joycons +- Add mDNS broadcast capabilities + +# Intiface Engine v1.4.2 (2023/07/16) + +## Features + +- Update to Buttplug v7.1.2 + - Device additions for Magic Motion, Lovense Connect bugfix + +# Intiface Engine v1.4.1 (2023/07/09) + +## Features + +- Update to Buttplug v7.1.1 + - Mostly device additions/updates + +# Intiface Engine v1.4.0 (2023/05/21) + +## Features + +- Update to Buttplug v7.1.0 + - Mostly device additions/updates + - Some fixes for user configs +- Move ButtplugRemoteServer into Intiface Engine + - Gives us more flexibility to change things in development +- Updates for user device config updates via Buttplug + +# Intiface Engine v1.3.0 (2023/02/19) + +## Features + +- Added Websocket Client argument for running the engine as a websocket client instead of a server +- Update to Buttplug v7.0.2 + - Hardware protocols updates for Kizuna/Svakom/Sakuraneko + +# Intiface Engine v1.2.2 (2023/01/30) + +## Bugfixes + +- Fix timing issue on sending EngineStopped message on exit + +# Intiface Engine v1.2.1 (2023/01/16) + +## Features + +- Update to Buttplug v7.0.1 + - Hardware protocol updates/fixed, see Buttplug CHANGELOG for more info. + +# Intiface Engine v1.2.0 (2023/01/01) + +## Features + +- Update to Buttplug v7.0.0 + - Major version move because of API breakage. + - Mostly bugfixes otherwise. + - Removes IPC Pipes, so removed them in Intiface Engine too. + +# Intiface Engine v1.1.0 (2022/12/19) + +## Features + +- Update to Buttplug v6.3.0 + - Lots of device additions + - Major bugfixes for WeVibe/Satisfyer/Magic Motion and Lovense Connect + +# Intiface Engine v1.0.5 (2022/11/27) + +## Bugfixes + +- Update to Buttplug v6.2.2 + - Fixes issues with platform dependencies and DCMs + - Fixes error message in common path in CoreBluetooth + - Stops devices when server disconnects + +# Intiface Engine v1.0.4 (2022/11/24) + +## Features + +- Update to Buttplug v6.2.1 +- Add optional tokio_console feature for task debugging +- Remove crash reporting for now + - Needs to be updated, more testing, etc... + +# Intiface Engine v1.0.3 (2022/11/05) + +## Features + +- Implemented BackdoorServer, which allows access to server devices directly, while still allowing a + client to access them simultaneously. Can't possibly see how this could go wrong. +- Added EngineServerCreated Event for IntifaceCentral to know when to bring up the BackdoorServer. + +## Bugfixes + +- Fixed issue where logging could stay alive through multiple server bringups when run in process. + +# Intiface Engine v1.0.2 (2022/10/18) + +## Bugfixes + +- Vergen should not block building as a library dependency + +# Intiface Engine v1.0.1 (2022/10/15) + +## Features + +- Update to Buttplug v6.1.0 + - Mostly bugfixes + - Now requires v2.x device config files + +# Intiface Engine v1.0.0 (2022/10/01) + +## Breaking Changes + +- Rebuilt command line arguments + - Now in kebab case format + - ALL DCMs require --use statements, there are no default DCMs anymore +- Incorporates changes made during the egui betas. +- The `--stay_open` argument is now assumed. The server will run until either Ctrl-C is pressed or + an IPC stop message is received. + +## Features + +- Intiface Engine is now compiled as both a CLI (for desktop) and a Library (for mobile). +- Updated to Buttplug v6 +- Moved to semantic versioning, major version denotes CLI argument or breaking IPC protocol change. + +# v101 (egui Beta 2) (2021/01/25) + +- Add websocket device server port selection + +# v100 (egui Beta 1) (2021/01/04) + +## Features + +- Use JSON over named pipes instead of protobufs over stdio +- Add sentry crash logging +- Server version now uses a shorter tag +- Update to Rust 2021 + +# v50 (2022/04/26) - Last version of Intiface CLI + +## Features + +- Update to Buttplug v5.1.9 + - Add Magic Motion Crystal support + - Fix issues with Satisfyer Plugalicious 2 connections + - Fix issues with Satisfyer device identification + +# v49 (2022/03/05) + +## Features + +- Update to Buttplug v5.1.8 + - Added Lelo F1s v2 support, more support for Mannuo/Magic Motion/OhMiBod devices + - May fix issues with windows bluetooth on older Win 10 versions + +# v48 (2021/01/24) + +## Features + +- Update to Buttplug v5.1.7 + - Lovense Calor support, Folove support, more WeVibe/Satisfyer support + +# v47 (2022/01/04) + +## Bugfixes + +- No changes to build, re-release to fix issue with a wrong tag getting pushed. + +# v46 (2022/01/01) + +## Bugfixes + +- Update to Buttplug v5.1.6 + - Fix issues with serial ports blocking, lovense connect data types, log message levels, etc... + - See Buttplug v5.1.6 changelog for more info. + (https://github.com/buttplugio/buttplug/blob/master/buttplug/CHANGELOG.md) + +# v45 (2021/12/19) + +## Bugfixes + +- Update to Buttplug v5.1.5 + - Fix issues with Satisfyer name detection and disconnection + - Fix issues with device scanning always saying it's instantly finished + +# v44 (2021/12/14) + +## Bugfixes + +- Update to Buttplug v5.1.4 + - Shouldn't change anything in here, all the fixes were FFI related, but eh. +- Try to get crash logs into frontend log output for easier debugging +- #14: Fix issue with intiface-cli not sending events to desktop after first disconnection + +# v43 (2021/12/04) + +## Bugfixes + +- Update to Buttplug v5.1.2 + - Fix race condition with bluetooth advertisements causing multiple simultaneous connects to + devices +- Update to vergen 5.2.0 + - Last version was yanked + +# v42 (2021/12/03) + +## Bugfixes + +- Update to Buttplug v5.1.1 + - Fix issues with devices w/ advertised services being ignored + - Fix issues with lovense dongle on linux + +# v41 (2021/12/02) + +## Features + +- Update to Buttplug v5.1 + - Bluetooth library updates + - Satisfyer/ManNuo/other device support (see Buttplug README) + - Lots of other fixes +- Update to vergen v5, tracing-subscriber v0.3 + +# v40 (2021/09/14) + +## Features + +- Update to Buttplug v5.0.1 + - Better MacOS bluetooth support + - Better Linux bluetooth support + - Tons of device additions (see Buttplug README) + - Adds websocket device interface + +# v39 (2021/07/05) + +## Features + +- Server now throws warnings whenever a client tries to connect when another client is already + connected. +- Update to Buttplug 4.0.4 + - Added hardware support for TCode devices, Patoo, Vorze Piston SA + +## Bugfixes + +- Fix cancellation of tasks on shutdown. + +# v38 (2021/06/18) + +## Bugfixes + +- Update to buttplug-rs 4.0.3, which fixes issues with Android phones using the Lovense Connect app. + +# v37 (2021/06/11) + +## Bugfixes + +- Fix timing issue where Process Ended message may not be seen by Intiface Desktop +- Update to buttplug-rs 4.0.2, fixing issue with Intiface Desktop stalling due to logging issues. +- Add Info.plist file for macOS Big Sur and later compat + +# v36 (2021/06/10) + +## Features + +- Added opt-in/out arguments for all available device communication managers +- Added support for Lovense Connect Service + +# v35 (2021/04/04) + +## Bugfixes + +- Update to Buttplug v2.1.9 + - Reduces error log messages thrown by lovense dongle + - Reduces panics in bluetooth handling on windows + - Fixes issue with battery checking on lovense devices stalling library on device disconnect + +# v34 (2021/03/25) + +## Bugfixes + +- Update to Buttplug v2.1.8 + - Possibly fixes issue with bluetooth devices not registering disconnection on windows. + +# v33 (2021/03/08) + +## Bugfixes + +- Update to Buttplug v2.1.7 + - Fixes legacy message issues with The Handy and Vorze toys + - Fixes init issues with some Kiiroo vibrators + +# v32 (2021/02/28) + +## Bugfixes + +- Update to Buttplug v2.1.6 + - Fixes issues with log message spamming + - Update btleplug to 0.7.0, lots of cleanup + +# v31 (2021/02/20) + +## Bugfixes + +- Update to Buttplug v2.1.5 + - Fixes panic in devices that disconnect during initialize(). + +# v30 (2021/02/13) + +## Features + +- Update to Buttplug v2.1.4 +- Added Hardware Support + - The Handy + +## Bugfixes + +- Fixes issues with the LoveAi Dolp and Lovense Serial Dongle + +# v29 (2021/02/06) + +## Bugfixes + +- Update to Buttplug v2.1.3 + - Fix StopAllDevices so it actually stops all devices again + - Allow for setting device intensity to 1.0 + +# v28 (2021/02/06) + +## Features + +- Update to Buttplug v2.1.1 + - Adds Lovense Diamo and Nobra's Silicone Dreams support + - Lots of bugfixes and more/better errors being emitted + +# v27 (2021/01/24) + +## Bugfixes + +- Update to Buttplug 2.0.5 + - Fixes issue with v2 protocol conflicts in DeviceMessageInfo + +# v26 (2021/01/24) + +## Bugfixes + +- Update to Buttplug 2.0.4 + - Fixes issue with XInput devices being misaddressed and stopping all scanning. + +# v25 (2021/01/19) + +## Bugfixes + +- Update to Buttplug 2.0.2 + - Fixes issue with scanning status getting stuck on Lovense dongles + +# v24 (Yanked) (2021/01/18) + +## Features + +- Update to Buttplug 2.0.1 + - Event system and API cleanup + - Lovense Ferri Support +- Backtraces now emitted via logging system when using frontend IPC + +# v23 (2021/01/01) + +## Bugfixes + +- Update to Buttplug 1.0.4 + - Fixes issues with XInput Gamepads causing intiface-cli-rs crashes on reconnect. + +# v22 (2021/01/01) + +## Bugfixes + +- Update to Buttplug 1.0.3 + - Fixes issues with BTLE advertisements and adds XInput device rescanning. + +# v21 (2020/12/31) + +## Bugfixes + +- Update to Buttplug 1.0.1 + - Fixes issue with device scanning races. + +# v20 (2020/12/22) + +## Bugfixes + +- Update to Buttplug 0.11.3 + - Fixes security issues and a memory leak when scanning is called often. + +# v19 (2020/12/11) + +## Bugfixes + +- Update to Buttplug 0.11.2 + - Emits Scanningfinished when scanning is finished. Finally. + +# v18 (2020/11/27) + +## Features + +- Update to buttplug-rs 0.11.1 + - System bugfixes + - Mysteryvibe support + +# v17 (2020/10/25) + +## Features + +- Update to buttplug-rs 0.10.1 + - Lovense Dongle Bugfixes + - BLE Toy Connection Bugfixes +- Fix logging output + - Pay attention to log option on command line again + - Outputs full tracing JSON to frontend + +# v16 (2020/10/17) + +## Features + +- Update to buttplug-rs 0.10.0 + - Kiiroo Keon Support + - New raw device commands (use --allowraw option for access) + +## Bugfixes + +- Update to buttplug-rs 0.10.0 + - Lots of websocket crash fixes + +# v15 (2020/10/05) + +## Bugfixes + +- Update to buttplug-rs 0.9.2 w/ btleplug 0.5.4, fixing an issue with macOS + panicing whenever it tries to read from a BLE device. + +# v14 (2020/10/05) + +## Bugfixes + +- Update to buttplug-rs 0.9.1 w/ btleplug 0.5.3, fixing an issue with macOS + panicing whenever it tries to write to a BLE device. + +# v13 (2020/10/04) + +## Features + +- Update to buttplug-rs 0.9.0, which now has Battery level reading capabilites + for some hardware. + +## Bugfixes + +- Update to buttplug-rs 0.9.0, which now does not crash when 2 devices are + connected and one disconnects. + +# v12 (2020/10/02) + +## Features + +- Update to Buttplug-rs 0.8.4, fixing a bunch of device issues. +- Default to outputting info level logs if no env log var set. (Should pick this + up from command line argument in future version) + +## Bugfixes + +- Only run for one connection attempt if --stayopen isn't passed in. + +# v11 (2020/09/20) + +## Bugfixes + +- Moves to buttplug-0.8.3, which fixes support for some programs using older + APIs (FleshlightLaunchFW12Cmd) for Kiiroo stroking products (Onyx, Fleshlight + Launch, etc). + +# v10 (2020/09/13) + +## Features + +- Added log handling from Buttplug library. Still needs protocol/CLI setting, + currently outputs everything INFO or higher. + +## Bugfixes + +- Moves to buttplug-0.8.2, fixing Lovense rotation and adding log output + support. + +# v9 (2020/09/11) + +## Bugfixes + +- Moves to buttplug-0.7.3, which loads both RSA and pkcs8 certificates. This + allows us to load the certs that come from Intiface Desktop. + +# v8 (2020/09/07) + +## Bugfixes + +- Move to buttplug-rs 0.7.2, which adds more device configurations and fixes + websocket listening on all interfaces. + +# v7 (2020/09/06) + +## Features + +- Move to buttplug-rs 0.7.1, which includes status emitting features and way + more device protocol support. +- Allow frontend to trigger process stop +- Send disconnect to frontend when client disconnects +- Can now relay connected/disconnected devices to GUIs via PBuf protocol + +# v6 (2020/08/06) + +## Features + +- Move to buttplug-rs 0.6.0, which integrates websockets and server lifetime + handling. intiface-cli-rs is now a very thin wrapper around buttplug-rs, + handling system bringup and frontend communication and that's about it. + +# v5 (2020/05/13) + +## Bugfixes + +- Move to buttplug-rs 0.3.1, with a couple of unwrap fixes + +# v4 (2020/05/10) + +## Features + +- --stayopen option now actually works, reusing the server between + client connections. + +# v3 (2020/05/09) + +## Features + +- Added protobuf basis for hooking CLI into Intiface Desktop + +## Bugfixes + +- Fixed bug where receiving ping message from async_tungstenite would + panic server +- Update to buttplug 0.2.4, which fixes ServerInfo message ID matching + +# v2 (2020/02/15) + +## Features + +- Move to using rolling versioning, since this is a binary +- Move to using buttplug 0.2, with full server implementation +- Add cert generation +- Add secure websocket capabilities +- Move to using async-tungstenite +- Use Buttplug's built in JSONWrapper +- Add XInput capability on windows +- Add CI building +- Add Simple GUI message output for Intiface Desktop + +# v1 (aka v0.0.1) (2020/02/15) + +## Features + +- First version +- Can bring up insecure websocket, run server, access toys +- Most options not used yet diff --git a/crates/intiface_engine/Cargo.toml b/crates/intiface_engine/Cargo.toml new file mode 100644 index 000000000..740e75e59 --- /dev/null +++ b/crates/intiface_engine/Cargo.toml @@ -0,0 +1,70 @@ +[package] +name = "intiface-engine" +version = "3.0.8" +authors = ["Nonpolynomial Labs, LLC "] +description = "CLI and Library frontend for the Buttplug sex toy control library" +license = "BSD-3-Clause" +homepage = "http://intiface.com" +repository = "https://github.com/intiface/intiface-engine.git" +readme = "README.md" +keywords = ["usb", "serial", "hardware", "bluetooth", "teledildonics"] +edition = "2024" +exclude = [".vscode/**"] + +[lib] +name = "intiface_engine" +path = "src/lib.rs" + +[[bin]] +name = "intiface-engine" +path = "src/bin/main.rs" + +[features] +default=[] +tokio-console=["console-subscriber"] + +[dependencies] +buttplug_core = { path = "../buttplug_core" } +buttplug_server = { path = "../buttplug_server" } +buttplug_server_device_config = { path = "../buttplug_server_device_config" } +buttplug_server_hwmgr_btleplug = { path = "../buttplug_server_hwmgr_btleplug" } +buttplug_server_hwmgr_hid = { path = "../buttplug_server_hwmgr_hid" } +buttplug_server_hwmgr_lovense_connect = { path = "../buttplug_server_hwmgr_lovense_connect" } +buttplug_server_hwmgr_lovense_dongle = { path = "../buttplug_server_hwmgr_lovense_dongle" } +buttplug_server_hwmgr_serial = { path = "../buttplug_server_hwmgr_serial" } +buttplug_server_hwmgr_websocket = { path = "../buttplug_server_hwmgr_websocket" } +buttplug_server_hwmgr_xinput = { path = "../buttplug_server_hwmgr_xinput" } +buttplug_transport_websocket_tungstenite = { path = "../buttplug_transport_websocket_tungstenite" } +buttplug_server_hwmgr_udp = { path = "../buttplug_server_hwmgr_udp" } +# buttplug = "9.0.8" +argh = "0.1.13" +log = "0.4.27" +futures = "0.3.31" +tracing-fmt = "0.1.1" +tracing-subscriber = { version = "0.3.19", features = ["env-filter", "json"] } +tracing = "0.1.41" +tokio = { version = "1.46.1", features = ["sync", "rt-multi-thread", "macros", "io-std", "fs", "signal", "io-util"] } +log-panics = { version = "2.1.0", features = ["with-backtrace"] } +backtrace = "0.3.75" +ctrlc = "3.4.7" +tokio-util = "0.7.15" +serde = "1.0.219" +serde_json = "1.0.140" +thiserror = "2.0.12" +getset = "0.1.6" +async-trait = "0.1.88" +once_cell = "1.21.3" +lazy_static = "1.5.0" +console-subscriber = { version="0.4.1", optional = true } +local-ip-address = "0.6.5" +rand = "0.9.1" +tokio-tungstenite = "0.27.0" +futures-util = "0.3.31" +url = "2.5.4" +libmdns = "0.9.1" +tokio-stream = "0.1.17" +dashmap = "6.1.0" + +[build-dependencies] +vergen-gitcl = {version = "1.0.8", features = ["build"]} +anyhow = "1.0.98" diff --git a/crates/intiface_engine/README.md b/crates/intiface_engine/README.md new file mode 100644 index 000000000..7270ee322 --- /dev/null +++ b/crates/intiface_engine/README.md @@ -0,0 +1,106 @@ +# Intiface Engine + +[![Patreon donate button](https://img.shields.io/badge/patreon-donate-yellow.svg)](https://www.patreon.com/qdot) +[![Github donate button](https://img.shields.io/badge/github-donate-ff69b4.svg)](https://www.github.com/sponsors/qdot) +[![Discourse Forums](https://img.shields.io/discourse/status?label=buttplug.io%20forums&server=https%3A%2F%2Fdiscuss.buttplug.io)](https://discuss.buttplug.io) +[![Discord](https://img.shields.io/discord/353303527587708932.svg?logo=discord)](https://discord.buttplug.io) +[![Twitter](https://img.shields.io/twitter/follow/buttplugio.svg?style=social&logo=twitter)](https://twitter.com/buttplugio) + +![Intiface Engine Build](https://github.com/intiface/intiface-engine/workflows/Intiface%20Engine%20Build/badge.svg) ![crates.io](https://img.shields.io/crates/v/intiface-engine.svg) + + +

+ +

+ +CLI and Library frontend for Buttplug + +Intiface Engine is just a front-end for [Buttplug](https://github.com/buttplugio/buttplug), +but since we're trying to not make people install a program named "Buttplug", here we are. + +While this program can be used standalone, it will mostly be featured as a backend/engine for +Intiface Central. + +## Running + +Command line options are as follows: + +| Option | Description | +| --------- | --------- | +| `version` | Print version and exit | +| `server-version` | Print version and exit (kept for legacy reasons) | +| `websocket-use-all-interfaces` | Websocket servers will listen on all interfaces (versus only on localhost, which is default) | +| `websocket-port [port]` | Network port for connecting via non-ssl (ws://) protocols | +| `frontend-websocket-port` | IPC JSON port for Intiface Central | +| `server-name` | Identifying name server should emit when asked for info | +| `device-config-file [file]` | Device configuration file to load (if omitted, uses internal) | +| `user-device-config-file [file]` | User device configuration file to load (if omitted, none used) | +| `max-ping-time [number]` | Milliseconds for ping time limit of server (if omitted, set to 0) | +| `log` | Level of logs to output by default (if omitted, set to None) | +| `allow-raw` | Allow clients to communicate using raw messages (DANGEROUS, CAN BRICK SOME DEVICES) | +| `use-bluetooth-le` | Use the Bluetooth LE Buttplug Device Communication Manager | +| `use-serial` | Use the Serial Port Buttplug Device Communication Manager | +| `use-hid` | Use the HID Buttplug Device Communication Manager | +| `use-lovense-dongle` | Use the HID Lovense Dongle Buttplug Device Communication Manager | +| `use-xinput` | Use the XInput Buttplug Device Communication Manager | +| `use-lovense-connect` | Use the Lovense Connect Buttplug Device Communication Manager | +| `use-device-websocket-server` | Use the Device Websocket Server Buttplug Device Communication Manager | +| `device-websocket-server-port` | Port for the device websocket server | +| `use-udp` | Use UDP Device Communication Manager | + +For example, to run the server on websockets at port 12345 with bluetooth device support: + +`intiface-engine --websocket-port 12345 --use-bluetooth-le` + +## Compiling + +Linux will have extra compilation dependency requirements via +[buttplug-rs](https://github.com/buttplugio/buttplug-rs). For pacakges required, +please check there. + +## Contributing + +Right now, we mostly need code/API style reviews and feedback. We don't really have any good +bite-sized chunks to mentor the implementation yet, but one we do, those will be marked "Help +Wanted" in our [github issues](https://github.com/buttplugio/buttplug-rs/issues). + +As we need money to keep up with supporting the latest and greatest hardware, we also have multiple +ways to donate! + +- [Patreon](https://patreon.com/qdot) +- [Github Sponsors](https://github.com/sponsors/qdot) +- [Ko-Fi](https://ko-fi.com/qdot76367) + +## License and Trademarks + +Intiface is a Registered Trademark of Nonpolynomial Labs, LLC + +Buttplug and Intiface are BSD licensed. + + Copyright (c) 2016-2022, Nonpolynomial Labs, LLC + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of buttplug nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/crates/intiface_engine/build.rs b/crates/intiface_engine/build.rs new file mode 100644 index 000000000..297db2e85 --- /dev/null +++ b/crates/intiface_engine/build.rs @@ -0,0 +1,14 @@ +use anyhow::Result; +use vergen_gitcl::{BuildBuilder, Emitter, GitclBuilder}; + +fn main() -> Result<()> { + let build = BuildBuilder::default().build_timestamp(true).build()?; + let gitcl = GitclBuilder::default().sha(true).build()?; + + Emitter::default() + .add_instructions(&build)? + .add_instructions(&gitcl)? + .emit()?; + + Ok(()) +} diff --git a/crates/intiface_engine/config.toml b/crates/intiface_engine/config.toml new file mode 100644 index 000000000..98b5c3744 --- /dev/null +++ b/crates/intiface_engine/config.toml @@ -0,0 +1,4 @@ +[target.x86_64-pc-windows-msvc] +rustflags = ["-Ctarget-feature=+crt-static"] +[target.i686-pc-windows-msvc] +rustflags = ["-Ctarget-feature=+crt-static"] diff --git a/crates/intiface_engine/rustfmt.toml b/crates/intiface_engine/rustfmt.toml new file mode 100644 index 000000000..6f2e07515 --- /dev/null +++ b/crates/intiface_engine/rustfmt.toml @@ -0,0 +1 @@ +tab_spaces = 2 \ No newline at end of file diff --git a/crates/intiface_engine/src/backdoor_server.rs b/crates/intiface_engine/src/backdoor_server.rs new file mode 100644 index 000000000..90fd62f8c --- /dev/null +++ b/crates/intiface_engine/src/backdoor_server.rs @@ -0,0 +1,76 @@ +use buttplug_core::{ + connector::transport::stream::ButtplugStreamTransport, + message::serializer::ButtplugSerializedMessage, + util::stream::convert_broadcast_receiver_to_stream, +}; +use buttplug_server::{connector::ButtplugRemoteServerConnector, device::ServerDeviceManager, message::serializer::ButtplugServerJSONSerializer, ButtplugServerBuilder}; +use std::sync::Arc; +use tokio::sync::{ + broadcast, + mpsc::{self, Sender}, +}; +use tokio_stream::Stream; + +use crate::ButtplugRemoteServer; + +// Allows direct access to the Device Manager of a running ButtplugServer. Bypasses requirements for +// client handshake, ping, etc... +pub struct BackdoorServer { + //server: ButtplugRemoteServer, + sender: Sender, + broadcaster: broadcast::Sender, +} + +impl BackdoorServer { + pub fn new(device_manager: Arc) -> Self { + let server = ButtplugRemoteServer::new( + ButtplugServerBuilder::with_shared_device_manager(device_manager.clone()) + .name("Intiface Backdoor Server") + .finish() + .unwrap(), + &None + ); + let (s_out, mut r_out) = mpsc::channel(255); + let (s_in, r_in) = mpsc::channel(255); + let (s_stream, _) = broadcast::channel(255); + tokio::spawn(async move { + if let Err(e) = server + .start(ButtplugRemoteServerConnector::< + _, + ButtplugServerJSONSerializer, + >::new(ButtplugStreamTransport::new(s_out, r_in))) + .await + { + // We can't do much if the server fails, but we *can* yell into the logs! + error!("Backdoor server error: {:?}", e); + } + }); + let sender_clone = s_stream.clone(); + tokio::spawn(async move { + while let Some(ButtplugSerializedMessage::Text(m)) = r_out.recv().await { + if sender_clone.receiver_count() == 0 { + continue; + } + if sender_clone.send(m).is_err() { + break; + } + } + }); + Self { + sender: s_in, + broadcaster: s_stream, + } + } + + pub fn event_stream(&self) -> impl Stream + '_ { + convert_broadcast_receiver_to_stream(self.broadcaster.subscribe()) + } + + pub async fn parse_message(&self, msg: &str) { + self + .sender + .send(ButtplugSerializedMessage::Text(msg.to_owned())) + .await + .unwrap(); + } +} diff --git a/crates/intiface_engine/src/bin/main.rs b/crates/intiface_engine/src/bin/main.rs new file mode 100644 index 000000000..c936f3ee9 --- /dev/null +++ b/crates/intiface_engine/src/bin/main.rs @@ -0,0 +1,315 @@ +use argh::FromArgs; +use getset::{CopyGetters, Getters}; +use intiface_engine::{ + EngineOptions, EngineOptionsBuilder, IntifaceEngine, IntifaceEngineError, IntifaceError, +}; +use std::fs; +use tokio::{select, signal::ctrl_c}; +use tracing::{debug, info, Level}; +use tracing_subscriber::{ + filter::{EnvFilter, LevelFilter}, + layer::SubscriberExt, + util::SubscriberInitExt, +}; + +const VERSION: &str = env!("CARGO_PKG_VERSION"); + +/// command line interface for intiface/buttplug. +/// +/// Note: Commands are one word to keep compat with C#/JS executables currently. +#[derive(FromArgs, Getters, CopyGetters)] +pub struct IntifaceCLIArguments { + // Options that do something then exit + /// print version and exit. + #[argh(switch)] + #[getset(get_copy = "pub")] + version: bool, + + /// print version and exit. + #[argh(switch)] + #[getset(get_copy = "pub")] + server_version: bool, + + // Options that set up the server networking + /// if passed, websocket server listens on all interfaces. Otherwise, only + /// listen on 127.0.0.1. + #[argh(switch)] + #[getset(get_copy = "pub")] + websocket_use_all_interfaces: bool, + + /// insecure port for websocket servers. + #[argh(option)] + #[getset(get_copy = "pub")] + websocket_port: Option, + + /// insecure address for connecting to websocket servers. + #[argh(option)] + #[getset(get = "pub")] + websocket_client_address: Option, + + // Options that set up communications with intiface GUI + /// if passed, output json for parent process via websockets + #[argh(option)] + #[getset(get_copy = "pub")] + frontend_websocket_port: Option, + + // Options that set up Buttplug server parameters + /// name of server to pass to connecting clients. + #[argh(option)] + #[argh(default = "\"Buttplug Server\".to_owned()")] + #[getset(get = "pub")] + server_name: String, + + /// path to the device configuration file + #[argh(option)] + #[getset(get = "pub")] + device_config_file: Option, + + /// path to user device configuration file + #[argh(option)] + #[getset(get = "pub")] + user_device_config_file: Option, + + /// ping timeout maximum for server (in milliseconds) + #[argh(option)] + #[argh(default = "0")] + #[getset(get_copy = "pub")] + max_ping_time: u32, + + /// set log level for output + #[allow(dead_code)] + #[argh(option)] + #[getset(get_copy = "pub")] + log: Option, + + /// turn off bluetooth le device support + #[argh(switch)] + #[getset(get_copy = "pub")] + use_bluetooth_le: bool, + + /// turn off serial device support + #[argh(switch)] + #[getset(get_copy = "pub")] + use_serial: bool, + + /// turn off hid device support + #[allow(dead_code)] + #[argh(switch)] + #[getset(get_copy = "pub")] + use_hid: bool, + + /// turn off lovense dongle serial device support + #[argh(switch)] + #[getset(get_copy = "pub")] + use_lovense_dongle_serial: bool, + + /// turn off lovense dongle hid device support + #[argh(switch)] + #[getset(get_copy = "pub")] + use_lovense_dongle_hid: bool, + + /// turn off xinput gamepad device support (windows only) + #[argh(switch)] + #[getset(get_copy = "pub")] + use_xinput: bool, + + /// turn on lovense connect app device support (off by default) + #[argh(switch)] + #[getset(get_copy = "pub")] + use_lovense_connect: bool, + + /// turn on websocket server device comm manager + #[argh(switch)] + #[getset(get_copy = "pub")] + use_device_websocket_server: bool, + + /// turn on udp device support + #[argh(switch)] + #[getset(get_copy = "pub")] + use_udp: bool, + + /// port for device websocket server comm manager (defaults to 54817) + #[argh(option)] + #[getset(get_copy = "pub")] + device_websocket_server_port: Option, + + /// if set, broadcast server port/service info via mdns + #[argh(switch)] + #[getset(get_copy = "pub")] + broadcast_server_mdns: bool, + + /// mdns suffix, will be appended to instance names for advertised mdns services (optional, ignored if broadcast_mdns is not set) + #[argh(option)] + #[getset(get = "pub")] + mdns_suffix: Option, + + /// if set, use repeater mode instead of engine mode + #[argh(switch)] + #[getset(get_copy = "pub")] + repeater: bool, + + /// if set, use repeater mode instead of engine mode + #[argh(option)] + #[getset(get_copy = "pub")] + repeater_port: Option, + + /// if set, use repeater mode instead of engine mode + #[argh(option)] + #[getset(get = "pub")] + repeater_remote_address: Option, + + #[cfg(debug_assertions)] + /// crash the main thread (that holds the runtime) + #[argh(switch)] + #[getset(get_copy = "pub")] + crash_main_thread: bool, + + #[allow(dead_code)] + #[cfg(debug_assertions)] + /// crash the task thread (for testing logging/reporting) + #[argh(switch)] + #[getset(get_copy = "pub")] + crash_task_thread: bool, +} + +pub fn setup_console_logging(log_level: Option) { + if log_level.is_some() { + tracing_subscriber::registry() + .with(tracing_subscriber::fmt::layer()) + .with(LevelFilter::from(log_level)) + .try_init() + .unwrap(); + } else { + tracing_subscriber::registry() + .with(tracing_subscriber::fmt::layer()) + .with( + EnvFilter::try_from_default_env() + .or_else(|_| EnvFilter::try_new("info")) + .unwrap(), + ) + .try_init() + .unwrap(); + }; + println!("Intiface Server, starting up with stdout output."); +} + +impl TryFrom for EngineOptions { + type Error = IntifaceError; + fn try_from(args: IntifaceCLIArguments) -> Result { + let mut builder = EngineOptionsBuilder::default(); + + if let Some(deviceconfig) = args.device_config_file() { + info!( + "Intiface CLI Options: External Device Config {}", + deviceconfig + ); + match fs::read_to_string(deviceconfig) { + Ok(cfg) => builder.device_config_json(&cfg), + Err(err) => { + return Err(IntifaceError::new(&format!( + "Error opening external device configuration: {:?}", + err + ))) + } + }; + } + + if let Some(userdeviceconfig) = args.user_device_config_file() { + info!( + "Intiface CLI Options: User Device Config {}", + userdeviceconfig + ); + match fs::read_to_string(userdeviceconfig) { + Ok(cfg) => builder.user_device_config_json(&cfg), + Err(err) => { + return Err(IntifaceError::new(&format!( + "Error opening user device configuration: {:?}", + err + ))) + } + }; + } + + builder + .websocket_use_all_interfaces(args.websocket_use_all_interfaces()) + .use_bluetooth_le(args.use_bluetooth_le()) + .use_serial_port(args.use_serial()) + .use_hid(args.use_hid()) + .use_lovense_dongle_serial(args.use_lovense_dongle_serial()) + .use_lovense_dongle_hid(args.use_lovense_dongle_hid()) + .use_xinput(args.use_xinput()) + .use_lovense_connect(args.use_lovense_connect()) + .use_device_websocket_server(args.use_device_websocket_server()) + .use_udp(args.use_udp()) + .max_ping_time(args.max_ping_time()) + .server_name(args.server_name()) + .broadcast_server_mdns(args.broadcast_server_mdns()); + + #[cfg(debug_assertions)] + { + builder + .crash_main_thread(args.crash_main_thread()) + .crash_task_thread(args.crash_task_thread()); + } + + if let Some(value) = args.websocket_port() { + builder.websocket_port(value); + } + if let Some(value) = args.websocket_client_address() { + builder.websocket_client_address(value); + } + if let Some(value) = args.frontend_websocket_port() { + builder.frontend_websocket_port(value); + } + if let Some(value) = args.device_websocket_server_port() { + builder.device_websocket_server_port(value); + } + if args.broadcast_server_mdns() { + if let Some(value) = args.mdns_suffix() { + builder.mdns_suffix(value); + } + } + Ok(builder.finish()) + } +} + +#[tokio::main(flavor = "current_thread")] //#[tokio::main] +async fn main() -> Result<(), IntifaceEngineError> { + let args: IntifaceCLIArguments = argh::from_env(); + if args.server_version() { + println!("{}", VERSION); + return Ok(()); + } + + if args.version() { + debug!("Server version command sent, printing and exiting."); + println!( + "Intiface CLI (Rust Edition) Version {}, Commit {}, Built {}", + VERSION, + option_env!("VERGEN_GIT_SHA_SHORT").unwrap_or("unknown"), + option_env!("VERGEN_BUILD_TIMESTAMP").unwrap_or("unknown") + ); + return Ok(()); + } + + if args.frontend_websocket_port().is_none() { + setup_console_logging(args.log()); + } + + let options = EngineOptions::try_from(args).map_err(IntifaceEngineError::from)?; + let engine = IntifaceEngine::default(); + select! { + result = engine.run(&options, None, &None) => { + if let Err(e) = result { + println!("Server errored while running:"); + println!("{:?}", e); + } + } + _ = ctrl_c() => { + info!("Control-c hit, exiting."); + engine.stop(); + } + } + + Ok(()) +} diff --git a/crates/intiface_engine/src/buttplug_server.rs b/crates/intiface_engine/src/buttplug_server.rs new file mode 100644 index 000000000..9d88d9706 --- /dev/null +++ b/crates/intiface_engine/src/buttplug_server.rs @@ -0,0 +1,184 @@ +use std::sync::Arc; + +use crate::{ + remote_server::ButtplugRemoteServerEvent, BackdoorServer, ButtplugRemoteServer, ButtplugServerConnectorError, EngineOptions, IntifaceEngineError, IntifaceError +}; +use buttplug_server::{ + ButtplugServerBuilder, + connector::ButtplugRemoteServerConnector, + device::{ServerDeviceManager, ServerDeviceManagerBuilder}, + message::serializer::ButtplugServerJSONSerializer, +}; +use buttplug_server_device_config::{DeviceConfigurationManager, load_protocol_configs}; +use buttplug_server_hwmgr_btleplug::BtlePlugCommunicationManagerBuilder; +use buttplug_server_hwmgr_lovense_connect::LovenseConnectServiceCommunicationManagerBuilder; +use buttplug_server_hwmgr_websocket::WebsocketServerDeviceCommunicationManagerBuilder; +use buttplug_server_hwmgr_udp::UdpCommunicationManagerBuilder; +use buttplug_transport_websocket_tungstenite::{ + ButtplugWebsocketClientTransport, ButtplugWebsocketServerTransportBuilder, +}; +use once_cell::sync::OnceCell; +use tokio::sync::broadcast::Sender; +// Device communication manager setup gets its own module because the includes and platform +// specifics are such a mess. + +pub fn setup_server_device_comm_managers( + args: &EngineOptions, + server_builder: &mut ServerDeviceManagerBuilder, +) { + if args.use_bluetooth_le() { + info!("Including Bluetooth LE (btleplug) Device Comm Manager Support"); + let mut command_manager_builder = BtlePlugCommunicationManagerBuilder::default(); + #[cfg(target_os = "ios")] + command_manager_builder.requires_keepalive(true); + #[cfg(not(target_os = "ios"))] + command_manager_builder.requires_keepalive(false); + server_builder.comm_manager(command_manager_builder); + } + if args.use_lovense_connect() { + info!("Including Lovense Connect App Support"); + server_builder.comm_manager(LovenseConnectServiceCommunicationManagerBuilder::default()); + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + use buttplug_server_hwmgr_hid::HidCommunicationManagerBuilder; + use buttplug_server_hwmgr_lovense_dongle::LovenseHIDDongleCommunicationManagerBuilder; + use buttplug_server_hwmgr_serial::SerialPortCommunicationManagerBuilder; + if args.use_lovense_dongle_hid() { + info!("Including Lovense HID Dongle Support"); + server_builder.comm_manager(LovenseHIDDongleCommunicationManagerBuilder::default()); + } + if args.use_serial_port() { + info!("Including Serial Port Support"); + server_builder.comm_manager(SerialPortCommunicationManagerBuilder::default()); + } + if args.use_hid() { + info!("Including Hid Support"); + server_builder.comm_manager(HidCommunicationManagerBuilder::default()); + } + #[cfg(target_os = "windows")] + { + use buttplug_server_hwmgr_xinput::XInputDeviceCommunicationManagerBuilder; + if args.use_xinput() { + info!("Including XInput Gamepad Support"); + server_builder.comm_manager(XInputDeviceCommunicationManagerBuilder::default()); + } + } + } + if args.use_device_websocket_server() { + info!("Including Websocket Server Device Support"); + let mut builder = + WebsocketServerDeviceCommunicationManagerBuilder::default().listen_on_all_interfaces(true); + if let Some(port) = args.device_websocket_server_port() { + builder = builder.server_port(port); + } + server_builder.comm_manager(builder); + } + if args.use_udp() { + info!("Including UDP Support"); + server_builder.comm_manager(UdpCommunicationManagerBuilder::default()); + } +} + +pub async fn reset_buttplug_server( + options: &EngineOptions, + device_manager: &Arc, + sender: &Sender +) -> Result { + match ButtplugServerBuilder::with_shared_device_manager(device_manager.clone()) + .name(options.server_name()) + .max_ping_time(options.max_ping_time()) + .finish() + { + Ok(server) => Ok(ButtplugRemoteServer::new(server, &Some(sender.clone()))), + Err(e) => { + error!("Error starting server: {:?}", e); + return Err(IntifaceEngineError::ButtplugServerError(e)); + } + } +} + +pub async fn setup_buttplug_server( + options: &EngineOptions, + backdoor_server: &OnceCell>, + dcm: &Option>, +) -> Result { + let mut dm_builder = if let Some(dcm) = dcm { + ServerDeviceManagerBuilder::new_with_arc(dcm.clone()) + } else { + let mut dcm_builder = load_protocol_configs( + options.device_config_json(), + options.user_device_config_json(), + false, + ) + .map_err(|e| IntifaceEngineError::ButtplugError(e.into()))?; + + ServerDeviceManagerBuilder::new( + dcm_builder + .finish() + .map_err(|e| IntifaceEngineError::ButtplugError(e.into()))?, + ) + }; + + setup_server_device_comm_managers(options, &mut dm_builder); + let mut server_builder = ButtplugServerBuilder::new( + dm_builder + .finish() + .map_err(|e| IntifaceEngineError::ButtplugServerError(e))?, + ); + + server_builder + .name(options.server_name()) + .max_ping_time(options.max_ping_time()); + + let core_server = match server_builder.finish() { + Ok(server) => server, + Err(e) => { + error!("Error starting server: {:?}", e); + return Err(IntifaceEngineError::ButtplugServerError(e)); + } + }; + if backdoor_server + .set(Arc::new(BackdoorServer::new(core_server.device_manager()))) + .is_err() + { + Err( + IntifaceError::new("BackdoorServer already initialized somehow! This should never happen!") + .into(), + ) + } else { + Ok(ButtplugRemoteServer::new(core_server, &None)) + } +} + +pub async fn run_server( + server: &ButtplugRemoteServer, + options: &EngineOptions, +) -> Result<(), ButtplugServerConnectorError> { + if let Some(port) = options.websocket_port() { + server + .start(ButtplugRemoteServerConnector::< + _, + ButtplugServerJSONSerializer, + >::new( + ButtplugWebsocketServerTransportBuilder::default() + .port(port) + .listen_on_all_interfaces(options.websocket_use_all_interfaces()) + .finish(), + )) + .await + } else if let Some(addr) = options.websocket_client_address() { + server + .start(ButtplugRemoteServerConnector::< + _, + ButtplugServerJSONSerializer, + >::new( + ButtplugWebsocketClientTransport::new_insecure_connector(&addr), + )) + .await + } else { + panic!( + "Websocket port not set, cannot create transport. Please specify a websocket port in arguments." + ); + } +} diff --git a/crates/intiface_engine/src/engine.rs b/crates/intiface_engine/src/engine.rs new file mode 100644 index 000000000..5d55396c3 --- /dev/null +++ b/crates/intiface_engine/src/engine.rs @@ -0,0 +1,218 @@ +use crate::{ + backdoor_server::BackdoorServer, + buttplug_server::{reset_buttplug_server, run_server, setup_buttplug_server}, + error::IntifaceEngineError, + frontend::{ + frontend_external_event_loop, frontend_server_event_loop, process_messages::EngineMessage, + Frontend, + }, + mdns::IntifaceMdns, + options::EngineOptions, + remote_server::ButtplugRemoteServerEvent, + ButtplugRepeater, +}; + +use buttplug_server_device_config::{DeviceConfigurationManager, save_user_config}; +use futures::{pin_mut, StreamExt}; +use once_cell::sync::OnceCell; +use std::{path::Path, sync::Arc, time::Duration}; +use tokio::{fs, select}; +use tokio_util::sync::CancellationToken; + +#[cfg(debug_assertions)] +pub fn maybe_crash_main_thread(options: &EngineOptions) { + if options.crash_main_thread() { + panic!("Crashing main thread by request"); + } +} + +#[allow(dead_code)] +#[cfg(debug_assertions)] +pub fn maybe_crash_task_thread(options: &EngineOptions) { + if options.crash_task_thread() { + tokio::spawn(async { + tokio::time::sleep(Duration::from_millis(100)).await; + panic!("Crashing a task thread by request"); + }); + } +} + +#[derive(Default)] +pub struct IntifaceEngine { + stop_token: Arc, + backdoor_server: OnceCell>, +} + +impl IntifaceEngine { + pub fn backdoor_server(&self) -> Option> { + Some(self.backdoor_server.get()?.clone()) + } + + pub async fn run( + &self, + options: &EngineOptions, + frontend: Option>, + dcm: &Option>, + ) -> Result<(), IntifaceEngineError> { + // Set up Frontend + if let Some(frontend) = &frontend { + let frontend_loop = frontend_external_event_loop(frontend.clone(), self.stop_token.clone()); + tokio::spawn(async move { + frontend_loop.await; + }); + + frontend.connect().await.unwrap(); + frontend.send(EngineMessage::EngineStarted {}).await; + } + + // Set up mDNS + let _mdns_server = if options.broadcast_server_mdns() { + // TODO Unregister whenever we have a live connection + + // TODO Support different services for engine versus repeater + Some(IntifaceMdns::new()) + } else { + None + }; + + // Set up Repeater (if in repeater mode) + if options.repeater_mode() { + info!("Starting repeater"); + + let repeater = ButtplugRepeater::new( + options.repeater_local_port().unwrap(), + &options.repeater_remote_address().as_ref().unwrap(), + self.stop_token.child_token(), + ); + select! { + _ = self.stop_token.cancelled() => { + info!("Owner requested process exit, exiting."); + } + _ = repeater.listen() => { + info!("Repeater listener stopped, exiting."); + } + }; + if let Some(frontend) = &frontend { + frontend.send(EngineMessage::EngineStopped {}).await; + tokio::time::sleep(Duration::from_millis(100)).await; + frontend.disconnect(); + } + return Ok(()); + } + + // Set up Engine (if in engine mode) + + // At this point we will have received and validated options. + + // Hang out until those listeners get sick of listening. + info!("Intiface CLI Setup finished, running server tasks until all joined."); + let mut server = setup_buttplug_server(options, &self.backdoor_server, &dcm).await?; + let dcm = server + .server() + .device_manager() + .device_configuration_manager() + .clone(); + if let Some(config_path) = options.user_device_config_path() { + let stream = server.event_stream(); + { + let config_path = config_path.to_owned(); + tokio::spawn(async move { + pin_mut!(stream); + loop { + if let Some(event) = stream.next().await { + match event { + ButtplugRemoteServerEvent::DeviceAdded { + index: _, + identifier: _, + name: _, + display_name: _, + } => { + if let Ok(config_str) = save_user_config(&dcm) { + // Should probably at least log if we fail to write the config file + let _ = fs::write(&Path::new(&config_path), config_str).await; + } + } + _ => continue, + } + }; + } + }); + } + } + if let Some(frontend) = &frontend { + frontend.send(EngineMessage::EngineServerCreated {}).await; + let event_receiver = server.event_stream(); + let frontend_clone = frontend.clone(); + let stop_child_token = self.stop_token.child_token(); + tokio::spawn(async move { + frontend_server_event_loop(event_receiver, frontend_clone, stop_child_token).await; + }); + } + + loop { + let session_connection_token = CancellationToken::new(); + info!("Starting server"); + + // Let everything spin up, then try crashing. + + #[cfg(debug_assertions)] + maybe_crash_main_thread(options); + + let mut exit_requested = false; + select! { + _ = self.stop_token.cancelled() => { + info!("Owner requested process exit, exiting."); + exit_requested = true; + } + result = run_server(&server, options) => { + match result { + Ok(_) => info!("Connection dropped, restarting stay open loop."), + Err(e) => { + error!("{}", format!("Process Error: {:?}", e)); + + if let Some(frontend) = &frontend { + frontend + .send(EngineMessage::EngineError{ error: format!("Process Error: {:?}", e).to_owned()}) + .await; + } + } + } + } + }; + match server.disconnect().await { + Ok(_) => { + info!("Client forcefully disconnected from server."); + if let Some(frontend) = &frontend { + frontend.send(EngineMessage::ClientDisconnected {}).await; + } + } + Err(_) => info!("Client already disconnected from server."), + }; + session_connection_token.cancel(); + if exit_requested { + info!("Breaking out of event loop in order to exit"); + break; + } + // We're not exiting, rebuild our server. + let dm = server.server().device_manager(); + server = reset_buttplug_server(options, &dm, server.event_sender()).await?; + info!("Server connection dropped, restarting"); + } + info!("Shutting down server..."); + if let Err(e) = server.shutdown().await { + error!("Shutdown failed: {:?}", e); + } + info!("Exiting"); + if let Some(frontend) = &frontend { + frontend.send(EngineMessage::EngineStopped {}).await; + tokio::time::sleep(Duration::from_millis(100)).await; + frontend.disconnect(); + } + Ok(()) + } + + pub fn stop(&self) { + info!("Engine stop called, cancelling token."); + self.stop_token.cancel(); + } +} diff --git a/crates/intiface_engine/src/error.rs b/crates/intiface_engine/src/error.rs new file mode 100644 index 000000000..40b23a12f --- /dev/null +++ b/crates/intiface_engine/src/error.rs @@ -0,0 +1,54 @@ +use buttplug_core::errors::ButtplugError; +use buttplug_server::ButtplugServerError; +use std::{error::Error, fmt}; + +#[derive(Debug)] +pub struct IntifaceError { + reason: String, +} + +impl IntifaceError { + pub fn new(error_msg: &str) -> Self { + Self { + reason: error_msg.to_owned(), + } + } +} + +impl fmt::Display for IntifaceError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.reason) + } +} + +impl Error for IntifaceError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + None + } +} + +#[derive(Debug)] +pub enum IntifaceEngineError { + IoError(std::io::Error), + ButtplugServerError(ButtplugServerError), + ButtplugError(ButtplugError), + IntifaceError(IntifaceError), +} + +impl From for IntifaceEngineError { + fn from(err: std::io::Error) -> Self { + IntifaceEngineError::IoError(err) + } +} + +impl From for IntifaceEngineError { + fn from(err: ButtplugError) -> Self { + IntifaceEngineError::ButtplugError(err) + } +} + +impl From for IntifaceEngineError { + fn from(err: IntifaceError) -> Self { + IntifaceEngineError::IntifaceError(err) + } +} diff --git a/crates/intiface_engine/src/frontend/mod.rs b/crates/intiface_engine/src/frontend/mod.rs new file mode 100644 index 000000000..749263a62 --- /dev/null +++ b/crates/intiface_engine/src/frontend/mod.rs @@ -0,0 +1,134 @@ +pub mod process_messages; +use crate::error::IntifaceError; +use crate::remote_server::ButtplugRemoteServerEvent; +use async_trait::async_trait; +use futures::{pin_mut, Stream, StreamExt}; +pub use process_messages::{EngineMessage, IntifaceMessage}; +use std::sync::Arc; +use tokio::{ + select, + sync::{broadcast, Notify}, +}; +use tokio_util::sync::CancellationToken; + +const VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[async_trait] +pub trait Frontend: Sync + Send { + async fn send(&self, msg: EngineMessage); + async fn connect(&self) -> Result<(), IntifaceError>; + fn disconnect_notifier(&self) -> Arc; + fn disconnect(&self); + fn event_stream(&self) -> broadcast::Receiver; +} + +pub async fn frontend_external_event_loop( + frontend: Arc, + connection_cancellation_token: Arc, +) { + let mut external_receiver = frontend.event_stream(); + loop { + select! { + external_message = external_receiver.recv() => { + match external_message { + Ok(message) => match message { + IntifaceMessage::RequestEngineVersion{expected_version:_} => { + // TODO We should check the version here and shut down on mismatch. + info!("Engine version request received from frontend."); + frontend + .send(EngineMessage::EngineVersion{ version: VERSION.to_owned() }) + .await; + }, + IntifaceMessage::Stop{} => { + connection_cancellation_token.cancel(); + info!("Got external stop request"); + break; + } + }, + Err(_) => { + info!("Frontend sender dropped, assuming connection lost, breaking."); + break; + } + } + }, + _ = connection_cancellation_token.cancelled() => { + info!("Connection cancellation token activated, breaking from frontend external event loop."); + break; + } + } + } +} + +pub async fn frontend_server_event_loop( + receiver: impl Stream, + frontend: Arc, + connection_cancellation_token: CancellationToken, +) { + pin_mut!(receiver); + + loop { + select! { + maybe_event = receiver.next() => { + match maybe_event { + Some(event) => match event { + ButtplugRemoteServerEvent::ClientConnected(client_name) => { + info!("Client connected: {}", client_name); + frontend.send(EngineMessage::ClientConnected{client_name}).await; + } + ButtplugRemoteServerEvent::ClientDisconnected => { + info!("Client disconnected."); + frontend + .send(EngineMessage::ClientDisconnected{}) + .await; + } + ButtplugRemoteServerEvent::DeviceAdded { index: device_id, name: device_name, identifier: device_address, display_name: device_display_name } => { + info!("Device Added: {} - {} - {:?}", device_id, device_name, device_address); + frontend + .send(EngineMessage::DeviceConnected { name: device_name, index: device_id, identifier: device_address, display_name: device_display_name }) + .await; + } + ButtplugRemoteServerEvent::DeviceRemoved { index: device_id } => { + info!("Device Removed: {}", device_id); + frontend + .send(EngineMessage::DeviceDisconnected{index: device_id}) + .await; + } + }, + None => { + info!("Lost connection with main thread, breaking."); + break; + }, + } + }, + _ = connection_cancellation_token.cancelled() => { + info!("Connection cancellation token activated, breaking from frontend server event loop"); + break; + } + } + } + info!("Exiting server event receiver loop"); +} +/* +#[derive(Default)] +struct NullFrontend { + notify: Arc, +} + +#[async_trait] +impl Frontend for NullFrontend { + async fn send(&self, _: EngineMessage) {} + async fn connect(&self) -> Result<(), IntifaceError> { + Ok(()) + } + fn disconnect(&self) { + self.notify.notify_waiters(); + } + fn disconnect_notifier(&self) -> Arc { + self.notify.clone() + } + fn event_stream(&self) -> broadcast::Receiver { + let (_, receiver) = broadcast::channel(255); + receiver + } +} +*/ \ No newline at end of file diff --git a/crates/intiface_engine/src/frontend/process_messages.rs b/crates/intiface_engine/src/frontend/process_messages.rs new file mode 100644 index 000000000..0c8c0e42f --- /dev/null +++ b/crates/intiface_engine/src/frontend/process_messages.rs @@ -0,0 +1,40 @@ +use buttplug_server_device_config::UserDeviceIdentifier; +use serde::{Deserialize, Serialize}; + +// Everything in this struct is an object, even if it has null contents. This is to make other +// languages happy when trying to recompose JSON into objects. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum EngineMessage { + EngineVersion { + version: String, + }, + EngineStarted {}, + EngineError { + error: String, + }, + EngineServerCreated {}, + EngineStopped {}, + ClientConnected { + client_name: String, + }, + ClientDisconnected {}, + DeviceConnected { + name: String, + index: u32, + identifier: UserDeviceIdentifier, + #[serde(skip_serializing_if = "Option::is_none")] + display_name: Option, + }, + DeviceDisconnected { + index: u32, + }, + ClientRejected { + reason: String, + }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum IntifaceMessage { + RequestEngineVersion { expected_version: u32 }, + Stop {}, +} diff --git a/crates/intiface_engine/src/lib.rs b/crates/intiface_engine/src/lib.rs new file mode 100644 index 000000000..16acb171d --- /dev/null +++ b/crates/intiface_engine/src/lib.rs @@ -0,0 +1,18 @@ +#[macro_use] +extern crate tracing; +mod backdoor_server; +mod buttplug_server; +mod engine; +mod error; +mod frontend; +mod mdns; +mod options; +mod remote_server; +mod repeater; +pub use backdoor_server::BackdoorServer; +pub use engine::IntifaceEngine; +pub use error::*; +pub use frontend::{EngineMessage, Frontend, IntifaceMessage}; +pub use options::{EngineOptions, EngineOptionsBuilder, EngineOptionsExternal}; +pub use remote_server::{ButtplugRemoteServer, ButtplugServerConnectorError}; +pub use repeater::ButtplugRepeater; diff --git a/crates/intiface_engine/src/mdns.rs b/crates/intiface_engine/src/mdns.rs new file mode 100644 index 000000000..f253a248e --- /dev/null +++ b/crates/intiface_engine/src/mdns.rs @@ -0,0 +1,31 @@ +use rand::distr::{Alphanumeric, SampleString}; + +pub struct IntifaceMdns { + _responder: libmdns::Responder, + _svc: libmdns::Service, +} + +impl IntifaceMdns { + pub fn new() -> Self { + let random_suffix = Alphanumeric.sample_string(&mut rand::rng(), 6); + let instance_name = format!("Intiface {}", random_suffix); + info!( + "Bringing up mDNS Advertisment using instance name {}", + instance_name + ); + + let (_responder, task) = libmdns::Responder::with_default_handle().unwrap(); + let _svc = _responder.register( + "_intiface_engine._tcp".to_owned(), + instance_name, + 12345, + &["path=/"], + ); + tokio::spawn(async move { + info!("Entering up mDNS task"); + task.await; + info!("Exiting mDNS task"); + }); + Self { _responder, _svc } + } +} diff --git a/crates/intiface_engine/src/options.rs b/crates/intiface_engine/src/options.rs new file mode 100644 index 000000000..4f0e42adc --- /dev/null +++ b/crates/intiface_engine/src/options.rs @@ -0,0 +1,278 @@ +use getset::{CopyGetters, Getters}; + +#[derive(CopyGetters, Getters, Default, Debug, Clone)] +pub struct EngineOptions { + #[getset(get = "pub")] + device_config_json: Option, + #[getset(get = "pub")] + user_device_config_json: Option, + #[getset(get = "pub")] + user_device_config_path: Option, + #[getset(get = "pub")] + server_name: String, + #[getset(get_copy = "pub")] + websocket_use_all_interfaces: bool, + #[getset(get_copy = "pub")] + websocket_port: Option, + #[getset(get = "pub")] + websocket_client_address: Option, + #[getset(get_copy = "pub")] + frontend_websocket_port: Option, + #[getset(get_copy = "pub")] + frontend_in_process_channel: bool, + #[getset(get_copy = "pub")] + max_ping_time: u32, + #[getset(get_copy = "pub")] + use_bluetooth_le: bool, + #[getset(get_copy = "pub")] + use_serial_port: bool, + #[getset(get_copy = "pub")] + use_hid: bool, + #[getset(get_copy = "pub")] + use_lovense_dongle_serial: bool, + #[getset(get_copy = "pub")] + use_lovense_dongle_hid: bool, + #[getset(get_copy = "pub")] + use_xinput: bool, + #[getset(get_copy = "pub")] + use_lovense_connect: bool, + #[getset(get_copy = "pub")] + use_device_websocket_server: bool, + #[getset(get_copy = "pub")] + use_udp: bool, + #[getset(get_copy = "pub")] + device_websocket_server_port: Option, + #[getset(get_copy = "pub")] + crash_main_thread: bool, + #[getset(get_copy = "pub")] + crash_task_thread: bool, + #[getset(get_copy = "pub")] + broadcast_server_mdns: bool, + #[getset(get = "pub")] + mdns_suffix: Option, + #[getset(get_copy = "pub")] + repeater_mode: bool, + #[getset(get_copy = "pub")] + repeater_local_port: Option, + #[getset(get = "pub")] + repeater_remote_address: Option, +} + +#[derive(Default, Debug, Clone)] +pub struct EngineOptionsExternal { + pub device_config_json: Option, + pub user_device_config_json: Option, + pub user_device_config_path: Option, + pub server_name: String, + pub websocket_use_all_interfaces: bool, + pub websocket_port: Option, + pub websocket_client_address: Option, + pub frontend_websocket_port: Option, + pub frontend_in_process_channel: bool, + pub max_ping_time: u32, + pub use_bluetooth_le: bool, + pub use_serial_port: bool, + pub use_hid: bool, + pub use_lovense_dongle_serial: bool, + pub use_lovense_dongle_hid: bool, + pub use_xinput: bool, + pub use_lovense_connect: bool, + pub use_device_websocket_server: bool, + pub use_udp: bool, + pub device_websocket_server_port: Option, + pub crash_main_thread: bool, + pub crash_task_thread: bool, + pub broadcast_server_mdns: bool, + pub mdns_suffix: Option, + pub repeater_mode: bool, + pub repeater_local_port: Option, + pub repeater_remote_address: Option, +} + +impl From for EngineOptions { + fn from(other: EngineOptionsExternal) -> Self { + Self { + device_config_json: other.device_config_json, + user_device_config_json: other.user_device_config_json, + user_device_config_path: other.user_device_config_path, + server_name: other.server_name, + websocket_use_all_interfaces: other.websocket_use_all_interfaces, + websocket_port: other.websocket_port, + websocket_client_address: other.websocket_client_address, + frontend_websocket_port: other.frontend_websocket_port, + frontend_in_process_channel: other.frontend_in_process_channel, + max_ping_time: other.max_ping_time, + use_bluetooth_le: other.use_bluetooth_le, + use_serial_port: other.use_serial_port, + use_hid: other.use_hid, + use_lovense_dongle_serial: other.use_lovense_dongle_serial, + use_lovense_dongle_hid: other.use_lovense_dongle_hid, + use_xinput: other.use_xinput, + use_lovense_connect: other.use_lovense_connect, + use_device_websocket_server: other.use_device_websocket_server, + use_udp: other.use_udp, + device_websocket_server_port: other.device_websocket_server_port, + crash_main_thread: other.crash_main_thread, + crash_task_thread: other.crash_task_thread, + broadcast_server_mdns: other.broadcast_server_mdns, + mdns_suffix: other.mdns_suffix, + repeater_mode: other.repeater_mode, + repeater_local_port: other.repeater_local_port, + repeater_remote_address: other.repeater_remote_address, + } + } +} + +#[derive(Default)] +pub struct EngineOptionsBuilder { + options: EngineOptions, +} + +impl EngineOptionsBuilder { + pub fn device_config_json(&mut self, value: &str) -> &mut Self { + self.options.device_config_json = Some(value.to_owned()); + self + } + + pub fn user_device_config_json(&mut self, value: &str) -> &mut Self { + self.options.user_device_config_json = Some(value.to_owned()); + self + } + + pub fn user_device_config_path(&mut self, value: &str) -> &mut Self { + self.options.user_device_config_path = Some(value.to_owned()); + self + } + + pub fn server_name(&mut self, value: &str) -> &mut Self { + self.options.server_name = value.to_owned(); + self + } + + #[cfg(debug_assertions)] + pub fn crash_main_thread(&mut self, value: bool) -> &mut Self { + #[cfg(debug_assertions)] + { + self.options.crash_main_thread = value; + } + self + } + + #[cfg(debug_assertions)] + pub fn crash_task_thread(&mut self, value: bool) -> &mut Self { + #[cfg(debug_assertions)] + { + self.options.crash_main_thread = value; + } + self + } + + pub fn websocket_use_all_interfaces(&mut self, value: bool) -> &mut Self { + self.options.websocket_use_all_interfaces = value; + self + } + + pub fn use_bluetooth_le(&mut self, value: bool) -> &mut Self { + self.options.use_bluetooth_le = value; + self + } + + pub fn use_serial_port(&mut self, value: bool) -> &mut Self { + self.options.use_serial_port = value; + self + } + + pub fn use_hid(&mut self, value: bool) -> &mut Self { + self.options.use_hid = value; + self + } + + pub fn use_lovense_dongle_serial(&mut self, value: bool) -> &mut Self { + self.options.use_lovense_dongle_serial = value; + self + } + + pub fn use_lovense_dongle_hid(&mut self, value: bool) -> &mut Self { + self.options.use_lovense_dongle_hid = value; + self + } + + pub fn use_xinput(&mut self, value: bool) -> &mut Self { + self.options.use_xinput = value; + self + } + + pub fn use_lovense_connect(&mut self, value: bool) -> &mut Self { + self.options.use_lovense_connect = value; + self + } + + pub fn use_device_websocket_server(&mut self, value: bool) -> &mut Self { + self.options.use_device_websocket_server = value; + self + } + + pub fn use_udp(&mut self, value: bool) -> &mut Self { + self.options.use_udp = value; + self + } + + pub fn websocket_port(&mut self, port: u16) -> &mut Self { + self.options.websocket_port = Some(port); + self + } + + pub fn websocket_client_address(&mut self, address: &str) -> &mut Self { + self.options.websocket_client_address = Some(address.to_owned()); + self + } + + pub fn frontend_websocket_port(&mut self, port: u16) -> &mut Self { + self.options.frontend_websocket_port = Some(port); + self + } + + pub fn frontend_in_process_channel(&mut self, value: bool) -> &mut Self { + self.options.frontend_in_process_channel = value; + self + } + + pub fn device_websocket_server_port(&mut self, port: u16) -> &mut Self { + self.options.device_websocket_server_port = Some(port); + self + } + + pub fn max_ping_time(&mut self, value: u32) -> &mut Self { + self.options.max_ping_time = value; + self + } + + pub fn broadcast_server_mdns(&mut self, value: bool) -> &mut Self { + self.options.broadcast_server_mdns = value; + self + } + + pub fn mdns_suffix(&mut self, name: &str) -> &mut Self { + self.options.mdns_suffix = Some(name.to_owned()); + self + } + + pub fn use_repeater_mode(&mut self) -> &mut Self { + self.options.repeater_mode = true; + self + } + + pub fn repeater_local_port(&mut self, port: u16) -> &mut Self { + self.options.repeater_local_port = Some(port); + self + } + + pub fn repeater_remote_address(&mut self, addr: &str) -> &mut Self { + self.options.repeater_remote_address = Some(addr.to_owned()); + self + } + + pub fn finish(&mut self) -> EngineOptions { + self.options.clone() + } +} diff --git a/crates/intiface_engine/src/remote_server.rs b/crates/intiface_engine/src/remote_server.rs new file mode 100644 index 000000000..aef2de446 --- /dev/null +++ b/crates/intiface_engine/src/remote_server.rs @@ -0,0 +1,310 @@ +// Buttplug Rust Source Code File - See https://buttplug.io for more info. +// +// Copyright 2016-2022 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 buttplug_core::{ + connector::ButtplugConnector, + errors::ButtplugError, + message::ButtplugServerMessageV4, + util::{async_manager, stream::convert_broadcast_receiver_to_stream}, +}; +use buttplug_server_device_config::UserDeviceIdentifier; +use buttplug_server::{ + message::{ButtplugClientMessageVariant, ButtplugServerMessageVariant}, ButtplugServer, ButtplugServerBuilder +}; +use dashmap::DashSet; +use futures::{future::Future, pin_mut, select, FutureExt, Stream, StreamExt}; +use getset::Getters; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use thiserror::Error; +use tokio::sync::{broadcast::{self, Sender}, mpsc, Notify}; + +// Clone derived here to satisfy tokio broadcast requirements. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum ButtplugRemoteServerEvent { + ClientConnected(String), + ClientDisconnected, + DeviceAdded { + index: u32, + identifier: UserDeviceIdentifier, + name: String, + display_name: Option, + }, + DeviceRemoved { + index: u32, + }, + //DeviceCommand(ButtplugDeviceCommandMessageUnion) +} + +#[derive(Error, Debug)] +pub enum ButtplugServerConnectorError { + #[error("Cannot bring up server for connection: {0}")] + ConnectorError(String), +} + +#[derive(Getters)] +pub struct ButtplugRemoteServer { + #[getset(get = "pub")] + server: Arc, + #[getset(get = "pub")] + event_sender: broadcast::Sender, + disconnect_notifier: Arc, +} + +async fn run_device_event_stream( + server: Arc, + remote_event_sender: broadcast::Sender, +) { + let server_receiver = server.server_version_event_stream(); + let known_indexes = DashSet::::default(); + + pin_mut!(server_receiver); + loop { + match server_receiver.next().await { + None => { + info!("Server disconnected via server disappearance, exiting loop."); + break; + } + Some(msg) => { + if let ButtplugServerMessageV4::DeviceList(dl) = msg && remote_event_sender.receiver_count() > 0 { + for da in dl.devices() { + if known_indexes.contains(&da.1.device_index()) { + continue; + } + if let Some(device_info) = server.device_manager().device_info(da.1.device_index()) { + let added_event = ButtplugRemoteServerEvent::DeviceAdded { + index: da.1.device_index(), + name: da.1.device_name().clone(), + identifier: device_info.identifier().clone().into(), + display_name: device_info.display_name().clone(), + }; + if remote_event_sender.send(added_event).is_err() { + error!("Cannot send event to owner, dropping and assuming local server thread has exited."); + } + known_indexes.insert(da.1.device_index()); + } + } + let indexes = known_indexes.clone(); + let current_indexes: Vec = dl.devices().keys().cloned().collect(); + for dr in indexes { + if current_indexes.contains(&dr) { + continue; + } + let removed_event = ButtplugRemoteServerEvent::DeviceRemoved { + index: dr, + }; + if remote_event_sender.send(removed_event).is_err() { + error!("Cannot send event to owner, dropping and assuming local server thread has exited."); + } + known_indexes.remove(&dr); + } + } + } + } + } +} + +async fn run_server( + server: Arc, + remote_event_sender: broadcast::Sender, + connector: ConnectorType, + mut connector_receiver: mpsc::Receiver, + disconnect_notifier: Arc, +) where + ConnectorType: + ButtplugConnector + 'static, +{ + info!("Starting remote server loop"); + let shared_connector = Arc::new(connector); + let server_receiver = server.server_version_event_stream(); + let client_version_receiver = server.event_stream(); + pin_mut!(server_receiver); + pin_mut!(client_version_receiver); + loop { + select! { + connector_msg = connector_receiver.recv().fuse() => match connector_msg { + None => { + info!("Connector disconnected, exiting loop."); + if remote_event_sender.receiver_count() > 0 && remote_event_sender.send(ButtplugRemoteServerEvent::ClientDisconnected).is_err() { + warn!("Cannot update remote about client disconnection"); + } + break; + } + Some(client_message) => { + trace!("Got message from connector: {:?}", client_message); + let server_clone = server.clone(); + let connected = server_clone.connected(); + let connector_clone = shared_connector.clone(); + let remote_event_sender_clone = remote_event_sender.clone(); + async_manager::spawn(async move { + match server_clone.parse_message(client_message.clone()).await { + Ok(ret_msg) => { + // Only send event if we just connected. Sucks to check it on every message but the boolean check should be quick. + if !connected && server_clone.connected() { + if remote_event_sender_clone.receiver_count() > 0 { + if remote_event_sender_clone.send(ButtplugRemoteServerEvent::ClientConnected(server_clone.client_name().unwrap_or("Buttplug Client (No name specified)".to_owned()).clone())).is_err() { + error!("Cannot send event to owner, dropping and assuming local server thread has exited."); + } + } + } + if connector_clone.send(ret_msg).await.is_err() { + error!("Cannot send reply to server, dropping and assuming remote server thread has exited."); + } + }, + Err(err_msg) => { + if connector_clone.send(err_msg.into()).await.is_err() { + error!("Cannot send reply to server, dropping and assuming remote server thread has exited."); + } + } + } + }); + } + }, + _ = disconnect_notifier.notified().fuse() => { + info!("Server disconnected via controller disappearance, exiting loop."); + break; + }, + server_msg = server_receiver.next().fuse() => match server_msg { + None => { + info!("Server disconnected via server disappearance, exiting loop."); + break; + } + Some(msg) => { + /* + if remote_event_sender.receiver_count() > 0 { + match &msg { + ButtplugServerMessageV4::DeviceAdded(da) => { + if let Some(device_info) = server.device_manager().device_info(da.device_index()) { + let added_event = ButtplugRemoteServerEvent::DeviceAdded { index: da.device_index(), name: da.device_name().clone(), identifier: device_info.identifier().clone().into(), display_name: device_info.display_name().clone() }; + if remote_event_sender.send(added_event).is_err() { + error!("Cannot send event to owner, dropping and assuming local server thread has exited."); + } + } + }, + ButtplugServerMessageV4::DeviceRemoved(dr) => { + let removed_event = ButtplugRemoteServerEvent::DeviceRemoved { index: dr.device_index() }; + if remote_event_sender.send(removed_event).is_err() { + error!("Cannot send event to owner, dropping and assuming local server thread has exited."); + } + }, + _ => {} + } + } + */ + } + }, + client_msg = client_version_receiver.next().fuse() => match client_msg { + None => { + info!("Server disconnected via server disappearance, exiting loop."); + break; + } + Some(msg) => { + let connector_clone = shared_connector.clone(); + if connector_clone.send(msg.into()).await.is_err() { + error!("Server disappeared, exiting remote server thread."); + } + } + } + }; + } + if let Err(err) = server.disconnect().await { + error!("Error disconnecting server: {:?}", err); + } + info!("Exiting remote server loop"); +} + +impl Default for ButtplugRemoteServer { + fn default() -> Self { + Self::new( + ButtplugServerBuilder::default() + .finish() + .expect("Default is infallible"), + &None + ) + } +} + +impl ButtplugRemoteServer { + pub fn new(server: ButtplugServer, event_sender: &Option>) -> Self { + let event_sender = if let Some(sender) = event_sender { + sender.clone() + } else { + broadcast::channel(256).0 + }; + // Thanks to the existence of the backdoor server, device updates can happen for the lifetime to + // the RemoteServer instance, not just during client connect. We need to make sure these are + // emitted to the frontend. + let server = Arc::new(server); + { + let server = server.clone(); + tokio::spawn({ + let server = server; + let event_sender = event_sender.clone(); + async move { + run_device_event_stream(server, event_sender).await; + } + }); + } + Self { + event_sender, + server: server, + disconnect_notifier: Arc::new(Notify::new()), + } + } + + pub fn event_stream(&self) -> impl Stream + use<> { + convert_broadcast_receiver_to_stream(self.event_sender.subscribe()) + } + + pub fn start( + &self, + mut connector: ConnectorType, + ) -> impl Future> + use + where + ConnectorType: + ButtplugConnector + 'static, + { + let server = self.server.clone(); + let event_sender = self.event_sender.clone(); + let disconnect_notifier = self.disconnect_notifier.clone(); + async move { + let (connector_sender, connector_receiver) = mpsc::channel(256); + // Due to the connect method requiring a mutable connector, we must connect before starting up + // our server loop. Anything that needs to happen outside of the client connection session + // should happen around this. This flow is locked. + connector + .connect(connector_sender) + .await + .map_err(|e| ButtplugServerConnectorError::ConnectorError(format!("{:?}", e)))?; + run_server( + server, + event_sender, + connector, + connector_receiver, + disconnect_notifier, + ) + .await; + Ok(()) + } + } + + pub async fn disconnect(&self) -> Result<(), ButtplugError> { + self.disconnect_notifier.notify_waiters(); + Ok(()) + } + + pub async fn shutdown(&self) -> Result<(), ButtplugError> { + self.server.shutdown().await?; + Ok(()) + } +} + +impl Drop for ButtplugRemoteServer { + fn drop(&mut self) { + self.disconnect_notifier.notify_waiters(); + } +} diff --git a/crates/intiface_engine/src/repeater.rs b/crates/intiface_engine/src/repeater.rs new file mode 100644 index 000000000..686e5f0e7 --- /dev/null +++ b/crates/intiface_engine/src/repeater.rs @@ -0,0 +1,101 @@ +// Is this just two examples from tokio_tungstenite glued together? +// +// It absolute is! + +use futures_util::{future, StreamExt, TryStreamExt}; +use log::info; +use tokio::{ + net::{TcpListener, TcpStream}, + select, +}; +use tokio_tungstenite::connect_async; +use tokio_util::sync::CancellationToken; + +pub struct ButtplugRepeater { + local_port: u16, + remote_address: String, + stop_token: CancellationToken, +} + +impl ButtplugRepeater { + pub fn new(local_port: u16, remote_address: &str, stop_token: CancellationToken) -> Self { + Self { + local_port, + remote_address: remote_address.to_owned(), + stop_token, + } + } + + pub async fn listen(&self) { + info!("Repeater loop starting"); + let addr = format!("127.0.0.1:{}", self.local_port); + + let try_socket = TcpListener::bind(&addr).await; + let listener = try_socket.expect("Failed to bind"); + info!("Listening on: {}", addr); + + loop { + select! { + stream_result = listener.accept() => { + match stream_result { + Ok((stream, _)) => { + let mut remote_address = self.remote_address.clone(); + if !remote_address.starts_with("ws://") { + remote_address.insert_str(0, "ws://"); + } + tokio::spawn(ButtplugRepeater::accept_connection(remote_address, stream)); + }, + Err(e) => { + error!("Error accepting new websocket for repeater: {:?}", e); + break; + } + } + }, + _ = self.stop_token.cancelled() => { + info!("Repeater loop requested to stop, breaking."); + break; + } + } + } + info!("Repeater loop exiting"); + } + + async fn accept_connection(server_addr: String, stream: TcpStream) { + let client_addr = stream + .peer_addr() + .expect("connected streams should have a peer address"); + info!("Client address: {}", client_addr); + + let client_ws_stream = tokio_tungstenite::accept_async(stream) + .await + .expect("Error during the websocket handshake occurred"); + + info!("New WebSocket connection: {}", client_addr); + + info!("Connecting to server {}", server_addr); + + let server_url = url::Url::parse(&server_addr).unwrap(); + + let ws_stream = match connect_async(&server_url).await { + Ok((stream, _)) => stream, + Err(e) => { + error!("Cannot connect: {:?}", e); + return; + } + }; + info!("WebSocket handshake has been successfully completed"); + + let (server_write, server_read) = ws_stream.split(); + + let (client_write, client_read) = client_ws_stream.split(); + + let client_fut = client_read + .try_filter(|msg| future::ready(msg.is_text() || msg.is_binary())) + .forward(server_write); + let server_fut = server_read + .try_filter(|msg| future::ready(msg.is_text() || msg.is_binary())) + .forward(client_write); + future::select(client_fut, server_fut).await; + info!("Closing repeater connection."); + } +} diff --git a/rustfmt.toml b/rustfmt.toml index c51666e8f..ae89efe62 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1 +1,5 @@ -edition = "2018" \ No newline at end of file +edition = "2024" +tab_spaces = 2 +empty_item_single_line = false +imports_layout = "HorizontalVertical" +newline_style = "Native" \ No newline at end of file