├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── docs.yaml ├── .gitignore ├── README.md ├── js ├── .eslintrc.js ├── .gitignore ├── .jscsrc ├── .jshintrc ├── .npmignore ├── .prettierrc.json ├── .yarnrc.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── node-test.ts ├── package.json ├── src │ ├── client │ │ ├── ButtplugBrowserWebsocketClientConnector.ts │ │ ├── ButtplugClientConnectorException.ts │ │ ├── ButtplugClientDevice.ts │ │ ├── ButtplugNodeWebsocketClientConnector.ts │ │ ├── Client.ts │ │ └── IButtplugClientConnector.ts │ ├── core │ │ ├── Exceptions.ts │ │ ├── Logging.ts │ │ ├── MessageUtils.ts │ │ ├── Messages.ts │ │ └── index.d.ts │ ├── index.ts │ └── utils │ │ ├── ButtplugBrowserWebsocketConnector.ts │ │ ├── ButtplugMessageSorter.ts │ │ └── Utils.ts ├── tests │ ├── test-client.ts │ ├── test-logging.ts │ ├── test-messages.ts │ ├── test-messageutils.ts │ ├── test-websocketclient.ts │ └── utils.ts ├── tsconfig.json ├── tsfmt.json ├── tslint.json ├── typedocconfig.js ├── vite.config.ts ├── web-tests │ ├── test-web-library.ts │ └── web-test.html └── yarn.lock └── wasm ├── .npmignore ├── .pnp.cjs ├── .pnp.loader.mjs ├── .yarnrc.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── example ├── .gitignore ├── .pnp.cjs ├── .pnp.loader.mjs ├── index.html ├── index.js ├── javascript.svg ├── package.json └── yarn.lock ├── package.json ├── rust ├── .cargo │ └── config.toml ├── Cargo.lock ├── Cargo.toml └── src │ ├── lib.rs │ └── webbluetooth │ ├── mod.rs │ ├── webbluetooth_hardware.rs │ └── webbluetooth_manager.rs ├── src └── index.ts ├── tsconfig.json ├── vite.config.ts └── yarn.lock /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: qdot 4 | patreon: qdot 5 | ko_fi: qdot76367 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Issues related to messages or system architecture 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. Remember, this should only pertain to bugs at the protocol or general architecture level. Library or application specific bugs should be files in their respective repos. 12 | 13 | **Expected behavior** 14 | A clear and concise description of what you expected to happen. 15 | 16 | **Additional context** 17 | Add any other context about the problem here. 18 | -------------------------------------------------------------------------------- /.github/workflows/docs.yaml: -------------------------------------------------------------------------------- 1 | name: Doc Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | publish-docs: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Set Node.js 18.x 14 | uses: actions/setup-node@v3 15 | with: 16 | node-version: 18.x 17 | - name: Run install 18 | uses: borales/actions-yarn@v4 19 | with: 20 | dir: js 21 | cmd: install # will run `yarn install` command 22 | - name: Build production bundle 23 | uses: borales/actions-yarn@v4 24 | with: 25 | dir: js 26 | cmd: build:doc 27 | - name: Deploy 28 | uses: peaceiris/actions-gh-pages@v3 29 | with: 30 | github_token: ${{ secrets.GITHUB_TOKEN }} 31 | publish_dir: ./js/doc/ 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.yarn/** 2 | .yarn 3 | **/.node_modules/** 4 | **/dist/** 5 | **/pkg/** 6 | **/target/** 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Buttplug JS and WASM 2 | 3 | This repo includes 2 projects: 4 | 5 | - buttplug-js: A pure Typescript/Javascript implementation of a Buttplug Client. This can be used to 6 | connect to Buttplug Servers like [Intiface Central](https://intiface.com/central) or the Buttplug WASM Server 7 | - buttplug-wasm: A WASM compilation of the Rust implementation of the Buttplug Server, with a 8 | WebBluetooth device communication manager. This will allow connection to hardware in browsers that have WebBluetooth connectivity (Chromium/Blink based browsers like Chrome, Microsoft Edge, Brave, Opera, etc...) 9 | 10 | See the README in each of the project directories for more info on the projects. -------------------------------------------------------------------------------- /js/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:@typescript-eslint/recommended', 10 | './node_modules/gts', 11 | ], 12 | overrides: [], 13 | parser: '@typescript-eslint/parser', 14 | parserOptions: { 15 | ecmaVersion: 'latest', 16 | sourceType: 'module', 17 | }, 18 | plugins: ['@typescript-eslint'], 19 | rules: { 20 | 'node/no-unsupported-features/es-syntax': [ 21 | 'error', 22 | { ignores: ['modules'] }, 23 | ], 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /js/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # VS files 40 | *.sln 41 | *.njsproj 42 | bin/ 43 | obj/ 44 | .vs/ 45 | 46 | # dist directories 47 | dist 48 | dist-bundle 49 | 50 | # typedoc output 51 | doc/** 52 | 53 | # cert files 54 | *.pem 55 | # pkg files 56 | rpt2* 57 | # pkg output 58 | 59 | 60 | # yarn metadata 61 | .yarn -------------------------------------------------------------------------------- /js/.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "google" 3 | } -------------------------------------------------------------------------------- /js/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node":true, 3 | "browser":true, 4 | "noyield":true, 5 | "esversion": 6 6 | } -------------------------------------------------------------------------------- /js/.npmignore: -------------------------------------------------------------------------------- 1 | tests 2 | web-tests 3 | .rpt2_cache 4 | coverage 5 | build 6 | node_modules 7 | yarn-error.log 8 | .yarn -------------------------------------------------------------------------------- /js/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "auto", 3 | "singleQuote": true 4 | } -------------------------------------------------------------------------------- /js/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: false 4 | 5 | nodeLinker: node-modules 6 | -------------------------------------------------------------------------------- /js/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v3.2.2 (2024/03/02) 2 | 3 | ## Bugfixes 4 | 5 | - Actually throw exceptions when there are websocket connection issues. (#257) 6 | 7 | # v3.2.1 (2023/09/23) 8 | 9 | ## Bugfixes 10 | 11 | - Fix issue with name minifying causing problems with class name reflection (again, see v3.1.0 notes 12 | for the first round of this) 13 | 14 | # v3.2.0 (2023/09/23) 15 | 16 | ## Features 17 | 18 | - Web package building now uses vite instead of webpack 19 | - Long live vite. May I never have to fucking deal with webpack ever again. 20 | 21 | ## Bugfixes 22 | 23 | - Cleaned up naming conventions 24 | - Changed connector interface (this is technically a breaking change but afaik no one else 25 | implements their own connector) 26 | - Fix linear attribute enumeration 27 | 28 | # v3.1.1 (2023/02/18) 29 | 30 | ## Bugfixes 31 | 32 | - Replace events with eventemitter3 33 | - API Compatible and easier for building across web/node 34 | - Remove blob reading from websocket connectors 35 | - We never use binary messages on websockets, and the extra filereader dep for node was 36 | causing issues with web builds. 37 | 38 | # v3.1.0 (2023/02/11) 39 | 40 | ## Features 41 | 42 | - Added support for Node Websockets via new connector class (#244) 43 | 44 | ## Bugfixes 45 | 46 | - Fixed issues with using buttplug-js in minified projects (#246) 47 | - Changed from type-based class resolution to static naming, class name mangling should no longer 48 | be an issue. 49 | 50 | # v3.0.0 (2022/12/30) 51 | 52 | ## Features 53 | 54 | - Back to pure Typescript. Back to the good shit. 55 | - Removed server 56 | - Brought implementation in line with the FFI Client API, so minimal changes should be needed for 57 | porting from v1. 58 | 59 | # v2.0.0 60 | 61 | ## Whatever 62 | 63 | - There is no buttplug-js v2 64 | - There is no Miss Zarves 65 | - (Version skipped to align JS and C# implementation versions, which will probably fall out of 66 | alignment again very quickly) 67 | 68 | # v1.0.16 (2021/10/16) (FFI Impl) 69 | 70 | ## Bugfixes 71 | 72 | - Revert WASM loading specification to fix webpack/MIME issues 73 | 74 | # v1.0.15 (2021/08/29) (FFI Impl) 75 | 76 | ## Features 77 | 78 | - Update to buttplug v5 79 | 80 | ## Bugfixes 81 | 82 | - Allow specification of WASM loading source for buttplug_init() 83 | - ButtplugClientDevice equality testing now works as expected 84 | 85 | # v1.0.14 (2021/03/21) (FFI Impl) 86 | 87 | ## Bugfixes 88 | 89 | - Actually fix #60 and #51 90 | 91 | # v1.0.13 (2021/03/21) (FFI Impl) 92 | 93 | ## Features 94 | 95 | - Update to buttplug-rs v2.1.7, adds Lovehoney Desire Egg support 96 | 97 | ## Bugfixes 98 | 99 | - #60: Expose Buttplug.Endpoint type publicly so Raw commands are usable 100 | - #59: Client device vibrate() call should take an array of numbers 101 | - #51: Fix ScanningFinished emission for WebBluetooth 102 | 103 | # v1.0.12 (2021/02/20) (FFI Impl) 104 | 105 | ## Bugfixes 106 | 107 | - Actually build the project before publishing this time. This is why I have CI. Why am I hand 108 | publish the project? (Because I am lazy. So lazy.) 109 | 110 | # v1.0.11 (2021/02/20) 111 | 112 | ## Bugfixes 113 | 114 | - Remove publicpath setting in CDN bundler. 115 | 116 | # v1.0.10 (2021/02/20) (FFI Impl) 117 | 118 | ## Bugfixes 119 | 120 | - Implement characteristic read in WebBluetooth WASM 121 | - Allows us to use the Handy on the web 122 | 123 | # v1.0.9 (2021/02/20) (FFI Impl) 124 | 125 | ## Bugfixes 126 | 127 | - Update to buttplug-rs v2.1.5. The Handy support, client connect race bugfixes, device 128 | disconnection panic bugfixes. 129 | 130 | # v1.0.8 (2021/02/10) (FFI Impl) 131 | 132 | ## Features 133 | 134 | - Update to buttplug-rs v2.1.3, lots more tests/fixes, Lovense Diamo support 135 | - Update to buttplug-rs-ffi core v1.0.12, fixes disconnect issues in WebBluetooth, updates 136 | connector API for buttplug-rs v2.1.x API 137 | 138 | # v1.0.7 (2021/01/24) (FFI Impl) 139 | 140 | ## Bugfixes 141 | 142 | - Update to buttplug-rs v2.0.5, fixes issue with DeviceMessageInfo deserialization 143 | 144 | # v1.0.6 (2021/01/24) (FFI Impl) 145 | 146 | ## Bugfixes 147 | 148 | - Print message and bail early if buttplugInit is called again after successful load. 149 | - This most likely exited quietly without breaking anything before, but now it's at least spewing 150 | some status too. 151 | - Update to buttplug-rs v2.0.4, fixing issues with native timers being compiled instead of WASM 152 | timers. 153 | 154 | # v1.0.5 (2021/01/22) (FFI Impl) 155 | 156 | ## Bugfixes 157 | 158 | - #49: Fix issue with incorrect type check on linear commands. 159 | 160 | # v1.0.4 (2021/01/21) (FFI Impl) 161 | 162 | ## Features 163 | 164 | - Update to Buttplug-rs v2.0.3 165 | - Fixes issues with Strokers/rotators not showing up due to invalid message attributes. 166 | 167 | # v1.0.3 (2021/01/18) (FFI Impl) 168 | 169 | ## Features 170 | 171 | - Update to Buttplug-rs v2.0.0 172 | - Lovense Ferri support 173 | - Init/Event API cleanup 174 | - Panic messages/stacks now emitted on WASM panic 175 | 176 | # v1.0.2 (2021/01/10) (FFI Impl) 177 | 178 | ## Features 179 | 180 | - Update to Buttplug-rs v1.0.5, with Libo and Prettylove support 181 | 182 | # v1.0.1 (2020/12/29) (FFI Impl) 183 | 184 | ## Bugfixes 185 | 186 | - Add protobufjs to dependencies, otherwise typescript compilation files during type resolution. 187 | 188 | # v1.0.0 (2020/12/27) (FFI Impl) 189 | 190 | ## Features 191 | 192 | - Update to Buttplug v1, with new device config file format. 193 | - Change package name back to "buttplug" 194 | 195 | # v1.0.0 Beta 7 (2020/12/20) (FFI Impl) 196 | 197 | ## Bugfixes 198 | 199 | - Fix browser websockets not throwing errors on invalid URLs or connection errors. 200 | 201 | # v1.0.0 Beta 6 (2020/12/20) (FFI Impl) 202 | 203 | ## Bugfixes 204 | 205 | - Fix webpack build/load strategies for static (CDN loadable) web package. 206 | 207 | # v1.0.0 Beta 5 (2020/12/19) (FFI Impl) 208 | 209 | ## Features 210 | 211 | - Completely rewrite surface API in Typescript, now uses core protobuf library, same as the other 212 | FFI layers. 213 | - Added log output capabilities (console only at the moment). 214 | 215 | # v1.0.0 Beta 4 (2020/12/05) (FFI Impl) 216 | 217 | ## Features 218 | 219 | - Actually throw error types instead of just casting to strings. Error types are reduces from Rust's 220 | verbose enums, but this is good enough. 221 | - Add stop() method to devices. 222 | 223 | # v1.0.0 Beta 3 (2020/12/04) (FFI Impl) 224 | 225 | ## API Changes 226 | 227 | - Make a single connect method on ButtplugClient 228 | - Brings API closer to other/old implementations 229 | 230 | # Version 0.13.2 - 2020/08/25 231 | 232 | ## Bugfixes 233 | 234 | - Make Android use WebBluetooth's acceptAllDevices so Lovense shows up again 235 | - namePrefix, which we use to wildcard Lovense devices, broke in Chrome 81. 236 | Fix is tracked for Chrome 87. 237 | - Fix type mismatch in inherited methods in ForwardedDeviceProtocol. 238 | 239 | # Version 0.13.1 - 2020/04/04 240 | 241 | ## Features 242 | 243 | - Added support for Connector Initializer 244 | - Allows using the Buttplug connector for auth or other communication before 245 | spinning up the protocol itself. 246 | 247 | # Version 0.13.0 - 2020/03/29 248 | 249 | ## Features 250 | 251 | - Added Device Forwarder Support 252 | - Allows developers to create a device manager that can accept "forwarded" 253 | devices from another client. Basically turns Buttplug into a full 254 | teledildonics system, using its own protocol. 255 | - ButtplugClientDevice now emits "deviceremoved" when it is disconnected. 256 | - This is alongside the client emitting it. 257 | 258 | ## Bugfixes 259 | 260 | - Fixed WeVibe Melt support 261 | - Fixed references to buttplug-server-cli in README 262 | - This is now at https://github.com/intiface/intiface-node-cli 263 | 264 | # Version 0.12.3 - 2020/03/25 265 | 266 | ## Features 267 | 268 | - Added Hardware Support 269 | - WebGamepad Haptics on Chrome 270 | 271 | # Version 0.12.2 - 2019/12/06 272 | 273 | ## Features 274 | 275 | - Added Hardware Support 276 | - WeVibe Vector 277 | - Magic Motion Vini, Fugu, Awaken, Equinox, Solstice 278 | 279 | # Version 0.12.1 - 2019/10/05 280 | 281 | ## Features 282 | 283 | - Add Motorbunny Support 284 | 285 | # Version 0.12.0 - 2019/07/27 286 | 287 | ## Features 288 | 289 | - Allow loading of device configuration file from CDN 290 | (https://buttplug-device-config.buttplug.io) 291 | - Remove yaml requirement for device config file, just use JSON (Saves 292 | 30% library size) 293 | 294 | # Version 0.11.8 - 2019/07/09 295 | 296 | ## Bugfixes 297 | 298 | - Updated built in device config file, including Cyclone SA fixes. 299 | 300 | # Version 0.11.7 - 2019/06/22 301 | 302 | ## Bugfixes 303 | 304 | - Dependency security updates 305 | 306 | # Version 0.11.6 - 2019/05/27 307 | 308 | ## Features 309 | 310 | - Added hardware support 311 | - Kiiroo Onyx 2 312 | - Kiiroo Pearl 2 313 | - Kiiroo/OhMiBod Fuse 314 | - Kiiroo Virtual Blowbot 315 | - Kiiroo Titan 316 | - Libo PiPiJing Elle/Whale 317 | - Libo Xiao Lu (Lottie) 318 | - Libo Lu Xiao Han (Lulu) 319 | - Libo Suo Yin Qiu (Karen) 320 | - Libo Bai Hu (LaLa) 321 | - Libo/Sistalk MonsterPub 322 | - Youcups Warrior 2 323 | - Vorze Bach 324 | - A whole bunch of Magic Motion toys I'm not gonna list here. 325 | 326 | # Version 0.11.5 - 2019/05/02 327 | 328 | ## Features 329 | 330 | - Change WebBluetooth calls to work with iOS WebBLE app 331 | 332 | # Version 0.11.3 - 2019/04/11 333 | 334 | ## Features 335 | 336 | - Updates dependencies, but otherwise this is a dependent library release. 337 | 338 | # Version 0.11.2 - 2019/03/16 339 | 340 | ## Bugfixes 341 | 342 | - Roll back to using webpack for web libraries until Rollup is fixed. 343 | 344 | # Version 0.11.1 - 2019/03/15 345 | 346 | ## Features 347 | 348 | - Update CLI to work with Intiface 349 | - That's it. No other changes. Maybe this Monorepo and lockstepped 350 | versioning thing wasn't such a good idea. :/ 351 | 352 | # Version 0.11.0 - 2019/03/09 353 | 354 | ## Features 355 | 356 | - ButtplugBrowserWebsocketConnector now exported from library 357 | - Add ability to use Device Configuration files, eliminating need to 358 | change code to add devices to protocols we already support. 359 | - Add Youou Wand support 360 | 361 | ## Bugfixes 362 | 363 | - Fixed lots of unhandled promises, turning them into exception 364 | throws. Also now have a linter rule to make sure this doesn't happen 365 | again. 366 | 367 | ## Other 368 | 369 | - Moved CI to Azure Pipelines 370 | - Moved project to being a monorepo for all buttplug-js core library, 371 | device subtype manager, connector, and server CLI projects 372 | - Removed Devtools package for time being, needs to be turned into its 373 | own module. 374 | - Not currently building CLIs for windows, because noble-uwp was 375 | having some problems compiling. 376 | - Removed ConnectLocal/ConnectWebsocket functions from Client, now 377 | requires a connector object. 378 | 379 | # Version 0.10.0 - 2018/12/03 380 | 381 | - Add way to pass loggers into DeviceSubtypeManagers (to bridge module scope issues) 382 | - Fix type error for Device Manager message callbacks 383 | 384 | # Version 0.9.0 - 2018/12/02 385 | 386 | - Move core/Device to client/ButtplugClientDevice, since only client uses it. 387 | - Create convenience Device command functions on ButtplugClientDevice. 388 | - Add specific Buttplug exception types. 389 | - Fix up error handling to always throw exceptions. 390 | - Add connection semantics to server. 391 | - Update dependencies. 392 | 393 | # Version 0.8.3 - 2018/11/24 394 | 395 | - Added Lovense Osci support 396 | - Updated schema with bugfixes to generic commands 397 | 398 | # Version 0.8.2 - 2018/07/12 399 | 400 | - Add Vorze UFO SA support 401 | 402 | # Version 0.8.1 - 2018/07/02 403 | 404 | - Make DevTools/Simulator loadable as a module 405 | - Fix bug in characteristic map calculation (caused Fleshlight Launch to stop working on 0.8.0) 406 | - Fix output of speed/position values in Simulator 407 | - Fix bug in DeviceList message construction in Server 408 | - Various other Simulator fixes. 409 | 410 | # Version 0.8.0 - 2018/06/27 411 | 412 | - Add BLE GATT Characteristic reading functions 413 | - Add ability to derive Lovense hardware info from device queries (no more name/UUID chasing) 414 | - Namespace devtools CSS rules to fix issue with CSS conflicts in devtools 415 | - Change Signature of CreateSimple*Message functions (breaking change) 416 | - Add IsScanning boolean getter to Client 417 | 418 | # Version 0.7.1 - 2018/05/02 419 | 420 | - Extra build config changes to fix webpack issues 421 | 422 | # Version 0.7.0 - 2018/05/02 423 | 424 | - Rolling version number due to device API change (added "Disconnect" method) 425 | - Update to Webpack 4 426 | - Fix server cleanup on shutdown (remove listeners, disconnect devices) 427 | - Add more Lovense device names/info 428 | 429 | # Version 0.6.1 - 2018/03/08 430 | 431 | - Expose feature counts of device command messages 432 | - Add CreateSimple*Cmd functions 433 | - Add new Lovense and WeVibe device names 434 | - Device counts now start at 0 instead of 1 435 | 436 | # Version 0.6.0 - 2018/02/05 437 | 438 | - Rolling version number due to devtools API change 439 | - TestDeviceManager no longer a singleton. That was a bad idea in the first place. 440 | - TestDeviceManagerPanel now requires a ButtplugServer as a parameter 441 | - Added Connector getter in ButtplugClient, as sometimes it's handy to pull an embedded connector 442 | and get the Server from it (For things like the TestDeviceManagerPanel). 443 | - Devices now have internal IDs, so deviceadded isn't fired multiple times for the same device 444 | - Added basic MaxPro Smart Vibrator support 445 | 446 | # Version 0.5.3 - 2018/01/29 447 | 448 | - Fix bug in devtools web exports 449 | - Add more styles to log panel so outside styles don't affect it. 450 | 451 | # Version 0.5.2 - 2018/01/26 452 | 453 | - Fix webpack settings so mangling doesn't destroy parser 454 | - Add new IDs for Lovense Domi and Lush 455 | 456 | # Version 0.5.1 - 2018/01/23 457 | 458 | - Remove node websocket connector and server, since it doesn't build/include nicely as a submodule. (#87) 459 | 460 | # Version 0.5.0 - 2018/01/22 461 | 462 | - Added Buttplug Spec v1 implementation 463 | - More generic message types (VibrateCmd, RotateCmd, LinearCmd) 464 | - Message attributes (device feature counts) 465 | - Message downgrading capabilities 466 | - Added tests. So many tests. 467 | - Divided devtools into core and web directories 468 | - Updated devtools to depend on buttplug as an external library (makes file sizes smaller) 469 | - Library now uses es6 by default 470 | - Lots of bug fixes due to aforementioned tests (Wevibe control issues, missing error message, etc...) 471 | 472 | # Version 0.4.3 - 2018/01/16 473 | 474 | - Fix many logging bugs 475 | - Add more log messages to library 476 | - Add devtools module, with log viewer, test device manager, and device visualizer 477 | - Add Node websocket connector and server, for native server capabilities 478 | 479 | # Version 0.4.2 - 2018/01/08 480 | 481 | - Added support for new Lovense devices (Domi with new firmware) 482 | 483 | # Version 0.4.1 - 2018/01/07 484 | 485 | - Message types can now be accessed via getter ([Message].Type) 486 | - Client now emits "disconnect" event on disconnection (either user or server triggered) 487 | - Fixed bug where ping timer wouldn't stop on disconnect 488 | - Moved test system to jest 489 | - Removed dependency on text-encoding package 490 | - Added support for new Lovense devices (Hush with new firmware) 491 | 492 | # Version 0.4.0 - 2017/12/03 493 | 494 | - Add webpack config to build library for web on release 495 | - Expose IButtplugConnector for building external connector interfaces 496 | - Actually write usage information in the README 497 | 498 | # Version 0.3.2 - 2017/12/02 499 | 500 | - Remove dist from .gitignore in release branch. Again. Ugh. 501 | 502 | # Version 0.3.1 - 2017/12/02 503 | 504 | - Create generic connect function to allow users to define their own connectors 505 | - Documentation updates 506 | - Added more WeVibe names 507 | 508 | # Version 0.3.0 - 2017/10/29 509 | 510 | - Remove all default exports, require verbose include 511 | - Prepare library for use with node servers as well as web browser servers 512 | 513 | # Version 0.2.2 - 2017/10/28 514 | 515 | - Fix emission of "scanningfinished" event and message in client/server 516 | 517 | # Version 0.2.1 - 2017/10/11 518 | 519 | - Added WebBluetooth support for the Vorze A10 Cyclone 520 | - Fixed types in VorzeA10CycloneCmd message 521 | 522 | # Version 0.2.0 - 2017/10/08 523 | 524 | - Simplified Client types. Now one client type with Websocket and Local connection functions. 525 | - Fixed bug where outbound messages were not checked against the message schema. 526 | 527 | # Version 0.1.1 - 2017/10/06 528 | 529 | - Add Lovense Domi, WeVibe toy support to server 530 | - Add ability to query for browser Bluetooth Support in server 531 | 532 | # Version 0.1.0 - 2017/08/20 533 | 534 | - Added Server functionality, with WebBluetooth device manager 535 | - Added logging system 536 | - Fixed KiirooCmd format 537 | 538 | # Version 0.0.9 - 2017/07/22 539 | 540 | - Start cleaning up library to prepare for Server implementation 541 | - Add VorzeA10CycloneCmd message 542 | 543 | # Version 0.0.8 - 2017/07/21 544 | 545 | - Add JSON schema validation 546 | 547 | # Version 0.0.7 - 2017/07/19 548 | 549 | - Add StopAllDevices function to client 550 | 551 | # Version 0.0.6 - 2017/07/16 552 | 553 | - Update of v0.0.5 with built files included 554 | 555 | # Version 0.0.5 - 2017/07/16 556 | 557 | - Add client disconnect functionality 558 | - Test updates 559 | 560 | # Version 0.0.4 - 2017/07/13 561 | 562 | - Add ErrorCode support to error messages 563 | - tslint addition and cleanup 564 | 565 | # Version 0.0.3 - 2017/06/13 566 | 567 | - Repo cleanup, typescript library additions 568 | 569 | # Version 0.0.2 - 2017/06/11 570 | 571 | - First released version of library 572 | - Core device/message implementation 573 | - Simple webclient with ability to connect over websockets, get device lists, send device messages 574 | 575 | # Version 0.0.1 - 2016/07/08 576 | 577 | - Project Repo Started 578 | -------------------------------------------------------------------------------- /js/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-2018, Nonpolynomial Labs LLC 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of buttplug-js nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /js/README.md: -------------------------------------------------------------------------------- 1 | # Buttplug Typescript/JS Client Implementation 2 | 3 | [![Patreon donate button](https://img.shields.io/badge/patreon-donate-yellow.svg)](https://www.patreon.com/qdot) 4 | [![Github donate button](https://img.shields.io/badge/github-donate-ff69b4.svg)](https://www.github.com/sponsors/qdot) 5 | 6 | [![Discourse Forum](https://img.shields.io/badge/discourse-forum-blue.svg)](https://discuss.buttplug.io) 7 | [![Discord](https://img.shields.io/discord/353303527587708932.svg?logo=discord)](https://discord.buttplug.io) 8 | [![Twitter](https://img.shields.io/twitter/follow/buttplugio.svg?style=social&logo=twitter)](https://twitter.com/buttplugio) 9 | 10 | A implementation of the Buttplug Client in Typescript/Javascript, implementing the Version 3 11 | Buttplug Spec. It is expected to run from a browser against either [Intiface Central 12 | (GUI)](https://intiface.com/central), [Initface Engine 13 | (CLI)](https://github.com/intiface/intiface-engine), or [the Buttplug WASM Server](https://github.com/buttplugio/buttplug-js). 14 | 15 | ## Compilation information 16 | 17 | buttplug-js builds to 3 different types of library: 18 | 19 | - CommonJS for node 20 | - UMD and ES Modules for the web 21 | 22 | For node, simply include the package as you would any other package. 23 | 24 | For inclusion in web projects, the UMD project can be found at `dist/web/buttplug.js` (Note that the namespace is `buttplug`, so you'll access types like `buttplug.ButtplugClient`, etc...), and the es6 module at `dist/web/buttplug.mjs`. 25 | 26 | ## Using buttplug-js with Node 27 | 28 | buttplug-js works with both pure web builds, as well as node applications. To use buttplug-js with 29 | node, use the `ButtplugNodeWebsocketClientConnector` class instead of the 30 | `ButtplugBrowserWebsocketClientConnector` class. That should be the only change needed, all of the 31 | API stays the same. See the Documentation section for more info. 32 | 33 | (The WASM Server *does not work* with pure node applications. It requires a browser environment in order to run. See the WASM project README for more info.) 34 | 35 | ## Documentation and Examples 36 | 37 | Documentation on how to use Buttplug in general, as well as examples for buttplug-js, can be found in the [Buttplug Developer Guide](https://docs.buttplug.io/docs/dev-guide). 38 | 39 | API documentation for buttplug-js can be found at https://buttplugio.github.io/buttplug-js. 40 | 41 | If you would like to see a demo of using Buttplug in a pure web context, check out the following glitch project, which shows how to pull the Buttplug libraries from a CDN and use them in a pure HTML/JS context without node: 42 | 43 | https://glitch.com/edit/#!/how-to-buttplug 44 | 45 | ## Contributing 46 | 47 | If you have issues or feature requests, [please feel free to file an issue on this repo](issues/). 48 | 49 | We are not looking for code contributions or pull requests at this time, and will not accept pull 50 | requests that do not have a matching issue where the matter was previously discussed. Pull requests 51 | should only be submitted after talking to [qdot](https://github.com/qdot) via issues on this repo 52 | (or on [discourse](https://discuss.buttplug.io) or [discord](https://discord.buttplug.io) if you 53 | would like to stay anonymous and out of recorded info on the repo) before submitting PRs. Random PRs 54 | without matching issues and discussion are likely to be closed without merging. and receiving 55 | approval to develop code based on an issue. Any random or non-issue pull requests will most likely 56 | be closed without merging. 57 | 58 | If you'd like to contribute in a non-technical way, we need money to keep up with supporting the 59 | latest and greatest hardware. We have multiple ways to donate! 60 | 61 | - [Patreon](https://patreon.com/qdot) 62 | - [Github Sponsors](https://github.com/sponsors/qdot) 63 | - [Ko-Fi](https://ko-fi.com/qdot76367) 64 | 65 | ## License 66 | 67 | This project is BSD 3-Clause licensed. 68 | 69 | ```text 70 | 71 | Copyright (c) 2016-2023, Nonpolynomial Labs, LLC 72 | All rights reserved. 73 | 74 | Redistribution and use in source and binary forms, with or without 75 | modification, are permitted provided that the following conditions are met: 76 | 77 | * Redistributions of source code must retain the above copyright notice, this 78 | list of conditions and the following disclaimer. 79 | 80 | * Redistributions in binary form must reproduce the above copyright notice, 81 | this list of conditions and the following disclaimer in the documentation 82 | and/or other materials provided with the distribution. 83 | 84 | * Neither the name of buttplug nor the names of its 85 | contributors may be used to endorse or promote products derived from 86 | this software without specific prior written permission. 87 | 88 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 89 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 90 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 91 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 92 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 93 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 94 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 95 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 96 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 97 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 98 | ``` -------------------------------------------------------------------------------- /js/node-test.ts: -------------------------------------------------------------------------------- 1 | import { ButtplugClient, ButtplugNodeWebsocketClientConnector } from '.'; 2 | 3 | const client = new ButtplugClient('Test Client'); 4 | client.connect(new ButtplugNodeWebsocketClientConnector('ws://127.0.0.1:12345')); 5 | 6 | -------------------------------------------------------------------------------- /js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "buttplug", 3 | "version": "3.2.2", 4 | "description": "Buttplug Client Implementation for Typescript/Javascript", 5 | "homepage": "https://github.com/buttplugio/buttplug-js/", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/buttplugio/buttplug-js.git" 9 | }, 10 | "author": "Nonpolynomial Labs, LLC", 11 | "keywords": [ 12 | "teledildonics", 13 | "hardware" 14 | ], 15 | "license": "BSD-3-Clause", 16 | "bugs": { 17 | "url": "https://github.com/buttplugio/buttplug-js/issues" 18 | }, 19 | "main": "./dist/main/src/index.js", 20 | "types": "./dist/main/src/index.d.ts", 21 | "scripts": { 22 | "build": "trash dist dist-bundle && yarn build:all", 23 | "build:all": "yarn build:main && yarn build:web", 24 | "build:main": "tsc -p tsconfig.json", 25 | "build:doc": "typedoc --options typedocconfig.js --out doc ./src/index.ts", 26 | "build:web": "vite build", 27 | "pretest": "yarn build:main", 28 | "test": "jest tests/*", 29 | "web-test": "jest web-tests/test-web-library.ts", 30 | "web-test-ci": "jest --runInBand web-tests/test-web-library.ts" 31 | }, 32 | "dependencies": { 33 | "class-transformer": "^0.5.1", 34 | "eventemitter3": "^5.0.1", 35 | "reflect-metadata": "^0.2.1", 36 | "ws": "^8.16.0" 37 | }, 38 | "devDependencies": { 39 | "@types/commander": "^2.12.2", 40 | "@types/expect-puppeteer": "^5.0.6", 41 | "@types/jest": "^29.5.12", 42 | "@types/jest-environment-puppeteer": "^5.0.6", 43 | "@types/node": "^20.11.24", 44 | "@types/uuid-parse": "^1.0.2", 45 | "@types/ws": "^8.5.10", 46 | "@typescript-eslint/eslint-plugin": "^7.1.0", 47 | "@typescript-eslint/parser": "^7.1.0", 48 | "copyfiles": "^2.4.1", 49 | "cross-env": "^7.0.3", 50 | "eslint": "^8.57.0", 51 | "eslint-plugin-node": "^11.1.0", 52 | "gts": "^5.2.0", 53 | "jest": "^29.7.0", 54 | "mock-socket": "^9.3.1", 55 | "pkg": "^5.8.1", 56 | "tmp": "^0.2.3", 57 | "trash": "^8.1.1", 58 | "trash-cli": "^5.0.0", 59 | "ts-jest": "^29.1.2", 60 | "ts-node": "^10.9.2", 61 | "tslib": "^2.6.2", 62 | "typedoc": "^0.25.9", 63 | "typescript": "^5.3.3", 64 | "vite": "^5.1.4", 65 | "vite-plugin-dts": "^3.7.3", 66 | "yarn": "^1.22.21" 67 | }, 68 | "jest": { 69 | "moduleFileExtensions": [ 70 | "ts", 71 | "js", 72 | "json" 73 | ], 74 | "transform": { 75 | "^.+\\.ts$": "ts-jest" 76 | }, 77 | "testMatch": [ 78 | "/tests/**/test-*.ts", 79 | "/web-tests/**/test-*.ts" 80 | ], 81 | "coverageDirectory": "./coverage/", 82 | "coverageReporters": [ 83 | "json" 84 | ], 85 | "collectCoverage": true 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /js/src/client/ButtplugBrowserWebsocketClientConnector.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Buttplug JS Source Code File - Visit https://buttplug.io for more info about 3 | * the project. Licensed under the BSD 3-Clause license. See LICENSE file in the 4 | * project root for full license information. 5 | * 6 | * @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | import { IButtplugClientConnector } from './IButtplugClientConnector'; 12 | import { ButtplugMessage } from '../core/Messages'; 13 | import { ButtplugBrowserWebsocketConnector } from '../utils/ButtplugBrowserWebsocketConnector'; 14 | 15 | export class ButtplugBrowserWebsocketClientConnector 16 | extends ButtplugBrowserWebsocketConnector 17 | implements IButtplugClientConnector 18 | { 19 | public send = (msg: ButtplugMessage): void => { 20 | if (!this.Connected) { 21 | throw new Error('ButtplugClient not connected'); 22 | } 23 | this.sendMessage(msg); 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /js/src/client/ButtplugClientConnectorException.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Buttplug JS Source Code File - Visit https://buttplug.io for more info about 3 | * the project. Licensed under the BSD 3-Clause license. See LICENSE file in the 4 | * project root for full license information. 5 | * 6 | * @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved. 7 | */ 8 | 9 | import { ButtplugError } from '../core/Exceptions'; 10 | import * as Messages from '../core/Messages'; 11 | 12 | export class ButtplugClientConnectorException extends ButtplugError { 13 | public constructor(message: string) { 14 | super(message, Messages.ErrorClass.ERROR_UNKNOWN); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /js/src/client/ButtplugClientDevice.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Buttplug JS Source Code File - Visit https://buttplug.io for more info about 3 | * the project. Licensed under the BSD 3-Clause license. See LICENSE file in the 4 | * project root for full license information. 5 | * 6 | * @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved. 7 | */ 8 | 9 | 'use strict'; 10 | import * as Messages from '../core/Messages'; 11 | import { 12 | ButtplugDeviceError, 13 | ButtplugError, 14 | ButtplugMessageError, 15 | } from '../core/Exceptions'; 16 | import { EventEmitter } from 'eventemitter3'; 17 | import { getMessageClassFromMessage } from '../core/MessageUtils'; 18 | 19 | /** 20 | * Represents an abstract device, capable of taking certain kinds of messages. 21 | */ 22 | export class ButtplugClientDevice extends EventEmitter { 23 | /** 24 | * Return the name of the device. 25 | */ 26 | public get name(): string { 27 | return this._deviceInfo.DeviceName; 28 | } 29 | 30 | /** 31 | * Return the user set name of the device. 32 | */ 33 | public get displayName(): string | undefined { 34 | return this._deviceInfo.DeviceDisplayName; 35 | } 36 | 37 | /** 38 | * Return the index of the device. 39 | */ 40 | public get index(): number { 41 | return this._deviceInfo.DeviceIndex; 42 | } 43 | 44 | /** 45 | * Return the index of the device. 46 | */ 47 | public get messageTimingGap(): number | undefined { 48 | return this._deviceInfo.DeviceMessageTimingGap; 49 | } 50 | 51 | /** 52 | * Return a list of message types the device accepts. 53 | */ 54 | public get messageAttributes(): Messages.MessageAttributes { 55 | return this._deviceInfo.DeviceMessages; 56 | } 57 | 58 | public static fromMsg( 59 | msg: Messages.DeviceInfo, 60 | sendClosure: ( 61 | device: ButtplugClientDevice, 62 | msg: Messages.ButtplugDeviceMessage 63 | ) => Promise 64 | ): ButtplugClientDevice { 65 | return new ButtplugClientDevice(msg, sendClosure); 66 | } 67 | 68 | // Map of messages and their attributes (feature count, etc...) 69 | private allowedMsgs: Map = new Map< 70 | string, 71 | Messages.MessageAttributes 72 | >(); 73 | 74 | /** 75 | * @param _index Index of the device, as created by the device manager. 76 | * @param _name Name of the device. 77 | * @param allowedMsgs Buttplug messages the device can receive. 78 | */ 79 | constructor( 80 | private _deviceInfo: Messages.DeviceInfo, 81 | private _sendClosure: ( 82 | device: ButtplugClientDevice, 83 | msg: Messages.ButtplugDeviceMessage 84 | ) => Promise 85 | ) { 86 | super(); 87 | _deviceInfo.DeviceMessages.update(); 88 | } 89 | 90 | public async send( 91 | msg: Messages.ButtplugDeviceMessage 92 | ): Promise { 93 | // Assume we're getting the closure from ButtplugClient, which does all of 94 | // the index/existence/connection/message checks for us. 95 | return await this._sendClosure(this, msg); 96 | } 97 | 98 | public async sendExpectOk( 99 | msg: Messages.ButtplugDeviceMessage 100 | ): Promise { 101 | const response = await this.send(msg); 102 | switch (getMessageClassFromMessage(response)) { 103 | case Messages.Ok: 104 | return; 105 | case Messages.Error: 106 | throw ButtplugError.FromError(response as Messages.Error); 107 | default: 108 | throw new ButtplugMessageError( 109 | `Message type ${response.constructor} not handled by SendMsgExpectOk` 110 | ); 111 | } 112 | } 113 | 114 | public async scalar( 115 | scalar: Messages.ScalarSubcommand | Messages.ScalarSubcommand[] 116 | ): Promise { 117 | if (Array.isArray(scalar)) { 118 | await this.sendExpectOk(new Messages.ScalarCmd(scalar, this.index)); 119 | } else { 120 | await this.sendExpectOk(new Messages.ScalarCmd([scalar], this.index)); 121 | } 122 | } 123 | 124 | private async scalarCommandBuilder( 125 | speed: number | number[], 126 | actuator: Messages.ActuatorType 127 | ) { 128 | const scalarAttrs = this.messageAttributes.ScalarCmd?.filter( 129 | (x) => x.ActuatorType === actuator 130 | ); 131 | if (!scalarAttrs || scalarAttrs.length === 0) { 132 | throw new ButtplugDeviceError( 133 | `Device ${this.name} has no ${actuator} capabilities` 134 | ); 135 | } 136 | const cmds: Messages.ScalarSubcommand[] = []; 137 | if (typeof speed === 'number') { 138 | scalarAttrs.forEach((x) => 139 | cmds.push(new Messages.ScalarSubcommand(x.Index, speed, actuator)) 140 | ); 141 | } else if (Array.isArray(speed)) { 142 | if (speed.length > scalarAttrs.length) { 143 | throw new ButtplugDeviceError( 144 | `${speed.length} commands send to a device with ${scalarAttrs.length} vibrators` 145 | ); 146 | } 147 | scalarAttrs.forEach((x, i) => { 148 | cmds.push(new Messages.ScalarSubcommand(x.Index, speed[i], actuator)); 149 | }); 150 | } else { 151 | throw new ButtplugDeviceError( 152 | `${actuator} can only take numbers or arrays of numbers.` 153 | ); 154 | } 155 | await this.scalar(cmds); 156 | } 157 | 158 | public get vibrateAttributes(): Messages.GenericDeviceMessageAttributes[] { 159 | return ( 160 | this.messageAttributes.ScalarCmd?.filter( 161 | (x) => x.ActuatorType === Messages.ActuatorType.Vibrate 162 | ) ?? [] 163 | ); 164 | } 165 | 166 | public async vibrate(speed: number | number[]): Promise { 167 | await this.scalarCommandBuilder(speed, Messages.ActuatorType.Vibrate); 168 | } 169 | 170 | public get oscillateAttributes(): Messages.GenericDeviceMessageAttributes[] { 171 | return ( 172 | this.messageAttributes.ScalarCmd?.filter( 173 | (x) => x.ActuatorType === Messages.ActuatorType.Oscillate 174 | ) ?? [] 175 | ); 176 | } 177 | 178 | public async oscillate(speed: number | number[]): Promise { 179 | await this.scalarCommandBuilder(speed, Messages.ActuatorType.Oscillate); 180 | } 181 | 182 | public get rotateAttributes(): Messages.GenericDeviceMessageAttributes[] { 183 | return this.messageAttributes.RotateCmd ?? []; 184 | } 185 | 186 | public async rotate( 187 | values: number | [number, boolean][], 188 | clockwise?: boolean 189 | ): Promise { 190 | const rotateAttrs = this.messageAttributes.RotateCmd; 191 | if (!rotateAttrs || rotateAttrs.length === 0) { 192 | throw new ButtplugDeviceError( 193 | `Device ${this.name} has no Rotate capabilities` 194 | ); 195 | } 196 | let msg: Messages.RotateCmd; 197 | if (typeof values === 'number') { 198 | msg = Messages.RotateCmd.Create( 199 | this.index, 200 | new Array(rotateAttrs.length).fill([values, clockwise]) 201 | ); 202 | } else if (Array.isArray(values)) { 203 | msg = Messages.RotateCmd.Create(this.index, values); 204 | } else { 205 | throw new ButtplugDeviceError( 206 | 'SendRotateCmd can only take a number and boolean, or an array of number/boolean tuples' 207 | ); 208 | } 209 | await this.sendExpectOk(msg); 210 | } 211 | 212 | public get linearAttributes(): Messages.GenericDeviceMessageAttributes[] { 213 | return this.messageAttributes.LinearCmd ?? []; 214 | } 215 | 216 | public async linear( 217 | values: number | [number, number][], 218 | duration?: number 219 | ): Promise { 220 | const linearAttrs = this.messageAttributes.LinearCmd; 221 | if (!linearAttrs || linearAttrs.length === 0) { 222 | throw new ButtplugDeviceError( 223 | `Device ${this.name} has no Linear capabilities` 224 | ); 225 | } 226 | let msg: Messages.LinearCmd; 227 | if (typeof values === 'number') { 228 | msg = Messages.LinearCmd.Create( 229 | this.index, 230 | new Array(linearAttrs.length).fill([values, duration]) 231 | ); 232 | } else if (Array.isArray(values)) { 233 | msg = Messages.LinearCmd.Create(this.index, values); 234 | } else { 235 | throw new ButtplugDeviceError( 236 | 'SendLinearCmd can only take a number and number, or an array of number/number tuples' 237 | ); 238 | } 239 | await this.sendExpectOk(msg); 240 | } 241 | 242 | public async sensorRead( 243 | sensorIndex: number, 244 | sensorType: Messages.SensorType 245 | ): Promise { 246 | const response = await this.send( 247 | new Messages.SensorReadCmd(this.index, sensorIndex, sensorType) 248 | ); 249 | switch (getMessageClassFromMessage(response)) { 250 | case Messages.SensorReading: 251 | return (response as Messages.SensorReading).Data; 252 | case Messages.Error: 253 | throw ButtplugError.FromError(response as Messages.Error); 254 | default: 255 | throw new ButtplugMessageError( 256 | `Message type ${response.constructor} not handled by sensorRead` 257 | ); 258 | } 259 | } 260 | 261 | public get hasBattery(): boolean { 262 | const batteryAttrs = this.messageAttributes.SensorReadCmd?.filter( 263 | (x) => x.SensorType === Messages.SensorType.Battery 264 | ); 265 | return batteryAttrs !== undefined && batteryAttrs.length > 0; 266 | } 267 | 268 | public async battery(): Promise { 269 | if (!this.hasBattery) { 270 | throw new ButtplugDeviceError( 271 | `Device ${this.name} has no Battery capabilities` 272 | ); 273 | } 274 | const batteryAttrs = this.messageAttributes.SensorReadCmd?.filter( 275 | (x) => x.SensorType === Messages.SensorType.Battery 276 | ); 277 | // Find the battery sensor, we'll need its index. 278 | const result = await this.sensorRead( 279 | batteryAttrs![0].Index, 280 | Messages.SensorType.Battery 281 | ); 282 | return result[0] / 100.0; 283 | } 284 | 285 | public get hasRssi(): boolean { 286 | const rssiAttrs = this.messageAttributes.SensorReadCmd?.filter( 287 | (x) => x.SensorType === Messages.SensorType.RSSI 288 | ); 289 | return rssiAttrs !== undefined && rssiAttrs.length === 0; 290 | } 291 | 292 | public async rssi(): Promise { 293 | if (!this.hasRssi) { 294 | throw new ButtplugDeviceError( 295 | `Device ${this.name} has no RSSI capabilities` 296 | ); 297 | } 298 | const rssiAttrs = this.messageAttributes.SensorReadCmd?.filter( 299 | (x) => x.SensorType === Messages.SensorType.RSSI 300 | ); 301 | // Find the battery sensor, we'll need its index. 302 | const result = await this.sensorRead( 303 | rssiAttrs![0].Index, 304 | Messages.SensorType.RSSI 305 | ); 306 | return result[0]; 307 | } 308 | 309 | public async rawRead( 310 | endpoint: string, 311 | expectedLength: number, 312 | timeout: number 313 | ): Promise { 314 | if (!this.messageAttributes.RawReadCmd) { 315 | throw new ButtplugDeviceError( 316 | `Device ${this.name} has no raw read capabilities` 317 | ); 318 | } 319 | if (this.messageAttributes.RawReadCmd.Endpoints.indexOf(endpoint) === -1) { 320 | throw new ButtplugDeviceError( 321 | `Device ${this.name} has no raw readable endpoint ${endpoint}` 322 | ); 323 | } 324 | const response = await this.send( 325 | new Messages.RawReadCmd(this.index, endpoint, expectedLength, timeout) 326 | ); 327 | switch (getMessageClassFromMessage(response)) { 328 | case Messages.RawReading: 329 | return new Uint8Array((response as Messages.RawReading).Data); 330 | case Messages.Error: 331 | throw ButtplugError.FromError(response as Messages.Error); 332 | default: 333 | throw new ButtplugMessageError( 334 | `Message type ${response.constructor} not handled by rawRead` 335 | ); 336 | } 337 | } 338 | 339 | public async rawWrite( 340 | endpoint: string, 341 | data: Uint8Array, 342 | writeWithResponse: boolean 343 | ): Promise { 344 | if (!this.messageAttributes.RawWriteCmd) { 345 | throw new ButtplugDeviceError( 346 | `Device ${this.name} has no raw write capabilities` 347 | ); 348 | } 349 | if (this.messageAttributes.RawWriteCmd.Endpoints.indexOf(endpoint) === -1) { 350 | throw new ButtplugDeviceError( 351 | `Device ${this.name} has no raw writable endpoint ${endpoint}` 352 | ); 353 | } 354 | await this.sendExpectOk( 355 | new Messages.RawWriteCmd(this.index, endpoint, data, writeWithResponse) 356 | ); 357 | } 358 | 359 | public async rawSubscribe(endpoint: string): Promise { 360 | if (!this.messageAttributes.RawSubscribeCmd) { 361 | throw new ButtplugDeviceError( 362 | `Device ${this.name} has no raw subscribe capabilities` 363 | ); 364 | } 365 | if ( 366 | this.messageAttributes.RawSubscribeCmd.Endpoints.indexOf(endpoint) === -1 367 | ) { 368 | throw new ButtplugDeviceError( 369 | `Device ${this.name} has no raw subscribable endpoint ${endpoint}` 370 | ); 371 | } 372 | await this.sendExpectOk(new Messages.RawSubscribeCmd(this.index, endpoint)); 373 | } 374 | 375 | public async rawUnsubscribe(endpoint: string): Promise { 376 | // This reuses raw subscribe's info. 377 | if (!this.messageAttributes.RawSubscribeCmd) { 378 | throw new ButtplugDeviceError( 379 | `Device ${this.name} has no raw unsubscribe capabilities` 380 | ); 381 | } 382 | if ( 383 | this.messageAttributes.RawSubscribeCmd.Endpoints.indexOf(endpoint) === -1 384 | ) { 385 | throw new ButtplugDeviceError( 386 | `Device ${this.name} has no raw unsubscribable endpoint ${endpoint}` 387 | ); 388 | } 389 | await this.sendExpectOk( 390 | new Messages.RawUnsubscribeCmd(this.index, endpoint) 391 | ); 392 | } 393 | 394 | public async stop(): Promise { 395 | await this.sendExpectOk(new Messages.StopDeviceCmd(this.index)); 396 | } 397 | 398 | public emitDisconnected() { 399 | this.emit('deviceremoved'); 400 | } 401 | } 402 | -------------------------------------------------------------------------------- /js/src/client/ButtplugNodeWebsocketClientConnector.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Buttplug JS Source Code File - Visit https://buttplug.io for more info about 3 | * the project. Licensed under the BSD 3-Clause license. See LICENSE file in the 4 | * project root for full license information. 5 | * 6 | * @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | import { ButtplugBrowserWebsocketClientConnector } from './ButtplugBrowserWebsocketClientConnector'; 12 | import { WebSocket as NodeWebSocket } from 'ws'; 13 | 14 | export class ButtplugNodeWebsocketClientConnector extends ButtplugBrowserWebsocketClientConnector { 15 | protected _websocketConstructor = 16 | NodeWebSocket as unknown as typeof WebSocket; 17 | } 18 | -------------------------------------------------------------------------------- /js/src/client/Client.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Buttplug JS Source Code File - Visit https://buttplug.io for more info about 3 | * the project. Licensed under the BSD 3-Clause license. See LICENSE file in the 4 | * project root for full license information. 5 | * 6 | * @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | import { ButtplugLogger } from '../core/Logging'; 12 | import { EventEmitter } from 'eventemitter3'; 13 | import { ButtplugClientDevice } from './ButtplugClientDevice'; 14 | import { IButtplugClientConnector } from './IButtplugClientConnector'; 15 | import { ButtplugMessageSorter } from '../utils/ButtplugMessageSorter'; 16 | 17 | import * as Messages from '../core/Messages'; 18 | import { 19 | ButtplugDeviceError, 20 | ButtplugError, 21 | ButtplugInitError, 22 | ButtplugMessageError, 23 | } from '../core/Exceptions'; 24 | import { ButtplugClientConnectorException } from './ButtplugClientConnectorException'; 25 | import { getMessageClassFromMessage } from '../core/MessageUtils'; 26 | 27 | export class ButtplugClient extends EventEmitter { 28 | protected _pingTimer: NodeJS.Timeout | null = null; 29 | protected _connector: IButtplugClientConnector | null = null; 30 | protected _devices: Map = new Map(); 31 | protected _clientName: string; 32 | protected _logger = ButtplugLogger.Logger; 33 | protected _isScanning = false; 34 | private _sorter: ButtplugMessageSorter = new ButtplugMessageSorter(true); 35 | 36 | constructor(clientName = 'Generic Buttplug Client') { 37 | super(); 38 | this._clientName = clientName; 39 | this._logger.Debug(`ButtplugClient: Client ${clientName} created.`); 40 | } 41 | 42 | public get connected(): boolean { 43 | return this._connector !== null && this._connector.Connected; 44 | } 45 | 46 | public get devices(): ButtplugClientDevice[] { 47 | // While this function doesn't actually send a message, if we don't have a 48 | // connector, we shouldn't have devices. 49 | this.checkConnector(); 50 | const devices: ButtplugClientDevice[] = []; 51 | this._devices.forEach((d) => { 52 | devices.push(d); 53 | }); 54 | return devices; 55 | } 56 | 57 | public get isScanning(): boolean { 58 | return this._isScanning; 59 | } 60 | 61 | public connect = async (connector: IButtplugClientConnector) => { 62 | this._logger.Info( 63 | `ButtplugClient: Connecting using ${connector.constructor.name}` 64 | ); 65 | await connector.connect(); 66 | this._connector = connector; 67 | this._connector.addListener('message', this.parseMessages); 68 | this._connector.addListener('disconnect', this.disconnectHandler); 69 | await this.initializeConnection(); 70 | }; 71 | 72 | public disconnect = async () => { 73 | this._logger.Debug('ButtplugClient: Disconnect called'); 74 | this.checkConnector(); 75 | await this.shutdownConnection(); 76 | await this._connector!.disconnect(); 77 | }; 78 | 79 | public startScanning = async () => { 80 | this._logger.Debug('ButtplugClient: StartScanning called'); 81 | this._isScanning = true; 82 | await this.sendMsgExpectOk(new Messages.StartScanning()); 83 | }; 84 | 85 | public stopScanning = async () => { 86 | this._logger.Debug('ButtplugClient: StopScanning called'); 87 | this._isScanning = false; 88 | await this.sendMsgExpectOk(new Messages.StopScanning()); 89 | }; 90 | 91 | public stopAllDevices = async () => { 92 | this._logger.Debug('ButtplugClient: StopAllDevices'); 93 | await this.sendMsgExpectOk(new Messages.StopAllDevices()); 94 | }; 95 | 96 | private async sendDeviceMessage( 97 | device: ButtplugClientDevice, 98 | deviceMsg: Messages.ButtplugDeviceMessage 99 | ): Promise { 100 | this.checkConnector(); 101 | const dev = this._devices.get(device.index); 102 | if (dev === undefined) { 103 | throw ButtplugError.LogAndError( 104 | ButtplugDeviceError, 105 | this._logger, 106 | `Device ${device.index} not available.` 107 | ); 108 | } 109 | deviceMsg.DeviceIndex = device.index; 110 | return await this.sendMessage(deviceMsg); 111 | } 112 | 113 | protected disconnectHandler = () => { 114 | this._logger.Info('ButtplugClient: Disconnect event receieved.'); 115 | this.emit('disconnect'); 116 | }; 117 | 118 | protected parseMessages = (msgs: Messages.ButtplugMessage[]) => { 119 | const leftoverMsgs = this._sorter.ParseIncomingMessages(msgs); 120 | for (const x of leftoverMsgs) { 121 | switch (getMessageClassFromMessage(x)) { 122 | case Messages.DeviceAdded: { 123 | const addedMsg = x as Messages.DeviceAdded; 124 | const addedDevice = ButtplugClientDevice.fromMsg( 125 | addedMsg, 126 | this.sendDeviceMessageClosure 127 | ); 128 | this._devices.set(addedMsg.DeviceIndex, addedDevice); 129 | this.emit('deviceadded', addedDevice); 130 | break; 131 | } 132 | case Messages.DeviceRemoved: { 133 | const removedMsg = x as Messages.DeviceRemoved; 134 | if (this._devices.has(removedMsg.DeviceIndex)) { 135 | const removedDevice = this._devices.get(removedMsg.DeviceIndex); 136 | removedDevice?.emitDisconnected(); 137 | this._devices.delete(removedMsg.DeviceIndex); 138 | this.emit('deviceremoved', removedDevice); 139 | } 140 | break; 141 | } 142 | case Messages.ScanningFinished: 143 | this._isScanning = false; 144 | this.emit('scanningfinished', x); 145 | break; 146 | } 147 | } 148 | }; 149 | 150 | protected initializeConnection = async (): Promise => { 151 | this.checkConnector(); 152 | const msg = await this.sendMessage( 153 | new Messages.RequestServerInfo( 154 | this._clientName, 155 | Messages.MESSAGE_SPEC_VERSION 156 | ) 157 | ); 158 | switch (getMessageClassFromMessage(msg)) { 159 | case Messages.ServerInfo: { 160 | const serverinfo = msg as Messages.ServerInfo; 161 | this._logger.Info( 162 | `ButtplugClient: Connected to Server ${serverinfo.ServerName}` 163 | ); 164 | // TODO: maybe store server name, do something with message template version? 165 | const ping = serverinfo.MaxPingTime; 166 | if (serverinfo.MessageVersion < Messages.MESSAGE_SPEC_VERSION) { 167 | // Disconnect and throw an exception explaining the version mismatch problem. 168 | await this._connector!.disconnect(); 169 | throw ButtplugError.LogAndError( 170 | ButtplugInitError, 171 | this._logger, 172 | `Server protocol version ${serverinfo.MessageVersion} is older than client protocol version ${Messages.MESSAGE_SPEC_VERSION}. Please update server.` 173 | ); 174 | } 175 | if (ping > 0) { 176 | /* 177 | this._pingTimer = setInterval(async () => { 178 | // If we've disconnected, stop trying to ping the server. 179 | if (!this.Connected) { 180 | await this.ShutdownConnection(); 181 | return; 182 | } 183 | await this.SendMessage(new Messages.Ping()); 184 | } , Math.round(ping / 2)); 185 | */ 186 | } 187 | await this.requestDeviceList(); 188 | return true; 189 | } 190 | case Messages.Error: { 191 | // Disconnect and throw an exception with the error message we got back. 192 | // This will usually only error out if we have a version mismatch that the 193 | // server has detected. 194 | await this._connector!.disconnect(); 195 | const err = msg as Messages.Error; 196 | throw ButtplugError.LogAndError( 197 | ButtplugInitError, 198 | this._logger, 199 | `Cannot connect to server. ${err.ErrorMessage}` 200 | ); 201 | } 202 | } 203 | return false; 204 | }; 205 | 206 | protected requestDeviceList = async () => { 207 | this.checkConnector(); 208 | this._logger.Debug('ButtplugClient: ReceiveDeviceList called'); 209 | const deviceList = (await this.sendMessage( 210 | new Messages.RequestDeviceList() 211 | )) as Messages.DeviceList; 212 | deviceList.Devices.forEach((d) => { 213 | if (!this._devices.has(d.DeviceIndex)) { 214 | const device = ButtplugClientDevice.fromMsg( 215 | d, 216 | this.sendDeviceMessageClosure 217 | ); 218 | this._logger.Debug(`ButtplugClient: Adding Device: ${device}`); 219 | this._devices.set(d.DeviceIndex, device); 220 | this.emit('deviceadded', device); 221 | } else { 222 | this._logger.Debug(`ButtplugClient: Device already added: ${d}`); 223 | } 224 | }); 225 | }; 226 | 227 | protected shutdownConnection = async () => { 228 | await this.stopAllDevices(); 229 | if (this._pingTimer !== null) { 230 | clearInterval(this._pingTimer); 231 | this._pingTimer = null; 232 | } 233 | }; 234 | 235 | protected async sendMessage( 236 | msg: Messages.ButtplugMessage 237 | ): Promise { 238 | this.checkConnector(); 239 | const p = this._sorter.PrepareOutgoingMessage(msg); 240 | await this._connector!.send(msg); 241 | return await p; 242 | } 243 | 244 | protected checkConnector() { 245 | if (!this.connected) { 246 | throw new ButtplugClientConnectorException( 247 | 'ButtplugClient not connected' 248 | ); 249 | } 250 | } 251 | 252 | protected sendMsgExpectOk = async ( 253 | msg: Messages.ButtplugMessage 254 | ): Promise => { 255 | const response = await this.sendMessage(msg); 256 | switch (getMessageClassFromMessage(response)) { 257 | case Messages.Ok: 258 | return; 259 | case Messages.Error: 260 | throw ButtplugError.FromError(response as Messages.Error); 261 | default: 262 | throw ButtplugError.LogAndError( 263 | ButtplugMessageError, 264 | this._logger, 265 | `Message type ${getMessageClassFromMessage(response)!.constructor} not handled by SendMsgExpectOk` 266 | ); 267 | } 268 | }; 269 | 270 | protected sendDeviceMessageClosure = async ( 271 | device: ButtplugClientDevice, 272 | msg: Messages.ButtplugDeviceMessage 273 | ): Promise => { 274 | return await this.sendDeviceMessage(device, msg); 275 | }; 276 | } 277 | -------------------------------------------------------------------------------- /js/src/client/IButtplugClientConnector.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Buttplug JS Source Code File - Visit https://buttplug.io for more info about 3 | * the project. Licensed under the BSD 3-Clause license. See LICENSE file in the 4 | * project root for full license information. 5 | * 6 | * @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved. 7 | */ 8 | 9 | import { ButtplugMessage } from '../core/Messages'; 10 | import { EventEmitter } from 'eventemitter3'; 11 | 12 | export interface IButtplugClientConnector extends EventEmitter { 13 | connect: () => Promise; 14 | disconnect: () => Promise; 15 | initialize: () => Promise; 16 | send: (msg: ButtplugMessage) => void; 17 | readonly Connected: boolean; 18 | } 19 | -------------------------------------------------------------------------------- /js/src/core/Exceptions.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Buttplug JS Source Code File - Visit https://buttplug.io for more info about 3 | * the project. Licensed under the BSD 3-Clause license. See LICENSE file in the 4 | * project root for full license information. 5 | * 6 | * @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved. 7 | */ 8 | 9 | import * as Messages from './Messages'; 10 | import { ButtplugLogger } from './Logging'; 11 | 12 | export class ButtplugError extends Error { 13 | public get ErrorClass(): Messages.ErrorClass { 14 | return this.errorClass; 15 | } 16 | 17 | public get InnerError(): Error | undefined { 18 | return this.innerError; 19 | } 20 | 21 | public get Id(): number | undefined { 22 | return this.messageId; 23 | } 24 | 25 | public get ErrorMessage(): Messages.ButtplugMessage { 26 | return new Messages.Error(this.message, this.ErrorClass, this.Id); 27 | } 28 | 29 | public static LogAndError( 30 | constructor: new (str: string, num: number) => T, 31 | logger: ButtplugLogger, 32 | message: string, 33 | id: number = Messages.SYSTEM_MESSAGE_ID 34 | ): T { 35 | logger.Error(message); 36 | return new constructor(message, id); 37 | } 38 | 39 | public static FromError(error: Messages.Error) { 40 | switch (error.ErrorCode) { 41 | case Messages.ErrorClass.ERROR_DEVICE: 42 | return new ButtplugDeviceError(error.ErrorMessage, error.Id); 43 | case Messages.ErrorClass.ERROR_INIT: 44 | return new ButtplugInitError(error.ErrorMessage, error.Id); 45 | case Messages.ErrorClass.ERROR_UNKNOWN: 46 | return new ButtplugUnknownError(error.ErrorMessage, error.Id); 47 | case Messages.ErrorClass.ERROR_PING: 48 | return new ButtplugPingError(error.ErrorMessage, error.Id); 49 | case Messages.ErrorClass.ERROR_MSG: 50 | return new ButtplugMessageError(error.ErrorMessage, error.Id); 51 | default: 52 | throw new Error(`Message type ${error.ErrorCode} not handled`); 53 | } 54 | } 55 | 56 | public errorClass: Messages.ErrorClass = Messages.ErrorClass.ERROR_UNKNOWN; 57 | public innerError: Error | undefined; 58 | public messageId: number | undefined; 59 | 60 | protected constructor( 61 | message: string, 62 | errorClass: Messages.ErrorClass, 63 | id: number = Messages.SYSTEM_MESSAGE_ID, 64 | inner?: Error 65 | ) { 66 | super(message); 67 | this.errorClass = errorClass; 68 | this.innerError = inner; 69 | this.messageId = id; 70 | } 71 | } 72 | 73 | export class ButtplugInitError extends ButtplugError { 74 | public constructor(message: string, id: number = Messages.SYSTEM_MESSAGE_ID) { 75 | super(message, Messages.ErrorClass.ERROR_INIT, id); 76 | } 77 | } 78 | 79 | export class ButtplugDeviceError extends ButtplugError { 80 | public constructor(message: string, id: number = Messages.SYSTEM_MESSAGE_ID) { 81 | super(message, Messages.ErrorClass.ERROR_DEVICE, id); 82 | } 83 | } 84 | 85 | export class ButtplugMessageError extends ButtplugError { 86 | public constructor(message: string, id: number = Messages.SYSTEM_MESSAGE_ID) { 87 | super(message, Messages.ErrorClass.ERROR_MSG, id); 88 | } 89 | } 90 | 91 | export class ButtplugPingError extends ButtplugError { 92 | public constructor(message: string, id: number = Messages.SYSTEM_MESSAGE_ID) { 93 | super(message, Messages.ErrorClass.ERROR_PING, id); 94 | } 95 | } 96 | 97 | export class ButtplugUnknownError extends ButtplugError { 98 | public constructor(message: string, id: number = Messages.SYSTEM_MESSAGE_ID) { 99 | super(message, Messages.ErrorClass.ERROR_UNKNOWN, id); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /js/src/core/Logging.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Buttplug JS Source Code File - Visit https://buttplug.io for more info about 3 | * the project. Licensed under the BSD 3-Clause license. See LICENSE file in the 4 | * project root for full license information. 5 | * 6 | * @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved. 7 | */ 8 | 9 | import { EventEmitter } from 'eventemitter3'; 10 | 11 | export enum ButtplugLogLevel { 12 | Off, 13 | Error, 14 | Warn, 15 | Info, 16 | Debug, 17 | Trace, 18 | } 19 | 20 | /** 21 | * Representation of log messages for the internal logging utility. 22 | */ 23 | export class LogMessage { 24 | /** Timestamp for the log message */ 25 | private timestamp: string; 26 | 27 | /** Log Message */ 28 | private logMessage: string; 29 | 30 | /** Log Level */ 31 | private logLevel: ButtplugLogLevel; 32 | 33 | /** 34 | * @param logMessage Log message. 35 | * @param logLevel: Log severity level. 36 | */ 37 | public constructor(logMessage: string, logLevel: ButtplugLogLevel) { 38 | const a = new Date(); 39 | const hour = a.getHours(); 40 | const min = a.getMinutes(); 41 | const sec = a.getSeconds(); 42 | this.timestamp = `${hour}:${min}:${sec}`; 43 | this.logMessage = logMessage; 44 | this.logLevel = logLevel; 45 | } 46 | 47 | /** 48 | * Returns the log message. 49 | */ 50 | public get Message() { 51 | return this.logMessage; 52 | } 53 | 54 | /** 55 | * Returns the log message level. 56 | */ 57 | public get LogLevel() { 58 | return this.logLevel; 59 | } 60 | 61 | /** 62 | * Returns the log message timestamp. 63 | */ 64 | public get Timestamp() { 65 | return this.timestamp; 66 | } 67 | 68 | /** 69 | * Returns a formatted string with timestamp, level, and message. 70 | */ 71 | public get FormattedMessage() { 72 | return `${ButtplugLogLevel[this.logLevel]} : ${this.timestamp} : ${ 73 | this.logMessage 74 | }`; 75 | } 76 | } 77 | 78 | /** 79 | * Simple, global logging utility for the Buttplug client and server. Keeps an 80 | * internal static reference to an instance of itself (singleton pattern, 81 | * basically), and allows message logging throughout the module. 82 | */ 83 | export class ButtplugLogger extends EventEmitter { 84 | /** Singleton instance for the logger */ 85 | protected static sLogger: ButtplugLogger | undefined = undefined; 86 | /** Sets maximum log level to log to console */ 87 | protected maximumConsoleLogLevel: ButtplugLogLevel = ButtplugLogLevel.Off; 88 | /** Sets maximum log level for all log messages */ 89 | protected maximumEventLogLevel: ButtplugLogLevel = ButtplugLogLevel.Off; 90 | 91 | /** 92 | * Returns the stored static instance of the logger, creating one if it 93 | * doesn't currently exist. 94 | */ 95 | public static get Logger(): ButtplugLogger { 96 | if (ButtplugLogger.sLogger === undefined) { 97 | ButtplugLogger.sLogger = new ButtplugLogger(); 98 | } 99 | return this.sLogger!; 100 | } 101 | 102 | /** 103 | * Constructor. Can only be called internally since we regulate ButtplugLogger 104 | * ownership. 105 | */ 106 | protected constructor() { 107 | super(); 108 | } 109 | 110 | /** 111 | * Set the maximum log level to output to console. 112 | */ 113 | public get MaximumConsoleLogLevel() { 114 | return this.maximumConsoleLogLevel; 115 | } 116 | 117 | /** 118 | * Get the maximum log level to output to console. 119 | */ 120 | public set MaximumConsoleLogLevel(buttplugLogLevel: ButtplugLogLevel) { 121 | this.maximumConsoleLogLevel = buttplugLogLevel; 122 | } 123 | 124 | /** 125 | * Set the global maximum log level 126 | */ 127 | public get MaximumEventLogLevel() { 128 | return this.maximumEventLogLevel; 129 | } 130 | 131 | /** 132 | * Get the global maximum log level 133 | */ 134 | public set MaximumEventLogLevel(logLevel: ButtplugLogLevel) { 135 | this.maximumEventLogLevel = logLevel; 136 | } 137 | 138 | /** 139 | * Log new message at Error level. 140 | */ 141 | public Error(msg: string) { 142 | this.AddLogMessage(msg, ButtplugLogLevel.Error); 143 | } 144 | 145 | /** 146 | * Log new message at Warn level. 147 | */ 148 | public Warn(msg: string) { 149 | this.AddLogMessage(msg, ButtplugLogLevel.Warn); 150 | } 151 | 152 | /** 153 | * Log new message at Info level. 154 | */ 155 | public Info(msg: string) { 156 | this.AddLogMessage(msg, ButtplugLogLevel.Info); 157 | } 158 | 159 | /** 160 | * Log new message at Debug level. 161 | */ 162 | public Debug(msg: string) { 163 | this.AddLogMessage(msg, ButtplugLogLevel.Debug); 164 | } 165 | 166 | /** 167 | * Log new message at Trace level. 168 | */ 169 | public Trace(msg: string) { 170 | this.AddLogMessage(msg, ButtplugLogLevel.Trace); 171 | } 172 | 173 | /** 174 | * Checks to see if message should be logged, and if so, adds message to the 175 | * log buffer. May also print message and emit event. 176 | */ 177 | protected AddLogMessage(msg: string, level: ButtplugLogLevel) { 178 | // If nothing wants the log message we have, ignore it. 179 | if ( 180 | level > this.maximumEventLogLevel && 181 | level > this.maximumConsoleLogLevel 182 | ) { 183 | return; 184 | } 185 | const logMsg = new LogMessage(msg, level); 186 | // Clients and console logging may have different needs. For instance, it 187 | // could be that the client requests trace level, while all we want in the 188 | // console is info level. This makes sure the client can't also spam the 189 | // console. 190 | if (level <= this.maximumConsoleLogLevel) { 191 | console.log(logMsg.FormattedMessage); 192 | } 193 | if (level <= this.maximumEventLogLevel) { 194 | this.emit('log', logMsg); 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /js/src/core/MessageUtils.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Buttplug JS Source Code File - Visit https://buttplug.io for more info about 3 | * the project. Licensed under the BSD 3-Clause license. See LICENSE file in the 4 | * project root for full license information. 5 | * 6 | * @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved. 7 | */ 8 | 9 | 'use strict'; 10 | import { plainToInstance } from 'class-transformer'; 11 | import * as Messages from './Messages'; 12 | 13 | function getMessageClass( 14 | type: string 15 | ): (new (...args: unknown[]) => Messages.ButtplugMessage) | null { 16 | for (const value of Object.values(Messages)) { 17 | if (typeof value === 'function' && 'Name' in value && value.Name === type) { 18 | return value; 19 | } 20 | } 21 | return null; 22 | } 23 | 24 | export function getMessageClassFromMessage( 25 | msg: Messages.ButtplugMessage 26 | ): (new (...args: unknown[]) => Messages.ButtplugMessage) | null { 27 | // Making the bold assumption all message classes have the Name static. Should define a 28 | // requirement for this in the abstract class. 29 | return getMessageClass(Object.getPrototypeOf(msg).constructor.Name); 30 | } 31 | 32 | export function fromJSON(str): Messages.ButtplugMessage[] { 33 | const msgarray: object[] = JSON.parse(str); 34 | const msgs: Messages.ButtplugMessage[] = []; 35 | for (const x of Array.from(msgarray)) { 36 | const type = Object.getOwnPropertyNames(x)[0]; 37 | const cls = getMessageClass(type); 38 | if (cls) { 39 | const msg = plainToInstance( 40 | cls, 41 | x[type] 42 | ); 43 | msg.update(); 44 | msgs.push(msg); 45 | } 46 | } 47 | return msgs; 48 | } 49 | -------------------------------------------------------------------------------- /js/src/core/Messages.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Buttplug JS Source Code File - Visit https://buttplug.io for more info about 3 | * the project. Licensed under the BSD 3-Clause license. See LICENSE file in the 4 | * project root for full license information. 5 | * 6 | * @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved. 7 | */ 8 | 9 | // tslint:disable:max-classes-per-file 10 | 'use strict'; 11 | 12 | import { instanceToPlain, Type } from 'class-transformer'; 13 | import 'reflect-metadata'; 14 | 15 | export const SYSTEM_MESSAGE_ID = 0; 16 | export const DEFAULT_MESSAGE_ID = 1; 17 | export const MAX_ID = 4294967295; 18 | export const MESSAGE_SPEC_VERSION = 3; 19 | 20 | export class MessageAttributes { 21 | public ScalarCmd?: Array; 22 | public RotateCmd?: Array; 23 | public LinearCmd?: Array; 24 | public RawReadCmd?: RawDeviceMessageAttributes; 25 | public RawWriteCmd?: RawDeviceMessageAttributes; 26 | public RawSubscribeCmd?: RawDeviceMessageAttributes; 27 | public SensorReadCmd?: Array; 28 | public SensorSubscribeCmd?: Array; 29 | public StopDeviceCmd: {}; 30 | 31 | constructor(data: Partial) { 32 | Object.assign(this, data); 33 | } 34 | 35 | public update() { 36 | this.ScalarCmd?.forEach((x, i) => (x.Index = i)); 37 | this.RotateCmd?.forEach((x, i) => (x.Index = i)); 38 | this.LinearCmd?.forEach((x, i) => (x.Index = i)); 39 | this.SensorReadCmd?.forEach((x, i) => (x.Index = i)); 40 | this.SensorSubscribeCmd?.forEach((x, i) => (x.Index = i)); 41 | } 42 | } 43 | 44 | export enum ActuatorType { 45 | Unknown = 'Unknown', 46 | Vibrate = 'Vibrate', 47 | Rotate = 'Rotate', 48 | Oscillate = 'Oscillate', 49 | Constrict = 'Constrict', 50 | Inflate = 'Inflate', 51 | Position = 'Position', 52 | } 53 | 54 | export enum SensorType { 55 | Unknown = 'Unknown', 56 | Battery = 'Battery', 57 | RSSI = 'RSSI', 58 | Button = 'Button', 59 | Pressure = 'Pressure', 60 | // Temperature, 61 | // Accelerometer, 62 | // Gyro, 63 | } 64 | 65 | export class GenericDeviceMessageAttributes { 66 | public FeatureDescriptor: string; 67 | public ActuatorType: ActuatorType; 68 | public StepCount: number; 69 | public Index = 0; 70 | constructor(data: Partial) { 71 | Object.assign(this, data); 72 | } 73 | } 74 | 75 | export class RawDeviceMessageAttributes { 76 | constructor(public Endpoints: Array) {} 77 | } 78 | 79 | export class SensorDeviceMessageAttributes { 80 | public FeatureDescriptor: string; 81 | public SensorType: SensorType; 82 | public StepRange: Array; 83 | public Index = 0; 84 | constructor(data: Partial) { 85 | Object.assign(this, data); 86 | } 87 | } 88 | 89 | export abstract class ButtplugMessage { 90 | constructor(public Id: number) {} 91 | 92 | // tslint:disable-next-line:ban-types 93 | public get Type(): Function { 94 | return this.constructor; 95 | } 96 | 97 | public toJSON(): string { 98 | return JSON.stringify(this.toProtocolFormat()); 99 | } 100 | 101 | public toProtocolFormat(): object { 102 | const jsonObj = {}; 103 | jsonObj[(this.constructor as unknown as { Name: string }).Name] = 104 | instanceToPlain(this); 105 | return jsonObj; 106 | } 107 | 108 | public update() {} 109 | } 110 | 111 | export abstract class ButtplugDeviceMessage extends ButtplugMessage { 112 | constructor(public DeviceIndex: number, public Id: number) { 113 | super(Id); 114 | } 115 | } 116 | 117 | export abstract class ButtplugSystemMessage extends ButtplugMessage { 118 | constructor(public Id: number = SYSTEM_MESSAGE_ID) { 119 | super(Id); 120 | } 121 | } 122 | 123 | export class Ok extends ButtplugSystemMessage { 124 | static Name = 'Ok'; 125 | 126 | constructor(public Id: number = DEFAULT_MESSAGE_ID) { 127 | super(Id); 128 | } 129 | } 130 | 131 | export class Ping extends ButtplugMessage { 132 | static Name = 'Ping'; 133 | 134 | constructor(public Id: number = DEFAULT_MESSAGE_ID) { 135 | super(Id); 136 | } 137 | } 138 | 139 | export enum ErrorClass { 140 | ERROR_UNKNOWN, 141 | ERROR_INIT, 142 | ERROR_PING, 143 | ERROR_MSG, 144 | ERROR_DEVICE, 145 | } 146 | 147 | export class Error extends ButtplugMessage { 148 | static Name = 'Error'; 149 | 150 | constructor( 151 | public ErrorMessage: string, 152 | public ErrorCode: ErrorClass = ErrorClass.ERROR_UNKNOWN, 153 | public Id: number = DEFAULT_MESSAGE_ID 154 | ) { 155 | super(Id); 156 | } 157 | 158 | get Schemversion() { 159 | return 0; 160 | } 161 | } 162 | 163 | export class DeviceInfo { 164 | public DeviceIndex: number; 165 | public DeviceName: string; 166 | @Type(() => MessageAttributes) 167 | public DeviceMessages: MessageAttributes; 168 | public DeviceDisplayName?: string; 169 | public DeviceMessageTimingGap?: number; 170 | 171 | constructor(data: Partial) { 172 | Object.assign(this, data); 173 | } 174 | } 175 | 176 | export class DeviceList extends ButtplugMessage { 177 | static Name = 'DeviceList'; 178 | 179 | @Type(() => DeviceInfo) 180 | public Devices: DeviceInfo[]; 181 | public Id: number; 182 | 183 | constructor(devices: DeviceInfo[], id: number = DEFAULT_MESSAGE_ID) { 184 | super(id); 185 | this.Devices = devices; 186 | this.Id = id; 187 | } 188 | 189 | public update() { 190 | for (const device of this.Devices) { 191 | device.DeviceMessages.update(); 192 | } 193 | } 194 | } 195 | 196 | export class DeviceAdded extends ButtplugSystemMessage { 197 | static Name = 'DeviceAdded'; 198 | 199 | public DeviceIndex: number; 200 | public DeviceName: string; 201 | @Type(() => MessageAttributes) 202 | public DeviceMessages: MessageAttributes; 203 | public DeviceDisplayName?: string; 204 | public DeviceMessageTimingGap?: number; 205 | 206 | constructor(data: Partial) { 207 | super(); 208 | Object.assign(this, data); 209 | } 210 | 211 | public update() { 212 | this.DeviceMessages.update(); 213 | } 214 | } 215 | 216 | export class DeviceRemoved extends ButtplugSystemMessage { 217 | static Name = 'DeviceRemoved'; 218 | 219 | constructor(public DeviceIndex: number) { 220 | super(); 221 | } 222 | } 223 | 224 | export class RequestDeviceList extends ButtplugMessage { 225 | static Name = 'RequestDeviceList'; 226 | 227 | constructor(public Id: number = DEFAULT_MESSAGE_ID) { 228 | super(Id); 229 | } 230 | } 231 | 232 | export class StartScanning extends ButtplugMessage { 233 | static Name = 'StartScanning'; 234 | 235 | constructor(public Id: number = DEFAULT_MESSAGE_ID) { 236 | super(Id); 237 | } 238 | } 239 | 240 | export class StopScanning extends ButtplugMessage { 241 | static Name = 'StopScanning'; 242 | 243 | constructor(public Id: number = DEFAULT_MESSAGE_ID) { 244 | super(Id); 245 | } 246 | } 247 | 248 | export class ScanningFinished extends ButtplugSystemMessage { 249 | static Name = 'ScanningFinished'; 250 | 251 | constructor() { 252 | super(); 253 | } 254 | } 255 | 256 | export class RequestServerInfo extends ButtplugMessage { 257 | static Name = 'RequestServerInfo'; 258 | 259 | constructor( 260 | public ClientName: string, 261 | public MessageVersion: number = 0, 262 | public Id: number = DEFAULT_MESSAGE_ID 263 | ) { 264 | super(Id); 265 | } 266 | } 267 | 268 | export class ServerInfo extends ButtplugSystemMessage { 269 | static Name = 'ServerInfo'; 270 | 271 | constructor( 272 | public MessageVersion: number, 273 | public MaxPingTime: number, 274 | public ServerName: string, 275 | public Id: number = DEFAULT_MESSAGE_ID 276 | ) { 277 | super(); 278 | } 279 | } 280 | 281 | export class StopDeviceCmd extends ButtplugDeviceMessage { 282 | static Name = 'StopDeviceCmd'; 283 | 284 | constructor( 285 | public DeviceIndex: number = -1, 286 | public Id: number = DEFAULT_MESSAGE_ID 287 | ) { 288 | super(DeviceIndex, Id); 289 | } 290 | } 291 | 292 | export class StopAllDevices extends ButtplugMessage { 293 | static Name = 'StopAllDevices'; 294 | 295 | constructor(public Id: number = DEFAULT_MESSAGE_ID) { 296 | super(Id); 297 | } 298 | } 299 | 300 | export class GenericMessageSubcommand { 301 | protected constructor(public Index: number) {} 302 | } 303 | 304 | export class ScalarSubcommand extends GenericMessageSubcommand { 305 | constructor( 306 | Index: number, 307 | public Scalar: number, 308 | public ActuatorType: ActuatorType 309 | ) { 310 | super(Index); 311 | } 312 | } 313 | 314 | export class ScalarCmd extends ButtplugDeviceMessage { 315 | static Name = 'ScalarCmd'; 316 | 317 | constructor( 318 | public Scalars: ScalarSubcommand[], 319 | public DeviceIndex: number = -1, 320 | public Id: number = DEFAULT_MESSAGE_ID 321 | ) { 322 | super(DeviceIndex, Id); 323 | } 324 | } 325 | 326 | export class RotateSubcommand extends GenericMessageSubcommand { 327 | constructor(Index: number, public Speed: number, public Clockwise: boolean) { 328 | super(Index); 329 | } 330 | } 331 | 332 | export class RotateCmd extends ButtplugDeviceMessage { 333 | static Name = 'RotateCmd'; 334 | 335 | public static Create( 336 | deviceIndex: number, 337 | commands: [number, boolean][] 338 | ): RotateCmd { 339 | const cmdList: RotateSubcommand[] = new Array(); 340 | 341 | let i = 0; 342 | for (const [speed, clockwise] of commands) { 343 | cmdList.push(new RotateSubcommand(i, speed, clockwise)); 344 | ++i; 345 | } 346 | 347 | return new RotateCmd(cmdList, deviceIndex); 348 | } 349 | constructor( 350 | public Rotations: RotateSubcommand[], 351 | public DeviceIndex: number = -1, 352 | public Id: number = DEFAULT_MESSAGE_ID 353 | ) { 354 | super(DeviceIndex, Id); 355 | } 356 | } 357 | 358 | export class VectorSubcommand extends GenericMessageSubcommand { 359 | constructor(Index: number, public Position: number, public Duration: number) { 360 | super(Index); 361 | } 362 | } 363 | 364 | export class LinearCmd extends ButtplugDeviceMessage { 365 | static Name = 'LinearCmd'; 366 | 367 | public static Create( 368 | deviceIndex: number, 369 | commands: [number, number][] 370 | ): LinearCmd { 371 | const cmdList: VectorSubcommand[] = new Array(); 372 | 373 | let i = 0; 374 | for (const cmd of commands) { 375 | cmdList.push(new VectorSubcommand(i, cmd[0], cmd[1])); 376 | ++i; 377 | } 378 | 379 | return new LinearCmd(cmdList, deviceIndex); 380 | } 381 | constructor( 382 | public Vectors: VectorSubcommand[], 383 | public DeviceIndex: number = -1, 384 | public Id: number = DEFAULT_MESSAGE_ID 385 | ) { 386 | super(DeviceIndex, Id); 387 | } 388 | } 389 | 390 | export class SensorReadCmd extends ButtplugDeviceMessage { 391 | static Name = 'SensorReadCmd'; 392 | 393 | constructor( 394 | public DeviceIndex: number, 395 | public SensorIndex: number, 396 | public SensorType: SensorType, 397 | public Id: number = DEFAULT_MESSAGE_ID 398 | ) { 399 | super(DeviceIndex, Id); 400 | } 401 | } 402 | 403 | export class SensorReading extends ButtplugDeviceMessage { 404 | static Name = 'SensorReading'; 405 | 406 | constructor( 407 | public DeviceIndex: number, 408 | public SensorIndex: number, 409 | public SensorType: SensorType, 410 | public Data: number[], 411 | public Id: number = DEFAULT_MESSAGE_ID 412 | ) { 413 | super(DeviceIndex, Id); 414 | } 415 | } 416 | 417 | export class RawReadCmd extends ButtplugDeviceMessage { 418 | static Name = 'RawReadCmd'; 419 | 420 | constructor( 421 | public DeviceIndex: number, 422 | public Endpoint: string, 423 | public ExpectedLength: number, 424 | public Timeout: number, 425 | public Id: number = DEFAULT_MESSAGE_ID 426 | ) { 427 | super(DeviceIndex, Id); 428 | } 429 | } 430 | 431 | export class RawWriteCmd extends ButtplugDeviceMessage { 432 | static Name = 'RawWriteCmd'; 433 | 434 | constructor( 435 | public DeviceIndex: number, 436 | public Endpoint: string, 437 | public Data: Uint8Array, 438 | public WriteWithResponse: boolean, 439 | public Id: number = DEFAULT_MESSAGE_ID 440 | ) { 441 | super(DeviceIndex, Id); 442 | } 443 | } 444 | 445 | export class RawSubscribeCmd extends ButtplugDeviceMessage { 446 | static Name = 'RawSubscribeCmd'; 447 | 448 | constructor( 449 | public DeviceIndex: number, 450 | public Endpoint: string, 451 | public Id: number = DEFAULT_MESSAGE_ID 452 | ) { 453 | super(DeviceIndex, Id); 454 | } 455 | } 456 | 457 | export class RawUnsubscribeCmd extends ButtplugDeviceMessage { 458 | static Name = 'RawUnsubscribeCmd'; 459 | 460 | constructor( 461 | public DeviceIndex: number, 462 | public Endpoint: string, 463 | public Id: number = DEFAULT_MESSAGE_ID 464 | ) { 465 | super(DeviceIndex, Id); 466 | } 467 | } 468 | 469 | export class RawReading extends ButtplugDeviceMessage { 470 | static Name = 'RawReading'; 471 | 472 | constructor( 473 | public DeviceIndex: number, 474 | public Endpoint: string, 475 | public Data: number[], 476 | public Id: number = DEFAULT_MESSAGE_ID 477 | ) { 478 | super(DeviceIndex, Id); 479 | } 480 | } 481 | -------------------------------------------------------------------------------- /js/src/core/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.json' { 2 | const content: string; 3 | export default content; 4 | } 5 | -------------------------------------------------------------------------------- /js/src/index.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Buttplug JS Source Code File - Visit https://buttplug.io for more info about 3 | * the project. Licensed under the BSD 3-Clause license. See LICENSE file in the 4 | * project root for full license information. 5 | * 6 | * @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved. 7 | */ 8 | 9 | export * from './client/Client'; 10 | export * from './client/ButtplugClientDevice'; 11 | export * from './client/ButtplugBrowserWebsocketClientConnector'; 12 | export * from './client/ButtplugNodeWebsocketClientConnector'; 13 | export * from './client/ButtplugClientConnectorException'; 14 | export * from './utils/ButtplugMessageSorter'; 15 | export * from './client/IButtplugClientConnector'; 16 | export * from './core/Messages'; 17 | export * from './core/MessageUtils'; 18 | export * from './core/Logging'; 19 | export * from './core/Exceptions'; 20 | -------------------------------------------------------------------------------- /js/src/utils/ButtplugBrowserWebsocketConnector.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Buttplug JS Source Code File - Visit https://buttplug.io for more info about 3 | * the project. Licensed under the BSD 3-Clause license. See LICENSE file in the 4 | * project root for full license information. 5 | * 6 | * @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | import { EventEmitter } from 'eventemitter3'; 12 | import { ButtplugMessage } from '../core/Messages'; 13 | import { fromJSON } from '../core/MessageUtils'; 14 | 15 | export class ButtplugBrowserWebsocketConnector extends EventEmitter { 16 | protected _ws: WebSocket | undefined; 17 | protected _websocketConstructor: typeof WebSocket | null = null; 18 | 19 | public constructor(private _url: string) { 20 | super(); 21 | } 22 | 23 | public get Connected(): boolean { 24 | return this._ws !== undefined; 25 | } 26 | 27 | public connect = async (): Promise => { 28 | return new Promise((resolve, reject) => { 29 | const ws = new (this._websocketConstructor ?? WebSocket)(this._url); 30 | const onErrorCallback = (event: Event) => {reject(event)} 31 | const onCloseCallback = (event: CloseEvent) => reject(event.reason) 32 | ws.addEventListener('open', async () => { 33 | this._ws = ws; 34 | try { 35 | await this.initialize(); 36 | this._ws.addEventListener('message', (msg) => { 37 | this.parseIncomingMessage(msg); 38 | }); 39 | this._ws.removeEventListener('close', onCloseCallback); 40 | this._ws.removeEventListener('error', onErrorCallback); 41 | this._ws.addEventListener('close', this.disconnect); 42 | resolve(); 43 | } catch (e) { 44 | reject(e); 45 | } 46 | }); 47 | // In websockets, our error rarely tells us much, as for security reasons 48 | // browsers usually only throw Error Code 1006. It's up to those using this 49 | // library to state what the problem might be. 50 | 51 | ws.addEventListener('error', onErrorCallback) 52 | ws.addEventListener('close', onCloseCallback); 53 | }); 54 | }; 55 | 56 | public disconnect = async (): Promise => { 57 | if (!this.Connected) { 58 | return; 59 | } 60 | this._ws!.close(); 61 | this._ws = undefined; 62 | this.emit('disconnect'); 63 | }; 64 | 65 | public sendMessage(msg: ButtplugMessage) { 66 | if (!this.Connected) { 67 | throw new Error('ButtplugBrowserWebsocketConnector not connected'); 68 | } 69 | this._ws!.send('[' + msg.toJSON() + ']'); 70 | } 71 | 72 | public initialize = async (): Promise => { 73 | return Promise.resolve(); 74 | }; 75 | 76 | protected parseIncomingMessage(event: MessageEvent) { 77 | if (typeof event.data === 'string') { 78 | const msgs = fromJSON(event.data); 79 | this.emit('message', msgs); 80 | } else if (event.data instanceof Blob) { 81 | // No-op, we only use text message types. 82 | } 83 | } 84 | 85 | protected onReaderLoad(event: Event) { 86 | const msgs = fromJSON((event.target as FileReader).result); 87 | this.emit('message', msgs); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /js/src/utils/ButtplugMessageSorter.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Buttplug JS Source Code File - Visit https://buttplug.io for more info about 3 | * the project. Licensed under the BSD 3-Clause license. See LICENSE file in the 4 | * project root for full license information. 5 | * 6 | * @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved. 7 | */ 8 | 9 | import * as Messages from '../core/Messages'; 10 | import { ButtplugError } from '../core/Exceptions'; 11 | 12 | export class ButtplugMessageSorter { 13 | protected _counter = 1; 14 | protected _waitingMsgs: Map< 15 | number, 16 | [(val: Messages.ButtplugMessage) => void, (err: Error) => void] 17 | > = new Map(); 18 | 19 | public constructor(private _useCounter: boolean) {} 20 | 21 | // One of the places we should actually return a promise, as we need to store 22 | // them while waiting for them to return across the line. 23 | // tslint:disable:promise-function-async 24 | public PrepareOutgoingMessage( 25 | msg: Messages.ButtplugMessage 26 | ): Promise { 27 | if (this._useCounter) { 28 | msg.Id = this._counter; 29 | // Always increment last, otherwise we might lose sync 30 | this._counter += 1; 31 | } 32 | let res; 33 | let rej; 34 | const msgPromise = new Promise( 35 | (resolve, reject) => { 36 | res = resolve; 37 | rej = reject; 38 | } 39 | ); 40 | this._waitingMsgs.set(msg.Id, [res, rej]); 41 | return msgPromise; 42 | } 43 | 44 | public ParseIncomingMessages( 45 | msgs: Messages.ButtplugMessage[] 46 | ): Messages.ButtplugMessage[] { 47 | const noMatch: Messages.ButtplugMessage[] = []; 48 | for (const x of msgs) { 49 | if (x.Id !== Messages.SYSTEM_MESSAGE_ID && this._waitingMsgs.has(x.Id)) { 50 | const [res, rej] = this._waitingMsgs.get(x.Id)!; 51 | // If we've gotten back an error, reject the related promise using a 52 | // ButtplugException derived type. 53 | if (x.Type === Messages.Error) { 54 | rej(ButtplugError.FromError(x as Messages.Error)); 55 | continue; 56 | } 57 | res(x); 58 | continue; 59 | } else { 60 | noMatch.push(x); 61 | } 62 | } 63 | return noMatch; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /js/src/utils/Utils.ts: -------------------------------------------------------------------------------- 1 | export function getRandomInt(max: number) { 2 | return Math.floor(Math.random() * Math.floor(max)); 3 | } 4 | -------------------------------------------------------------------------------- /js/tests/test-client.ts: -------------------------------------------------------------------------------- 1 | /* 2 | import { ButtplugClient, ButtplugLogger, 3 | ButtplugServer, ButtplugEmbeddedClientConnector } from "../src/index"; 4 | import { TestDeviceSubtypeManager } from "../src/test/TestDeviceSubtypeManager"; 5 | import * as Messages from "../src/core/Messages"; 6 | import { BPTestClient, SetupTestSuite, SetupTestServer } from "./utils"; 7 | import { ButtplugMessageError, ButtplugDeviceError } from "../src/core/Errors"; 8 | 9 | SetupTestSuite(); 10 | 11 | describe("Client Tests", () => { 12 | let p; 13 | let res; 14 | let rej; 15 | 16 | beforeEach(() => { 17 | p = new Promise((resolve, reject) => { res = resolve; rej = reject; }); 18 | }); 19 | 20 | async function SetupServer(): Promise { 21 | const bp = new BPTestClient("Test Buttplug Client"); 22 | await bp.Connect(new ButtplugEmbeddedClientConnector()); 23 | return bp; 24 | } 25 | 26 | it("Should return a test message.", async () => { 27 | const bp = await SetupServer(); 28 | await expect(bp.SendMessage(new Messages.Test("Test"))) 29 | .resolves 30 | .toEqual(new Messages.Test("Test")); 31 | }); 32 | 33 | it("Should emit a log message on requestlog (testing basic event emitters)", async () => { 34 | const bp = await SetupServer(); 35 | await bp.RequestLog("Error"); 36 | let called = false; 37 | process.nextTick(() => { 38 | bp.on("log", async (x) => { 39 | // This will fire if we get another log message after turning things off. 40 | if (called) { 41 | rej(); 42 | } 43 | called = true; 44 | expect(x).toEqual(new Messages.Log("Error", "Test")); 45 | // Turn logging events back off. 46 | await bp.RequestLog("Off"); 47 | // Make sure we don't get called again. 48 | ButtplugLogger.Logger.Error("Test"); 49 | bp.removeAllListeners(); 50 | res(); 51 | }); 52 | // We shouldn't see this one. 53 | ButtplugLogger.Logger.Trace("Test"); 54 | ButtplugLogger.Logger.Error("Test"); 55 | }); 56 | return p; 57 | }); 58 | 59 | it("Should emit a device on addition", async () => { 60 | const connector = await SetupTestServer(); 61 | const tdm = connector.TestDeviceManager; 62 | const bpClient = connector.Client; 63 | bpClient.on("deviceadded", (x) => { 64 | tdm.VibrationDevice.Disconnect(); 65 | }); 66 | bpClient.on("deviceremoved", (x) => { 67 | res(); 68 | }); 69 | await bpClient.StartScanning(); 70 | return p; 71 | }); 72 | 73 | it("Should emit a device on connection when device already attached", async () => { 74 | const client = new ButtplugClient("Test Client"); 75 | client.on("deviceadded", (x) => { 76 | res(); 77 | }); 78 | const server = new ButtplugServer("Test Server"); 79 | const tdm = new TestDeviceSubtypeManager(); 80 | server.AddDeviceManager(tdm); 81 | tdm.ConnectLinearDevice(); 82 | const localConnector = new ButtplugEmbeddedClientConnector(); 83 | localConnector.Server = server; 84 | await client.Connect(localConnector); 85 | return p; 86 | }); 87 | 88 | it("Should emit when device scanning is over", async () => { 89 | const bp = (await SetupTestServer()).Client; 90 | bp.on("scanningfinished", (x) => { 91 | bp.removeAllListeners("scanningfinished"); 92 | res(); 93 | }); 94 | await bp.StartScanning(); 95 | return p; 96 | }); 97 | 98 | it("Should allow correct device messages and reject unauthorized", async () => { 99 | const bp = (await SetupTestServer()).Client; 100 | 101 | bp.on("scanningfinished", async () => { 102 | try { 103 | await bp.Devices[0].SendVibrateCmd(1); 104 | } catch (e) { 105 | rej(); 106 | } 107 | 108 | await expect(bp.SendDeviceMessage(bp.Devices[0], new Messages.KiirooCmd(2))) 109 | .rejects 110 | .toBeInstanceOf(ButtplugDeviceError); 111 | res(); 112 | }); 113 | await bp.StartScanning(); 114 | return p; 115 | }); 116 | 117 | it("Should reject schema violating message", async () => { 118 | const bp: ButtplugClient = (await SetupTestServer()).Client; 119 | bp.on("scanningfinished", async (x) => { 120 | expect(bp.Devices.length).toBeGreaterThan(0); 121 | await expect(bp.SendDeviceMessage(bp.Devices[0], new Messages.SingleMotorVibrateCmd(50))) 122 | .rejects 123 | .toBeInstanceOf(ButtplugMessageError); 124 | res(); 125 | }); 126 | await bp.StartScanning(); 127 | return p; 128 | }); 129 | 130 | it("Should receive disconnect event on disconnect", async () => { 131 | const bplocal = new ButtplugClient("Test Client"); 132 | bplocal.addListener("disconnect", () => { res(); }); 133 | await bplocal.Connect(new ButtplugEmbeddedClientConnector()); 134 | await bplocal.Disconnect(); 135 | return p; 136 | }); 137 | 138 | it("Should shut down ping timer on disconnect", async () => { 139 | const bplocal = new BPTestClient("Test Client"); 140 | bplocal.addListener("disconnect", () => { 141 | expect(bplocal.PingTimer).toEqual(null); 142 | res(); 143 | }); 144 | await bplocal.Connect(new ButtplugEmbeddedClientConnector()); 145 | await bplocal.Disconnect(); 146 | return p; 147 | }); 148 | 149 | it("Should get error on scanning when no device managers available.", async () => { 150 | const bplocal = new ButtplugClient("Test Client"); 151 | await bplocal.Connect(new ButtplugEmbeddedClientConnector()); 152 | await expect(bplocal.StartScanning()).rejects.toBeInstanceOf(ButtplugDeviceError); 153 | }); 154 | }); 155 | */ -------------------------------------------------------------------------------- /js/tests/test-logging.ts: -------------------------------------------------------------------------------- 1 | import { ButtplugLogLevel, ButtplugLogger } from "../src/core/Logging"; 2 | import { SetupTestSuite } from "./utils"; 3 | 4 | SetupTestSuite(); 5 | 6 | describe("Logging Tests", () => { 7 | class TestLogger extends ButtplugLogger { 8 | public static ResetLogger() { 9 | ButtplugLogger.sLogger = new ButtplugLogger(); 10 | } 11 | } 12 | 13 | let logger = TestLogger.Logger; 14 | 15 | beforeEach(() => { 16 | TestLogger.ResetLogger(); 17 | logger = TestLogger.Logger; 18 | }); 19 | 20 | it("Should log nothing at start.", async () => { 21 | let res; 22 | let rej; 23 | const p = new Promise((rs, rj) => { res = rs; rej = rj; }); 24 | logger.addListener("log", (msg) => { 25 | rej(); 26 | }); 27 | logger.Debug("test"); 28 | logger.Error("test"); 29 | logger.Warn("test"); 30 | logger.Info("test"); 31 | logger.Trace("test"); 32 | res(); 33 | return p; 34 | }); 35 | 36 | it("Should log everything on trace.", async () => { 37 | let res; 38 | let rej; 39 | let count = 0; 40 | const p = new Promise((rs, rj) => { res = rs; rej = rj; }); 41 | logger.MaximumEventLogLevel = ButtplugLogLevel.Trace; 42 | 43 | logger.addListener("log", (msg) => { 44 | count++; 45 | }); 46 | logger.Debug("test"); 47 | logger.Error("test"); 48 | logger.Warn("test"); 49 | logger.Info("test"); 50 | logger.Trace("test"); 51 | 52 | if (count === 5) { 53 | return Promise.resolve(); 54 | } 55 | return Promise.reject("Log event count incorrect!"); 56 | }); 57 | 58 | it("Should deal with different log levels for console and events", async () => { 59 | jest.spyOn(global.console, "log"); 60 | let res; 61 | let rej; 62 | const p = new Promise((rs, rj) => { res = rs; rej = rj; }); 63 | logger.addListener("log", (msg) => { 64 | rej(); 65 | }); 66 | logger.MaximumEventLogLevel = ButtplugLogLevel.Debug; 67 | logger.MaximumConsoleLogLevel = ButtplugLogLevel.Trace; 68 | logger.Trace("test"); 69 | expect(console.log).toBeCalled(); 70 | res(); 71 | return p; 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /js/tests/test-messages.ts: -------------------------------------------------------------------------------- 1 | import * as Messages from "../src/core/Messages"; 2 | import { ButtplugClient } from "../src/client/Client"; 3 | import { fromJSON } from "../src/core/MessageUtils"; 4 | import { SetupTestSuite } from "./utils"; 5 | import { ScalarSubcommand, VectorSubcommand, RotateSubcommand } from "../src/core/Messages"; 6 | 7 | SetupTestSuite(); 8 | 9 | describe("Message", () => { 10 | it("Converts ok message to json correctly", 11 | () => { 12 | const ok = new Messages.Ok(2); 13 | expect(ok.toJSON()).toEqual('{"Ok":{"Id":2}}'); 14 | }); 15 | it("Converts ok message from json correctly", 16 | () => { 17 | const jsonStr = '[{"Ok":{"Id":2}}]'; 18 | expect(fromJSON(jsonStr)).toEqual([new Messages.Ok(2)]); 19 | }); 20 | it("Converts DeviceList message from json correctly", 21 | () => { 22 | // tslint:disable-next-line:max-line-length 23 | const jsonStr = ` 24 | [ 25 | { 26 | "DeviceList": { 27 | "Id": 1, 28 | "Devices": [ 29 | { 30 | "DeviceName": "Test Vibrator", 31 | "DeviceIndex": 0, 32 | "DeviceMessages": { 33 | "ScalarCmd": [ 34 | { 35 | "StepCount": 20, 36 | "FeatureDescriptor": "Clitoral Stimulator", 37 | "ActuatorType": "Vibrate" 38 | }, 39 | { 40 | "StepCount": 20, 41 | "FeatureDescriptor": "Insertable Vibrator", 42 | "ActuatorType": "Vibrate" 43 | } 44 | ], 45 | "StopDeviceCmd": {} 46 | } 47 | }, 48 | { 49 | "DeviceName": "Test Stroker", 50 | "DeviceIndex": 1, 51 | "DeviceMessageTimingGap": 100, 52 | "DeviceDisplayName": "User set name", 53 | "DeviceMessages": { 54 | "LinearCmd": [ { 55 | "StepCount": 100, 56 | "ActuatorType": "Position", 57 | "FeatureDescriptor": "Stroker" 58 | } ], 59 | "StopDeviceCmd": {} 60 | } 61 | } 62 | ] 63 | } 64 | } 65 | ] 66 | `; 67 | expect(fromJSON(jsonStr)) 68 | .toEqual( 69 | [ 70 | new Messages.DeviceList( 71 | [ 72 | new Messages.DeviceInfo({ 73 | DeviceIndex: 0, 74 | DeviceName: "Test Vibrator", 75 | DeviceMessages: 76 | new Messages.MessageAttributes({ 77 | ScalarCmd: [ 78 | new Messages.GenericDeviceMessageAttributes({ FeatureDescriptor: "Clitoral Stimulator", ActuatorType: Messages.ActuatorType.Vibrate, StepCount: 20, Index: 0 }), 79 | new Messages.GenericDeviceMessageAttributes({ FeatureDescriptor: "Insertable Vibrator", ActuatorType: Messages.ActuatorType.Vibrate, StepCount: 20, Index: 1 }), 80 | ], 81 | StopDeviceCmd: {} 82 | })}), 83 | new Messages.DeviceInfo({ 84 | DeviceIndex: 1, 85 | DeviceName: "Test Stroker", 86 | DeviceMessages: 87 | new Messages.MessageAttributes({ 88 | LinearCmd: [new Messages.GenericDeviceMessageAttributes({ FeatureDescriptor: "Stroker", ActuatorType: Messages.ActuatorType.Position, StepCount: 100 })], 89 | StopDeviceCmd: {} 90 | }), DeviceDisplayName: "User set name", DeviceMessageTimingGap: 100}) 91 | ], 92 | 1) 93 | ]); 94 | }); 95 | it("Converts DeviceAdded message from json correctly", 96 | () => { 97 | const jsonStr = ` 98 | [ 99 | { 100 | "DeviceAdded": { 101 | "Id": 0, 102 | "DeviceName": "Test Vibrator", 103 | "DeviceIndex": 0, 104 | "DeviceMessageTimingGap": 100, 105 | "DeviceDisplayName": "Rabbit Vibrator", 106 | "DeviceMessages": { 107 | "ScalarCmd": [ 108 | { 109 | "StepCount": 20, 110 | "FeatureDescriptor": "Clitoral Stimulator", 111 | "ActuatorType": "Vibrate" 112 | }, 113 | { 114 | "StepCount": 20, 115 | "FeatureDescriptor": "Insertable Vibrator", 116 | "ActuatorType": "Vibrate" 117 | } 118 | ], 119 | "StopDeviceCmd": {} 120 | } 121 | } 122 | } 123 | ]`; 124 | expect(fromJSON(jsonStr)[0] as Messages.DeviceAdded) 125 | .toEqual( 126 | new Messages.DeviceAdded({ 127 | DeviceIndex: 0, 128 | DeviceName: "Test Vibrator", 129 | DeviceMessages: 130 | new Messages.MessageAttributes({ 131 | ScalarCmd: [ 132 | new Messages.GenericDeviceMessageAttributes({ FeatureDescriptor: "Clitoral Stimulator", ActuatorType: Messages.ActuatorType.Vibrate, StepCount: 20 }), 133 | new Messages.GenericDeviceMessageAttributes({ FeatureDescriptor: "Insertable Vibrator", ActuatorType: Messages.ActuatorType.Vibrate, StepCount: 20, Index: 1 }) 134 | ], 135 | StopDeviceCmd: {} 136 | }), DeviceDisplayName: "Rabbit Vibrator", DeviceMessageTimingGap: 100}) 137 | ); 138 | }); 139 | it("Converts Error message from json correctly", 140 | () => { 141 | const jsonStr = '[{"Error":{"Id":2,"ErrorCode":3,"ErrorMessage":"TestError"}}]'; 142 | expect(fromJSON(jsonStr)).toEqual([new Messages.Error("TestError", 143 | Messages.ErrorClass.ERROR_MSG, 144 | 2)]); 145 | }); 146 | /* 147 | it("Handles Device Commands with Subcommand arrays correctly", 148 | () => { 149 | const jsonStr = '[{"VibrateCmd":{"Id":2, "DeviceIndex": 3, "Speeds": [{ "Index": 0, "Speed": 1.0}, {"Index": 1, "Speed": 0.5}]}}]'; 150 | expect(fromJSON(jsonStr)).toEqual([new Messages.VibrateCmd([{Index: 0, Speed: 1.0}, {Index: 1, Speed: 0.5}], 3, 2)]); 151 | }); 152 | */ 153 | }); 154 | -------------------------------------------------------------------------------- /js/tests/test-messageutils.ts: -------------------------------------------------------------------------------- 1 | import { SetupTestSuite } from "./utils"; 2 | import {ButtplugClientDevice, RotateCmd, LinearCmd, VectorSubcommand, 3 | RotateSubcommand, StopDeviceCmd, ButtplugDeviceMessage, ButtplugDeviceError } from "../src/index"; 4 | 5 | SetupTestSuite(); 6 | /* 7 | describe("Message Utils Tests", () => { 8 | 9 | let lastMsg: ButtplugDeviceMessage; 10 | const testDevice = new ButtplugClientDevice(0, "Test Device", { 11 | VibrateCmd: { FeatureCount: 2 }, 12 | RotateCmd: { FeatureCount: 1 }, 13 | LinearCmd: { FeatureCount: 1 }, 14 | StopDeviceCmd: {}, 15 | }, async (device, msg) => { lastMsg = msg; }); 16 | 17 | const testVibrateDevice = new ButtplugClientDevice(0, "Test Vibrate Device", { 18 | VibrateCmd: { FeatureCount: 2 }, 19 | }, async (device, msg) => { lastMsg = msg; }); 20 | const testRotateDevice = new ButtplugClientDevice(0, "Test Rotate Device", { 21 | RotateCmd: { FeatureCount: 1 }, 22 | }, async (device, msg) => { lastMsg = msg; }); 23 | const testLinearDevice = new ButtplugClientDevice(0, "Test Linear Device", { 24 | LinearCmd: { FeatureCount: 1 }, 25 | }, async (device, msg) => { lastMsg = msg; }); 26 | 27 | it("should create correct message using internal functions", async () => { 28 | await testDevice.SendVibrateCmd(0.5); 29 | expect(lastMsg).toEqual(new VibrateCmd([new SpeedSubcommand(0, 0.5), 30 | new SpeedSubcommand(1, 0.5)], 31 | 0)); 32 | 33 | await testDevice.SendVibrateCmd([0.5, 1.0]); 34 | expect(lastMsg).toEqual(new VibrateCmd([new SpeedSubcommand(0, 0.5), 35 | new SpeedSubcommand(1, 1.0)], 36 | 0)); 37 | 38 | await testDevice.SendRotateCmd(0.5, true); 39 | expect(lastMsg).toEqual(new RotateCmd([new RotateSubcommand(0, 0.5, true)], 40 | 0)); 41 | 42 | await testDevice.SendRotateCmd([[0.5, true]]); 43 | expect(lastMsg).toEqual(new RotateCmd([new RotateSubcommand(0, 0.5, true)], 44 | 0)); 45 | 46 | await testDevice.SendLinearCmd(0.5, 1.5); 47 | expect(lastMsg).toEqual(new LinearCmd([new VectorSubcommand(0, 0.5, 1.5)], 48 | 0)); 49 | 50 | await testDevice.SendLinearCmd([[0.5, 1.5]]); 51 | expect(lastMsg).toEqual(new LinearCmd([new VectorSubcommand(0, 0.5, 1.5)], 52 | 0)); 53 | 54 | await testDevice.SendStopDeviceCmd(); 55 | expect(lastMsg).toEqual(new StopDeviceCmd(0)); 56 | }); 57 | 58 | it("should throw on wrong allowed messages", async () => { 59 | await expect(testRotateDevice.SendVibrateCmd(0.5)).rejects.toBeInstanceOf(ButtplugDeviceError); 60 | await expect(testVibrateDevice.SendRotateCmd(0.5, true)).rejects.toBeInstanceOf(ButtplugDeviceError); 61 | await expect(testVibrateDevice.SendLinearCmd(0.5, 1.0)).rejects.toBeInstanceOf(ButtplugDeviceError); 62 | }); 63 | 64 | it("should reject on out of bounds arguments", async () => { 65 | await expect(testVibrateDevice.SendVibrateCmd([0.5, 0.5, 0.5])).rejects.toBeInstanceOf(ButtplugDeviceError); 66 | }); 67 | }); 68 | */ -------------------------------------------------------------------------------- /js/tests/test-websocketclient.ts: -------------------------------------------------------------------------------- 1 | import { Server, WebSocket, Client } from "mock-socket"; 2 | import { ButtplugClient } from "../src/client/Client"; 3 | import * as Messages from "../src/core/Messages"; 4 | import { ButtplugLogLevel } from "../src/core/Logging"; 5 | import { fromJSON } from "../src/core/MessageUtils"; 6 | import { SetupTestSuite } from "./utils"; 7 | import { ButtplugMessageError, ButtplugBrowserWebsocketClientConnector } from "../src"; 8 | 9 | SetupTestSuite(); 10 | 11 | describe("Websocket Client Tests", () => { 12 | let mockServer: Server; 13 | let socket: Client; 14 | let bp: ButtplugClient; 15 | let p; 16 | let res; 17 | let rej; 18 | class BPTestClient extends ButtplugClient { 19 | constructor(ClientName: string) { 20 | super(ClientName); 21 | } 22 | public get PingTimer() { 23 | return this._pingTimer; 24 | } 25 | } 26 | beforeEach(async () => { 27 | mockServer = new Server("ws://localhost:6868"); 28 | p = new Promise((resolve, reject) => { res = resolve; rej = reject; }); 29 | const serverInfo = (jsonmsg: string) => { 30 | const msg: Messages.ButtplugMessage = fromJSON(jsonmsg)[0] as Messages.ButtplugMessage; 31 | if (msg.Type === Messages.RequestServerInfo) { 32 | delaySend(new Messages.ServerInfo(3, 0, "Test Server", msg.Id)); 33 | } 34 | if (msg.Type === Messages.RequestDeviceList) { 35 | delaySend(new Messages.DeviceList([], msg.Id)); 36 | // (socket as any).removeListener("message", serverInfo); 37 | } 38 | }; 39 | mockServer.on("connection", (connectedSocket: Client) => { 40 | socket = connectedSocket; 41 | // TODO Bug in typescript defs for mock-socket 8 means we can't use the 42 | // socket type as it was meant. See 43 | // https://github.com/thoov/mock-socket/issues/224 44 | (socket as any).on("message", (data: string) => serverInfo(data)); 45 | }); 46 | bp = new ButtplugClient("Test Buttplug Client"); 47 | await bp.connect(new ButtplugBrowserWebsocketClientConnector("ws://localhost:6868")); 48 | }); 49 | 50 | afterEach(function(done) { 51 | mockServer.stop(done); 52 | }); 53 | 54 | function delaySend(msg: Messages.ButtplugMessage) { 55 | process.nextTick(() => socket.send("[" + msg.toJSON() + "]")); 56 | } 57 | 58 | it("Should deal with request/reply correctly", async () => { 59 | (socket as any).on("message", (jsonmsg: string) => { 60 | const msg: Messages.ButtplugMessage = fromJSON(jsonmsg)[0] as Messages.ButtplugMessage; 61 | delaySend(new Messages.Ok(msg.Id)); 62 | }); 63 | await bp.startScanning(); 64 | await bp.stopScanning(); 65 | }); 66 | 67 | it("Should receive disconnect event on websocket disconnect", async () => { 68 | bp.addListener("disconnect", () => { res(); }); 69 | mockServer.close(); 70 | return p; 71 | }); 72 | 73 | it("Should throw Error on return of error message", async () => { 74 | (socket as any).on("message", (jsonmsg: string) => { 75 | const msg: Messages.ButtplugMessage = fromJSON(jsonmsg)[0] as Messages.ButtplugMessage; 76 | if (msg.Type === Messages.StopAllDevices) { 77 | delaySend(new Messages.Error("Error", Messages.ErrorClass.ERROR_MSG, msg.Id)); 78 | } 79 | }); 80 | await expect(bp.stopAllDevices()).rejects.toBeInstanceOf(ButtplugMessageError); 81 | }); 82 | 83 | it("Should throw Error on TCPCONNECTREFUSED", async () => { 84 | const connector = new ButtplugBrowserWebsocketClientConnector("ws://localhost:31000") 85 | try { 86 | await connector.connect(); 87 | } catch (e) { 88 | expect(e).toBeDefined(); 89 | } 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /js/tests/utils.ts: -------------------------------------------------------------------------------- 1 | import { ButtplugClient} from "../src/index"; 2 | import * as Messages from "../src/core/Messages"; 3 | import { ButtplugClientConnectorException } from "client/ButtplugClientConnectorException"; 4 | 5 | export class BPTestClient extends ButtplugClient { 6 | constructor(ClientName: string) { 7 | super(ClientName); 8 | } 9 | public get PingTimer() { 10 | return this._pingTimer; 11 | } 12 | 13 | public async sendMessage(msg: Messages.ButtplugMessage): Promise { 14 | return super.sendMessage(msg); 15 | } 16 | } 17 | 18 | export function SetupTestSuite() { 19 | // None of our tests should take very long. 20 | jest.setTimeout(1000); 21 | process.on("unhandledRejection", (reason: Error, p) => { 22 | throw new Error(`Unhandled Promise rejection!\n---\n${reason.stack}\n---\n`); 23 | }); 24 | } 25 | /* 26 | 27 | export async function SetupTestServer(): Promise<{Client: ButtplugClient, 28 | Server: ButtplugServer, 29 | TestDeviceManager: TestDeviceSubtypeManager, 30 | Connector: ButtplugEmbeddedClientConnector}> { 31 | const client = new ButtplugClient("Test Client"); 32 | const server = new ButtplugServer("Test Server"); 33 | server.ClearDeviceManagers(); 34 | const testdevicemanager = new TestDeviceSubtypeManager(); 35 | server.AddDeviceManager(testdevicemanager); 36 | const localConnector = new ButtplugEmbeddedClientConnector(); 37 | localConnector.Server = server; 38 | await client.Connect(localConnector); 39 | return Promise.resolve({Client: client, 40 | Server: server, 41 | TestDeviceManager: testdevicemanager, 42 | Connector: localConnector}); 43 | } 44 | */ -------------------------------------------------------------------------------- /js/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions" : { 3 | "baseUrl": "./src", 4 | "lib": ["es2015", "dom", "es6"], 5 | "target": "es6", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "sourceMap": true, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "strictNullChecks": true, 12 | "noImplicitThis": true, 13 | "declaration": true, 14 | "outDir": "./dist/main", 15 | "esModuleInterop": true 16 | }, 17 | "include": [ 18 | "./src/*.ts", 19 | "./src/**/*.ts", 20 | "./tests/**/*.ts" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /js/tsfmt.json: -------------------------------------------------------------------------------- 1 | { 2 | "indentSize": 2, 3 | "tabSize": 2, 4 | "convertTabsToSpaces": true, 5 | "insertSpaceAfterCommaDelimiter": true, 6 | "insertSpaceAfterSemicolonInForStatements": true, 7 | "insertSpaceBeforeAndAfterBinaryOperators": true, 8 | "insertSpaceAfterKeywordsInControlFlowStatements": true, 9 | "insertSpaceAfterFunctionKeywordForAnonymousFunctions": false, 10 | "insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": false, 11 | "insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": false, 12 | "placeOpenBraceOnNewLineForFunctions": false, 13 | "placeOpenBraceOnNewLineForControlBlocks": false 14 | } 15 | -------------------------------------------------------------------------------- /js/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:recommended" 4 | ], 5 | "linterOptions": { 6 | "exclude": [ 7 | "**/buttplug-gui-proto.d.ts" 8 | ] 9 | }, 10 | "jsRules": {}, 11 | "rules": { 12 | "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore", "allow-pascal-case"], 13 | "only-arrow-functions": [false], 14 | "indent": [true, "spaces", 2], 15 | "no-var-requires": [false], 16 | "ordered-imports": [false], 17 | "no-console": [false], 18 | "object-literal-sort-keys": [false], 19 | "max-classes-per-file": [false], 20 | "no-bitwise": [false], 21 | "semicolon": [true, "always"], 22 | "promise-function-async": true, 23 | "no-invalid-template-strings": true, 24 | "no-floating-promises": true 25 | }, 26 | "rulesDirectory": [] 27 | } 28 | -------------------------------------------------------------------------------- /js/typedocconfig.js: -------------------------------------------------------------------------------- 1 | /** @type {import('typedoc').TypeDocOptions} */ 2 | module.exports = { 3 | exclude: '**/+(test|example|node_modules)/**/*.ts', 4 | excludeExternals: true, 5 | excludePrivate: true, 6 | }; 7 | -------------------------------------------------------------------------------- /js/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { defineConfig } from 'vite'; 3 | import dts from 'vite-plugin-dts'; 4 | 5 | export default defineConfig({ 6 | build: { 7 | lib: { 8 | // Could also be a dictionary or array of multiple entry points 9 | entry: resolve(__dirname, 'src/index.ts'), 10 | name: 'buttplug', 11 | // the proper extensions will be added 12 | fileName: (format): string => { 13 | if (format === 'umd') { 14 | return 'buttplug.js'; 15 | } 16 | return 'buttplug.mjs'; 17 | }, 18 | }, 19 | outDir: 'dist/web', 20 | }, 21 | plugins: [ 22 | dts({ 23 | exclude: ['tests'], 24 | }), 25 | ], 26 | }); 27 | -------------------------------------------------------------------------------- /js/web-tests/test-web-library.ts: -------------------------------------------------------------------------------- 1 | import * as puppeteer from "puppeteer"; 2 | import * as fs from "fs"; 3 | import * as Buttplug from "buttplug"; 4 | 5 | describe("Web library tests", async () => { 6 | 7 | let browser; 8 | let page; 9 | 10 | beforeEach(async () => { 11 | browser = await puppeteer.launch({ 12 | headless: true, 13 | args: ["--no-sandbox", "--disable-setuid-sandbox"], 14 | }); 15 | page = await browser.newPage(); 16 | await page.goto(`file:///${__dirname}/web-test.html`); 17 | page.waitForSelector(".title"); 18 | const html = await page.$eval(".title", e => e.innerHTML); 19 | expect(html).toBe("I'm a test page!"); 20 | }, 10000); 21 | 22 | afterEach(() => { 23 | browser.close(); 24 | }, 10000); 25 | 26 | it("should run basic smoke test", async () => { 27 | await page.evaluate(() => { 28 | const connector = new Buttplug.ButtplugEmbeddedClientConnector(); 29 | const client = new Buttplug.ButtplugClient("Test"); 30 | return client.Connect(connector); 31 | }); 32 | }, 10000); 33 | it("should fail on incorrect connector address", async () => { 34 | await expect(page.evaluate(() => { 35 | const connector = new Buttplug.ButtplugBrowserWebsocketClientConnector("notanaddress"); 36 | const client = new Buttplug.ButtplugClient("Test"); 37 | return client.Connect(connector); 38 | })).rejects.toThrowError(); 39 | }, 10000); 40 | }); 41 | -------------------------------------------------------------------------------- /js/web-tests/web-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 |

I'm a test page!

9 | 10 | 11 | -------------------------------------------------------------------------------- /wasm/.npmignore: -------------------------------------------------------------------------------- 1 | tests 2 | web-tests 3 | .rpt2_cache 4 | coverage 5 | build 6 | node_modules 7 | yarn-error.log 8 | .yarn 9 | rust -------------------------------------------------------------------------------- /wasm/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: false 4 | -------------------------------------------------------------------------------- /wasm/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.0.1 (2023-09-23) 2 | 3 | ## Features 4 | 5 | - Bump Buttplug version to v7.1.5 6 | 7 | ## Bugfixes 8 | 9 | - Bump to buttplug-js v3.2.1, fixing minifinier errors 10 | - Fix issue with misnamed arguments causing messages not to emit. 11 | 12 | # 2.0.0 (2023-09-23) 13 | 14 | ## Features 15 | 16 | - First actual release! 17 | - Mostly because this repo was just a test repo which became the main buttplug-js repo for a 18 | while. 19 | - Implements a connector for buttplug-js that allows for running completely in the browser using 20 | WASM and WebBluetooth -------------------------------------------------------------------------------- /wasm/LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021-2023, Nonpolynomial Labs, LLC 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of buttplug nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /wasm/README.md: -------------------------------------------------------------------------------- 1 | # Buttplug Server - WASM Version 2 | 3 | A WASM compilation of the Rust implementation of the Buttplug Server. This projects allows the Buttplug Server (hardware connecting portion of Buttplug) to be run completely in the browser without the need to connect to [Intiface Central](https://intiface.com/central). 4 | 5 | This will only work in browser that include WebBluetooth implementations, like Google Chrome, Microsoft Edge, etc... 6 | 7 | If you are going to use this project, it is recommended that you provide users with the choice of connecting to Intiface Central *or* using the WASM server, as you will need to update the WASM server every time we release a new version to stay up to date with the latest hardware and protocol changes. Allowing the user to also connect to Intiface Central means that if your WASM server version becomes outdated, the user can still update Intiface Central and connect to it for support wiht newer hardware. 8 | 9 | ## Examples 10 | 11 | Examples of how to use this system are contained in this repo (the `example` directory), as well as in this glitch project: 12 | 13 | https://glitch.com/edit/#!/how-to-buttplug-wasm 14 | 15 | ## Distribution and Size Warnings 16 | 17 | As this project only works on the web, it is distributed as an ES Module. In order to accommodate loading from a CDN, the WASM blob is encoded to base64 and loaded within the module. The WASM blob itself is quite large due to bringing in the Rust standard library and requiring a lot of code internally. 18 | 19 | **THIS MEANS THE ES MODULE WILL BE ANYWHERE FROM 1.5MB (ZIPPED) TO 5MB (UNZIPPED).** 20 | 21 | You will need to take measures to show the user some sort of feedback while loading this module, as on some connections this may be quite slow. 22 | 23 | This is a tradeoff you must be willing to make to use this library. 24 | 25 | ## Contributing 26 | 27 | If you have issues or feature requests, [please feel free to file an issue on this repo](issues/). 28 | 29 | We are not looking for code contributions or pull requests at this time, and will not accept pull 30 | requests that do not have a matching issue where the matter was previously discussed. Pull requests 31 | should only be submitted after talking to [qdot](https://github.com/qdot) via issues on this repo 32 | (or on [discourse](https://discuss.buttplug.io) or [discord](https://discord.buttplug.io) if you 33 | would like to stay anonymous and out of recorded info on the repo) before submitting PRs. Random PRs 34 | without matching issues and discussion are likely to be closed without merging. and receiving 35 | approval to develop code based on an issue. Any random or non-issue pull requests will most likely 36 | be closed without merging. 37 | 38 | If you'd like to contribute in a non-technical way, we need money to keep up with supporting the 39 | latest and greatest hardware. We have multiple ways to donate! 40 | 41 | - [Patreon](https://patreon.com/qdot) 42 | - [Github Sponsors](https://github.com/sponsors/qdot) 43 | - [Ko-Fi](https://ko-fi.com/qdot76367) 44 | 45 | ## License 46 | 47 | This project is BSD 3-Clause licensed. 48 | 49 | ```text 50 | 51 | Copyright (c) 2016-2023, Nonpolynomial, LLC 52 | All rights reserved. 53 | 54 | Redistribution and use in source and binary forms, with or without 55 | modification, are permitted provided that the following conditions are met: 56 | 57 | * Redistributions of source code must retain the above copyright notice, this 58 | list of conditions and the following disclaimer. 59 | 60 | * Redistributions in binary form must reproduce the above copyright notice, 61 | this list of conditions and the following disclaimer in the documentation 62 | and/or other materials provided with the distribution. 63 | 64 | * Neither the name of buttplug nor the names of its 65 | contributors may be used to endorse or promote products derived from 66 | this software without specific prior written permission. 67 | 68 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 69 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 70 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 71 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 72 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 73 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 74 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 75 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 76 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 77 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 78 | ``` -------------------------------------------------------------------------------- /wasm/example/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /wasm/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | My Rust + Webpack project! 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /wasm/example/index.js: -------------------------------------------------------------------------------- 1 | import {ButtplugWasmClientConnector} from "../dist/buttplug-wasm.mjs"; 2 | import {ButtplugClient} from "../../js/dist/web/buttplug.mjs"; 3 | 4 | async function test_wasm() { 5 | let client = new ButtplugClient("Test Client"); 6 | await ButtplugWasmClientConnector.activateLogging(); 7 | await client.connect(new ButtplugWasmClientConnector()); 8 | await client.startScanning(); 9 | } 10 | onload = () => document.getElementById('b').onclick = () => test_wasm(); 11 | -------------------------------------------------------------------------------- /wasm/example/javascript.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wasm/example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wasm-example", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "devDependencies": { 12 | "vite": "^4.4.5" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /wasm/example/yarn.lock: -------------------------------------------------------------------------------- 1 | # This file is generated by running "yarn install" inside your project. 2 | # Manual changes might be lost - proceed with caution! 3 | 4 | __metadata: 5 | version: 6 6 | cacheKey: 8 7 | 8 | "@esbuild/android-arm64@npm:0.18.20": 9 | version: 0.18.20 10 | resolution: "@esbuild/android-arm64@npm:0.18.20" 11 | conditions: os=android & cpu=arm64 12 | languageName: node 13 | linkType: hard 14 | 15 | "@esbuild/android-arm@npm:0.18.20": 16 | version: 0.18.20 17 | resolution: "@esbuild/android-arm@npm:0.18.20" 18 | conditions: os=android & cpu=arm 19 | languageName: node 20 | linkType: hard 21 | 22 | "@esbuild/android-x64@npm:0.18.20": 23 | version: 0.18.20 24 | resolution: "@esbuild/android-x64@npm:0.18.20" 25 | conditions: os=android & cpu=x64 26 | languageName: node 27 | linkType: hard 28 | 29 | "@esbuild/darwin-arm64@npm:0.18.20": 30 | version: 0.18.20 31 | resolution: "@esbuild/darwin-arm64@npm:0.18.20" 32 | conditions: os=darwin & cpu=arm64 33 | languageName: node 34 | linkType: hard 35 | 36 | "@esbuild/darwin-x64@npm:0.18.20": 37 | version: 0.18.20 38 | resolution: "@esbuild/darwin-x64@npm:0.18.20" 39 | conditions: os=darwin & cpu=x64 40 | languageName: node 41 | linkType: hard 42 | 43 | "@esbuild/freebsd-arm64@npm:0.18.20": 44 | version: 0.18.20 45 | resolution: "@esbuild/freebsd-arm64@npm:0.18.20" 46 | conditions: os=freebsd & cpu=arm64 47 | languageName: node 48 | linkType: hard 49 | 50 | "@esbuild/freebsd-x64@npm:0.18.20": 51 | version: 0.18.20 52 | resolution: "@esbuild/freebsd-x64@npm:0.18.20" 53 | conditions: os=freebsd & cpu=x64 54 | languageName: node 55 | linkType: hard 56 | 57 | "@esbuild/linux-arm64@npm:0.18.20": 58 | version: 0.18.20 59 | resolution: "@esbuild/linux-arm64@npm:0.18.20" 60 | conditions: os=linux & cpu=arm64 61 | languageName: node 62 | linkType: hard 63 | 64 | "@esbuild/linux-arm@npm:0.18.20": 65 | version: 0.18.20 66 | resolution: "@esbuild/linux-arm@npm:0.18.20" 67 | conditions: os=linux & cpu=arm 68 | languageName: node 69 | linkType: hard 70 | 71 | "@esbuild/linux-ia32@npm:0.18.20": 72 | version: 0.18.20 73 | resolution: "@esbuild/linux-ia32@npm:0.18.20" 74 | conditions: os=linux & cpu=ia32 75 | languageName: node 76 | linkType: hard 77 | 78 | "@esbuild/linux-loong64@npm:0.18.20": 79 | version: 0.18.20 80 | resolution: "@esbuild/linux-loong64@npm:0.18.20" 81 | conditions: os=linux & cpu=loong64 82 | languageName: node 83 | linkType: hard 84 | 85 | "@esbuild/linux-mips64el@npm:0.18.20": 86 | version: 0.18.20 87 | resolution: "@esbuild/linux-mips64el@npm:0.18.20" 88 | conditions: os=linux & cpu=mips64el 89 | languageName: node 90 | linkType: hard 91 | 92 | "@esbuild/linux-ppc64@npm:0.18.20": 93 | version: 0.18.20 94 | resolution: "@esbuild/linux-ppc64@npm:0.18.20" 95 | conditions: os=linux & cpu=ppc64 96 | languageName: node 97 | linkType: hard 98 | 99 | "@esbuild/linux-riscv64@npm:0.18.20": 100 | version: 0.18.20 101 | resolution: "@esbuild/linux-riscv64@npm:0.18.20" 102 | conditions: os=linux & cpu=riscv64 103 | languageName: node 104 | linkType: hard 105 | 106 | "@esbuild/linux-s390x@npm:0.18.20": 107 | version: 0.18.20 108 | resolution: "@esbuild/linux-s390x@npm:0.18.20" 109 | conditions: os=linux & cpu=s390x 110 | languageName: node 111 | linkType: hard 112 | 113 | "@esbuild/linux-x64@npm:0.18.20": 114 | version: 0.18.20 115 | resolution: "@esbuild/linux-x64@npm:0.18.20" 116 | conditions: os=linux & cpu=x64 117 | languageName: node 118 | linkType: hard 119 | 120 | "@esbuild/netbsd-x64@npm:0.18.20": 121 | version: 0.18.20 122 | resolution: "@esbuild/netbsd-x64@npm:0.18.20" 123 | conditions: os=netbsd & cpu=x64 124 | languageName: node 125 | linkType: hard 126 | 127 | "@esbuild/openbsd-x64@npm:0.18.20": 128 | version: 0.18.20 129 | resolution: "@esbuild/openbsd-x64@npm:0.18.20" 130 | conditions: os=openbsd & cpu=x64 131 | languageName: node 132 | linkType: hard 133 | 134 | "@esbuild/sunos-x64@npm:0.18.20": 135 | version: 0.18.20 136 | resolution: "@esbuild/sunos-x64@npm:0.18.20" 137 | conditions: os=sunos & cpu=x64 138 | languageName: node 139 | linkType: hard 140 | 141 | "@esbuild/win32-arm64@npm:0.18.20": 142 | version: 0.18.20 143 | resolution: "@esbuild/win32-arm64@npm:0.18.20" 144 | conditions: os=win32 & cpu=arm64 145 | languageName: node 146 | linkType: hard 147 | 148 | "@esbuild/win32-ia32@npm:0.18.20": 149 | version: 0.18.20 150 | resolution: "@esbuild/win32-ia32@npm:0.18.20" 151 | conditions: os=win32 & cpu=ia32 152 | languageName: node 153 | linkType: hard 154 | 155 | "@esbuild/win32-x64@npm:0.18.20": 156 | version: 0.18.20 157 | resolution: "@esbuild/win32-x64@npm:0.18.20" 158 | conditions: os=win32 & cpu=x64 159 | languageName: node 160 | linkType: hard 161 | 162 | "@isaacs/cliui@npm:^8.0.2": 163 | version: 8.0.2 164 | resolution: "@isaacs/cliui@npm:8.0.2" 165 | dependencies: 166 | string-width: ^5.1.2 167 | string-width-cjs: "npm:string-width@^4.2.0" 168 | strip-ansi: ^7.0.1 169 | strip-ansi-cjs: "npm:strip-ansi@^6.0.1" 170 | wrap-ansi: ^8.1.0 171 | wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0" 172 | checksum: 4a473b9b32a7d4d3cfb7a614226e555091ff0c5a29a1734c28c72a182c2f6699b26fc6b5c2131dfd841e86b185aea714c72201d7c98c2fba5f17709333a67aeb 173 | languageName: node 174 | linkType: hard 175 | 176 | "@npmcli/fs@npm:^3.1.0": 177 | version: 3.1.0 178 | resolution: "@npmcli/fs@npm:3.1.0" 179 | dependencies: 180 | semver: ^7.3.5 181 | checksum: a50a6818de5fc557d0b0e6f50ec780a7a02ab8ad07e5ac8b16bf519e0ad60a144ac64f97d05c443c3367235d337182e1d012bbac0eb8dbae8dc7b40b193efd0e 182 | languageName: node 183 | linkType: hard 184 | 185 | "@pkgjs/parseargs@npm:^0.11.0": 186 | version: 0.11.0 187 | resolution: "@pkgjs/parseargs@npm:0.11.0" 188 | checksum: 6ad6a00fc4f2f2cfc6bff76fb1d88b8ee20bc0601e18ebb01b6d4be583733a860239a521a7fbca73b612e66705078809483549d2b18f370eb346c5155c8e4a0f 189 | languageName: node 190 | linkType: hard 191 | 192 | "@tootallnate/once@npm:2": 193 | version: 2.0.0 194 | resolution: "@tootallnate/once@npm:2.0.0" 195 | checksum: ad87447820dd3f24825d2d947ebc03072b20a42bfc96cbafec16bff8bbda6c1a81fcb0be56d5b21968560c5359a0af4038a68ba150c3e1694fe4c109a063bed8 196 | languageName: node 197 | linkType: hard 198 | 199 | "abbrev@npm:^1.0.0": 200 | version: 1.1.1 201 | resolution: "abbrev@npm:1.1.1" 202 | checksum: a4a97ec07d7ea112c517036882b2ac22f3109b7b19077dc656316d07d308438aac28e4d9746dc4d84bf6b1e75b4a7b0a5f3cb30592419f128ca9a8cee3bcfa17 203 | languageName: node 204 | linkType: hard 205 | 206 | "agent-base@npm:6, agent-base@npm:^6.0.2": 207 | version: 6.0.2 208 | resolution: "agent-base@npm:6.0.2" 209 | dependencies: 210 | debug: 4 211 | checksum: f52b6872cc96fd5f622071b71ef200e01c7c4c454ee68bc9accca90c98cfb39f2810e3e9aa330435835eedc8c23f4f8a15267f67c6e245d2b33757575bdac49d 212 | languageName: node 213 | linkType: hard 214 | 215 | "agentkeepalive@npm:^4.2.1": 216 | version: 4.5.0 217 | resolution: "agentkeepalive@npm:4.5.0" 218 | dependencies: 219 | humanize-ms: ^1.2.1 220 | checksum: 13278cd5b125e51eddd5079f04d6fe0914ac1b8b91c1f3db2c1822f99ac1a7457869068997784342fe455d59daaff22e14fb7b8c3da4e741896e7e31faf92481 221 | languageName: node 222 | linkType: hard 223 | 224 | "aggregate-error@npm:^3.0.0": 225 | version: 3.1.0 226 | resolution: "aggregate-error@npm:3.1.0" 227 | dependencies: 228 | clean-stack: ^2.0.0 229 | indent-string: ^4.0.0 230 | checksum: 1101a33f21baa27a2fa8e04b698271e64616b886795fd43c31068c07533c7b3facfcaf4e9e0cab3624bd88f729a592f1c901a1a229c9e490eafce411a8644b79 231 | languageName: node 232 | linkType: hard 233 | 234 | "ansi-regex@npm:^5.0.1": 235 | version: 5.0.1 236 | resolution: "ansi-regex@npm:5.0.1" 237 | checksum: 2aa4bb54caf2d622f1afdad09441695af2a83aa3fe8b8afa581d205e57ed4261c183c4d3877cee25794443fde5876417d859c108078ab788d6af7e4fe52eb66b 238 | languageName: node 239 | linkType: hard 240 | 241 | "ansi-regex@npm:^6.0.1": 242 | version: 6.0.1 243 | resolution: "ansi-regex@npm:6.0.1" 244 | checksum: 1ff8b7667cded1de4fa2c9ae283e979fc87036864317da86a2e546725f96406746411d0d85e87a2d12fa5abd715d90006de7fa4fa0477c92321ad3b4c7d4e169 245 | languageName: node 246 | linkType: hard 247 | 248 | "ansi-styles@npm:^4.0.0": 249 | version: 4.3.0 250 | resolution: "ansi-styles@npm:4.3.0" 251 | dependencies: 252 | color-convert: ^2.0.1 253 | checksum: 513b44c3b2105dd14cc42a19271e80f386466c4be574bccf60b627432f9198571ebf4ab1e4c3ba17347658f4ee1711c163d574248c0c1cdc2d5917a0ad582ec4 254 | languageName: node 255 | linkType: hard 256 | 257 | "ansi-styles@npm:^6.1.0": 258 | version: 6.2.1 259 | resolution: "ansi-styles@npm:6.2.1" 260 | checksum: ef940f2f0ced1a6347398da88a91da7930c33ecac3c77b72c5905f8b8fe402c52e6fde304ff5347f616e27a742da3f1dc76de98f6866c69251ad0b07a66776d9 261 | languageName: node 262 | linkType: hard 263 | 264 | "aproba@npm:^1.0.3 || ^2.0.0": 265 | version: 2.0.0 266 | resolution: "aproba@npm:2.0.0" 267 | checksum: 5615cadcfb45289eea63f8afd064ab656006361020e1735112e346593856f87435e02d8dcc7ff0d11928bc7d425f27bc7c2a84f6c0b35ab0ff659c814c138a24 268 | languageName: node 269 | linkType: hard 270 | 271 | "are-we-there-yet@npm:^3.0.0": 272 | version: 3.0.1 273 | resolution: "are-we-there-yet@npm:3.0.1" 274 | dependencies: 275 | delegates: ^1.0.0 276 | readable-stream: ^3.6.0 277 | checksum: 52590c24860fa7173bedeb69a4c05fb573473e860197f618b9a28432ee4379049336727ae3a1f9c4cb083114601c1140cee578376164d0e651217a9843f9fe83 278 | languageName: node 279 | linkType: hard 280 | 281 | "balanced-match@npm:^1.0.0": 282 | version: 1.0.2 283 | resolution: "balanced-match@npm:1.0.2" 284 | checksum: 9706c088a283058a8a99e0bf91b0a2f75497f185980d9ffa8b304de1d9e58ebda7c72c07ebf01dadedaac5b2907b2c6f566f660d62bd336c3468e960403b9d65 285 | languageName: node 286 | linkType: hard 287 | 288 | "brace-expansion@npm:^1.1.7": 289 | version: 1.1.11 290 | resolution: "brace-expansion@npm:1.1.11" 291 | dependencies: 292 | balanced-match: ^1.0.0 293 | concat-map: 0.0.1 294 | checksum: faf34a7bb0c3fcf4b59c7808bc5d2a96a40988addf2e7e09dfbb67a2251800e0d14cd2bfc1aa79174f2f5095c54ff27f46fb1289fe2d77dac755b5eb3434cc07 295 | languageName: node 296 | linkType: hard 297 | 298 | "brace-expansion@npm:^2.0.1": 299 | version: 2.0.1 300 | resolution: "brace-expansion@npm:2.0.1" 301 | dependencies: 302 | balanced-match: ^1.0.0 303 | checksum: a61e7cd2e8a8505e9f0036b3b6108ba5e926b4b55089eeb5550cd04a471fe216c96d4fe7e4c7f995c728c554ae20ddfc4244cad10aef255e72b62930afd233d1 304 | languageName: node 305 | linkType: hard 306 | 307 | "cacache@npm:^17.0.0": 308 | version: 17.1.4 309 | resolution: "cacache@npm:17.1.4" 310 | dependencies: 311 | "@npmcli/fs": ^3.1.0 312 | fs-minipass: ^3.0.0 313 | glob: ^10.2.2 314 | lru-cache: ^7.7.1 315 | minipass: ^7.0.3 316 | minipass-collect: ^1.0.2 317 | minipass-flush: ^1.0.5 318 | minipass-pipeline: ^1.2.4 319 | p-map: ^4.0.0 320 | ssri: ^10.0.0 321 | tar: ^6.1.11 322 | unique-filename: ^3.0.0 323 | checksum: b7751df756656954a51201335addced8f63fc53266fa56392c9f5ae83c8d27debffb4458ac2d168a744a4517ec3f2163af05c20097f93d17bdc2dc8a385e14a6 324 | languageName: node 325 | linkType: hard 326 | 327 | "chownr@npm:^2.0.0": 328 | version: 2.0.0 329 | resolution: "chownr@npm:2.0.0" 330 | checksum: c57cf9dd0791e2f18a5ee9c1a299ae6e801ff58fee96dc8bfd0dcb4738a6ce58dd252a3605b1c93c6418fe4f9d5093b28ffbf4d66648cb2a9c67eaef9679be2f 331 | languageName: node 332 | linkType: hard 333 | 334 | "clean-stack@npm:^2.0.0": 335 | version: 2.2.0 336 | resolution: "clean-stack@npm:2.2.0" 337 | checksum: 2ac8cd2b2f5ec986a3c743935ec85b07bc174d5421a5efc8017e1f146a1cf5f781ae962618f416352103b32c9cd7e203276e8c28241bbe946160cab16149fb68 338 | languageName: node 339 | linkType: hard 340 | 341 | "color-convert@npm:^2.0.1": 342 | version: 2.0.1 343 | resolution: "color-convert@npm:2.0.1" 344 | dependencies: 345 | color-name: ~1.1.4 346 | checksum: 79e6bdb9fd479a205c71d89574fccfb22bd9053bd98c6c4d870d65c132e5e904e6034978e55b43d69fcaa7433af2016ee203ce76eeba9cfa554b373e7f7db336 347 | languageName: node 348 | linkType: hard 349 | 350 | "color-name@npm:~1.1.4": 351 | version: 1.1.4 352 | resolution: "color-name@npm:1.1.4" 353 | checksum: b0445859521eb4021cd0fb0cc1a75cecf67fceecae89b63f62b201cca8d345baf8b952c966862a9d9a2632987d4f6581f0ec8d957dfacece86f0a7919316f610 354 | languageName: node 355 | linkType: hard 356 | 357 | "color-support@npm:^1.1.3": 358 | version: 1.1.3 359 | resolution: "color-support@npm:1.1.3" 360 | bin: 361 | color-support: bin.js 362 | checksum: 9b7356817670b9a13a26ca5af1c21615463b500783b739b7634a0c2047c16cef4b2865d7576875c31c3cddf9dd621fa19285e628f20198b233a5cfdda6d0793b 363 | languageName: node 364 | linkType: hard 365 | 366 | "concat-map@npm:0.0.1": 367 | version: 0.0.1 368 | resolution: "concat-map@npm:0.0.1" 369 | checksum: 902a9f5d8967a3e2faf138d5cb784b9979bad2e6db5357c5b21c568df4ebe62bcb15108af1b2253744844eb964fc023fbd9afbbbb6ddd0bcc204c6fb5b7bf3af 370 | languageName: node 371 | linkType: hard 372 | 373 | "console-control-strings@npm:^1.1.0": 374 | version: 1.1.0 375 | resolution: "console-control-strings@npm:1.1.0" 376 | checksum: 8755d76787f94e6cf79ce4666f0c5519906d7f5b02d4b884cf41e11dcd759ed69c57da0670afd9236d229a46e0f9cf519db0cd829c6dca820bb5a5c3def584ed 377 | languageName: node 378 | linkType: hard 379 | 380 | "cross-spawn@npm:^7.0.0": 381 | version: 7.0.3 382 | resolution: "cross-spawn@npm:7.0.3" 383 | dependencies: 384 | path-key: ^3.1.0 385 | shebang-command: ^2.0.0 386 | which: ^2.0.1 387 | checksum: 671cc7c7288c3a8406f3c69a3ae2fc85555c04169e9d611def9a675635472614f1c0ed0ef80955d5b6d4e724f6ced67f0ad1bb006c2ea643488fcfef994d7f52 388 | languageName: node 389 | linkType: hard 390 | 391 | "debug@npm:4, debug@npm:^4.3.3": 392 | version: 4.3.4 393 | resolution: "debug@npm:4.3.4" 394 | dependencies: 395 | ms: 2.1.2 396 | peerDependenciesMeta: 397 | supports-color: 398 | optional: true 399 | checksum: 3dbad3f94ea64f34431a9cbf0bafb61853eda57bff2880036153438f50fb5a84f27683ba0d8e5426bf41a8c6ff03879488120cf5b3a761e77953169c0600a708 400 | languageName: node 401 | linkType: hard 402 | 403 | "delegates@npm:^1.0.0": 404 | version: 1.0.0 405 | resolution: "delegates@npm:1.0.0" 406 | checksum: a51744d9b53c164ba9c0492471a1a2ffa0b6727451bdc89e31627fdf4adda9d51277cfcbfb20f0a6f08ccb3c436f341df3e92631a3440226d93a8971724771fd 407 | languageName: node 408 | linkType: hard 409 | 410 | "eastasianwidth@npm:^0.2.0": 411 | version: 0.2.0 412 | resolution: "eastasianwidth@npm:0.2.0" 413 | checksum: 7d00d7cd8e49b9afa762a813faac332dee781932d6f2c848dc348939c4253f1d4564341b7af1d041853bc3f32c2ef141b58e0a4d9862c17a7f08f68df1e0f1ed 414 | languageName: node 415 | linkType: hard 416 | 417 | "emoji-regex@npm:^8.0.0": 418 | version: 8.0.0 419 | resolution: "emoji-regex@npm:8.0.0" 420 | checksum: d4c5c39d5a9868b5fa152f00cada8a936868fd3367f33f71be515ecee4c803132d11b31a6222b2571b1e5f7e13890156a94880345594d0ce7e3c9895f560f192 421 | languageName: node 422 | linkType: hard 423 | 424 | "emoji-regex@npm:^9.2.2": 425 | version: 9.2.2 426 | resolution: "emoji-regex@npm:9.2.2" 427 | checksum: 8487182da74aabd810ac6d6f1994111dfc0e331b01271ae01ec1eb0ad7b5ecc2bbbbd2f053c05cb55a1ac30449527d819bbfbf0e3de1023db308cbcb47f86601 428 | languageName: node 429 | linkType: hard 430 | 431 | "encoding@npm:^0.1.13": 432 | version: 0.1.13 433 | resolution: "encoding@npm:0.1.13" 434 | dependencies: 435 | iconv-lite: ^0.6.2 436 | checksum: bb98632f8ffa823996e508ce6a58ffcf5856330fde839ae42c9e1f436cc3b5cc651d4aeae72222916545428e54fd0f6aa8862fd8d25bdbcc4589f1e3f3715e7f 437 | languageName: node 438 | linkType: hard 439 | 440 | "env-paths@npm:^2.2.0": 441 | version: 2.2.1 442 | resolution: "env-paths@npm:2.2.1" 443 | checksum: 65b5df55a8bab92229ab2b40dad3b387fad24613263d103a97f91c9fe43ceb21965cd3392b1ccb5d77088021e525c4e0481adb309625d0cb94ade1d1fb8dc17e 444 | languageName: node 445 | linkType: hard 446 | 447 | "err-code@npm:^2.0.2": 448 | version: 2.0.3 449 | resolution: "err-code@npm:2.0.3" 450 | checksum: 8b7b1be20d2de12d2255c0bc2ca638b7af5171142693299416e6a9339bd7d88fc8d7707d913d78e0993176005405a236b066b45666b27b797252c771156ace54 451 | languageName: node 452 | linkType: hard 453 | 454 | "esbuild@npm:^0.18.10": 455 | version: 0.18.20 456 | resolution: "esbuild@npm:0.18.20" 457 | dependencies: 458 | "@esbuild/android-arm": 0.18.20 459 | "@esbuild/android-arm64": 0.18.20 460 | "@esbuild/android-x64": 0.18.20 461 | "@esbuild/darwin-arm64": 0.18.20 462 | "@esbuild/darwin-x64": 0.18.20 463 | "@esbuild/freebsd-arm64": 0.18.20 464 | "@esbuild/freebsd-x64": 0.18.20 465 | "@esbuild/linux-arm": 0.18.20 466 | "@esbuild/linux-arm64": 0.18.20 467 | "@esbuild/linux-ia32": 0.18.20 468 | "@esbuild/linux-loong64": 0.18.20 469 | "@esbuild/linux-mips64el": 0.18.20 470 | "@esbuild/linux-ppc64": 0.18.20 471 | "@esbuild/linux-riscv64": 0.18.20 472 | "@esbuild/linux-s390x": 0.18.20 473 | "@esbuild/linux-x64": 0.18.20 474 | "@esbuild/netbsd-x64": 0.18.20 475 | "@esbuild/openbsd-x64": 0.18.20 476 | "@esbuild/sunos-x64": 0.18.20 477 | "@esbuild/win32-arm64": 0.18.20 478 | "@esbuild/win32-ia32": 0.18.20 479 | "@esbuild/win32-x64": 0.18.20 480 | dependenciesMeta: 481 | "@esbuild/android-arm": 482 | optional: true 483 | "@esbuild/android-arm64": 484 | optional: true 485 | "@esbuild/android-x64": 486 | optional: true 487 | "@esbuild/darwin-arm64": 488 | optional: true 489 | "@esbuild/darwin-x64": 490 | optional: true 491 | "@esbuild/freebsd-arm64": 492 | optional: true 493 | "@esbuild/freebsd-x64": 494 | optional: true 495 | "@esbuild/linux-arm": 496 | optional: true 497 | "@esbuild/linux-arm64": 498 | optional: true 499 | "@esbuild/linux-ia32": 500 | optional: true 501 | "@esbuild/linux-loong64": 502 | optional: true 503 | "@esbuild/linux-mips64el": 504 | optional: true 505 | "@esbuild/linux-ppc64": 506 | optional: true 507 | "@esbuild/linux-riscv64": 508 | optional: true 509 | "@esbuild/linux-s390x": 510 | optional: true 511 | "@esbuild/linux-x64": 512 | optional: true 513 | "@esbuild/netbsd-x64": 514 | optional: true 515 | "@esbuild/openbsd-x64": 516 | optional: true 517 | "@esbuild/sunos-x64": 518 | optional: true 519 | "@esbuild/win32-arm64": 520 | optional: true 521 | "@esbuild/win32-ia32": 522 | optional: true 523 | "@esbuild/win32-x64": 524 | optional: true 525 | bin: 526 | esbuild: bin/esbuild 527 | checksum: 5d253614e50cdb6ec22095afd0c414f15688e7278a7eb4f3720a6dd1306b0909cf431e7b9437a90d065a31b1c57be60130f63fe3e8d0083b588571f31ee6ec7b 528 | languageName: node 529 | linkType: hard 530 | 531 | "exponential-backoff@npm:^3.1.1": 532 | version: 3.1.1 533 | resolution: "exponential-backoff@npm:3.1.1" 534 | checksum: 3d21519a4f8207c99f7457287291316306255a328770d320b401114ec8481986e4e467e854cb9914dd965e0a1ca810a23ccb559c642c88f4c7f55c55778a9b48 535 | languageName: node 536 | linkType: hard 537 | 538 | "foreground-child@npm:^3.1.0": 539 | version: 3.1.1 540 | resolution: "foreground-child@npm:3.1.1" 541 | dependencies: 542 | cross-spawn: ^7.0.0 543 | signal-exit: ^4.0.1 544 | checksum: 139d270bc82dc9e6f8bc045fe2aae4001dc2472157044fdfad376d0a3457f77857fa883c1c8b21b491c6caade9a926a4bed3d3d2e8d3c9202b151a4cbbd0bcd5 545 | languageName: node 546 | linkType: hard 547 | 548 | "fs-minipass@npm:^2.0.0": 549 | version: 2.1.0 550 | resolution: "fs-minipass@npm:2.1.0" 551 | dependencies: 552 | minipass: ^3.0.0 553 | checksum: 1b8d128dae2ac6cc94230cc5ead341ba3e0efaef82dab46a33d171c044caaa6ca001364178d42069b2809c35a1c3c35079a32107c770e9ffab3901b59af8c8b1 554 | languageName: node 555 | linkType: hard 556 | 557 | "fs-minipass@npm:^3.0.0": 558 | version: 3.0.3 559 | resolution: "fs-minipass@npm:3.0.3" 560 | dependencies: 561 | minipass: ^7.0.3 562 | checksum: 8722a41109130851d979222d3ec88aabaceeaaf8f57b2a8f744ef8bd2d1ce95453b04a61daa0078822bc5cd21e008814f06fe6586f56fef511e71b8d2394d802 563 | languageName: node 564 | linkType: hard 565 | 566 | "fs.realpath@npm:^1.0.0": 567 | version: 1.0.0 568 | resolution: "fs.realpath@npm:1.0.0" 569 | checksum: 99ddea01a7e75aa276c250a04eedeffe5662bce66c65c07164ad6264f9de18fb21be9433ead460e54cff20e31721c811f4fb5d70591799df5f85dce6d6746fd0 570 | languageName: node 571 | linkType: hard 572 | 573 | "fsevents@npm:~2.3.2": 574 | version: 2.3.3 575 | resolution: "fsevents@npm:2.3.3" 576 | dependencies: 577 | node-gyp: latest 578 | checksum: 11e6ea6fea15e42461fc55b4b0e4a0a3c654faa567f1877dbd353f39156f69def97a69936d1746619d656c4b93de2238bf731f6085a03a50cabf287c9d024317 579 | conditions: os=darwin 580 | languageName: node 581 | linkType: hard 582 | 583 | "fsevents@patch:fsevents@~2.3.2#~builtin": 584 | version: 2.3.3 585 | resolution: "fsevents@patch:fsevents@npm%3A2.3.3#~builtin::version=2.3.3&hash=df0bf1" 586 | dependencies: 587 | node-gyp: latest 588 | conditions: os=darwin 589 | languageName: node 590 | linkType: hard 591 | 592 | "gauge@npm:^4.0.3": 593 | version: 4.0.4 594 | resolution: "gauge@npm:4.0.4" 595 | dependencies: 596 | aproba: ^1.0.3 || ^2.0.0 597 | color-support: ^1.1.3 598 | console-control-strings: ^1.1.0 599 | has-unicode: ^2.0.1 600 | signal-exit: ^3.0.7 601 | string-width: ^4.2.3 602 | strip-ansi: ^6.0.1 603 | wide-align: ^1.1.5 604 | checksum: 788b6bfe52f1dd8e263cda800c26ac0ca2ff6de0b6eee2fe0d9e3abf15e149b651bd27bf5226be10e6e3edb5c4e5d5985a5a1a98137e7a892f75eff76467ad2d 605 | languageName: node 606 | linkType: hard 607 | 608 | "glob@npm:^10.2.2": 609 | version: 10.3.5 610 | resolution: "glob@npm:10.3.5" 611 | dependencies: 612 | foreground-child: ^3.1.0 613 | jackspeak: ^2.0.3 614 | minimatch: ^9.0.1 615 | minipass: ^5.0.0 || ^6.0.2 || ^7.0.0 616 | path-scurry: ^1.10.1 617 | bin: 618 | glob: dist/cjs/src/bin.js 619 | checksum: 564f4799cae48c0bcc841c88a20b539b5701c27ed5596f8623f588b3c523262d3fc20eb1ea89cab9c75b0912faf40ca5501fc835f982225d0d0599282b09e97a 620 | languageName: node 621 | linkType: hard 622 | 623 | "glob@npm:^7.1.3, glob@npm:^7.1.4": 624 | version: 7.2.3 625 | resolution: "glob@npm:7.2.3" 626 | dependencies: 627 | fs.realpath: ^1.0.0 628 | inflight: ^1.0.4 629 | inherits: 2 630 | minimatch: ^3.1.1 631 | once: ^1.3.0 632 | path-is-absolute: ^1.0.0 633 | checksum: 29452e97b38fa704dabb1d1045350fb2467cf0277e155aa9ff7077e90ad81d1ea9d53d3ee63bd37c05b09a065e90f16aec4a65f5b8de401d1dac40bc5605d133 634 | languageName: node 635 | linkType: hard 636 | 637 | "graceful-fs@npm:^4.2.6": 638 | version: 4.2.11 639 | resolution: "graceful-fs@npm:4.2.11" 640 | checksum: ac85f94da92d8eb6b7f5a8b20ce65e43d66761c55ce85ac96df6865308390da45a8d3f0296dd3a663de65d30ba497bd46c696cc1e248c72b13d6d567138a4fc7 641 | languageName: node 642 | linkType: hard 643 | 644 | "has-unicode@npm:^2.0.1": 645 | version: 2.0.1 646 | resolution: "has-unicode@npm:2.0.1" 647 | checksum: 1eab07a7436512db0be40a710b29b5dc21fa04880b7f63c9980b706683127e3c1b57cb80ea96d47991bdae2dfe479604f6a1ba410106ee1046a41d1bd0814400 648 | languageName: node 649 | linkType: hard 650 | 651 | "http-cache-semantics@npm:^4.1.1": 652 | version: 4.1.1 653 | resolution: "http-cache-semantics@npm:4.1.1" 654 | checksum: 83ac0bc60b17a3a36f9953e7be55e5c8f41acc61b22583060e8dedc9dd5e3607c823a88d0926f9150e571f90946835c7fe150732801010845c72cd8bbff1a236 655 | languageName: node 656 | linkType: hard 657 | 658 | "http-proxy-agent@npm:^5.0.0": 659 | version: 5.0.0 660 | resolution: "http-proxy-agent@npm:5.0.0" 661 | dependencies: 662 | "@tootallnate/once": 2 663 | agent-base: 6 664 | debug: 4 665 | checksum: e2ee1ff1656a131953839b2a19cd1f3a52d97c25ba87bd2559af6ae87114abf60971e498021f9b73f9fd78aea8876d1fb0d4656aac8a03c6caa9fc175f22b786 666 | languageName: node 667 | linkType: hard 668 | 669 | "https-proxy-agent@npm:^5.0.0": 670 | version: 5.0.1 671 | resolution: "https-proxy-agent@npm:5.0.1" 672 | dependencies: 673 | agent-base: 6 674 | debug: 4 675 | checksum: 571fccdf38184f05943e12d37d6ce38197becdd69e58d03f43637f7fa1269cf303a7d228aa27e5b27bbd3af8f09fd938e1c91dcfefff2df7ba77c20ed8dfc765 676 | languageName: node 677 | linkType: hard 678 | 679 | "humanize-ms@npm:^1.2.1": 680 | version: 1.2.1 681 | resolution: "humanize-ms@npm:1.2.1" 682 | dependencies: 683 | ms: ^2.0.0 684 | checksum: 9c7a74a2827f9294c009266c82031030eae811ca87b0da3dceb8d6071b9bde22c9f3daef0469c3c533cc67a97d8a167cd9fc0389350e5f415f61a79b171ded16 685 | languageName: node 686 | linkType: hard 687 | 688 | "iconv-lite@npm:^0.6.2": 689 | version: 0.6.3 690 | resolution: "iconv-lite@npm:0.6.3" 691 | dependencies: 692 | safer-buffer: ">= 2.1.2 < 3.0.0" 693 | checksum: 3f60d47a5c8fc3313317edfd29a00a692cc87a19cac0159e2ce711d0ebc9019064108323b5e493625e25594f11c6236647d8e256fbe7a58f4a3b33b89e6d30bf 694 | languageName: node 695 | linkType: hard 696 | 697 | "imurmurhash@npm:^0.1.4": 698 | version: 0.1.4 699 | resolution: "imurmurhash@npm:0.1.4" 700 | checksum: 7cae75c8cd9a50f57dadd77482359f659eaebac0319dd9368bcd1714f55e65badd6929ca58569da2b6494ef13fdd5598cd700b1eba23f8b79c5f19d195a3ecf7 701 | languageName: node 702 | linkType: hard 703 | 704 | "indent-string@npm:^4.0.0": 705 | version: 4.0.0 706 | resolution: "indent-string@npm:4.0.0" 707 | checksum: 824cfb9929d031dabf059bebfe08cf3137365e112019086ed3dcff6a0a7b698cb80cf67ccccde0e25b9e2d7527aa6cc1fed1ac490c752162496caba3e6699612 708 | languageName: node 709 | linkType: hard 710 | 711 | "inflight@npm:^1.0.4": 712 | version: 1.0.6 713 | resolution: "inflight@npm:1.0.6" 714 | dependencies: 715 | once: ^1.3.0 716 | wrappy: 1 717 | checksum: f4f76aa072ce19fae87ce1ef7d221e709afb59d445e05d47fba710e85470923a75de35bfae47da6de1b18afc3ce83d70facf44cfb0aff89f0a3f45c0a0244dfd 718 | languageName: node 719 | linkType: hard 720 | 721 | "inherits@npm:2, inherits@npm:^2.0.3": 722 | version: 2.0.4 723 | resolution: "inherits@npm:2.0.4" 724 | checksum: 4a48a733847879d6cf6691860a6b1e3f0f4754176e4d71494c41f3475553768b10f84b5ce1d40fbd0e34e6bfbb864ee35858ad4dd2cf31e02fc4a154b724d7f1 725 | languageName: node 726 | linkType: hard 727 | 728 | "ip@npm:^2.0.0": 729 | version: 2.0.0 730 | resolution: "ip@npm:2.0.0" 731 | checksum: cfcfac6b873b701996d71ec82a7dd27ba92450afdb421e356f44044ed688df04567344c36cbacea7d01b1c39a4c732dc012570ebe9bebfb06f27314bca625349 732 | languageName: node 733 | linkType: hard 734 | 735 | "is-fullwidth-code-point@npm:^3.0.0": 736 | version: 3.0.0 737 | resolution: "is-fullwidth-code-point@npm:3.0.0" 738 | checksum: 44a30c29457c7fb8f00297bce733f0a64cd22eca270f83e58c105e0d015e45c019491a4ab2faef91ab51d4738c670daff901c799f6a700e27f7314029e99e348 739 | languageName: node 740 | linkType: hard 741 | 742 | "is-lambda@npm:^1.0.1": 743 | version: 1.0.1 744 | resolution: "is-lambda@npm:1.0.1" 745 | checksum: 93a32f01940220532e5948538699ad610d5924ac86093fcee83022252b363eb0cc99ba53ab084a04e4fb62bf7b5731f55496257a4c38adf87af9c4d352c71c35 746 | languageName: node 747 | linkType: hard 748 | 749 | "isexe@npm:^2.0.0": 750 | version: 2.0.0 751 | resolution: "isexe@npm:2.0.0" 752 | checksum: 26bf6c5480dda5161c820c5b5c751ae1e766c587b1f951ea3fcfc973bafb7831ae5b54a31a69bd670220e42e99ec154475025a468eae58ea262f813fdc8d1c62 753 | languageName: node 754 | linkType: hard 755 | 756 | "jackspeak@npm:^2.0.3": 757 | version: 2.3.3 758 | resolution: "jackspeak@npm:2.3.3" 759 | dependencies: 760 | "@isaacs/cliui": ^8.0.2 761 | "@pkgjs/parseargs": ^0.11.0 762 | dependenciesMeta: 763 | "@pkgjs/parseargs": 764 | optional: true 765 | checksum: 4313a7c0cc44c7753c4cb9869935f0b06f4cf96827515f63f58ff46b3d2f6e29aba6b3b5151778397c3f5ae67ef8bfc48871967bd10343c27e90cff198ec7808 766 | languageName: node 767 | linkType: hard 768 | 769 | "lru-cache@npm:^6.0.0": 770 | version: 6.0.0 771 | resolution: "lru-cache@npm:6.0.0" 772 | dependencies: 773 | yallist: ^4.0.0 774 | checksum: f97f499f898f23e4585742138a22f22526254fdba6d75d41a1c2526b3b6cc5747ef59c5612ba7375f42aca4f8461950e925ba08c991ead0651b4918b7c978297 775 | languageName: node 776 | linkType: hard 777 | 778 | "lru-cache@npm:^7.7.1": 779 | version: 7.18.3 780 | resolution: "lru-cache@npm:7.18.3" 781 | checksum: e550d772384709deea3f141af34b6d4fa392e2e418c1498c078de0ee63670f1f46f5eee746e8ef7e69e1c895af0d4224e62ee33e66a543a14763b0f2e74c1356 782 | languageName: node 783 | linkType: hard 784 | 785 | "lru-cache@npm:^9.1.1 || ^10.0.0": 786 | version: 10.0.1 787 | resolution: "lru-cache@npm:10.0.1" 788 | checksum: 06f8d0e1ceabd76bb6f644a26dbb0b4c471b79c7b514c13c6856113879b3bf369eb7b497dad4ff2b7e2636db202412394865b33c332100876d838ad1372f0181 789 | languageName: node 790 | linkType: hard 791 | 792 | "make-fetch-happen@npm:^11.0.3": 793 | version: 11.1.1 794 | resolution: "make-fetch-happen@npm:11.1.1" 795 | dependencies: 796 | agentkeepalive: ^4.2.1 797 | cacache: ^17.0.0 798 | http-cache-semantics: ^4.1.1 799 | http-proxy-agent: ^5.0.0 800 | https-proxy-agent: ^5.0.0 801 | is-lambda: ^1.0.1 802 | lru-cache: ^7.7.1 803 | minipass: ^5.0.0 804 | minipass-fetch: ^3.0.0 805 | minipass-flush: ^1.0.5 806 | minipass-pipeline: ^1.2.4 807 | negotiator: ^0.6.3 808 | promise-retry: ^2.0.1 809 | socks-proxy-agent: ^7.0.0 810 | ssri: ^10.0.0 811 | checksum: 7268bf274a0f6dcf0343829489a4506603ff34bd0649c12058753900b0eb29191dce5dba12680719a5d0a983d3e57810f594a12f3c18494e93a1fbc6348a4540 812 | languageName: node 813 | linkType: hard 814 | 815 | "minimatch@npm:^3.1.1": 816 | version: 3.1.2 817 | resolution: "minimatch@npm:3.1.2" 818 | dependencies: 819 | brace-expansion: ^1.1.7 820 | checksum: c154e566406683e7bcb746e000b84d74465b3a832c45d59912b9b55cd50dee66e5c4b1e5566dba26154040e51672f9aa450a9aef0c97cfc7336b78b7afb9540a 821 | languageName: node 822 | linkType: hard 823 | 824 | "minimatch@npm:^9.0.1": 825 | version: 9.0.3 826 | resolution: "minimatch@npm:9.0.3" 827 | dependencies: 828 | brace-expansion: ^2.0.1 829 | checksum: 253487976bf485b612f16bf57463520a14f512662e592e95c571afdab1442a6a6864b6c88f248ce6fc4ff0b6de04ac7aa6c8bb51e868e99d1d65eb0658a708b5 830 | languageName: node 831 | linkType: hard 832 | 833 | "minipass-collect@npm:^1.0.2": 834 | version: 1.0.2 835 | resolution: "minipass-collect@npm:1.0.2" 836 | dependencies: 837 | minipass: ^3.0.0 838 | checksum: 14df761028f3e47293aee72888f2657695ec66bd7d09cae7ad558da30415fdc4752bbfee66287dcc6fd5e6a2fa3466d6c484dc1cbd986525d9393b9523d97f10 839 | languageName: node 840 | linkType: hard 841 | 842 | "minipass-fetch@npm:^3.0.0": 843 | version: 3.0.4 844 | resolution: "minipass-fetch@npm:3.0.4" 845 | dependencies: 846 | encoding: ^0.1.13 847 | minipass: ^7.0.3 848 | minipass-sized: ^1.0.3 849 | minizlib: ^2.1.2 850 | dependenciesMeta: 851 | encoding: 852 | optional: true 853 | checksum: af7aad15d5c128ab1ebe52e043bdf7d62c3c6f0cecb9285b40d7b395e1375b45dcdfd40e63e93d26a0e8249c9efd5c325c65575aceee192883970ff8cb11364a 854 | languageName: node 855 | linkType: hard 856 | 857 | "minipass-flush@npm:^1.0.5": 858 | version: 1.0.5 859 | resolution: "minipass-flush@npm:1.0.5" 860 | dependencies: 861 | minipass: ^3.0.0 862 | checksum: 56269a0b22bad756a08a94b1ffc36b7c9c5de0735a4dd1ab2b06c066d795cfd1f0ac44a0fcae13eece5589b908ecddc867f04c745c7009be0b566421ea0944cf 863 | languageName: node 864 | linkType: hard 865 | 866 | "minipass-pipeline@npm:^1.2.4": 867 | version: 1.2.4 868 | resolution: "minipass-pipeline@npm:1.2.4" 869 | dependencies: 870 | minipass: ^3.0.0 871 | checksum: b14240dac0d29823c3d5911c286069e36d0b81173d7bdf07a7e4a91ecdef92cdff4baaf31ea3746f1c61e0957f652e641223970870e2353593f382112257971b 872 | languageName: node 873 | linkType: hard 874 | 875 | "minipass-sized@npm:^1.0.3": 876 | version: 1.0.3 877 | resolution: "minipass-sized@npm:1.0.3" 878 | dependencies: 879 | minipass: ^3.0.0 880 | checksum: 79076749fcacf21b5d16dd596d32c3b6bf4d6e62abb43868fac21674078505c8b15eaca4e47ed844985a4514854f917d78f588fcd029693709417d8f98b2bd60 881 | languageName: node 882 | linkType: hard 883 | 884 | "minipass@npm:^3.0.0": 885 | version: 3.3.6 886 | resolution: "minipass@npm:3.3.6" 887 | dependencies: 888 | yallist: ^4.0.0 889 | checksum: a30d083c8054cee83cdcdc97f97e4641a3f58ae743970457b1489ce38ee1167b3aaf7d815cd39ec7a99b9c40397fd4f686e83750e73e652b21cb516f6d845e48 890 | languageName: node 891 | linkType: hard 892 | 893 | "minipass@npm:^5.0.0": 894 | version: 5.0.0 895 | resolution: "minipass@npm:5.0.0" 896 | checksum: 425dab288738853fded43da3314a0b5c035844d6f3097a8e3b5b29b328da8f3c1af6fc70618b32c29ff906284cf6406b6841376f21caaadd0793c1d5a6a620ea 897 | languageName: node 898 | linkType: hard 899 | 900 | "minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.3": 901 | version: 7.0.3 902 | resolution: "minipass@npm:7.0.3" 903 | checksum: 6f1614f5b5b55568a46bca5fec0e7c46dac027691db27d0e1923a8192866903144cd962ac772c0e9f89b608ea818b702709c042bce98e190d258847d85461531 904 | languageName: node 905 | linkType: hard 906 | 907 | "minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": 908 | version: 2.1.2 909 | resolution: "minizlib@npm:2.1.2" 910 | dependencies: 911 | minipass: ^3.0.0 912 | yallist: ^4.0.0 913 | checksum: f1fdeac0b07cf8f30fcf12f4b586795b97be856edea22b5e9072707be51fc95d41487faec3f265b42973a304fe3a64acd91a44a3826a963e37b37bafde0212c3 914 | languageName: node 915 | linkType: hard 916 | 917 | "mkdirp@npm:^1.0.3": 918 | version: 1.0.4 919 | resolution: "mkdirp@npm:1.0.4" 920 | bin: 921 | mkdirp: bin/cmd.js 922 | checksum: a96865108c6c3b1b8e1d5e9f11843de1e077e57737602de1b82030815f311be11f96f09cce59bd5b903d0b29834733e5313f9301e3ed6d6f6fba2eae0df4298f 923 | languageName: node 924 | linkType: hard 925 | 926 | "ms@npm:2.1.2": 927 | version: 2.1.2 928 | resolution: "ms@npm:2.1.2" 929 | checksum: 673cdb2c3133eb050c745908d8ce632ed2c02d85640e2edb3ace856a2266a813b30c613569bf3354fdf4ea7d1a1494add3bfa95e2713baa27d0c2c71fc44f58f 930 | languageName: node 931 | linkType: hard 932 | 933 | "ms@npm:^2.0.0": 934 | version: 2.1.3 935 | resolution: "ms@npm:2.1.3" 936 | checksum: aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d 937 | languageName: node 938 | linkType: hard 939 | 940 | "nanoid@npm:^3.3.6": 941 | version: 3.3.6 942 | resolution: "nanoid@npm:3.3.6" 943 | bin: 944 | nanoid: bin/nanoid.cjs 945 | checksum: 7d0eda657002738aa5206107bd0580aead6c95c460ef1bdd0b1a87a9c7ae6277ac2e9b945306aaa5b32c6dcb7feaf462d0f552e7f8b5718abfc6ead5c94a71b3 946 | languageName: node 947 | linkType: hard 948 | 949 | "negotiator@npm:^0.6.3": 950 | version: 0.6.3 951 | resolution: "negotiator@npm:0.6.3" 952 | checksum: b8ffeb1e262eff7968fc90a2b6767b04cfd9842582a9d0ece0af7049537266e7b2506dfb1d107a32f06dd849ab2aea834d5830f7f4d0e5cb7d36e1ae55d021d9 953 | languageName: node 954 | linkType: hard 955 | 956 | "node-gyp@npm:latest": 957 | version: 9.4.0 958 | resolution: "node-gyp@npm:9.4.0" 959 | dependencies: 960 | env-paths: ^2.2.0 961 | exponential-backoff: ^3.1.1 962 | glob: ^7.1.4 963 | graceful-fs: ^4.2.6 964 | make-fetch-happen: ^11.0.3 965 | nopt: ^6.0.0 966 | npmlog: ^6.0.0 967 | rimraf: ^3.0.2 968 | semver: ^7.3.5 969 | tar: ^6.1.2 970 | which: ^2.0.2 971 | bin: 972 | node-gyp: bin/node-gyp.js 973 | checksum: 78b404e2e0639d64e145845f7f5a3cb20c0520cdaf6dda2f6e025e9b644077202ea7de1232396ba5bde3fee84cdc79604feebe6ba3ec84d464c85d407bb5da99 974 | languageName: node 975 | linkType: hard 976 | 977 | "nopt@npm:^6.0.0": 978 | version: 6.0.0 979 | resolution: "nopt@npm:6.0.0" 980 | dependencies: 981 | abbrev: ^1.0.0 982 | bin: 983 | nopt: bin/nopt.js 984 | checksum: 82149371f8be0c4b9ec2f863cc6509a7fd0fa729929c009f3a58e4eb0c9e4cae9920e8f1f8eb46e7d032fec8fb01bede7f0f41a67eb3553b7b8e14fa53de1dac 985 | languageName: node 986 | linkType: hard 987 | 988 | "npmlog@npm:^6.0.0": 989 | version: 6.0.2 990 | resolution: "npmlog@npm:6.0.2" 991 | dependencies: 992 | are-we-there-yet: ^3.0.0 993 | console-control-strings: ^1.1.0 994 | gauge: ^4.0.3 995 | set-blocking: ^2.0.0 996 | checksum: ae238cd264a1c3f22091cdd9e2b106f684297d3c184f1146984ecbe18aaa86343953f26b9520dedd1b1372bc0316905b736c1932d778dbeb1fcf5a1001390e2a 997 | languageName: node 998 | linkType: hard 999 | 1000 | "once@npm:^1.3.0": 1001 | version: 1.4.0 1002 | resolution: "once@npm:1.4.0" 1003 | dependencies: 1004 | wrappy: 1 1005 | checksum: cd0a88501333edd640d95f0d2700fbde6bff20b3d4d9bdc521bdd31af0656b5706570d6c6afe532045a20bb8dc0849f8332d6f2a416e0ba6d3d3b98806c7db68 1006 | languageName: node 1007 | linkType: hard 1008 | 1009 | "p-map@npm:^4.0.0": 1010 | version: 4.0.0 1011 | resolution: "p-map@npm:4.0.0" 1012 | dependencies: 1013 | aggregate-error: ^3.0.0 1014 | checksum: cb0ab21ec0f32ddffd31dfc250e3afa61e103ef43d957cc45497afe37513634589316de4eb88abdfd969fe6410c22c0b93ab24328833b8eb1ccc087fc0442a1c 1015 | languageName: node 1016 | linkType: hard 1017 | 1018 | "path-is-absolute@npm:^1.0.0": 1019 | version: 1.0.1 1020 | resolution: "path-is-absolute@npm:1.0.1" 1021 | checksum: 060840f92cf8effa293bcc1bea81281bd7d363731d214cbe5c227df207c34cd727430f70c6037b5159c8a870b9157cba65e775446b0ab06fd5ecc7e54615a3b8 1022 | languageName: node 1023 | linkType: hard 1024 | 1025 | "path-key@npm:^3.1.0": 1026 | version: 3.1.1 1027 | resolution: "path-key@npm:3.1.1" 1028 | checksum: 55cd7a9dd4b343412a8386a743f9c746ef196e57c823d90ca3ab917f90ab9f13dd0ded27252ba49dbdfcab2b091d998bc446f6220cd3cea65db407502a740020 1029 | languageName: node 1030 | linkType: hard 1031 | 1032 | "path-scurry@npm:^1.10.1": 1033 | version: 1.10.1 1034 | resolution: "path-scurry@npm:1.10.1" 1035 | dependencies: 1036 | lru-cache: ^9.1.1 || ^10.0.0 1037 | minipass: ^5.0.0 || ^6.0.2 || ^7.0.0 1038 | checksum: e2557cff3a8fb8bc07afdd6ab163a92587884f9969b05bbbaf6fe7379348bfb09af9ed292af12ed32398b15fb443e81692047b786d1eeb6d898a51eb17ed7d90 1039 | languageName: node 1040 | linkType: hard 1041 | 1042 | "picocolors@npm:^1.0.0": 1043 | version: 1.0.0 1044 | resolution: "picocolors@npm:1.0.0" 1045 | checksum: a2e8092dd86c8396bdba9f2b5481032848525b3dc295ce9b57896f931e63fc16f79805144321f72976383fc249584672a75cc18d6777c6b757603f372f745981 1046 | languageName: node 1047 | linkType: hard 1048 | 1049 | "postcss@npm:^8.4.27": 1050 | version: 8.4.31 1051 | resolution: "postcss@npm:8.4.31" 1052 | dependencies: 1053 | nanoid: ^3.3.6 1054 | picocolors: ^1.0.0 1055 | source-map-js: ^1.0.2 1056 | checksum: 1d8611341b073143ad90486fcdfeab49edd243377b1f51834dc4f6d028e82ce5190e4f11bb2633276864503654fb7cab28e67abdc0fbf9d1f88cad4a0ff0beea 1057 | languageName: node 1058 | linkType: hard 1059 | 1060 | "promise-retry@npm:^2.0.1": 1061 | version: 2.0.1 1062 | resolution: "promise-retry@npm:2.0.1" 1063 | dependencies: 1064 | err-code: ^2.0.2 1065 | retry: ^0.12.0 1066 | checksum: f96a3f6d90b92b568a26f71e966cbbc0f63ab85ea6ff6c81284dc869b41510e6cdef99b6b65f9030f0db422bf7c96652a3fff9f2e8fb4a0f069d8f4430359429 1067 | languageName: node 1068 | linkType: hard 1069 | 1070 | "readable-stream@npm:^3.6.0": 1071 | version: 3.6.2 1072 | resolution: "readable-stream@npm:3.6.2" 1073 | dependencies: 1074 | inherits: ^2.0.3 1075 | string_decoder: ^1.1.1 1076 | util-deprecate: ^1.0.1 1077 | checksum: bdcbe6c22e846b6af075e32cf8f4751c2576238c5043169a1c221c92ee2878458a816a4ea33f4c67623c0b6827c8a400409bfb3cf0bf3381392d0b1dfb52ac8d 1078 | languageName: node 1079 | linkType: hard 1080 | 1081 | "retry@npm:^0.12.0": 1082 | version: 0.12.0 1083 | resolution: "retry@npm:0.12.0" 1084 | checksum: 623bd7d2e5119467ba66202d733ec3c2e2e26568074923bc0585b6b99db14f357e79bdedb63cab56cec47491c4a0da7e6021a7465ca6dc4f481d3898fdd3158c 1085 | languageName: node 1086 | linkType: hard 1087 | 1088 | "rimraf@npm:^3.0.2": 1089 | version: 3.0.2 1090 | resolution: "rimraf@npm:3.0.2" 1091 | dependencies: 1092 | glob: ^7.1.3 1093 | bin: 1094 | rimraf: bin.js 1095 | checksum: 87f4164e396f0171b0a3386cc1877a817f572148ee13a7e113b238e48e8a9f2f31d009a92ec38a591ff1567d9662c6b67fd8818a2dbbaed74bc26a87a2a4a9a0 1096 | languageName: node 1097 | linkType: hard 1098 | 1099 | "rollup@npm:^3.27.1": 1100 | version: 3.29.2 1101 | resolution: "rollup@npm:3.29.2" 1102 | dependencies: 1103 | fsevents: ~2.3.2 1104 | dependenciesMeta: 1105 | fsevents: 1106 | optional: true 1107 | bin: 1108 | rollup: dist/bin/rollup 1109 | checksum: 2eacb5a2522cb41e46e0bd78cca2c2da29b09b1fbd5b7c6ebb0afb3864af125a06fba528dfd6699704e49384e106ff58b359ce4abef61d7db12a7840d3b56e54 1110 | languageName: node 1111 | linkType: hard 1112 | 1113 | "safe-buffer@npm:~5.2.0": 1114 | version: 5.2.1 1115 | resolution: "safe-buffer@npm:5.2.1" 1116 | checksum: b99c4b41fdd67a6aaf280fcd05e9ffb0813654894223afb78a31f14a19ad220bba8aba1cb14eddce1fcfb037155fe6de4e861784eb434f7d11ed58d1e70dd491 1117 | languageName: node 1118 | linkType: hard 1119 | 1120 | "safer-buffer@npm:>= 2.1.2 < 3.0.0": 1121 | version: 2.1.2 1122 | resolution: "safer-buffer@npm:2.1.2" 1123 | checksum: cab8f25ae6f1434abee8d80023d7e72b598cf1327164ddab31003c51215526801e40b66c5e65d658a0af1e9d6478cadcb4c745f4bd6751f97d8644786c0978b0 1124 | languageName: node 1125 | linkType: hard 1126 | 1127 | "semver@npm:^7.3.5": 1128 | version: 7.5.4 1129 | resolution: "semver@npm:7.5.4" 1130 | dependencies: 1131 | lru-cache: ^6.0.0 1132 | bin: 1133 | semver: bin/semver.js 1134 | checksum: 12d8ad952fa353b0995bf180cdac205a4068b759a140e5d3c608317098b3575ac2f1e09182206bf2eb26120e1c0ed8fb92c48c592f6099680de56bb071423ca3 1135 | languageName: node 1136 | linkType: hard 1137 | 1138 | "set-blocking@npm:^2.0.0": 1139 | version: 2.0.0 1140 | resolution: "set-blocking@npm:2.0.0" 1141 | checksum: 6e65a05f7cf7ebdf8b7c75b101e18c0b7e3dff4940d480efed8aad3a36a4005140b660fa1d804cb8bce911cac290441dc728084a30504d3516ac2ff7ad607b02 1142 | languageName: node 1143 | linkType: hard 1144 | 1145 | "shebang-command@npm:^2.0.0": 1146 | version: 2.0.0 1147 | resolution: "shebang-command@npm:2.0.0" 1148 | dependencies: 1149 | shebang-regex: ^3.0.0 1150 | checksum: 6b52fe87271c12968f6a054e60f6bde5f0f3d2db483a1e5c3e12d657c488a15474121a1d55cd958f6df026a54374ec38a4a963988c213b7570e1d51575cea7fa 1151 | languageName: node 1152 | linkType: hard 1153 | 1154 | "shebang-regex@npm:^3.0.0": 1155 | version: 3.0.0 1156 | resolution: "shebang-regex@npm:3.0.0" 1157 | checksum: 1a2bcae50de99034fcd92ad4212d8e01eedf52c7ec7830eedcf886622804fe36884278f2be8be0ea5fde3fd1c23911643a4e0f726c8685b61871c8908af01222 1158 | languageName: node 1159 | linkType: hard 1160 | 1161 | "signal-exit@npm:^3.0.7": 1162 | version: 3.0.7 1163 | resolution: "signal-exit@npm:3.0.7" 1164 | checksum: a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318 1165 | languageName: node 1166 | linkType: hard 1167 | 1168 | "signal-exit@npm:^4.0.1": 1169 | version: 4.1.0 1170 | resolution: "signal-exit@npm:4.1.0" 1171 | checksum: 64c757b498cb8629ffa5f75485340594d2f8189e9b08700e69199069c8e3070fb3e255f7ab873c05dc0b3cec412aea7402e10a5990cb6a050bd33ba062a6c549 1172 | languageName: node 1173 | linkType: hard 1174 | 1175 | "smart-buffer@npm:^4.2.0": 1176 | version: 4.2.0 1177 | resolution: "smart-buffer@npm:4.2.0" 1178 | checksum: b5167a7142c1da704c0e3af85c402002b597081dd9575031a90b4f229ca5678e9a36e8a374f1814c8156a725d17008ae3bde63b92f9cfd132526379e580bec8b 1179 | languageName: node 1180 | linkType: hard 1181 | 1182 | "socks-proxy-agent@npm:^7.0.0": 1183 | version: 7.0.0 1184 | resolution: "socks-proxy-agent@npm:7.0.0" 1185 | dependencies: 1186 | agent-base: ^6.0.2 1187 | debug: ^4.3.3 1188 | socks: ^2.6.2 1189 | checksum: 720554370154cbc979e2e9ce6a6ec6ced205d02757d8f5d93fe95adae454fc187a5cbfc6b022afab850a5ce9b4c7d73e0f98e381879cf45f66317a4895953846 1190 | languageName: node 1191 | linkType: hard 1192 | 1193 | "socks@npm:^2.6.2": 1194 | version: 2.7.1 1195 | resolution: "socks@npm:2.7.1" 1196 | dependencies: 1197 | ip: ^2.0.0 1198 | smart-buffer: ^4.2.0 1199 | checksum: 259d9e3e8e1c9809a7f5c32238c3d4d2a36b39b83851d0f573bfde5f21c4b1288417ce1af06af1452569cd1eb0841169afd4998f0e04ba04656f6b7f0e46d748 1200 | languageName: node 1201 | linkType: hard 1202 | 1203 | "source-map-js@npm:^1.0.2": 1204 | version: 1.0.2 1205 | resolution: "source-map-js@npm:1.0.2" 1206 | checksum: c049a7fc4deb9a7e9b481ae3d424cc793cb4845daa690bc5a05d428bf41bf231ced49b4cf0c9e77f9d42fdb3d20d6187619fc586605f5eabe995a316da8d377c 1207 | languageName: node 1208 | linkType: hard 1209 | 1210 | "ssri@npm:^10.0.0": 1211 | version: 10.0.5 1212 | resolution: "ssri@npm:10.0.5" 1213 | dependencies: 1214 | minipass: ^7.0.3 1215 | checksum: 0a31b65f21872dea1ed3f7c200d7bc1c1b91c15e419deca14f282508ba917cbb342c08a6814c7f68ca4ca4116dd1a85da2bbf39227480e50125a1ceffeecb750 1216 | languageName: node 1217 | linkType: hard 1218 | 1219 | "string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.3": 1220 | version: 4.2.3 1221 | resolution: "string-width@npm:4.2.3" 1222 | dependencies: 1223 | emoji-regex: ^8.0.0 1224 | is-fullwidth-code-point: ^3.0.0 1225 | strip-ansi: ^6.0.1 1226 | checksum: e52c10dc3fbfcd6c3a15f159f54a90024241d0f149cf8aed2982a2d801d2e64df0bf1dc351cf8e95c3319323f9f220c16e740b06faecd53e2462df1d2b5443fb 1227 | languageName: node 1228 | linkType: hard 1229 | 1230 | "string-width@npm:^5.0.1, string-width@npm:^5.1.2": 1231 | version: 5.1.2 1232 | resolution: "string-width@npm:5.1.2" 1233 | dependencies: 1234 | eastasianwidth: ^0.2.0 1235 | emoji-regex: ^9.2.2 1236 | strip-ansi: ^7.0.1 1237 | checksum: 7369deaa29f21dda9a438686154b62c2c5f661f8dda60449088f9f980196f7908fc39fdd1803e3e01541970287cf5deae336798337e9319a7055af89dafa7193 1238 | languageName: node 1239 | linkType: hard 1240 | 1241 | "string_decoder@npm:^1.1.1": 1242 | version: 1.3.0 1243 | resolution: "string_decoder@npm:1.3.0" 1244 | dependencies: 1245 | safe-buffer: ~5.2.0 1246 | checksum: 8417646695a66e73aefc4420eb3b84cc9ffd89572861fe004e6aeb13c7bc00e2f616247505d2dbbef24247c372f70268f594af7126f43548565c68c117bdeb56 1247 | languageName: node 1248 | linkType: hard 1249 | 1250 | "strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": 1251 | version: 6.0.1 1252 | resolution: "strip-ansi@npm:6.0.1" 1253 | dependencies: 1254 | ansi-regex: ^5.0.1 1255 | checksum: f3cd25890aef3ba6e1a74e20896c21a46f482e93df4a06567cebf2b57edabb15133f1f94e57434e0a958d61186087b1008e89c94875d019910a213181a14fc8c 1256 | languageName: node 1257 | linkType: hard 1258 | 1259 | "strip-ansi@npm:^7.0.1": 1260 | version: 7.1.0 1261 | resolution: "strip-ansi@npm:7.1.0" 1262 | dependencies: 1263 | ansi-regex: ^6.0.1 1264 | checksum: 859c73fcf27869c22a4e4d8c6acfe690064659e84bef9458aa6d13719d09ca88dcfd40cbf31fd0be63518ea1a643fe070b4827d353e09533a5b0b9fd4553d64d 1265 | languageName: node 1266 | linkType: hard 1267 | 1268 | "tar@npm:^6.1.11, tar@npm:^6.1.2": 1269 | version: 6.2.0 1270 | resolution: "tar@npm:6.2.0" 1271 | dependencies: 1272 | chownr: ^2.0.0 1273 | fs-minipass: ^2.0.0 1274 | minipass: ^5.0.0 1275 | minizlib: ^2.1.1 1276 | mkdirp: ^1.0.3 1277 | yallist: ^4.0.0 1278 | checksum: db4d9fe74a2082c3a5016630092c54c8375ff3b280186938cfd104f2e089c4fd9bad58688ef6be9cf186a889671bf355c7cda38f09bbf60604b281715ca57f5c 1279 | languageName: node 1280 | linkType: hard 1281 | 1282 | "unique-filename@npm:^3.0.0": 1283 | version: 3.0.0 1284 | resolution: "unique-filename@npm:3.0.0" 1285 | dependencies: 1286 | unique-slug: ^4.0.0 1287 | checksum: 8e2f59b356cb2e54aab14ff98a51ac6c45781d15ceaab6d4f1c2228b780193dc70fae4463ce9e1df4479cb9d3304d7c2043a3fb905bdeca71cc7e8ce27e063df 1288 | languageName: node 1289 | linkType: hard 1290 | 1291 | "unique-slug@npm:^4.0.0": 1292 | version: 4.0.0 1293 | resolution: "unique-slug@npm:4.0.0" 1294 | dependencies: 1295 | imurmurhash: ^0.1.4 1296 | checksum: 0884b58365af59f89739e6f71e3feacb5b1b41f2df2d842d0757933620e6de08eff347d27e9d499b43c40476cbaf7988638d3acb2ffbcb9d35fd035591adfd15 1297 | languageName: node 1298 | linkType: hard 1299 | 1300 | "util-deprecate@npm:^1.0.1": 1301 | version: 1.0.2 1302 | resolution: "util-deprecate@npm:1.0.2" 1303 | checksum: 474acf1146cb2701fe3b074892217553dfcf9a031280919ba1b8d651a068c9b15d863b7303cb15bd00a862b498e6cf4ad7b4a08fb134edd5a6f7641681cb54a2 1304 | languageName: node 1305 | linkType: hard 1306 | 1307 | "vite@npm:^4.4.5": 1308 | version: 4.4.9 1309 | resolution: "vite@npm:4.4.9" 1310 | dependencies: 1311 | esbuild: ^0.18.10 1312 | fsevents: ~2.3.2 1313 | postcss: ^8.4.27 1314 | rollup: ^3.27.1 1315 | peerDependencies: 1316 | "@types/node": ">= 14" 1317 | less: "*" 1318 | lightningcss: ^1.21.0 1319 | sass: "*" 1320 | stylus: "*" 1321 | sugarss: "*" 1322 | terser: ^5.4.0 1323 | dependenciesMeta: 1324 | fsevents: 1325 | optional: true 1326 | peerDependenciesMeta: 1327 | "@types/node": 1328 | optional: true 1329 | less: 1330 | optional: true 1331 | lightningcss: 1332 | optional: true 1333 | sass: 1334 | optional: true 1335 | stylus: 1336 | optional: true 1337 | sugarss: 1338 | optional: true 1339 | terser: 1340 | optional: true 1341 | bin: 1342 | vite: bin/vite.js 1343 | checksum: c511024ceae39c68c7dbf2ac4381ee655cd7bb62cf43867a14798bc835d3320b8fa7867a336143c30825c191c1fb4e9aa3348fce831ab617e96203080d3d2908 1344 | languageName: node 1345 | linkType: hard 1346 | 1347 | "wasm-example@workspace:.": 1348 | version: 0.0.0-use.local 1349 | resolution: "wasm-example@workspace:." 1350 | dependencies: 1351 | vite: ^4.4.5 1352 | languageName: unknown 1353 | linkType: soft 1354 | 1355 | "which@npm:^2.0.1, which@npm:^2.0.2": 1356 | version: 2.0.2 1357 | resolution: "which@npm:2.0.2" 1358 | dependencies: 1359 | isexe: ^2.0.0 1360 | bin: 1361 | node-which: ./bin/node-which 1362 | checksum: 1a5c563d3c1b52d5f893c8b61afe11abc3bab4afac492e8da5bde69d550de701cf9806235f20a47b5c8fa8a1d6a9135841de2596535e998027a54589000e66d1 1363 | languageName: node 1364 | linkType: hard 1365 | 1366 | "wide-align@npm:^1.1.5": 1367 | version: 1.1.5 1368 | resolution: "wide-align@npm:1.1.5" 1369 | dependencies: 1370 | string-width: ^1.0.2 || 2 || 3 || 4 1371 | checksum: d5fc37cd561f9daee3c80e03b92ed3e84d80dde3365a8767263d03dacfc8fa06b065ffe1df00d8c2a09f731482fcacae745abfbb478d4af36d0a891fad4834d3 1372 | languageName: node 1373 | linkType: hard 1374 | 1375 | "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": 1376 | version: 7.0.0 1377 | resolution: "wrap-ansi@npm:7.0.0" 1378 | dependencies: 1379 | ansi-styles: ^4.0.0 1380 | string-width: ^4.1.0 1381 | strip-ansi: ^6.0.0 1382 | checksum: a790b846fd4505de962ba728a21aaeda189b8ee1c7568ca5e817d85930e06ef8d1689d49dbf0e881e8ef84436af3a88bc49115c2e2788d841ff1b8b5b51a608b 1383 | languageName: node 1384 | linkType: hard 1385 | 1386 | "wrap-ansi@npm:^8.1.0": 1387 | version: 8.1.0 1388 | resolution: "wrap-ansi@npm:8.1.0" 1389 | dependencies: 1390 | ansi-styles: ^6.1.0 1391 | string-width: ^5.0.1 1392 | strip-ansi: ^7.0.1 1393 | checksum: 371733296dc2d616900ce15a0049dca0ef67597d6394c57347ba334393599e800bab03c41d4d45221b6bc967b8c453ec3ae4749eff3894202d16800fdfe0e238 1394 | languageName: node 1395 | linkType: hard 1396 | 1397 | "wrappy@npm:1": 1398 | version: 1.0.2 1399 | resolution: "wrappy@npm:1.0.2" 1400 | checksum: 159da4805f7e84a3d003d8841557196034155008f817172d4e986bd591f74aa82aa7db55929a54222309e01079a65a92a9e6414da5a6aa4b01ee44a511ac3ee5 1401 | languageName: node 1402 | linkType: hard 1403 | 1404 | "yallist@npm:^4.0.0": 1405 | version: 4.0.0 1406 | resolution: "yallist@npm:4.0.0" 1407 | checksum: 343617202af32df2a15a3be36a5a8c0c8545208f3d3dfbc6bb7c3e3b7e8c6f8e7485432e4f3b88da3031a6e20afa7c711eded32ddfb122896ac5d914e75848d5 1408 | languageName: node 1409 | linkType: hard 1410 | -------------------------------------------------------------------------------- /wasm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "buttplug-wasm", 3 | "version": "2.0.1", 4 | "scripts": { 5 | "build:main": "tsc -p tsconfig.json", 6 | "build:web": "vite build", 7 | "build:wasm": "cd rust; wasm-pack build" 8 | }, 9 | "type": "module", 10 | "dependencies": { 11 | "buttplug": "^3.2.1", 12 | "eventemitter3": "^5.0.1" 13 | }, 14 | "devDependencies": { 15 | "rollup": "^4.12.0", 16 | "typescript": "^5.3.3", 17 | "vite": "^5.1.4", 18 | "vite-plugin-dts": "^3.7.3", 19 | "vite-plugin-top-level-await": "^1.4.1", 20 | "vite-plugin-wasm": "^3.3.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /wasm/rust/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags=["--cfg=web_sys_unstable_apis"] -------------------------------------------------------------------------------- /wasm/rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "buttplug_wasm" 3 | version = "7.1.5" 4 | authors = ["Nonpolynomial Labs, LLC "] 5 | description = "WASM Interop for the Buttplug Intimate Hardware Control Library" 6 | license = "BSD-3-Clause" 7 | homepage = "http://buttplug.io" 8 | repository = "https://github.com/buttplugio/buttplug.git" 9 | readme = "./README.md" 10 | keywords = ["usb", "serial", "hardware", "bluetooth", "teledildonics"] 11 | edition = "2021" 12 | 13 | [lib] 14 | crate-type = ["cdylib", "rlib"] 15 | name = "buttplug_wasm" 16 | path = "src/lib.rs" 17 | 18 | [dependencies] 19 | buttplug = { version = "7.1.13", default-features = false, features = ["wasm"] } 20 | # buttplug = { path = "../../../buttplug/buttplug", default-features = false, features = ["wasm"] } 21 | # buttplug_derive = { path = "../buttplug_derive" } 22 | js-sys = "0.3.68" 23 | tracing-wasm = "0.2.1" 24 | log-panics = { version = "2.1.0", features = ["with-backtrace"] } 25 | console_error_panic_hook = "0.1.7" 26 | wasmtimer = "0.2.0" 27 | wasm-bindgen = { version = "0.2.91", features = ["serde-serialize"] } 28 | tokio = { version = "1.36.0", features = ["sync", "macros", "io-util"] } 29 | tokio-stream = "0.1.14" 30 | tracing = "0.1.40" 31 | tracing-futures = "0.2.5" 32 | tracing-subscriber = { version = "0.3.18", features = ["json"] } 33 | futures = "0.3.30" 34 | futures-util = "0.3.30" 35 | async-trait = "0.1.77" 36 | wasm-bindgen-futures = "0.4.41" 37 | 38 | [dependencies.web-sys] 39 | version = "0.3.68" 40 | # path = "../../wasm-bindgen/crates/web-sys" 41 | #git = "https://github.com/rustwasm/wasm-bindgen" 42 | features = [ 43 | "Navigator", 44 | "Bluetooth", 45 | "BluetoothDevice", 46 | "BluetoothLeScanFilterInit", 47 | "BluetoothRemoteGattCharacteristic", 48 | "BluetoothRemoteGattServer", 49 | "BluetoothRemoteGattService", 50 | "BinaryType", 51 | "Blob", 52 | "console", 53 | "ErrorEvent", 54 | "Event", 55 | "FileReader", 56 | "MessageEvent", 57 | "ProgressEvent", 58 | "RequestDeviceOptions", 59 | "WebSocket", 60 | "Window" 61 | ] 62 | -------------------------------------------------------------------------------- /wasm/rust/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate tracing; 3 | #[macro_use] 4 | extern crate futures; 5 | 6 | 7 | mod webbluetooth; 8 | use js_sys; 9 | use tokio_stream::StreamExt; 10 | use crate::webbluetooth::*; 11 | use buttplug::{ 12 | core::message::{ButtplugCurrentSpecServerMessage, serializer::vec_to_protocol_json}, 13 | server::ButtplugServer, 14 | util::async_manager, server::ButtplugServerBuilder, core::message::{BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION, serializer::{ButtplugSerializedMessage, ButtplugMessageSerializer, ButtplugServerJSONSerializer}} 15 | }; 16 | 17 | type FFICallback = js_sys::Function; 18 | type FFICallbackContext = u32; 19 | 20 | #[derive(Clone, Copy)] 21 | pub struct FFICallbackContextWrapper(FFICallbackContext); 22 | 23 | unsafe impl Send for FFICallbackContextWrapper { 24 | } 25 | unsafe impl Sync for FFICallbackContextWrapper { 26 | } 27 | 28 | use console_error_panic_hook; 29 | use tracing_subscriber::{layer::SubscriberExt, Registry}; 30 | use tracing_wasm::{WASMLayer, WASMLayerConfig}; 31 | use wasm_bindgen::prelude::*; 32 | use std::sync::Arc; 33 | use js_sys::Uint8Array; 34 | 35 | pub type ButtplugWASMServer = Arc; 36 | 37 | pub fn send_server_message( 38 | message: &ButtplugCurrentSpecServerMessage, 39 | callback: &FFICallback, 40 | ) { 41 | let msg_array = [message.clone()]; 42 | let json_msg = vec_to_protocol_json(&msg_array); 43 | let buf = json_msg.as_bytes(); 44 | { 45 | let this = JsValue::null(); 46 | let uint8buf = unsafe { Uint8Array::new(&Uint8Array::view(buf)) }; 47 | callback.call1(&this, &JsValue::from(uint8buf)); 48 | } 49 | } 50 | 51 | #[no_mangle] 52 | #[wasm_bindgen] 53 | pub fn buttplug_create_embedded_wasm_server( 54 | callback: &FFICallback, 55 | ) -> *mut ButtplugWASMServer { 56 | console_error_panic_hook::set_once(); 57 | let mut builder = ButtplugServerBuilder::default(); 58 | builder.comm_manager(WebBluetoothCommunicationManagerBuilder::default()); 59 | let server = Arc::new(builder.finish().unwrap()); 60 | let event_stream = server.event_stream(); 61 | let callback = callback.clone(); 62 | async_manager::spawn(async move { 63 | pin_mut!(event_stream); 64 | while let Some(message) = event_stream.next().await { 65 | send_server_message(&ButtplugCurrentSpecServerMessage::try_from(message).unwrap(), &callback); 66 | } 67 | }); 68 | 69 | Box::into_raw(Box::new(server)) 70 | } 71 | 72 | #[no_mangle] 73 | #[wasm_bindgen] 74 | pub fn buttplug_free_embedded_wasm_server(ptr: *mut ButtplugWASMServer) { 75 | if !ptr.is_null() { 76 | unsafe { 77 | let _ = Box::from_raw(ptr); 78 | } 79 | } 80 | } 81 | 82 | 83 | #[no_mangle] 84 | #[wasm_bindgen] 85 | pub fn buttplug_client_send_json_message( 86 | server_ptr: *mut ButtplugWASMServer, 87 | buf: &[u8], 88 | callback: &FFICallback, 89 | ) { 90 | let server = unsafe { 91 | assert!(!server_ptr.is_null()); 92 | &mut *server_ptr 93 | }; 94 | let callback = callback.clone(); 95 | let serializer = ButtplugServerJSONSerializer::default(); 96 | serializer.force_message_version(&BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION); 97 | let input_msg = serializer.deserialize(&ButtplugSerializedMessage::Text(std::str::from_utf8(buf).unwrap().to_owned())).unwrap(); 98 | async_manager::spawn(async move { 99 | let response = server.parse_message(input_msg[0].clone()).await.unwrap(); 100 | send_server_message(&response.try_into().unwrap(), &callback); 101 | }); 102 | } 103 | 104 | #[no_mangle] 105 | #[wasm_bindgen] 106 | pub fn buttplug_activate_env_logger(max_level: &str) { 107 | tracing::subscriber::set_global_default( 108 | Registry::default() 109 | //.with(EnvFilter::new(max_level)) 110 | .with(WASMLayer::new(WASMLayerConfig::default())), 111 | ) 112 | .expect("default global"); 113 | } 114 | -------------------------------------------------------------------------------- /wasm/rust/src/webbluetooth/mod.rs: -------------------------------------------------------------------------------- 1 | 2 | mod webbluetooth_hardware; 3 | mod webbluetooth_manager; 4 | 5 | pub use webbluetooth_hardware::{WebBluetoothHardwareConnector, WebBluetoothHardware}; 6 | pub use webbluetooth_manager::{WebBluetoothCommunicationManagerBuilder,WebBluetoothCommunicationManager}; 7 | -------------------------------------------------------------------------------- /wasm/rust/src/webbluetooth/webbluetooth_hardware.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use buttplug::{ 3 | core::{ 4 | errors::ButtplugDeviceError, 5 | message::Endpoint, 6 | }, 7 | server::device::{ 8 | configuration::{BluetoothLESpecifier, ProtocolCommunicationSpecifier}, 9 | hardware::{ 10 | Hardware, 11 | HardwareConnector, 12 | HardwareEvent, 13 | HardwareInternal, 14 | HardwareReadCmd, 15 | HardwareReading, 16 | HardwareSpecializer, 17 | HardwareSubscribeCmd, 18 | HardwareUnsubscribeCmd, 19 | HardwareWriteCmd, 20 | }, 21 | }, 22 | util::future::{ButtplugFuture, ButtplugFutureStateShared}, 23 | }; 24 | use futures::future::{self, BoxFuture}; 25 | use js_sys::{DataView, Uint8Array}; 26 | use std::{ 27 | collections::HashMap, 28 | convert::TryFrom, 29 | fmt::{self, Debug}, 30 | }; 31 | use tokio::sync::{broadcast, mpsc}; 32 | use wasm_bindgen::prelude::*; 33 | use wasm_bindgen::JsCast; 34 | use wasm_bindgen_futures::{spawn_local, JsFuture}; 35 | use web_sys::{ 36 | BluetoothDevice, 37 | BluetoothRemoteGattCharacteristic, 38 | BluetoothRemoteGattServer, 39 | BluetoothRemoteGattService, 40 | Event, 41 | MessageEvent, 42 | }; 43 | 44 | type WebBluetoothResultFuture = ButtplugFuture>; 45 | type WebBluetoothReadResultFuture = ButtplugFuture>; 46 | 47 | struct BluetoothDeviceWrapper { 48 | pub device: BluetoothDevice 49 | } 50 | 51 | 52 | unsafe impl Send for BluetoothDeviceWrapper { 53 | } 54 | unsafe impl Sync for BluetoothDeviceWrapper { 55 | } 56 | 57 | 58 | pub struct WebBluetoothHardwareConnector { 59 | device: Option, 60 | } 61 | 62 | impl WebBluetoothHardwareConnector { 63 | pub fn new( 64 | device: BluetoothDevice, 65 | ) -> Self { 66 | Self { 67 | device: Some(BluetoothDeviceWrapper { 68 | device, 69 | }) 70 | } 71 | } 72 | } 73 | 74 | impl Debug for WebBluetoothHardwareConnector { 75 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 76 | f.debug_struct("WebBluetoothHardwareCreator") 77 | .field("name", &self.device.as_ref().unwrap().device.name().unwrap()) 78 | .finish() 79 | } 80 | } 81 | 82 | #[async_trait] 83 | impl HardwareConnector for WebBluetoothHardwareConnector { 84 | fn specifier(&self) -> ProtocolCommunicationSpecifier { 85 | ProtocolCommunicationSpecifier::BluetoothLE(BluetoothLESpecifier::new_from_device( 86 | &self.device.as_ref().unwrap().device.name().unwrap(), 87 | &HashMap::new(), 88 | &[] 89 | )) 90 | } 91 | 92 | async fn connect(&mut self) -> Result, ButtplugDeviceError> { 93 | Ok(Box::new(WebBluetoothHardwareSpecializer::new(self.device.take().unwrap()))) 94 | } 95 | } 96 | 97 | 98 | pub struct WebBluetoothHardwareSpecializer { 99 | device: Option, 100 | } 101 | 102 | impl WebBluetoothHardwareSpecializer { 103 | fn new(device: BluetoothDeviceWrapper) -> Self { 104 | Self { 105 | device: Some(device), 106 | } 107 | } 108 | } 109 | 110 | #[async_trait] 111 | impl HardwareSpecializer for WebBluetoothHardwareSpecializer { 112 | async fn specialize( 113 | &mut self, 114 | specifiers: &[ProtocolCommunicationSpecifier], 115 | ) -> Result { 116 | let (sender, mut receiver) = mpsc::channel(256); 117 | let (command_sender, command_receiver) = mpsc::channel(256); 118 | let name; 119 | let address; 120 | let event_sender; 121 | // This block limits the lifetime of device. Since the compiler doesn't 122 | // realize we move device in the spawn_local block, it'll complain that 123 | // device's lifetime lives across the channel await, which gets all 124 | // angry because it's a *mut u8. So this limits the visible lifetime to 125 | // before we start waiting for the reply from the event loop. 126 | let protocol = if let ProtocolCommunicationSpecifier::BluetoothLE(btle) = &specifiers[0] { 127 | btle 128 | } else { 129 | panic!("No bluetooth, we quit"); 130 | }; 131 | { 132 | let device = self.device.take().unwrap().device; 133 | name = device.name().unwrap(); 134 | address = device.id(); 135 | let (es, _) = broadcast::channel(256); 136 | event_sender = es; 137 | let event_loop_fut = run_webbluetooth_loop( 138 | device, 139 | protocol.clone(), 140 | sender, 141 | event_sender.clone(), 142 | command_receiver, 143 | ); 144 | spawn_local(async move { 145 | event_loop_fut.await; 146 | }); 147 | } 148 | 149 | match receiver.recv().await.unwrap() { 150 | WebBluetoothEvent::Connected(_) => { 151 | info!("Web Bluetooth device connected, returning device"); 152 | 153 | let device_impl: Box = Box::new(WebBluetoothHardware::new( 154 | event_sender, 155 | receiver, 156 | command_sender, 157 | )); 158 | Ok(Hardware::new(&name, &address, &[], device_impl)) 159 | } 160 | WebBluetoothEvent::Disconnected => Err( 161 | ButtplugDeviceError::DeviceCommunicationError( 162 | "Could not connect to WebBluetooth device".to_string(), 163 | ) 164 | .into(), 165 | ), 166 | } 167 | } 168 | } 169 | 170 | #[derive(Debug, Clone)] 171 | pub enum WebBluetoothEvent { 172 | // This is the only way we have to get our endpoints back to device creation 173 | // right now. My god this is a mess. 174 | Connected(Vec), 175 | Disconnected, 176 | } 177 | 178 | pub enum WebBluetoothDeviceCommand { 179 | Write( 180 | HardwareWriteCmd, 181 | ButtplugFutureStateShared>, 182 | ), 183 | Read( 184 | HardwareReadCmd, 185 | ButtplugFutureStateShared>, 186 | ), 187 | Subscribe( 188 | HardwareSubscribeCmd, 189 | ButtplugFutureStateShared>, 190 | ), 191 | Unsubscribe( 192 | HardwareUnsubscribeCmd, 193 | ButtplugFutureStateShared>, 194 | ), 195 | } 196 | 197 | async fn run_webbluetooth_loop( 198 | device: BluetoothDevice, 199 | btle_protocol: BluetoothLESpecifier, 200 | device_local_event_sender: mpsc::Sender, 201 | device_external_event_sender: broadcast::Sender, 202 | mut device_command_receiver: mpsc::Receiver, 203 | ) { 204 | //let device = self.device.take().unwrap(); 205 | let mut char_map = HashMap::new(); 206 | let connect_future = device.gatt().unwrap().connect(); 207 | let server: BluetoothRemoteGattServer = match JsFuture::from(connect_future).await { 208 | Ok(val) => val.into(), 209 | Err(_) => { 210 | device_local_event_sender 211 | .send(WebBluetoothEvent::Disconnected) 212 | .await 213 | .unwrap(); 214 | return; 215 | } 216 | }; 217 | for (service_uuid, service_endpoints) in btle_protocol.services() { 218 | let service = if let Ok(serv) = 219 | JsFuture::from(server.get_primary_service_with_str(&service_uuid.to_string())).await 220 | { 221 | info!( 222 | "Service {} found on device {}", 223 | service_uuid, 224 | device.name().unwrap() 225 | ); 226 | BluetoothRemoteGattService::from(serv) 227 | } else { 228 | info!( 229 | "Service {} not found on device {}", 230 | service_uuid, 231 | device.name().unwrap() 232 | ); 233 | continue; 234 | }; 235 | for (chr_name, chr_uuid) in service_endpoints.iter() { 236 | info!("Connecting chr {} {}", chr_name, chr_uuid.to_string()); 237 | let char: BluetoothRemoteGattCharacteristic = 238 | JsFuture::from(service.get_characteristic_with_str(&chr_uuid.to_string())) 239 | .await 240 | .unwrap() 241 | .into(); 242 | char_map.insert(chr_name.clone(), char); 243 | } 244 | } 245 | { 246 | let event_sender = device_external_event_sender.clone(); 247 | let id = device.id().clone(); 248 | let ondisconnected_callback = Closure::wrap(Box::new(move |_: Event| { 249 | info!("device disconnected!"); 250 | event_sender 251 | .send(HardwareEvent::Disconnected(id.clone())) 252 | .unwrap(); 253 | }) as Box); 254 | // set disconnection event handler on BluetoothDevice 255 | device.set_ongattserverdisconnected(Some(ondisconnected_callback.as_ref().unchecked_ref())); 256 | ondisconnected_callback.forget(); 257 | } 258 | //let web_btle_device = WebBluetoothDeviceImpl::new(device, char_map); 259 | info!("device created!"); 260 | let endpoints = char_map.keys().into_iter().cloned().collect(); 261 | device_local_event_sender 262 | .send(WebBluetoothEvent::Connected(endpoints)) 263 | .await; 264 | while let Some(msg) = device_command_receiver.recv().await { 265 | match msg { 266 | WebBluetoothDeviceCommand::Write(write_cmd, waker) => { 267 | debug!("Writing to endpoint {:?}", write_cmd.endpoint()); 268 | let chr = char_map.get(&write_cmd.endpoint()).unwrap().clone(); 269 | spawn_local(async move { 270 | JsFuture::from(chr.write_value_with_u8_array(&mut write_cmd.data().clone())) 271 | .await 272 | .unwrap(); 273 | waker.set_reply(Ok(())); 274 | }); 275 | } 276 | WebBluetoothDeviceCommand::Read(read_cmd, waker) => { 277 | debug!("Writing to endpoint {:?}", read_cmd.endpoint()); 278 | let chr = char_map.get(&read_cmd.endpoint()).unwrap().clone(); 279 | spawn_local(async move { 280 | let read_value = JsFuture::from(chr.read_value()).await.unwrap(); 281 | let data_view = DataView::try_from(read_value).unwrap(); 282 | let mut body = vec![0; data_view.byte_length() as usize]; 283 | Uint8Array::new(&data_view).copy_to(&mut body[..]); 284 | let reading = HardwareReading::new(read_cmd.endpoint(), &body); 285 | waker.set_reply(Ok(reading)); 286 | }); 287 | } 288 | WebBluetoothDeviceCommand::Subscribe(subscribe_cmd, waker) => { 289 | debug!("Subscribing to endpoint {:?}", subscribe_cmd.endpoint()); 290 | let chr = char_map.get(&subscribe_cmd.endpoint()).unwrap().clone(); 291 | let ep = subscribe_cmd.endpoint(); 292 | let event_sender = device_external_event_sender.clone(); 293 | let id = device.id().clone(); 294 | let onchange_callback = Closure::wrap(Box::new(move |e: MessageEvent| { 295 | let event_chr: BluetoothRemoteGattCharacteristic = 296 | BluetoothRemoteGattCharacteristic::from(JsValue::from(e.target().unwrap())); 297 | let value = Uint8Array::new_with_byte_offset( 298 | &JsValue::from(event_chr.value().unwrap().buffer()), 299 | 0, 300 | ); 301 | let value_vec = value.to_vec(); 302 | debug!("Subscription notification from {}: {:?}", ep, value_vec); 303 | event_sender 304 | .send(HardwareEvent::Notification(id.clone(), ep, value_vec)) 305 | .unwrap(); 306 | }) as Box); 307 | // set message event handler on WebSocket 308 | chr.set_oncharacteristicvaluechanged(Some(onchange_callback.as_ref().unchecked_ref())); 309 | onchange_callback.forget(); 310 | spawn_local(async move { 311 | JsFuture::from(chr.start_notifications()).await.unwrap(); 312 | debug!("Endpoint subscribed"); 313 | waker.set_reply(Ok(())); 314 | }); 315 | } 316 | WebBluetoothDeviceCommand::Unsubscribe(_unsubscribe_cmd, _waker) => {} 317 | } 318 | } 319 | debug!("run_webbluetooth_loop exited!"); 320 | } 321 | 322 | 323 | #[derive(Debug)] 324 | pub struct WebBluetoothHardware { 325 | device_command_sender: mpsc::Sender, 326 | device_event_receiver: mpsc::Receiver, 327 | event_sender: broadcast::Sender, 328 | } 329 | /* 330 | unsafe impl Send for WebBluetoothHardware { 331 | } 332 | unsafe impl Sync for WebBluetoothHardware { 333 | } 334 | */ 335 | 336 | impl WebBluetoothHardware { 337 | pub fn new( 338 | event_sender: broadcast::Sender, 339 | device_event_receiver: mpsc::Receiver, 340 | device_command_sender: mpsc::Sender, 341 | ) -> Self { 342 | Self { 343 | event_sender, 344 | device_event_receiver, 345 | device_command_sender, 346 | } 347 | } 348 | } 349 | 350 | impl HardwareInternal for WebBluetoothHardware { 351 | fn event_stream(&self) -> broadcast::Receiver { 352 | self.event_sender.subscribe() 353 | } 354 | 355 | fn disconnect(&self) -> BoxFuture<'static, Result<(), ButtplugDeviceError>> { 356 | Box::pin(future::ready(Ok(()))) 357 | } 358 | 359 | fn read_value( 360 | &self, 361 | msg: &HardwareReadCmd, 362 | ) -> BoxFuture<'static, Result> { 363 | let sender = self.device_command_sender.clone(); 364 | let msg = msg.clone(); 365 | Box::pin(async move { 366 | let fut = WebBluetoothReadResultFuture::default(); 367 | let waker = fut.get_state_clone(); 368 | sender 369 | .send(WebBluetoothDeviceCommand::Read(msg, waker)) 370 | .await; 371 | fut.await 372 | }) 373 | } 374 | 375 | fn write_value(&self, msg: &HardwareWriteCmd) -> BoxFuture<'static, Result<(), ButtplugDeviceError>> { 376 | let sender = self.device_command_sender.clone(); 377 | let msg = msg.clone(); 378 | Box::pin(async move { 379 | let fut = WebBluetoothResultFuture::default(); 380 | let waker = fut.get_state_clone(); 381 | sender 382 | .send(WebBluetoothDeviceCommand::Write(msg.clone(), waker)) 383 | .await; 384 | fut.await 385 | }) 386 | } 387 | 388 | fn subscribe(&self, msg: &HardwareSubscribeCmd) -> BoxFuture<'static, Result<(), ButtplugDeviceError>> { 389 | let sender = self.device_command_sender.clone(); 390 | let msg = msg.clone(); 391 | Box::pin(async move { 392 | let fut = WebBluetoothResultFuture::default(); 393 | let waker = fut.get_state_clone(); 394 | sender 395 | .send(WebBluetoothDeviceCommand::Subscribe(msg.clone(), waker)) 396 | .await; 397 | fut.await 398 | }) 399 | } 400 | 401 | fn unsubscribe(&self, _msg: &HardwareUnsubscribeCmd) -> BoxFuture<'static, Result<(), ButtplugDeviceError>> { 402 | Box::pin(async move { 403 | error!("IMPLEMENT UNSUBSCRIBE FOR WEBBLUETOOTH WASM"); 404 | Ok(()) 405 | }) 406 | } 407 | } 408 | -------------------------------------------------------------------------------- /wasm/rust/src/webbluetooth/webbluetooth_manager.rs: -------------------------------------------------------------------------------- 1 | use super::webbluetooth_hardware::WebBluetoothHardwareConnector; 2 | 3 | use buttplug::{ 4 | core::ButtplugResultFuture, 5 | server::device::{ 6 | configuration::ProtocolCommunicationSpecifier, 7 | hardware::communication::{ 8 | HardwareCommunicationManager, HardwareCommunicationManagerBuilder, 9 | HardwareCommunicationManagerEvent, 10 | }, 11 | }, 12 | util::device_configuration::create_test_dcm, 13 | }; 14 | use futures::future; 15 | use js_sys::Array; 16 | use tokio::sync::mpsc::Sender; 17 | use wasm_bindgen::prelude::*; 18 | use wasm_bindgen_futures::{spawn_local, JsFuture}; 19 | use web_sys::BluetoothDevice; 20 | 21 | #[derive(Default)] 22 | pub struct WebBluetoothCommunicationManagerBuilder { 23 | } 24 | 25 | impl HardwareCommunicationManagerBuilder for WebBluetoothCommunicationManagerBuilder { 26 | fn finish(&mut self, sender: Sender) -> Box { 27 | Box::new(WebBluetoothCommunicationManager { 28 | sender, 29 | }) 30 | } 31 | } 32 | 33 | pub struct WebBluetoothCommunicationManager { 34 | sender: Sender, 35 | } 36 | 37 | #[wasm_bindgen] 38 | extern "C" { 39 | // Use `js_namespace` here to bind `console.log(..)` instead of just 40 | // `log(..)` 41 | #[wasm_bindgen(js_namespace = console)] 42 | fn log(s: &str); 43 | } 44 | 45 | impl HardwareCommunicationManager for WebBluetoothCommunicationManager { 46 | fn name(&self) -> &'static str { 47 | "WebBluetoothCommunicationManager" 48 | } 49 | 50 | fn can_scan(&self) -> bool { 51 | true 52 | } 53 | 54 | fn start_scanning(&mut self) -> ButtplugResultFuture { 55 | info!("WebBluetooth manager scanning"); 56 | let sender_clone = self.sender.clone(); 57 | spawn_local(async move { 58 | // Build the filter block 59 | let nav = web_sys::window().unwrap().navigator(); 60 | if nav.bluetooth().is_none() { 61 | error!("WebBluetooth is not supported on this browser"); 62 | return; 63 | } 64 | info!("WebBluetooth supported by browser, continuing with scan."); 65 | // HACK: As of buttplug v5, we can't just create a HardwareCommunicationManager anymore. This is 66 | // using a test method to create a filled out DCM, which will work for now because there's no 67 | // way for anyone to add device configurations through FFI yet anyways. 68 | let config_manager = create_test_dcm(false); 69 | let mut options = web_sys::RequestDeviceOptions::new(); 70 | let filters = Array::new(); 71 | let optional_services = Array::new(); 72 | for vals in config_manager.protocol_device_configurations().iter() { 73 | for config in vals.1 { 74 | if let ProtocolCommunicationSpecifier::BluetoothLE(btle) = &config { 75 | for name in btle.names() { 76 | let mut filter = web_sys::BluetoothLeScanFilterInit::new(); 77 | if name.contains("*") { 78 | let mut name_clone = name.clone(); 79 | name_clone.pop(); 80 | filter.name_prefix(&name_clone); 81 | } else { 82 | filter.name(&name); 83 | } 84 | filters.push(&filter.into()); 85 | } 86 | for (service, _) in btle.services() { 87 | optional_services.push(&service.to_string().into()); 88 | } 89 | } 90 | } 91 | } 92 | options.filters(&filters.into()); 93 | options.optional_services(&optional_services.into()); 94 | let nav = web_sys::window().unwrap().navigator(); 95 | //nav.bluetooth().get_availability(); 96 | //JsFuture::from(nav.bluetooth().request_device()).await; 97 | match JsFuture::from(nav.bluetooth().unwrap().request_device(&options)).await { 98 | Ok(device) => { 99 | let bt_device = BluetoothDevice::from(device); 100 | if bt_device.name().is_none() { 101 | return; 102 | } 103 | let name = bt_device.name().unwrap(); 104 | let address = bt_device.id(); 105 | let device_creator = Box::new(WebBluetoothHardwareConnector::new(bt_device)); 106 | if sender_clone 107 | .send(HardwareCommunicationManagerEvent::DeviceFound { 108 | name, 109 | address, 110 | creator: device_creator, 111 | }) 112 | .await 113 | .is_err() 114 | { 115 | error!("Device manager receiver dropped, cannot send device found message."); 116 | } else { 117 | info!("WebBluetooth device found."); 118 | } 119 | } 120 | Err(e) => { 121 | error!("Error while trying to start bluetooth scan: {:?}", e); 122 | } 123 | }; 124 | let _ = sender_clone 125 | .send(HardwareCommunicationManagerEvent::ScanningFinished) 126 | .await; 127 | }); 128 | Box::pin(future::ready(Ok(()))) 129 | } 130 | 131 | fn stop_scanning(&mut self) -> ButtplugResultFuture { 132 | Box::pin(future::ready(Ok(()))) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /wasm/src/index.ts: -------------------------------------------------------------------------------- 1 | import { ButtplugMessage, IButtplugClientConnector, fromJSON } from 'buttplug'; 2 | import { EventEmitter } from 'eventemitter3'; 3 | 4 | export class ButtplugWasmClientConnector extends EventEmitter implements IButtplugClientConnector { 5 | private static _loggingActivated = false; 6 | private static wasmInstance: any; 7 | private _connected: boolean = false; 8 | private client: any; 9 | private serverPtr: any; 10 | 11 | constructor() { 12 | super(); 13 | } 14 | 15 | public get Connected(): boolean { return this._connected } 16 | 17 | private static maybeLoadWasm = async() => { 18 | if (ButtplugWasmClientConnector.wasmInstance == undefined) { 19 | ButtplugWasmClientConnector.wasmInstance = await import('@/../rust/pkg/buttplug_wasm.js'); 20 | } 21 | } 22 | 23 | public static activateLogging = async (logLevel: string = "debug") => { 24 | await ButtplugWasmClientConnector.maybeLoadWasm(); 25 | if (this._loggingActivated) { 26 | console.log("Logging already activated, ignoring."); 27 | return; 28 | } 29 | console.log("Turning on logging."); 30 | ButtplugWasmClientConnector.wasmInstance.buttplug_activate_env_logger(logLevel); 31 | } 32 | 33 | public initialize = async (): Promise => {}; 34 | 35 | public connect = async (): Promise => { 36 | await ButtplugWasmClientConnector.maybeLoadWasm(); 37 | //ButtplugWasmClientConnector.wasmInstance.buttplug_activate_env_logger('debug'); 38 | this.client = ButtplugWasmClientConnector.wasmInstance.buttplug_create_embedded_wasm_server((msgs) => { 39 | this.emitMessage(msgs); 40 | }, this.serverPtr); 41 | this._connected = true; 42 | }; 43 | 44 | public disconnect = async (): Promise => {}; 45 | 46 | public send = (msg: ButtplugMessage): void => { 47 | ButtplugWasmClientConnector.wasmInstance.buttplug_client_send_json_message(this.client, new TextEncoder().encode('[' + msg.toJSON() + ']'), (output) => { 48 | this.emitMessage(output); 49 | }); 50 | }; 51 | 52 | private emitMessage = (msg: Uint8Array) => { 53 | let str = new TextDecoder().decode(msg); 54 | // This needs to use buttplug-js's fromJSON, otherwise we won't resolve the message name correctly. 55 | this.emit('message', fromJSON(str)); 56 | } 57 | } -------------------------------------------------------------------------------- /wasm/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions" : { 3 | "baseUrl": "./src", 4 | "lib": ["es2015", "dom", "es6"], 5 | "target": "es6", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "sourceMap": true, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "strictNullChecks": true, 12 | "noImplicitThis": true, 13 | "declaration": true, 14 | "outDir": "./dist/main", 15 | "esModuleInterop": true 16 | }, 17 | "include": [ 18 | "./src/*.ts", 19 | "./src/**/*.ts", 20 | "./tests/**/*.ts" 21 | ] 22 | } -------------------------------------------------------------------------------- /wasm/vite.config.ts: -------------------------------------------------------------------------------- 1 | // vite.config.js 2 | import { resolve } from 'path'; 3 | import { fileURLToPath, URL } from "url"; 4 | import { defineConfig } from 'vite'; 5 | import dts from 'vite-plugin-dts'; 6 | import wasm from 'vite-plugin-wasm'; 7 | import topLevelAwait from "vite-plugin-top-level-await"; 8 | 9 | export default defineConfig({ 10 | build: { 11 | lib: { 12 | // Could also be a dictionary or array of multiple entry points 13 | entry: resolve(__dirname, 'src/index.ts'), 14 | name: 'buttplug-wasm', 15 | // the proper extensions will be added 16 | fileName: (format) => 'buttplug-wasm.mjs', 17 | formats: ['es'], 18 | }, 19 | outDir: 'dist', 20 | }, 21 | resolve: { 22 | alias: { 23 | "@": fileURLToPath(new URL("./src", import.meta.url)), 24 | }, 25 | }, 26 | plugins: [ 27 | wasm(), 28 | topLevelAwait(), 29 | dts({ 30 | exclude: ['tests'], 31 | }), 32 | ], 33 | }); 34 | --------------------------------------------------------------------------------