A new start
This commit is contained in:
2
packages/buttplug/.cargo/config.toml
Normal file
2
packages/buttplug/.cargo/config.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[target.wasm32-unknown-unknown]
|
||||
rustflags = ['--cfg', 'getrandom_backend="wasm_js"', "--cfg=web_sys_unstable_apis"]
|
||||
2503
packages/buttplug/Cargo.lock
generated
Normal file
2503
packages/buttplug/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
63
packages/buttplug/Cargo.toml
Normal file
63
packages/buttplug/Cargo.toml
Normal file
@@ -0,0 +1,63 @@
|
||||
[package]
|
||||
name = "buttplug_wasm"
|
||||
version = "9.0.9"
|
||||
authors = ["Nonpolynomial Labs, LLC <kyle@nonpolynomial.com>"]
|
||||
description = "WASM Interop for the Buttplug Intimate Hardware Control Library"
|
||||
license = "BSD-3-Clause"
|
||||
homepage = "http://buttplug.io"
|
||||
repository = "https://github.com/buttplugio/buttplug.git"
|
||||
readme = "./README.md"
|
||||
keywords = ["usb", "serial", "hardware", "bluetooth", "teledildonics"]
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
name = "buttplug_wasm"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
buttplug = { version = "9.0.9", default-features = false, features = ["wasm"] }
|
||||
# buttplug = { path = "../../../buttplug/buttplug", default-features = false, features = ["wasm"] }
|
||||
# buttplug_derive = { path = "../buttplug_derive" }
|
||||
js-sys = "0.3.80"
|
||||
tracing-wasm = "0.2.1"
|
||||
log-panics = { version = "2.1.0", features = ["with-backtrace"] }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
wasmtimer = "0.4.3"
|
||||
wasm-bindgen = { version = "0.2.103", features = ["serde-serialize"] }
|
||||
tokio = { version = "1.47.1", features = ["sync", "macros", "io-util"] }
|
||||
tokio-stream = "0.1.17"
|
||||
tracing = "0.1.41"
|
||||
tracing-futures = "0.2.5"
|
||||
tracing-subscriber = { version = "0.3.20", features = ["json"] }
|
||||
futures = "0.3.31"
|
||||
futures-util = "0.3.31"
|
||||
async-trait = "0.1.89"
|
||||
wasm-bindgen-futures = "0.4.53"
|
||||
getrandom = { version = "0.3", features = ["wasm_js"] }
|
||||
parking_lot = { version = "0.11.1", features = ["wasm-bindgen"]}
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.80"
|
||||
# path = "../../wasm-bindgen/crates/web-sys"
|
||||
#git = "https://github.com/rustwasm/wasm-bindgen"
|
||||
features = [
|
||||
"Navigator",
|
||||
"Bluetooth",
|
||||
"BluetoothDevice",
|
||||
"BluetoothLeScanFilterInit",
|
||||
"BluetoothRemoteGattCharacteristic",
|
||||
"BluetoothRemoteGattServer",
|
||||
"BluetoothRemoteGattService",
|
||||
"BinaryType",
|
||||
"Blob",
|
||||
"console",
|
||||
"ErrorEvent",
|
||||
"Event",
|
||||
"FileReader",
|
||||
"MessageEvent",
|
||||
"ProgressEvent",
|
||||
"RequestDeviceOptions",
|
||||
"WebSocket",
|
||||
"Window"
|
||||
]
|
||||
27
packages/buttplug/package.json
Normal file
27
packages/buttplug/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "@sexy.pivoine.art/buttplug",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"build:wasm": "wasm-pack build --out-dir wasm --out-name index --target bundler --release"
|
||||
},
|
||||
"dependencies": {
|
||||
"class-transformer": "^0.5.1",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^7.1.4",
|
||||
"vite-plugin-wasm": "3.5.0",
|
||||
"ws": "^8.18.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"wasm-pack": "^0.13.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*!
|
||||
* Buttplug JS Source Code File - Visit https://buttplug.io for more info about
|
||||
* the project. Licensed under the BSD 3-Clause license. See LICENSE file in the
|
||||
* project root for full license information.
|
||||
*
|
||||
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
import { IButtplugClientConnector } from "./IButtplugClientConnector";
|
||||
import { ButtplugMessage } from "../core/Messages";
|
||||
import { ButtplugBrowserWebsocketConnector } from "../utils/ButtplugBrowserWebsocketConnector";
|
||||
|
||||
export class ButtplugBrowserWebsocketClientConnector
|
||||
extends ButtplugBrowserWebsocketConnector
|
||||
implements IButtplugClientConnector
|
||||
{
|
||||
public send = (msg: ButtplugMessage): void => {
|
||||
if (!this.Connected) {
|
||||
throw new Error("ButtplugClient not connected");
|
||||
}
|
||||
this.sendMessage(msg);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*!
|
||||
* Buttplug JS Source Code File - Visit https://buttplug.io for more info about
|
||||
* the project. Licensed under the BSD 3-Clause license. See LICENSE file in the
|
||||
* project root for full license information.
|
||||
*
|
||||
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
||||
*/
|
||||
|
||||
import { ButtplugError } from "../core/Exceptions";
|
||||
import * as Messages from "../core/Messages";
|
||||
|
||||
export class ButtplugClientConnectorException extends ButtplugError {
|
||||
public constructor(message: string) {
|
||||
super(message, Messages.ErrorClass.ERROR_UNKNOWN);
|
||||
}
|
||||
}
|
||||
401
packages/buttplug/src/client/ButtplugClientDevice.ts
Normal file
401
packages/buttplug/src/client/ButtplugClientDevice.ts
Normal file
@@ -0,0 +1,401 @@
|
||||
/*!
|
||||
* Buttplug JS Source Code File - Visit https://buttplug.io for more info about
|
||||
* the project. Licensed under the BSD 3-Clause license. See LICENSE file in the
|
||||
* project root for full license information.
|
||||
*
|
||||
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
import * as Messages from "../core/Messages";
|
||||
import {
|
||||
ButtplugDeviceError,
|
||||
ButtplugError,
|
||||
ButtplugMessageError,
|
||||
} from "../core/Exceptions";
|
||||
import { EventEmitter } from "eventemitter3";
|
||||
import { getMessageClassFromMessage } from "../core/MessageUtils";
|
||||
|
||||
/**
|
||||
* Represents an abstract device, capable of taking certain kinds of messages.
|
||||
*/
|
||||
export class ButtplugClientDevice extends EventEmitter {
|
||||
/**
|
||||
* Return the name of the device.
|
||||
*/
|
||||
public get name(): string {
|
||||
return this._deviceInfo.DeviceName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the user set name of the device.
|
||||
*/
|
||||
public get displayName(): string | undefined {
|
||||
return this._deviceInfo.DeviceDisplayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the index of the device.
|
||||
*/
|
||||
public get index(): number {
|
||||
return this._deviceInfo.DeviceIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the index of the device.
|
||||
*/
|
||||
public get messageTimingGap(): number | undefined {
|
||||
return this._deviceInfo.DeviceMessageTimingGap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of message types the device accepts.
|
||||
*/
|
||||
public get messageAttributes(): Messages.MessageAttributes {
|
||||
return this._deviceInfo.DeviceMessages;
|
||||
}
|
||||
|
||||
public static fromMsg(
|
||||
msg: Messages.DeviceInfo,
|
||||
sendClosure: (
|
||||
device: ButtplugClientDevice,
|
||||
msg: Messages.ButtplugDeviceMessage,
|
||||
) => Promise<Messages.ButtplugMessage>,
|
||||
): ButtplugClientDevice {
|
||||
return new ButtplugClientDevice(msg, sendClosure);
|
||||
}
|
||||
|
||||
// Map of messages and their attributes (feature count, etc...)
|
||||
private allowedMsgs: Map<string, Messages.MessageAttributes> = new Map<
|
||||
string,
|
||||
Messages.MessageAttributes
|
||||
>();
|
||||
|
||||
/**
|
||||
* @param _index Index of the device, as created by the device manager.
|
||||
* @param _name Name of the device.
|
||||
* @param allowedMsgs Buttplug messages the device can receive.
|
||||
*/
|
||||
constructor(
|
||||
private _deviceInfo: Messages.DeviceInfo,
|
||||
private _sendClosure: (
|
||||
device: ButtplugClientDevice,
|
||||
msg: Messages.ButtplugDeviceMessage,
|
||||
) => Promise<Messages.ButtplugMessage>,
|
||||
) {
|
||||
super();
|
||||
_deviceInfo.DeviceMessages.update();
|
||||
}
|
||||
|
||||
public async send(
|
||||
msg: Messages.ButtplugDeviceMessage,
|
||||
): Promise<Messages.ButtplugMessage> {
|
||||
// Assume we're getting the closure from ButtplugClient, which does all of
|
||||
// the index/existence/connection/message checks for us.
|
||||
return await this._sendClosure(this, msg);
|
||||
}
|
||||
|
||||
public async sendExpectOk(
|
||||
msg: Messages.ButtplugDeviceMessage,
|
||||
): Promise<void> {
|
||||
const response = await this.send(msg);
|
||||
switch (getMessageClassFromMessage(response)) {
|
||||
case Messages.Ok:
|
||||
return;
|
||||
case Messages.Error:
|
||||
throw ButtplugError.FromError(response as Messages.Error);
|
||||
default:
|
||||
throw new ButtplugMessageError(
|
||||
`Message type ${response.constructor} not handled by SendMsgExpectOk`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async scalar(
|
||||
scalar: Messages.ScalarSubcommand | Messages.ScalarSubcommand[],
|
||||
): Promise<void> {
|
||||
if (Array.isArray(scalar)) {
|
||||
await this.sendExpectOk(new Messages.ScalarCmd(scalar, this.index));
|
||||
} else {
|
||||
await this.sendExpectOk(new Messages.ScalarCmd([scalar], this.index));
|
||||
}
|
||||
}
|
||||
|
||||
private async scalarCommandBuilder(
|
||||
speed: number | number[],
|
||||
actuator: Messages.ActuatorType,
|
||||
) {
|
||||
const scalarAttrs = this.messageAttributes.ScalarCmd?.filter(
|
||||
(x) => x.ActuatorType === actuator,
|
||||
);
|
||||
if (!scalarAttrs || scalarAttrs.length === 0) {
|
||||
throw new ButtplugDeviceError(
|
||||
`Device ${this.name} has no ${actuator} capabilities`,
|
||||
);
|
||||
}
|
||||
const cmds: Messages.ScalarSubcommand[] = [];
|
||||
if (typeof speed === "number") {
|
||||
scalarAttrs.forEach((x) =>
|
||||
cmds.push(new Messages.ScalarSubcommand(x.Index, speed, actuator)),
|
||||
);
|
||||
} else if (Array.isArray(speed)) {
|
||||
if (speed.length > scalarAttrs.length) {
|
||||
throw new ButtplugDeviceError(
|
||||
`${speed.length} commands send to a device with ${scalarAttrs.length} vibrators`,
|
||||
);
|
||||
}
|
||||
scalarAttrs.forEach((x, i) => {
|
||||
cmds.push(new Messages.ScalarSubcommand(x.Index, speed[i], actuator));
|
||||
});
|
||||
} else {
|
||||
throw new ButtplugDeviceError(
|
||||
`${actuator} can only take numbers or arrays of numbers.`,
|
||||
);
|
||||
}
|
||||
await this.scalar(cmds);
|
||||
}
|
||||
|
||||
public get vibrateAttributes(): Messages.GenericDeviceMessageAttributes[] {
|
||||
return (
|
||||
this.messageAttributes.ScalarCmd?.filter(
|
||||
(x) => x.ActuatorType === Messages.ActuatorType.Vibrate,
|
||||
) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
public async vibrate(speed: number | number[]): Promise<void> {
|
||||
await this.scalarCommandBuilder(speed, Messages.ActuatorType.Vibrate);
|
||||
}
|
||||
|
||||
public get oscillateAttributes(): Messages.GenericDeviceMessageAttributes[] {
|
||||
return (
|
||||
this.messageAttributes.ScalarCmd?.filter(
|
||||
(x) => x.ActuatorType === Messages.ActuatorType.Oscillate,
|
||||
) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
public async oscillate(speed: number | number[]): Promise<void> {
|
||||
await this.scalarCommandBuilder(speed, Messages.ActuatorType.Oscillate);
|
||||
}
|
||||
|
||||
public get rotateAttributes(): Messages.GenericDeviceMessageAttributes[] {
|
||||
return this.messageAttributes.RotateCmd ?? [];
|
||||
}
|
||||
|
||||
public async rotate(
|
||||
values: number | [number, boolean][],
|
||||
clockwise?: boolean,
|
||||
): Promise<void> {
|
||||
const rotateAttrs = this.messageAttributes.RotateCmd;
|
||||
if (!rotateAttrs || rotateAttrs.length === 0) {
|
||||
throw new ButtplugDeviceError(
|
||||
`Device ${this.name} has no Rotate capabilities`,
|
||||
);
|
||||
}
|
||||
let msg: Messages.RotateCmd;
|
||||
if (typeof values === "number") {
|
||||
msg = Messages.RotateCmd.Create(
|
||||
this.index,
|
||||
new Array(rotateAttrs.length).fill([values, clockwise]),
|
||||
);
|
||||
} else if (Array.isArray(values)) {
|
||||
msg = Messages.RotateCmd.Create(this.index, values);
|
||||
} else {
|
||||
throw new ButtplugDeviceError(
|
||||
"SendRotateCmd can only take a number and boolean, or an array of number/boolean tuples",
|
||||
);
|
||||
}
|
||||
await this.sendExpectOk(msg);
|
||||
}
|
||||
|
||||
public get linearAttributes(): Messages.GenericDeviceMessageAttributes[] {
|
||||
return this.messageAttributes.LinearCmd ?? [];
|
||||
}
|
||||
|
||||
public async linear(
|
||||
values: number | [number, number][],
|
||||
duration?: number,
|
||||
): Promise<void> {
|
||||
const linearAttrs = this.messageAttributes.LinearCmd;
|
||||
if (!linearAttrs || linearAttrs.length === 0) {
|
||||
throw new ButtplugDeviceError(
|
||||
`Device ${this.name} has no Linear capabilities`,
|
||||
);
|
||||
}
|
||||
let msg: Messages.LinearCmd;
|
||||
if (typeof values === "number") {
|
||||
msg = Messages.LinearCmd.Create(
|
||||
this.index,
|
||||
new Array(linearAttrs.length).fill([values, duration]),
|
||||
);
|
||||
} else if (Array.isArray(values)) {
|
||||
msg = Messages.LinearCmd.Create(this.index, values);
|
||||
} else {
|
||||
throw new ButtplugDeviceError(
|
||||
"SendLinearCmd can only take a number and number, or an array of number/number tuples",
|
||||
);
|
||||
}
|
||||
await this.sendExpectOk(msg);
|
||||
}
|
||||
|
||||
public async sensorRead(
|
||||
sensorIndex: number,
|
||||
sensorType: Messages.SensorType,
|
||||
): Promise<number[]> {
|
||||
const response = await this.send(
|
||||
new Messages.SensorReadCmd(this.index, sensorIndex, sensorType),
|
||||
);
|
||||
switch (getMessageClassFromMessage(response)) {
|
||||
case Messages.SensorReading:
|
||||
return (response as Messages.SensorReading).Data;
|
||||
case Messages.Error:
|
||||
throw ButtplugError.FromError(response as Messages.Error);
|
||||
default:
|
||||
throw new ButtplugMessageError(
|
||||
`Message type ${response.constructor} not handled by sensorRead`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public get hasBattery(): boolean {
|
||||
const batteryAttrs = this.messageAttributes.SensorReadCmd?.filter(
|
||||
(x) => x.SensorType === Messages.SensorType.Battery,
|
||||
);
|
||||
return batteryAttrs !== undefined && batteryAttrs.length > 0;
|
||||
}
|
||||
|
||||
public async battery(): Promise<number> {
|
||||
if (!this.hasBattery) {
|
||||
throw new ButtplugDeviceError(
|
||||
`Device ${this.name} has no Battery capabilities`,
|
||||
);
|
||||
}
|
||||
const batteryAttrs = this.messageAttributes.SensorReadCmd?.filter(
|
||||
(x) => x.SensorType === Messages.SensorType.Battery,
|
||||
);
|
||||
// Find the battery sensor, we'll need its index.
|
||||
const result = await this.sensorRead(
|
||||
batteryAttrs![0].Index,
|
||||
Messages.SensorType.Battery,
|
||||
);
|
||||
return result[0] / 100.0;
|
||||
}
|
||||
|
||||
public get hasRssi(): boolean {
|
||||
const rssiAttrs = this.messageAttributes.SensorReadCmd?.filter(
|
||||
(x) => x.SensorType === Messages.SensorType.RSSI,
|
||||
);
|
||||
return rssiAttrs !== undefined && rssiAttrs.length === 0;
|
||||
}
|
||||
|
||||
public async rssi(): Promise<number> {
|
||||
if (!this.hasRssi) {
|
||||
throw new ButtplugDeviceError(
|
||||
`Device ${this.name} has no RSSI capabilities`,
|
||||
);
|
||||
}
|
||||
const rssiAttrs = this.messageAttributes.SensorReadCmd?.filter(
|
||||
(x) => x.SensorType === Messages.SensorType.RSSI,
|
||||
);
|
||||
// Find the battery sensor, we'll need its index.
|
||||
const result = await this.sensorRead(
|
||||
rssiAttrs![0].Index,
|
||||
Messages.SensorType.RSSI,
|
||||
);
|
||||
return result[0];
|
||||
}
|
||||
|
||||
public async rawRead(
|
||||
endpoint: string,
|
||||
expectedLength: number,
|
||||
timeout: number,
|
||||
): Promise<Uint8Array> {
|
||||
if (!this.messageAttributes.RawReadCmd) {
|
||||
throw new ButtplugDeviceError(
|
||||
`Device ${this.name} has no raw read capabilities`,
|
||||
);
|
||||
}
|
||||
if (this.messageAttributes.RawReadCmd.Endpoints.indexOf(endpoint) === -1) {
|
||||
throw new ButtplugDeviceError(
|
||||
`Device ${this.name} has no raw readable endpoint ${endpoint}`,
|
||||
);
|
||||
}
|
||||
const response = await this.send(
|
||||
new Messages.RawReadCmd(this.index, endpoint, expectedLength, timeout),
|
||||
);
|
||||
switch (getMessageClassFromMessage(response)) {
|
||||
case Messages.RawReading:
|
||||
return new Uint8Array((response as Messages.RawReading).Data);
|
||||
case Messages.Error:
|
||||
throw ButtplugError.FromError(response as Messages.Error);
|
||||
default:
|
||||
throw new ButtplugMessageError(
|
||||
`Message type ${response.constructor} not handled by rawRead`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async rawWrite(
|
||||
endpoint: string,
|
||||
data: Uint8Array,
|
||||
writeWithResponse: boolean,
|
||||
): Promise<void> {
|
||||
if (!this.messageAttributes.RawWriteCmd) {
|
||||
throw new ButtplugDeviceError(
|
||||
`Device ${this.name} has no raw write capabilities`,
|
||||
);
|
||||
}
|
||||
if (this.messageAttributes.RawWriteCmd.Endpoints.indexOf(endpoint) === -1) {
|
||||
throw new ButtplugDeviceError(
|
||||
`Device ${this.name} has no raw writable endpoint ${endpoint}`,
|
||||
);
|
||||
}
|
||||
await this.sendExpectOk(
|
||||
new Messages.RawWriteCmd(this.index, endpoint, data, writeWithResponse),
|
||||
);
|
||||
}
|
||||
|
||||
public async rawSubscribe(endpoint: string): Promise<void> {
|
||||
if (!this.messageAttributes.RawSubscribeCmd) {
|
||||
throw new ButtplugDeviceError(
|
||||
`Device ${this.name} has no raw subscribe capabilities`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
this.messageAttributes.RawSubscribeCmd.Endpoints.indexOf(endpoint) === -1
|
||||
) {
|
||||
throw new ButtplugDeviceError(
|
||||
`Device ${this.name} has no raw subscribable endpoint ${endpoint}`,
|
||||
);
|
||||
}
|
||||
await this.sendExpectOk(new Messages.RawSubscribeCmd(this.index, endpoint));
|
||||
}
|
||||
|
||||
public async rawUnsubscribe(endpoint: string): Promise<void> {
|
||||
// This reuses raw subscribe's info.
|
||||
if (!this.messageAttributes.RawSubscribeCmd) {
|
||||
throw new ButtplugDeviceError(
|
||||
`Device ${this.name} has no raw unsubscribe capabilities`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
this.messageAttributes.RawSubscribeCmd.Endpoints.indexOf(endpoint) === -1
|
||||
) {
|
||||
throw new ButtplugDeviceError(
|
||||
`Device ${this.name} has no raw unsubscribable endpoint ${endpoint}`,
|
||||
);
|
||||
}
|
||||
await this.sendExpectOk(
|
||||
new Messages.RawUnsubscribeCmd(this.index, endpoint),
|
||||
);
|
||||
}
|
||||
|
||||
public async stop(): Promise<void> {
|
||||
await this.sendExpectOk(new Messages.StopDeviceCmd(this.index));
|
||||
}
|
||||
|
||||
public emitDisconnected() {
|
||||
this.emit("deviceremoved");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*!
|
||||
* Buttplug JS Source Code File - Visit https://buttplug.io for more info about
|
||||
* the project. Licensed under the BSD 3-Clause license. See LICENSE file in the
|
||||
* project root for full license information.
|
||||
*
|
||||
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
import { ButtplugBrowserWebsocketClientConnector } from "./ButtplugBrowserWebsocketClientConnector";
|
||||
import { WebSocket as NodeWebSocket } from "ws";
|
||||
|
||||
export class ButtplugNodeWebsocketClientConnector extends ButtplugBrowserWebsocketClientConnector {
|
||||
protected _websocketConstructor =
|
||||
NodeWebSocket as unknown as typeof WebSocket;
|
||||
}
|
||||
276
packages/buttplug/src/client/Client.ts
Normal file
276
packages/buttplug/src/client/Client.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
/*!
|
||||
* Buttplug JS Source Code File - Visit https://buttplug.io for more info about
|
||||
* the project. Licensed under the BSD 3-Clause license. See LICENSE file in the
|
||||
* project root for full license information.
|
||||
*
|
||||
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
import { ButtplugLogger } from "../core/Logging";
|
||||
import { EventEmitter } from "eventemitter3";
|
||||
import { ButtplugClientDevice } from "./ButtplugClientDevice";
|
||||
import { IButtplugClientConnector } from "./IButtplugClientConnector";
|
||||
import { ButtplugMessageSorter } from "../utils/ButtplugMessageSorter";
|
||||
|
||||
import * as Messages from "../core/Messages";
|
||||
import {
|
||||
ButtplugDeviceError,
|
||||
ButtplugError,
|
||||
ButtplugInitError,
|
||||
ButtplugMessageError,
|
||||
} from "../core/Exceptions";
|
||||
import { ButtplugClientConnectorException } from "./ButtplugClientConnectorException";
|
||||
import { getMessageClassFromMessage } from "../core/MessageUtils";
|
||||
|
||||
export class ButtplugClient extends EventEmitter {
|
||||
protected _pingTimer: NodeJS.Timeout | null = null;
|
||||
protected _connector: IButtplugClientConnector | null = null;
|
||||
protected _devices: Map<number, ButtplugClientDevice> = new Map();
|
||||
protected _clientName: string;
|
||||
protected _logger = ButtplugLogger.Logger;
|
||||
protected _isScanning = false;
|
||||
private _sorter: ButtplugMessageSorter = new ButtplugMessageSorter(true);
|
||||
|
||||
constructor(clientName = "Generic Buttplug Client") {
|
||||
super();
|
||||
this._clientName = clientName;
|
||||
this._logger.Debug(`ButtplugClient: Client ${clientName} created.`);
|
||||
}
|
||||
|
||||
public get connected(): boolean {
|
||||
return this._connector !== null && this._connector.Connected;
|
||||
}
|
||||
|
||||
public get devices(): ButtplugClientDevice[] {
|
||||
// While this function doesn't actually send a message, if we don't have a
|
||||
// connector, we shouldn't have devices.
|
||||
this.checkConnector();
|
||||
const devices: ButtplugClientDevice[] = [];
|
||||
this._devices.forEach((d) => {
|
||||
devices.push(d);
|
||||
});
|
||||
return devices;
|
||||
}
|
||||
|
||||
public get isScanning(): boolean {
|
||||
return this._isScanning;
|
||||
}
|
||||
|
||||
public connect = async (connector: IButtplugClientConnector) => {
|
||||
this._logger.Info(
|
||||
`ButtplugClient: Connecting using ${connector.constructor.name}`,
|
||||
);
|
||||
await connector.connect();
|
||||
this._connector = connector;
|
||||
this._connector.addListener("message", this.parseMessages);
|
||||
this._connector.addListener("disconnect", this.disconnectHandler);
|
||||
await this.initializeConnection();
|
||||
};
|
||||
|
||||
public disconnect = async () => {
|
||||
this._logger.Debug("ButtplugClient: Disconnect called");
|
||||
this.checkConnector();
|
||||
await this.shutdownConnection();
|
||||
await this._connector!.disconnect();
|
||||
};
|
||||
|
||||
public startScanning = async () => {
|
||||
this._logger.Debug("ButtplugClient: StartScanning called");
|
||||
this._isScanning = true;
|
||||
await this.sendMsgExpectOk(new Messages.StartScanning());
|
||||
};
|
||||
|
||||
public stopScanning = async () => {
|
||||
this._logger.Debug("ButtplugClient: StopScanning called");
|
||||
this._isScanning = false;
|
||||
await this.sendMsgExpectOk(new Messages.StopScanning());
|
||||
};
|
||||
|
||||
public stopAllDevices = async () => {
|
||||
this._logger.Debug("ButtplugClient: StopAllDevices");
|
||||
await this.sendMsgExpectOk(new Messages.StopAllDevices());
|
||||
};
|
||||
|
||||
private async sendDeviceMessage(
|
||||
device: ButtplugClientDevice,
|
||||
deviceMsg: Messages.ButtplugDeviceMessage,
|
||||
): Promise<Messages.ButtplugMessage> {
|
||||
this.checkConnector();
|
||||
const dev = this._devices.get(device.index);
|
||||
if (dev === undefined) {
|
||||
throw ButtplugError.LogAndError(
|
||||
ButtplugDeviceError,
|
||||
this._logger,
|
||||
`Device ${device.index} not available.`,
|
||||
);
|
||||
}
|
||||
deviceMsg.DeviceIndex = device.index;
|
||||
return await this.sendMessage(deviceMsg);
|
||||
}
|
||||
|
||||
protected disconnectHandler = () => {
|
||||
this._logger.Info("ButtplugClient: Disconnect event receieved.");
|
||||
this.emit("disconnect");
|
||||
};
|
||||
|
||||
protected parseMessages = (msgs: Messages.ButtplugMessage[]) => {
|
||||
const leftoverMsgs = this._sorter.ParseIncomingMessages(msgs);
|
||||
for (const x of leftoverMsgs) {
|
||||
switch (getMessageClassFromMessage(x)) {
|
||||
case Messages.DeviceAdded: {
|
||||
const addedMsg = x as Messages.DeviceAdded;
|
||||
const addedDevice = ButtplugClientDevice.fromMsg(
|
||||
addedMsg,
|
||||
this.sendDeviceMessageClosure,
|
||||
);
|
||||
this._devices.set(addedMsg.DeviceIndex, addedDevice);
|
||||
this.emit("deviceadded", addedMsg, addedDevice);
|
||||
break;
|
||||
}
|
||||
case Messages.DeviceRemoved: {
|
||||
const removedMsg = x as Messages.DeviceRemoved;
|
||||
if (this._devices.has(removedMsg.DeviceIndex)) {
|
||||
const removedDevice = this._devices.get(removedMsg.DeviceIndex);
|
||||
removedDevice?.emitDisconnected();
|
||||
this._devices.delete(removedMsg.DeviceIndex);
|
||||
this.emit("deviceremoved", removedMsg, removedDevice);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Messages.ScanningFinished:
|
||||
this._isScanning = false;
|
||||
this.emit("scanningfinished", x);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
protected initializeConnection = async (): Promise<boolean> => {
|
||||
this.checkConnector();
|
||||
const msg = await this.sendMessage(
|
||||
new Messages.RequestServerInfo(
|
||||
this._clientName,
|
||||
Messages.MESSAGE_SPEC_VERSION,
|
||||
),
|
||||
);
|
||||
switch (getMessageClassFromMessage(msg)) {
|
||||
case Messages.ServerInfo: {
|
||||
const serverinfo = msg as Messages.ServerInfo;
|
||||
this._logger.Info(
|
||||
`ButtplugClient: Connected to Server ${serverinfo.ServerName}`,
|
||||
);
|
||||
// TODO: maybe store server name, do something with message template version?
|
||||
const ping = serverinfo.MaxPingTime;
|
||||
if (serverinfo.MessageVersion < Messages.MESSAGE_SPEC_VERSION) {
|
||||
// Disconnect and throw an exception explaining the version mismatch problem.
|
||||
await this._connector!.disconnect();
|
||||
throw ButtplugError.LogAndError(
|
||||
ButtplugInitError,
|
||||
this._logger,
|
||||
`Server protocol version ${serverinfo.MessageVersion} is older than client protocol version ${Messages.MESSAGE_SPEC_VERSION}. Please update server.`,
|
||||
);
|
||||
}
|
||||
if (ping > 0) {
|
||||
/*
|
||||
this._pingTimer = setInterval(async () => {
|
||||
// If we've disconnected, stop trying to ping the server.
|
||||
if (!this.Connected) {
|
||||
await this.ShutdownConnection();
|
||||
return;
|
||||
}
|
||||
await this.SendMessage(new Messages.Ping());
|
||||
} , Math.round(ping / 2));
|
||||
*/
|
||||
}
|
||||
await this.requestDeviceList();
|
||||
return true;
|
||||
}
|
||||
case Messages.Error: {
|
||||
// Disconnect and throw an exception with the error message we got back.
|
||||
// This will usually only error out if we have a version mismatch that the
|
||||
// server has detected.
|
||||
await this._connector!.disconnect();
|
||||
const err = msg as Messages.Error;
|
||||
throw ButtplugError.LogAndError(
|
||||
ButtplugInitError,
|
||||
this._logger,
|
||||
`Cannot connect to server. ${err.ErrorMessage}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
protected requestDeviceList = async () => {
|
||||
this.checkConnector();
|
||||
this._logger.Debug("ButtplugClient: ReceiveDeviceList called");
|
||||
const deviceList = (await this.sendMessage(
|
||||
new Messages.RequestDeviceList(),
|
||||
)) as Messages.DeviceList;
|
||||
deviceList.Devices.forEach((d) => {
|
||||
if (!this._devices.has(d.DeviceIndex)) {
|
||||
const device = ButtplugClientDevice.fromMsg(
|
||||
d,
|
||||
this.sendDeviceMessageClosure,
|
||||
);
|
||||
this._logger.Debug(`ButtplugClient: Adding Device: ${device}`);
|
||||
this._devices.set(d.DeviceIndex, device);
|
||||
this.emit("deviceadded", device);
|
||||
} else {
|
||||
this._logger.Debug(`ButtplugClient: Device already added: ${d}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
protected shutdownConnection = async () => {
|
||||
await this.stopAllDevices();
|
||||
if (this._pingTimer !== null) {
|
||||
clearInterval(this._pingTimer);
|
||||
this._pingTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
protected async sendMessage(
|
||||
msg: Messages.ButtplugMessage,
|
||||
): Promise<Messages.ButtplugMessage> {
|
||||
this.checkConnector();
|
||||
const p = this._sorter.PrepareOutgoingMessage(msg);
|
||||
await this._connector!.send(msg);
|
||||
return await p;
|
||||
}
|
||||
|
||||
protected checkConnector() {
|
||||
if (!this.connected) {
|
||||
throw new ButtplugClientConnectorException(
|
||||
"ButtplugClient not connected",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected sendMsgExpectOk = async (
|
||||
msg: Messages.ButtplugMessage,
|
||||
): Promise<void> => {
|
||||
const response = await this.sendMessage(msg);
|
||||
switch (getMessageClassFromMessage(response)) {
|
||||
case Messages.Ok:
|
||||
return;
|
||||
case Messages.Error:
|
||||
throw ButtplugError.FromError(response as Messages.Error);
|
||||
default:
|
||||
throw ButtplugError.LogAndError(
|
||||
ButtplugMessageError,
|
||||
this._logger,
|
||||
`Message type ${getMessageClassFromMessage(response)!.constructor} not handled by SendMsgExpectOk`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
protected sendDeviceMessageClosure = async (
|
||||
device: ButtplugClientDevice,
|
||||
msg: Messages.ButtplugDeviceMessage,
|
||||
): Promise<Messages.ButtplugMessage> => {
|
||||
return await this.sendDeviceMessage(device, msg);
|
||||
};
|
||||
}
|
||||
18
packages/buttplug/src/client/IButtplugClientConnector.ts
Normal file
18
packages/buttplug/src/client/IButtplugClientConnector.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/*!
|
||||
* Buttplug JS Source Code File - Visit https://buttplug.io for more info about
|
||||
* the project. Licensed under the BSD 3-Clause license. See LICENSE file in the
|
||||
* project root for full license information.
|
||||
*
|
||||
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
||||
*/
|
||||
|
||||
import { ButtplugMessage } from "../core/Messages";
|
||||
import { EventEmitter } from "eventemitter3";
|
||||
|
||||
export interface IButtplugClientConnector extends EventEmitter {
|
||||
connect: () => Promise<void>;
|
||||
disconnect: () => Promise<void>;
|
||||
initialize: () => Promise<void>;
|
||||
send: (msg: ButtplugMessage) => void;
|
||||
readonly Connected: boolean;
|
||||
}
|
||||
101
packages/buttplug/src/core/Exceptions.ts
Normal file
101
packages/buttplug/src/core/Exceptions.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
/*!
|
||||
* Buttplug JS Source Code File - Visit https://buttplug.io for more info about
|
||||
* the project. Licensed under the BSD 3-Clause license. See LICENSE file in the
|
||||
* project root for full license information.
|
||||
*
|
||||
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
||||
*/
|
||||
|
||||
import * as Messages from "./Messages";
|
||||
import { ButtplugLogger } from "./Logging";
|
||||
|
||||
export class ButtplugError extends Error {
|
||||
public get ErrorClass(): Messages.ErrorClass {
|
||||
return this.errorClass;
|
||||
}
|
||||
|
||||
public get InnerError(): Error | undefined {
|
||||
return this.innerError;
|
||||
}
|
||||
|
||||
public get Id(): number | undefined {
|
||||
return this.messageId;
|
||||
}
|
||||
|
||||
public get ErrorMessage(): Messages.ButtplugMessage {
|
||||
return new Messages.Error(this.message, this.ErrorClass, this.Id);
|
||||
}
|
||||
|
||||
public static LogAndError<T extends ButtplugError>(
|
||||
constructor: new (str: string, num: number) => T,
|
||||
logger: ButtplugLogger,
|
||||
message: string,
|
||||
id: number = Messages.SYSTEM_MESSAGE_ID,
|
||||
): T {
|
||||
logger.Error(message);
|
||||
return new constructor(message, id);
|
||||
}
|
||||
|
||||
public static FromError(error: Messages.Error) {
|
||||
switch (error.ErrorCode) {
|
||||
case Messages.ErrorClass.ERROR_DEVICE:
|
||||
return new ButtplugDeviceError(error.ErrorMessage, error.Id);
|
||||
case Messages.ErrorClass.ERROR_INIT:
|
||||
return new ButtplugInitError(error.ErrorMessage, error.Id);
|
||||
case Messages.ErrorClass.ERROR_UNKNOWN:
|
||||
return new ButtplugUnknownError(error.ErrorMessage, error.Id);
|
||||
case Messages.ErrorClass.ERROR_PING:
|
||||
return new ButtplugPingError(error.ErrorMessage, error.Id);
|
||||
case Messages.ErrorClass.ERROR_MSG:
|
||||
return new ButtplugMessageError(error.ErrorMessage, error.Id);
|
||||
default:
|
||||
throw new Error(`Message type ${error.ErrorCode} not handled`);
|
||||
}
|
||||
}
|
||||
|
||||
public errorClass: Messages.ErrorClass = Messages.ErrorClass.ERROR_UNKNOWN;
|
||||
public innerError: Error | undefined;
|
||||
public messageId: number | undefined;
|
||||
|
||||
protected constructor(
|
||||
message: string,
|
||||
errorClass: Messages.ErrorClass,
|
||||
id: number = Messages.SYSTEM_MESSAGE_ID,
|
||||
inner?: Error,
|
||||
) {
|
||||
super(message);
|
||||
this.errorClass = errorClass;
|
||||
this.innerError = inner;
|
||||
this.messageId = id;
|
||||
}
|
||||
}
|
||||
|
||||
export class ButtplugInitError extends ButtplugError {
|
||||
public constructor(message: string, id: number = Messages.SYSTEM_MESSAGE_ID) {
|
||||
super(message, Messages.ErrorClass.ERROR_INIT, id);
|
||||
}
|
||||
}
|
||||
|
||||
export class ButtplugDeviceError extends ButtplugError {
|
||||
public constructor(message: string, id: number = Messages.SYSTEM_MESSAGE_ID) {
|
||||
super(message, Messages.ErrorClass.ERROR_DEVICE, id);
|
||||
}
|
||||
}
|
||||
|
||||
export class ButtplugMessageError extends ButtplugError {
|
||||
public constructor(message: string, id: number = Messages.SYSTEM_MESSAGE_ID) {
|
||||
super(message, Messages.ErrorClass.ERROR_MSG, id);
|
||||
}
|
||||
}
|
||||
|
||||
export class ButtplugPingError extends ButtplugError {
|
||||
public constructor(message: string, id: number = Messages.SYSTEM_MESSAGE_ID) {
|
||||
super(message, Messages.ErrorClass.ERROR_PING, id);
|
||||
}
|
||||
}
|
||||
|
||||
export class ButtplugUnknownError extends ButtplugError {
|
||||
public constructor(message: string, id: number = Messages.SYSTEM_MESSAGE_ID) {
|
||||
super(message, Messages.ErrorClass.ERROR_UNKNOWN, id);
|
||||
}
|
||||
}
|
||||
195
packages/buttplug/src/core/Logging.ts
Normal file
195
packages/buttplug/src/core/Logging.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
/*!
|
||||
* Buttplug JS Source Code File - Visit https://buttplug.io for more info about
|
||||
* the project. Licensed under the BSD 3-Clause license. See LICENSE file in the
|
||||
* project root for full license information.
|
||||
*
|
||||
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "eventemitter3";
|
||||
|
||||
export enum ButtplugLogLevel {
|
||||
Off,
|
||||
Error,
|
||||
Warn,
|
||||
Info,
|
||||
Debug,
|
||||
Trace,
|
||||
}
|
||||
|
||||
/**
|
||||
* Representation of log messages for the internal logging utility.
|
||||
*/
|
||||
export class LogMessage {
|
||||
/** Timestamp for the log message */
|
||||
private timestamp: string;
|
||||
|
||||
/** Log Message */
|
||||
private logMessage: string;
|
||||
|
||||
/** Log Level */
|
||||
private logLevel: ButtplugLogLevel;
|
||||
|
||||
/**
|
||||
* @param logMessage Log message.
|
||||
* @param logLevel: Log severity level.
|
||||
*/
|
||||
public constructor(logMessage: string, logLevel: ButtplugLogLevel) {
|
||||
const a = new Date();
|
||||
const hour = a.getHours();
|
||||
const min = a.getMinutes();
|
||||
const sec = a.getSeconds();
|
||||
this.timestamp = `${hour}:${min}:${sec}`;
|
||||
this.logMessage = logMessage;
|
||||
this.logLevel = logLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the log message.
|
||||
*/
|
||||
public get Message() {
|
||||
return this.logMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the log message level.
|
||||
*/
|
||||
public get LogLevel() {
|
||||
return this.logLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the log message timestamp.
|
||||
*/
|
||||
public get Timestamp() {
|
||||
return this.timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a formatted string with timestamp, level, and message.
|
||||
*/
|
||||
public get FormattedMessage() {
|
||||
return `${ButtplugLogLevel[this.logLevel]} : ${this.timestamp} : ${this.logMessage}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple, global logging utility for the Buttplug client and server. Keeps an
|
||||
* internal static reference to an instance of itself (singleton pattern,
|
||||
* basically), and allows message logging throughout the module.
|
||||
*/
|
||||
export class ButtplugLogger extends EventEmitter {
|
||||
/** Singleton instance for the logger */
|
||||
protected static sLogger: ButtplugLogger | undefined = undefined;
|
||||
/** Sets maximum log level to log to console */
|
||||
protected maximumConsoleLogLevel: ButtplugLogLevel = ButtplugLogLevel.Off;
|
||||
/** Sets maximum log level for all log messages */
|
||||
protected maximumEventLogLevel: ButtplugLogLevel = ButtplugLogLevel.Off;
|
||||
|
||||
/**
|
||||
* Returns the stored static instance of the logger, creating one if it
|
||||
* doesn't currently exist.
|
||||
*/
|
||||
public static get Logger(): ButtplugLogger {
|
||||
if (ButtplugLogger.sLogger === undefined) {
|
||||
ButtplugLogger.sLogger = new ButtplugLogger();
|
||||
}
|
||||
return this.sLogger!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor. Can only be called internally since we regulate ButtplugLogger
|
||||
* ownership.
|
||||
*/
|
||||
protected constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the maximum log level to output to console.
|
||||
*/
|
||||
public get MaximumConsoleLogLevel() {
|
||||
return this.maximumConsoleLogLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the maximum log level to output to console.
|
||||
*/
|
||||
public set MaximumConsoleLogLevel(buttplugLogLevel: ButtplugLogLevel) {
|
||||
this.maximumConsoleLogLevel = buttplugLogLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the global maximum log level
|
||||
*/
|
||||
public get MaximumEventLogLevel() {
|
||||
return this.maximumEventLogLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the global maximum log level
|
||||
*/
|
||||
public set MaximumEventLogLevel(logLevel: ButtplugLogLevel) {
|
||||
this.maximumEventLogLevel = logLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log new message at Error level.
|
||||
*/
|
||||
public Error(msg: string) {
|
||||
this.AddLogMessage(msg, ButtplugLogLevel.Error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log new message at Warn level.
|
||||
*/
|
||||
public Warn(msg: string) {
|
||||
this.AddLogMessage(msg, ButtplugLogLevel.Warn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log new message at Info level.
|
||||
*/
|
||||
public Info(msg: string) {
|
||||
this.AddLogMessage(msg, ButtplugLogLevel.Info);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log new message at Debug level.
|
||||
*/
|
||||
public Debug(msg: string) {
|
||||
this.AddLogMessage(msg, ButtplugLogLevel.Debug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log new message at Trace level.
|
||||
*/
|
||||
public Trace(msg: string) {
|
||||
this.AddLogMessage(msg, ButtplugLogLevel.Trace);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if message should be logged, and if so, adds message to the
|
||||
* log buffer. May also print message and emit event.
|
||||
*/
|
||||
protected AddLogMessage(msg: string, level: ButtplugLogLevel) {
|
||||
// If nothing wants the log message we have, ignore it.
|
||||
if (
|
||||
level > this.maximumEventLogLevel &&
|
||||
level > this.maximumConsoleLogLevel
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const logMsg = new LogMessage(msg, level);
|
||||
// Clients and console logging may have different needs. For instance, it
|
||||
// could be that the client requests trace level, while all we want in the
|
||||
// console is info level. This makes sure the client can't also spam the
|
||||
// console.
|
||||
if (level <= this.maximumConsoleLogLevel) {
|
||||
console.log(logMsg.FormattedMessage);
|
||||
}
|
||||
if (level <= this.maximumEventLogLevel) {
|
||||
this.emit("log", logMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
48
packages/buttplug/src/core/MessageUtils.ts
Normal file
48
packages/buttplug/src/core/MessageUtils.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/*!
|
||||
* Buttplug JS Source Code File - Visit https://buttplug.io for more info about
|
||||
* the project. Licensed under the BSD 3-Clause license. See LICENSE file in the
|
||||
* project root for full license information.
|
||||
*
|
||||
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
import * as Messages from "./Messages";
|
||||
|
||||
function getMessageClass(
|
||||
type: string,
|
||||
): (new (...args: unknown[]) => Messages.ButtplugMessage) | null {
|
||||
for (const value of Object.values(Messages)) {
|
||||
if (typeof value === "function" && "Name" in value && value.Name === type) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getMessageClassFromMessage(
|
||||
msg: Messages.ButtplugMessage,
|
||||
): (new (...args: unknown[]) => Messages.ButtplugMessage) | null {
|
||||
// Making the bold assumption all message classes have the Name static. Should define a
|
||||
// requirement for this in the abstract class.
|
||||
return getMessageClass(Object.getPrototypeOf(msg).constructor.Name);
|
||||
}
|
||||
|
||||
export function fromJSON(str): Messages.ButtplugMessage[] {
|
||||
const msgarray: object[] = JSON.parse(str);
|
||||
const msgs: Messages.ButtplugMessage[] = [];
|
||||
for (const x of Array.from(msgarray)) {
|
||||
const type = Object.getOwnPropertyNames(x)[0];
|
||||
const cls = getMessageClass(type);
|
||||
if (cls) {
|
||||
const msg = plainToInstance<Messages.ButtplugMessage, unknown>(
|
||||
cls,
|
||||
x[type],
|
||||
);
|
||||
msg.update();
|
||||
msgs.push(msg);
|
||||
}
|
||||
}
|
||||
return msgs;
|
||||
}
|
||||
491
packages/buttplug/src/core/Messages.ts
Normal file
491
packages/buttplug/src/core/Messages.ts
Normal file
@@ -0,0 +1,491 @@
|
||||
/*!
|
||||
* Buttplug JS Source Code File - Visit https://buttplug.io for more info about
|
||||
* the project. Licensed under the BSD 3-Clause license. See LICENSE file in the
|
||||
* project root for full license information.
|
||||
*
|
||||
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
||||
*/
|
||||
|
||||
// tslint:disable:max-classes-per-file
|
||||
"use strict";
|
||||
|
||||
import { instanceToPlain, Type } from "class-transformer";
|
||||
import "reflect-metadata";
|
||||
|
||||
export const SYSTEM_MESSAGE_ID = 0;
|
||||
export const DEFAULT_MESSAGE_ID = 1;
|
||||
export const MAX_ID = 4294967295;
|
||||
export const MESSAGE_SPEC_VERSION = 3;
|
||||
|
||||
export class MessageAttributes {
|
||||
public ScalarCmd?: Array<GenericDeviceMessageAttributes>;
|
||||
public RotateCmd?: Array<GenericDeviceMessageAttributes>;
|
||||
public LinearCmd?: Array<GenericDeviceMessageAttributes>;
|
||||
public RawReadCmd?: RawDeviceMessageAttributes;
|
||||
public RawWriteCmd?: RawDeviceMessageAttributes;
|
||||
public RawSubscribeCmd?: RawDeviceMessageAttributes;
|
||||
public SensorReadCmd?: Array<SensorDeviceMessageAttributes>;
|
||||
public SensorSubscribeCmd?: Array<SensorDeviceMessageAttributes>;
|
||||
public StopDeviceCmd: {};
|
||||
|
||||
constructor(data: Partial<MessageAttributes>) {
|
||||
Object.assign(this, data);
|
||||
}
|
||||
|
||||
public update() {
|
||||
this.ScalarCmd?.forEach((x, i) => (x.Index = i));
|
||||
this.RotateCmd?.forEach((x, i) => (x.Index = i));
|
||||
this.LinearCmd?.forEach((x, i) => (x.Index = i));
|
||||
this.SensorReadCmd?.forEach((x, i) => (x.Index = i));
|
||||
this.SensorSubscribeCmd?.forEach((x, i) => (x.Index = i));
|
||||
}
|
||||
}
|
||||
|
||||
export enum ActuatorType {
|
||||
Unknown = "Unknown",
|
||||
Vibrate = "Vibrate",
|
||||
Rotate = "Rotate",
|
||||
Oscillate = "Oscillate",
|
||||
Constrict = "Constrict",
|
||||
Inflate = "Inflate",
|
||||
Position = "Position",
|
||||
}
|
||||
|
||||
export enum SensorType {
|
||||
Unknown = "Unknown",
|
||||
Battery = "Battery",
|
||||
RSSI = "RSSI",
|
||||
Button = "Button",
|
||||
Pressure = "Pressure",
|
||||
// Temperature,
|
||||
// Accelerometer,
|
||||
// Gyro,
|
||||
}
|
||||
|
||||
export class GenericDeviceMessageAttributes {
|
||||
public FeatureDescriptor: string;
|
||||
public ActuatorType: ActuatorType;
|
||||
public StepCount: number;
|
||||
public Index = 0;
|
||||
constructor(data: Partial<GenericDeviceMessageAttributes>) {
|
||||
Object.assign(this, data);
|
||||
}
|
||||
}
|
||||
|
||||
export class RawDeviceMessageAttributes {
|
||||
constructor(public Endpoints: Array<string>) {}
|
||||
}
|
||||
|
||||
export class SensorDeviceMessageAttributes {
|
||||
public FeatureDescriptor: string;
|
||||
public SensorType: SensorType;
|
||||
public StepRange: Array<number>;
|
||||
public Index = 0;
|
||||
constructor(data: Partial<GenericDeviceMessageAttributes>) {
|
||||
Object.assign(this, data);
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class ButtplugMessage {
|
||||
constructor(public Id: number) {}
|
||||
|
||||
// tslint:disable-next-line:ban-types
|
||||
public get Type(): Function {
|
||||
return this.constructor;
|
||||
}
|
||||
|
||||
public toJSON(): string {
|
||||
return JSON.stringify(this.toProtocolFormat());
|
||||
}
|
||||
|
||||
public toProtocolFormat(): object {
|
||||
const jsonObj = {};
|
||||
jsonObj[(this.constructor as unknown as { Name: string }).Name] =
|
||||
instanceToPlain(this);
|
||||
return jsonObj;
|
||||
}
|
||||
|
||||
public update() {}
|
||||
}
|
||||
|
||||
export abstract class ButtplugDeviceMessage extends ButtplugMessage {
|
||||
constructor(
|
||||
public DeviceIndex: number,
|
||||
public Id: number,
|
||||
) {
|
||||
super(Id);
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class ButtplugSystemMessage extends ButtplugMessage {
|
||||
constructor(public Id: number = SYSTEM_MESSAGE_ID) {
|
||||
super(Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class Ok extends ButtplugSystemMessage {
|
||||
static Name = "Ok";
|
||||
|
||||
constructor(public Id: number = DEFAULT_MESSAGE_ID) {
|
||||
super(Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class Ping extends ButtplugMessage {
|
||||
static Name = "Ping";
|
||||
|
||||
constructor(public Id: number = DEFAULT_MESSAGE_ID) {
|
||||
super(Id);
|
||||
}
|
||||
}
|
||||
|
||||
export enum ErrorClass {
|
||||
ERROR_UNKNOWN,
|
||||
ERROR_INIT,
|
||||
ERROR_PING,
|
||||
ERROR_MSG,
|
||||
ERROR_DEVICE,
|
||||
}
|
||||
|
||||
export class Error extends ButtplugMessage {
|
||||
static Name = "Error";
|
||||
|
||||
constructor(
|
||||
public ErrorMessage: string,
|
||||
public ErrorCode: ErrorClass = ErrorClass.ERROR_UNKNOWN,
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(Id);
|
||||
}
|
||||
|
||||
get Schemversion() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
export class DeviceInfo {
|
||||
public DeviceIndex: number;
|
||||
public DeviceName: string;
|
||||
@Type(() => MessageAttributes)
|
||||
public DeviceMessages: MessageAttributes;
|
||||
public DeviceDisplayName?: string;
|
||||
public DeviceMessageTimingGap?: number;
|
||||
|
||||
constructor(data: Partial<DeviceInfo>) {
|
||||
Object.assign(this, data);
|
||||
}
|
||||
}
|
||||
|
||||
export class DeviceList extends ButtplugMessage {
|
||||
static Name = "DeviceList";
|
||||
|
||||
@Type(() => DeviceInfo)
|
||||
public Devices: DeviceInfo[];
|
||||
public Id: number;
|
||||
|
||||
constructor(devices: DeviceInfo[], id: number = DEFAULT_MESSAGE_ID) {
|
||||
super(id);
|
||||
this.Devices = devices;
|
||||
this.Id = id;
|
||||
}
|
||||
|
||||
public update() {
|
||||
for (const device of this.Devices) {
|
||||
device.DeviceMessages.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class DeviceAdded extends ButtplugSystemMessage {
|
||||
static Name = "DeviceAdded";
|
||||
|
||||
public DeviceIndex: number;
|
||||
public DeviceName: string;
|
||||
@Type(() => MessageAttributes)
|
||||
public DeviceMessages: MessageAttributes;
|
||||
public DeviceDisplayName?: string;
|
||||
public DeviceMessageTimingGap?: number;
|
||||
|
||||
constructor(data: Partial<DeviceAdded>) {
|
||||
super();
|
||||
Object.assign(this, data);
|
||||
}
|
||||
|
||||
public update() {
|
||||
this.DeviceMessages.update();
|
||||
}
|
||||
}
|
||||
|
||||
export class DeviceRemoved extends ButtplugSystemMessage {
|
||||
static Name = "DeviceRemoved";
|
||||
|
||||
constructor(public DeviceIndex: number) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class RequestDeviceList extends ButtplugMessage {
|
||||
static Name = "RequestDeviceList";
|
||||
|
||||
constructor(public Id: number = DEFAULT_MESSAGE_ID) {
|
||||
super(Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class StartScanning extends ButtplugMessage {
|
||||
static Name = "StartScanning";
|
||||
|
||||
constructor(public Id: number = DEFAULT_MESSAGE_ID) {
|
||||
super(Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class StopScanning extends ButtplugMessage {
|
||||
static Name = "StopScanning";
|
||||
|
||||
constructor(public Id: number = DEFAULT_MESSAGE_ID) {
|
||||
super(Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class ScanningFinished extends ButtplugSystemMessage {
|
||||
static Name = "ScanningFinished";
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class RequestServerInfo extends ButtplugMessage {
|
||||
static Name = "RequestServerInfo";
|
||||
|
||||
constructor(
|
||||
public ClientName: string,
|
||||
public MessageVersion: number = 0,
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class ServerInfo extends ButtplugSystemMessage {
|
||||
static Name = "ServerInfo";
|
||||
|
||||
constructor(
|
||||
public MessageVersion: number,
|
||||
public MaxPingTime: number,
|
||||
public ServerName: string,
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class StopDeviceCmd extends ButtplugDeviceMessage {
|
||||
static Name = "StopDeviceCmd";
|
||||
|
||||
constructor(
|
||||
public DeviceIndex: number = -1,
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(DeviceIndex, Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class StopAllDevices extends ButtplugMessage {
|
||||
static Name = "StopAllDevices";
|
||||
|
||||
constructor(public Id: number = DEFAULT_MESSAGE_ID) {
|
||||
super(Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class GenericMessageSubcommand {
|
||||
protected constructor(public Index: number) {}
|
||||
}
|
||||
|
||||
export class ScalarSubcommand extends GenericMessageSubcommand {
|
||||
constructor(
|
||||
Index: number,
|
||||
public Scalar: number,
|
||||
public ActuatorType: ActuatorType,
|
||||
) {
|
||||
super(Index);
|
||||
}
|
||||
}
|
||||
|
||||
export class ScalarCmd extends ButtplugDeviceMessage {
|
||||
static Name = "ScalarCmd";
|
||||
|
||||
constructor(
|
||||
public Scalars: ScalarSubcommand[],
|
||||
public DeviceIndex: number = -1,
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(DeviceIndex, Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class RotateSubcommand extends GenericMessageSubcommand {
|
||||
constructor(
|
||||
Index: number,
|
||||
public Speed: number,
|
||||
public Clockwise: boolean,
|
||||
) {
|
||||
super(Index);
|
||||
}
|
||||
}
|
||||
|
||||
export class RotateCmd extends ButtplugDeviceMessage {
|
||||
static Name = "RotateCmd";
|
||||
|
||||
public static Create(
|
||||
deviceIndex: number,
|
||||
commands: [number, boolean][],
|
||||
): RotateCmd {
|
||||
const cmdList: RotateSubcommand[] = new Array<RotateSubcommand>();
|
||||
|
||||
let i = 0;
|
||||
for (const [speed, clockwise] of commands) {
|
||||
cmdList.push(new RotateSubcommand(i, speed, clockwise));
|
||||
++i;
|
||||
}
|
||||
|
||||
return new RotateCmd(cmdList, deviceIndex);
|
||||
}
|
||||
constructor(
|
||||
public Rotations: RotateSubcommand[],
|
||||
public DeviceIndex: number = -1,
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(DeviceIndex, Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class VectorSubcommand extends GenericMessageSubcommand {
|
||||
constructor(
|
||||
Index: number,
|
||||
public Position: number,
|
||||
public Duration: number,
|
||||
) {
|
||||
super(Index);
|
||||
}
|
||||
}
|
||||
|
||||
export class LinearCmd extends ButtplugDeviceMessage {
|
||||
static Name = "LinearCmd";
|
||||
|
||||
public static Create(
|
||||
deviceIndex: number,
|
||||
commands: [number, number][],
|
||||
): LinearCmd {
|
||||
const cmdList: VectorSubcommand[] = new Array<VectorSubcommand>();
|
||||
|
||||
let i = 0;
|
||||
for (const cmd of commands) {
|
||||
cmdList.push(new VectorSubcommand(i, cmd[0], cmd[1]));
|
||||
++i;
|
||||
}
|
||||
|
||||
return new LinearCmd(cmdList, deviceIndex);
|
||||
}
|
||||
constructor(
|
||||
public Vectors: VectorSubcommand[],
|
||||
public DeviceIndex: number = -1,
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(DeviceIndex, Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class SensorReadCmd extends ButtplugDeviceMessage {
|
||||
static Name = "SensorReadCmd";
|
||||
|
||||
constructor(
|
||||
public DeviceIndex: number,
|
||||
public SensorIndex: number,
|
||||
public SensorType: SensorType,
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(DeviceIndex, Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class SensorReading extends ButtplugDeviceMessage {
|
||||
static Name = "SensorReading";
|
||||
|
||||
constructor(
|
||||
public DeviceIndex: number,
|
||||
public SensorIndex: number,
|
||||
public SensorType: SensorType,
|
||||
public Data: number[],
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(DeviceIndex, Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class RawReadCmd extends ButtplugDeviceMessage {
|
||||
static Name = "RawReadCmd";
|
||||
|
||||
constructor(
|
||||
public DeviceIndex: number,
|
||||
public Endpoint: string,
|
||||
public ExpectedLength: number,
|
||||
public Timeout: number,
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(DeviceIndex, Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class RawWriteCmd extends ButtplugDeviceMessage {
|
||||
static Name = "RawWriteCmd";
|
||||
|
||||
constructor(
|
||||
public DeviceIndex: number,
|
||||
public Endpoint: string,
|
||||
public Data: Uint8Array,
|
||||
public WriteWithResponse: boolean,
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(DeviceIndex, Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class RawSubscribeCmd extends ButtplugDeviceMessage {
|
||||
static Name = "RawSubscribeCmd";
|
||||
|
||||
constructor(
|
||||
public DeviceIndex: number,
|
||||
public Endpoint: string,
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(DeviceIndex, Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class RawUnsubscribeCmd extends ButtplugDeviceMessage {
|
||||
static Name = "RawUnsubscribeCmd";
|
||||
|
||||
constructor(
|
||||
public DeviceIndex: number,
|
||||
public Endpoint: string,
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(DeviceIndex, Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class RawReading extends ButtplugDeviceMessage {
|
||||
static Name = "RawReading";
|
||||
|
||||
constructor(
|
||||
public DeviceIndex: number,
|
||||
public Endpoint: string,
|
||||
public Data: number[],
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(DeviceIndex, Id);
|
||||
}
|
||||
}
|
||||
4
packages/buttplug/src/core/index.d.ts
vendored
Normal file
4
packages/buttplug/src/core/index.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module "*.json" {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
88
packages/buttplug/src/index.ts
Normal file
88
packages/buttplug/src/index.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { ButtplugMessage } from "./core/Messages";
|
||||
import { IButtplugClientConnector } from "./client/IButtplugClientConnector";
|
||||
import { fromJSON } from "./core/MessageUtils";
|
||||
import { EventEmitter } from "eventemitter3";
|
||||
|
||||
export * from "./client/Client";
|
||||
export * from "./client/ButtplugClientDevice";
|
||||
export * from "./client/ButtplugBrowserWebsocketClientConnector";
|
||||
export * from "./client/ButtplugNodeWebsocketClientConnector";
|
||||
export * from "./client/ButtplugClientConnectorException";
|
||||
export * from "./utils/ButtplugMessageSorter";
|
||||
export * from "./client/IButtplugClientConnector";
|
||||
export * from "./core/Messages";
|
||||
export * from "./core/MessageUtils";
|
||||
export * from "./core/Logging";
|
||||
export * from "./core/Exceptions";
|
||||
|
||||
export class ButtplugWasmClientConnector
|
||||
extends EventEmitter
|
||||
implements IButtplugClientConnector
|
||||
{
|
||||
private static _loggingActivated = false;
|
||||
private static wasmInstance;
|
||||
private _connected: boolean = false;
|
||||
private client;
|
||||
private serverPtr;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public get Connected(): boolean {
|
||||
return this._connected;
|
||||
}
|
||||
|
||||
private static maybeLoadWasm = async () => {
|
||||
if (ButtplugWasmClientConnector.wasmInstance == undefined) {
|
||||
ButtplugWasmClientConnector.wasmInstance = await import(
|
||||
"../wasm/index.js"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
public static activateLogging = async (logLevel: string = "debug") => {
|
||||
await ButtplugWasmClientConnector.maybeLoadWasm();
|
||||
if (this._loggingActivated) {
|
||||
console.log("Logging already activated, ignoring.");
|
||||
return;
|
||||
}
|
||||
console.log("Turning on logging.");
|
||||
ButtplugWasmClientConnector.wasmInstance.buttplug_activate_env_logger(
|
||||
logLevel,
|
||||
);
|
||||
};
|
||||
|
||||
public initialize = async (): Promise<void> => {};
|
||||
|
||||
public connect = async (): Promise<void> => {
|
||||
await ButtplugWasmClientConnector.maybeLoadWasm();
|
||||
//ButtplugWasmClientConnector.wasmInstance.buttplug_activate_env_logger('debug');
|
||||
this.client =
|
||||
ButtplugWasmClientConnector.wasmInstance.buttplug_create_embedded_wasm_server(
|
||||
(msgs) => {
|
||||
this.emitMessage(msgs);
|
||||
},
|
||||
this.serverPtr,
|
||||
);
|
||||
this._connected = true;
|
||||
};
|
||||
|
||||
public disconnect = async (): Promise<void> => {};
|
||||
|
||||
public send = (msg: ButtplugMessage): void => {
|
||||
ButtplugWasmClientConnector.wasmInstance.buttplug_client_send_json_message(
|
||||
this.client,
|
||||
new TextEncoder().encode("[" + msg.toJSON() + "]"),
|
||||
(output) => {
|
||||
this.emitMessage(output);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
private emitMessage = (msg: Uint8Array) => {
|
||||
const str = new TextDecoder().decode(msg);
|
||||
// This needs to use buttplug-js's fromJSON, otherwise we won't resolve the message name correctly.
|
||||
this.emit("message", fromJSON(str));
|
||||
};
|
||||
}
|
||||
130
packages/buttplug/src/lib.rs
Normal file
130
packages/buttplug/src/lib.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
#[macro_use]
|
||||
extern crate tracing;
|
||||
#[macro_use]
|
||||
extern crate futures;
|
||||
|
||||
|
||||
mod webbluetooth;
|
||||
use js_sys;
|
||||
use tokio_stream::StreamExt;
|
||||
use crate::webbluetooth::{WebBluetoothCommunicationManagerBuilder};
|
||||
use buttplug::{
|
||||
core::message::{ButtplugServerMessageCurrent,serializer::vec_to_protocol_json},
|
||||
server::{ButtplugServerBuilder,ButtplugServerDowngradeWrapper,device::{ServerDeviceManagerBuilder,configuration::{DeviceConfigurationManager}}},
|
||||
util::async_manager, core::message::{BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION, ButtplugServerMessageVariant, serializer::{ButtplugSerializedMessage, ButtplugMessageSerializer, ButtplugServerJSONSerializer}},
|
||||
util::device_configuration::load_protocol_configs
|
||||
};
|
||||
|
||||
type FFICallback = js_sys::Function;
|
||||
type FFICallbackContext = u32;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct FFICallbackContextWrapper(FFICallbackContext);
|
||||
|
||||
unsafe impl Send for FFICallbackContextWrapper {
|
||||
}
|
||||
unsafe impl Sync for FFICallbackContextWrapper {
|
||||
}
|
||||
|
||||
use console_error_panic_hook;
|
||||
use tracing_subscriber::{layer::SubscriberExt, Registry};
|
||||
use tracing_wasm::{WASMLayer, WASMLayerConfig};
|
||||
use wasm_bindgen::prelude::*;
|
||||
use std::sync::Arc;
|
||||
use js_sys::Uint8Array;
|
||||
|
||||
pub type ButtplugWASMServer = Arc<ButtplugServerDowngradeWrapper>;
|
||||
|
||||
pub fn send_server_message(
|
||||
message: &ButtplugServerMessageCurrent,
|
||||
callback: &FFICallback,
|
||||
) {
|
||||
let msg_array = [message.clone()];
|
||||
let json_msg = vec_to_protocol_json(&msg_array);
|
||||
let buf = json_msg.as_bytes();
|
||||
{
|
||||
let this = JsValue::null();
|
||||
let uint8buf = unsafe { Uint8Array::new(&Uint8Array::view(buf)) };
|
||||
callback.call1(&this, &JsValue::from(uint8buf));
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub fn create_test_dcm(allow_raw_messages: bool) -> DeviceConfigurationManager {
|
||||
load_protocol_configs(&None, &None, false)
|
||||
.expect("If this fails, the whole library goes with it.")
|
||||
.allow_raw_messages(allow_raw_messages)
|
||||
.finish()
|
||||
.expect("If this fails, the whole library goes with it.")
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_bindgen]
|
||||
pub fn buttplug_create_embedded_wasm_server(
|
||||
callback: &FFICallback,
|
||||
) -> *mut ButtplugWASMServer {
|
||||
console_error_panic_hook::set_once();
|
||||
let dcm = create_test_dcm(false);
|
||||
let mut sdm = ServerDeviceManagerBuilder::new(dcm);
|
||||
sdm.comm_manager(WebBluetoothCommunicationManagerBuilder::default());
|
||||
let builder = ButtplugServerBuilder::new(sdm.finish().unwrap());
|
||||
let server = builder.finish().unwrap();
|
||||
let wrapper = Arc::new(ButtplugServerDowngradeWrapper::new(server));
|
||||
let event_stream = wrapper.server_version_event_stream();
|
||||
let callback = callback.clone();
|
||||
async_manager::spawn(async move {
|
||||
pin_mut!(event_stream);
|
||||
while let Some(message) = event_stream.next().await {
|
||||
send_server_message(&ButtplugServerMessageCurrent::try_from(message).unwrap(), &callback);
|
||||
}
|
||||
});
|
||||
|
||||
Box::into_raw(Box::new(wrapper))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_bindgen]
|
||||
pub fn buttplug_free_embedded_wasm_server(ptr: *mut ButtplugWASMServer) {
|
||||
if !ptr.is_null() {
|
||||
unsafe {
|
||||
let _ = Box::from_raw(ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_bindgen]
|
||||
pub fn buttplug_client_send_json_message(
|
||||
server_ptr: *mut ButtplugWASMServer,
|
||||
buf: &[u8],
|
||||
callback: &FFICallback,
|
||||
) {
|
||||
let server = unsafe {
|
||||
assert!(!server_ptr.is_null());
|
||||
&mut *server_ptr
|
||||
};
|
||||
let callback = callback.clone();
|
||||
let serializer = ButtplugServerJSONSerializer::default();
|
||||
serializer.force_message_version(&BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION);
|
||||
let input_msg = serializer.deserialize(&ButtplugSerializedMessage::Text(std::str::from_utf8(buf).unwrap().to_owned())).unwrap();
|
||||
async_manager::spawn(async move {
|
||||
let msg = input_msg[0].clone();
|
||||
let response = server.parse_message(msg).await.unwrap();
|
||||
if let ButtplugServerMessageVariant::V3(response) = response {
|
||||
send_server_message(&response, &callback);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_bindgen]
|
||||
pub fn buttplug_activate_env_logger(_max_level: &str) {
|
||||
tracing::subscriber::set_global_default(
|
||||
Registry::default()
|
||||
//.with(EnvFilter::new(max_level))
|
||||
.with(WASMLayer::new(WASMLayerConfig::default())),
|
||||
)
|
||||
.expect("default global");
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*!
|
||||
* Buttplug JS Source Code File - Visit https://buttplug.io for more info about
|
||||
* the project. Licensed under the BSD 3-Clause license. See LICENSE file in the
|
||||
* project root for full license information.
|
||||
*
|
||||
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
import { EventEmitter } from "eventemitter3";
|
||||
import { ButtplugMessage } from "../core/Messages";
|
||||
import { fromJSON } from "../core/MessageUtils";
|
||||
|
||||
export class ButtplugBrowserWebsocketConnector extends EventEmitter {
|
||||
protected _ws: WebSocket | undefined;
|
||||
protected _websocketConstructor: typeof WebSocket | null = null;
|
||||
|
||||
public constructor(private _url: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
public get Connected(): boolean {
|
||||
return this._ws !== undefined;
|
||||
}
|
||||
|
||||
public connect = async (): Promise<void> => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const ws = new (this._websocketConstructor ?? WebSocket)(this._url);
|
||||
const onErrorCallback = (event: Event) => {
|
||||
reject(event);
|
||||
};
|
||||
const onCloseCallback = (event: CloseEvent) => reject(event.reason);
|
||||
ws.addEventListener("open", async () => {
|
||||
this._ws = ws;
|
||||
try {
|
||||
await this.initialize();
|
||||
this._ws.addEventListener("message", (msg) => {
|
||||
this.parseIncomingMessage(msg);
|
||||
});
|
||||
this._ws.removeEventListener("close", onCloseCallback);
|
||||
this._ws.removeEventListener("error", onErrorCallback);
|
||||
this._ws.addEventListener("close", this.disconnect);
|
||||
resolve();
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
// In websockets, our error rarely tells us much, as for security reasons
|
||||
// browsers usually only throw Error Code 1006. It's up to those using this
|
||||
// library to state what the problem might be.
|
||||
|
||||
ws.addEventListener("error", onErrorCallback);
|
||||
ws.addEventListener("close", onCloseCallback);
|
||||
});
|
||||
};
|
||||
|
||||
public disconnect = async (): Promise<void> => {
|
||||
if (!this.Connected) {
|
||||
return;
|
||||
}
|
||||
this._ws!.close();
|
||||
this._ws = undefined;
|
||||
this.emit("disconnect");
|
||||
};
|
||||
|
||||
public sendMessage(msg: ButtplugMessage) {
|
||||
if (!this.Connected) {
|
||||
throw new Error("ButtplugBrowserWebsocketConnector not connected");
|
||||
}
|
||||
this._ws!.send("[" + msg.toJSON() + "]");
|
||||
}
|
||||
|
||||
public initialize = async (): Promise<void> => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
protected parseIncomingMessage(event: MessageEvent) {
|
||||
if (typeof event.data === "string") {
|
||||
const msgs = fromJSON(event.data);
|
||||
this.emit("message", msgs);
|
||||
} else if (event.data instanceof Blob) {
|
||||
// No-op, we only use text message types.
|
||||
}
|
||||
}
|
||||
|
||||
protected onReaderLoad(event: Event) {
|
||||
const msgs = fromJSON((event.target as FileReader).result);
|
||||
this.emit("message", msgs);
|
||||
}
|
||||
}
|
||||
65
packages/buttplug/src/utils/ButtplugMessageSorter.ts
Normal file
65
packages/buttplug/src/utils/ButtplugMessageSorter.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/*!
|
||||
* Buttplug JS Source Code File - Visit https://buttplug.io for more info about
|
||||
* the project. Licensed under the BSD 3-Clause license. See LICENSE file in the
|
||||
* project root for full license information.
|
||||
*
|
||||
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
||||
*/
|
||||
|
||||
import * as Messages from "../core/Messages";
|
||||
import { ButtplugError } from "../core/Exceptions";
|
||||
|
||||
export class ButtplugMessageSorter {
|
||||
protected _counter = 1;
|
||||
protected _waitingMsgs: Map<
|
||||
number,
|
||||
[(val: Messages.ButtplugMessage) => void, (err: Error) => void]
|
||||
> = new Map();
|
||||
|
||||
public constructor(private _useCounter: boolean) {}
|
||||
|
||||
// One of the places we should actually return a promise, as we need to store
|
||||
// them while waiting for them to return across the line.
|
||||
// tslint:disable:promise-function-async
|
||||
public PrepareOutgoingMessage(
|
||||
msg: Messages.ButtplugMessage,
|
||||
): Promise<Messages.ButtplugMessage> {
|
||||
if (this._useCounter) {
|
||||
msg.Id = this._counter;
|
||||
// Always increment last, otherwise we might lose sync
|
||||
this._counter += 1;
|
||||
}
|
||||
let res;
|
||||
let rej;
|
||||
const msgPromise = new Promise<Messages.ButtplugMessage>(
|
||||
(resolve, reject) => {
|
||||
res = resolve;
|
||||
rej = reject;
|
||||
},
|
||||
);
|
||||
this._waitingMsgs.set(msg.Id, [res, rej]);
|
||||
return msgPromise;
|
||||
}
|
||||
|
||||
public ParseIncomingMessages(
|
||||
msgs: Messages.ButtplugMessage[],
|
||||
): Messages.ButtplugMessage[] {
|
||||
const noMatch: Messages.ButtplugMessage[] = [];
|
||||
for (const x of msgs) {
|
||||
if (x.Id !== Messages.SYSTEM_MESSAGE_ID && this._waitingMsgs.has(x.Id)) {
|
||||
const [res, rej] = this._waitingMsgs.get(x.Id)!;
|
||||
// If we've gotten back an error, reject the related promise using a
|
||||
// ButtplugException derived type.
|
||||
if (x.Type === Messages.Error) {
|
||||
rej(ButtplugError.FromError(x as Messages.Error));
|
||||
continue;
|
||||
}
|
||||
res(x);
|
||||
continue;
|
||||
} else {
|
||||
noMatch.push(x);
|
||||
}
|
||||
}
|
||||
return noMatch;
|
||||
}
|
||||
}
|
||||
3
packages/buttplug/src/utils/Utils.ts
Normal file
3
packages/buttplug/src/utils/Utils.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function getRandomInt(max: number) {
|
||||
return Math.floor(Math.random() * Math.floor(max));
|
||||
}
|
||||
6
packages/buttplug/src/webbluetooth/mod.rs
Normal file
6
packages/buttplug/src/webbluetooth/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
|
||||
mod webbluetooth_hardware;
|
||||
mod webbluetooth_manager;
|
||||
|
||||
// pub use webbluetooth_hardware::{WebBluetoothHardwareConnector, WebBluetoothHardware};
|
||||
pub use webbluetooth_manager::{WebBluetoothCommunicationManagerBuilder};
|
||||
432
packages/buttplug/src/webbluetooth/webbluetooth_hardware.rs
Normal file
432
packages/buttplug/src/webbluetooth/webbluetooth_hardware.rs
Normal file
@@ -0,0 +1,432 @@
|
||||
use async_trait::async_trait;
|
||||
use buttplug::{
|
||||
core::{
|
||||
errors::ButtplugDeviceError,
|
||||
message::Endpoint,
|
||||
},
|
||||
server::device::{
|
||||
configuration::{BluetoothLESpecifier, ProtocolCommunicationSpecifier},
|
||||
hardware::{
|
||||
Hardware,
|
||||
HardwareConnector,
|
||||
HardwareEvent,
|
||||
HardwareInternal,
|
||||
HardwareReadCmd,
|
||||
HardwareReading,
|
||||
HardwareSpecializer,
|
||||
HardwareSubscribeCmd,
|
||||
HardwareUnsubscribeCmd,
|
||||
HardwareWriteCmd,
|
||||
},
|
||||
},
|
||||
util::future::{ButtplugFuture, ButtplugFutureStateShared},
|
||||
};
|
||||
use futures::future::{self, BoxFuture};
|
||||
use js_sys::{DataView, Uint8Array};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
convert::TryFrom,
|
||||
fmt::{self, Debug},
|
||||
};
|
||||
use tokio::sync::{broadcast, mpsc};
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_futures::{spawn_local, JsFuture};
|
||||
use web_sys::{
|
||||
BluetoothDevice,
|
||||
BluetoothRemoteGattCharacteristic,
|
||||
BluetoothRemoteGattServer,
|
||||
BluetoothRemoteGattService,
|
||||
Event,
|
||||
MessageEvent,
|
||||
};
|
||||
|
||||
type WebBluetoothResultFuture = ButtplugFuture<Result<(), ButtplugDeviceError>>;
|
||||
type WebBluetoothReadResultFuture = ButtplugFuture<Result<HardwareReading, ButtplugDeviceError>>;
|
||||
|
||||
struct BluetoothDeviceWrapper {
|
||||
pub device: BluetoothDevice
|
||||
}
|
||||
|
||||
|
||||
unsafe impl Send for BluetoothDeviceWrapper {
|
||||
}
|
||||
unsafe impl Sync for BluetoothDeviceWrapper {
|
||||
}
|
||||
|
||||
|
||||
pub struct WebBluetoothHardwareConnector {
|
||||
device: Option<BluetoothDeviceWrapper>,
|
||||
}
|
||||
|
||||
impl WebBluetoothHardwareConnector {
|
||||
pub fn new(
|
||||
device: BluetoothDevice,
|
||||
) -> Self {
|
||||
Self {
|
||||
device: Some(BluetoothDeviceWrapper {
|
||||
device,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for WebBluetoothHardwareConnector {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let device = &self.device.as_ref();
|
||||
|
||||
match device {
|
||||
Some(device) => {
|
||||
f.debug_struct("WebBluetoothHardwareCreator")
|
||||
.field("name", &device.device.name().unwrap())
|
||||
.finish()
|
||||
}
|
||||
&None => {
|
||||
f.debug_struct("WebBluetoothHardwareCreator")
|
||||
.field("name", &"Unknown device")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl HardwareConnector for WebBluetoothHardwareConnector {
|
||||
fn specifier(&self) -> ProtocolCommunicationSpecifier {
|
||||
let device = &self.device.as_ref();
|
||||
|
||||
match device {
|
||||
Some(device) => {
|
||||
ProtocolCommunicationSpecifier::BluetoothLE(BluetoothLESpecifier::new_from_device(
|
||||
&device.device.name().unwrap(),
|
||||
&HashMap::new(),
|
||||
&[]
|
||||
))
|
||||
}
|
||||
&None => {
|
||||
ProtocolCommunicationSpecifier::BluetoothLE(BluetoothLESpecifier::new_from_device(
|
||||
"Unknown device",
|
||||
&HashMap::new(),
|
||||
&[]
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect(&mut self) -> Result<Box<dyn HardwareSpecializer>, ButtplugDeviceError> {
|
||||
Ok(Box::new(WebBluetoothHardwareSpecializer::new(self.device.take().unwrap())))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub struct WebBluetoothHardwareSpecializer {
|
||||
device: Option<BluetoothDeviceWrapper>,
|
||||
}
|
||||
|
||||
impl WebBluetoothHardwareSpecializer {
|
||||
fn new(device: BluetoothDeviceWrapper) -> Self {
|
||||
Self {
|
||||
device: Some(device),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl HardwareSpecializer for WebBluetoothHardwareSpecializer {
|
||||
async fn specialize(
|
||||
&mut self,
|
||||
specifiers: &[ProtocolCommunicationSpecifier],
|
||||
) -> Result<Hardware, ButtplugDeviceError> {
|
||||
let (sender, mut receiver) = mpsc::channel(256);
|
||||
let (command_sender, command_receiver) = mpsc::channel(256);
|
||||
let name;
|
||||
let address;
|
||||
let event_sender;
|
||||
// This block limits the lifetime of device. Since the compiler doesn't
|
||||
// realize we move device in the spawn_local block, it'll complain that
|
||||
// device's lifetime lives across the channel await, which gets all
|
||||
// angry because it's a *mut u8. So this limits the visible lifetime to
|
||||
// before we start waiting for the reply from the event loop.
|
||||
let protocol = if let ProtocolCommunicationSpecifier::BluetoothLE(btle) = &specifiers[0] {
|
||||
btle
|
||||
} else {
|
||||
panic!("No bluetooth, we quit");
|
||||
};
|
||||
{
|
||||
let device = self.device.take().unwrap().device;
|
||||
name = device.name().unwrap();
|
||||
address = device.id();
|
||||
let (es, _) = broadcast::channel(256);
|
||||
event_sender = es;
|
||||
let event_loop_fut = run_webbluetooth_loop(
|
||||
device,
|
||||
protocol.clone(),
|
||||
sender,
|
||||
event_sender.clone(),
|
||||
command_receiver,
|
||||
);
|
||||
spawn_local(async move {
|
||||
event_loop_fut.await;
|
||||
});
|
||||
}
|
||||
|
||||
match receiver.recv().await.unwrap() {
|
||||
WebBluetoothEvent::Connected(_) => {
|
||||
info!("Web Bluetooth device connected, returning device");
|
||||
|
||||
let device_impl: Box<dyn HardwareInternal> = Box::new(WebBluetoothHardware::new(
|
||||
event_sender,
|
||||
receiver,
|
||||
command_sender,
|
||||
));
|
||||
Ok(Hardware::new(&name, &address, &[], device_impl))
|
||||
}
|
||||
WebBluetoothEvent::Disconnected => Err(
|
||||
ButtplugDeviceError::DeviceCommunicationError(
|
||||
"Could not connect to WebBluetooth device".to_string(),
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum WebBluetoothEvent {
|
||||
// This is the only way we have to get our endpoints back to device creation
|
||||
// right now. My god this is a mess.
|
||||
Connected(Vec<Endpoint>),
|
||||
Disconnected,
|
||||
}
|
||||
|
||||
pub enum WebBluetoothDeviceCommand {
|
||||
Write(
|
||||
HardwareWriteCmd,
|
||||
ButtplugFutureStateShared<Result<(), ButtplugDeviceError>>,
|
||||
),
|
||||
Read(
|
||||
HardwareReadCmd,
|
||||
ButtplugFutureStateShared<Result<HardwareReading, ButtplugDeviceError>>,
|
||||
),
|
||||
Subscribe(
|
||||
HardwareSubscribeCmd,
|
||||
ButtplugFutureStateShared<Result<(), ButtplugDeviceError>>,
|
||||
),
|
||||
Unsubscribe(
|
||||
HardwareUnsubscribeCmd,
|
||||
ButtplugFutureStateShared<Result<(), ButtplugDeviceError>>,
|
||||
),
|
||||
}
|
||||
|
||||
async fn run_webbluetooth_loop(
|
||||
device: BluetoothDevice,
|
||||
btle_protocol: BluetoothLESpecifier,
|
||||
device_local_event_sender: mpsc::Sender<WebBluetoothEvent>,
|
||||
device_external_event_sender: broadcast::Sender<HardwareEvent>,
|
||||
mut device_command_receiver: mpsc::Receiver<WebBluetoothDeviceCommand>,
|
||||
) {
|
||||
//let device = self.device.take().unwrap();
|
||||
let mut char_map = HashMap::new();
|
||||
let connect_future = device.gatt().unwrap().connect();
|
||||
let server: BluetoothRemoteGattServer = match JsFuture::from(connect_future).await {
|
||||
Ok(val) => val.into(),
|
||||
Err(_) => {
|
||||
device_local_event_sender
|
||||
.send(WebBluetoothEvent::Disconnected)
|
||||
.await
|
||||
.unwrap();
|
||||
return;
|
||||
}
|
||||
};
|
||||
for (service_uuid, service_endpoints) in btle_protocol.services() {
|
||||
let service = if let Ok(serv) =
|
||||
JsFuture::from(server.get_primary_service_with_str(&service_uuid.to_string())).await
|
||||
{
|
||||
info!(
|
||||
"Service {} found on device {}",
|
||||
service_uuid,
|
||||
device.name().unwrap()
|
||||
);
|
||||
BluetoothRemoteGattService::from(serv)
|
||||
} else {
|
||||
info!(
|
||||
"Service {} not found on device {}",
|
||||
service_uuid,
|
||||
device.name().unwrap()
|
||||
);
|
||||
continue;
|
||||
};
|
||||
for (chr_name, chr_uuid) in service_endpoints.iter() {
|
||||
info!("Connecting chr {} {}", chr_name, chr_uuid.to_string());
|
||||
let char: BluetoothRemoteGattCharacteristic =
|
||||
JsFuture::from(service.get_characteristic_with_str(&chr_uuid.to_string()))
|
||||
.await
|
||||
.unwrap()
|
||||
.into();
|
||||
char_map.insert(chr_name.clone(), char);
|
||||
}
|
||||
}
|
||||
{
|
||||
let event_sender = device_external_event_sender.clone();
|
||||
let id = device.id().clone();
|
||||
let ondisconnected_callback = Closure::wrap(Box::new(move |_: Event| {
|
||||
info!("device disconnected!");
|
||||
event_sender
|
||||
.send(HardwareEvent::Disconnected(id.clone()))
|
||||
.unwrap();
|
||||
}) as Box<dyn FnMut(Event)>);
|
||||
// set disconnection event handler on BluetoothDevice
|
||||
device.set_ongattserverdisconnected(Some(ondisconnected_callback.as_ref().unchecked_ref()));
|
||||
ondisconnected_callback.forget();
|
||||
}
|
||||
//let web_btle_device = WebBluetoothDeviceImpl::new(device, char_map);
|
||||
info!("device created!");
|
||||
let endpoints = char_map.keys().into_iter().cloned().collect();
|
||||
device_local_event_sender
|
||||
.send(WebBluetoothEvent::Connected(endpoints))
|
||||
.await;
|
||||
while let Some(msg) = device_command_receiver.recv().await {
|
||||
match msg {
|
||||
WebBluetoothDeviceCommand::Write(write_cmd, waker) => {
|
||||
debug!("Writing to endpoint {:?}", write_cmd.endpoint());
|
||||
let chr = char_map.get(&write_cmd.endpoint()).unwrap().clone();
|
||||
spawn_local(async move {
|
||||
let uint8buf = unsafe { Uint8Array::new(&Uint8Array::view(&write_cmd.data().clone())) };
|
||||
JsFuture::from(chr.write_value_with_u8_array(&uint8buf).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
waker.set_reply(Ok(()));
|
||||
});
|
||||
}
|
||||
WebBluetoothDeviceCommand::Read(read_cmd, waker) => {
|
||||
debug!("Writing to endpoint {:?}", read_cmd.endpoint());
|
||||
let chr = char_map.get(&read_cmd.endpoint()).unwrap().clone();
|
||||
spawn_local(async move {
|
||||
let read_value = JsFuture::from(chr.read_value()).await.unwrap();
|
||||
let data_view = DataView::try_from(read_value).unwrap();
|
||||
let mut body = vec![0; data_view.byte_length() as usize];
|
||||
Uint8Array::new(&data_view).copy_to(&mut body[..]);
|
||||
let reading = HardwareReading::new(read_cmd.endpoint(), &body);
|
||||
waker.set_reply(Ok(reading));
|
||||
});
|
||||
}
|
||||
WebBluetoothDeviceCommand::Subscribe(subscribe_cmd, waker) => {
|
||||
debug!("Subscribing to endpoint {:?}", subscribe_cmd.endpoint());
|
||||
let chr = char_map.get(&subscribe_cmd.endpoint()).unwrap().clone();
|
||||
let ep = subscribe_cmd.endpoint();
|
||||
let event_sender = device_external_event_sender.clone();
|
||||
let id = device.id().clone();
|
||||
let onchange_callback = Closure::wrap(Box::new(move |e: MessageEvent| {
|
||||
let event_chr: BluetoothRemoteGattCharacteristic =
|
||||
BluetoothRemoteGattCharacteristic::from(JsValue::from(e.target().unwrap()));
|
||||
let value = Uint8Array::new_with_byte_offset(
|
||||
&JsValue::from(event_chr.value().unwrap().buffer()),
|
||||
0,
|
||||
);
|
||||
let value_vec = value.to_vec();
|
||||
debug!("Subscription notification from {}: {:?}", ep, value_vec);
|
||||
event_sender
|
||||
.send(HardwareEvent::Notification(id.clone(), ep, value_vec))
|
||||
.unwrap();
|
||||
}) as Box<dyn FnMut(MessageEvent)>);
|
||||
// set message event handler on WebSocket
|
||||
chr.set_oncharacteristicvaluechanged(Some(onchange_callback.as_ref().unchecked_ref()));
|
||||
onchange_callback.forget();
|
||||
spawn_local(async move {
|
||||
JsFuture::from(chr.start_notifications()).await.unwrap();
|
||||
debug!("Endpoint subscribed");
|
||||
waker.set_reply(Ok(()));
|
||||
});
|
||||
}
|
||||
WebBluetoothDeviceCommand::Unsubscribe(_unsubscribe_cmd, _waker) => {}
|
||||
}
|
||||
}
|
||||
debug!("run_webbluetooth_loop exited!");
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WebBluetoothHardware {
|
||||
device_command_sender: mpsc::Sender<WebBluetoothDeviceCommand>,
|
||||
device_event_receiver: mpsc::Receiver<WebBluetoothEvent>,
|
||||
event_sender: broadcast::Sender<HardwareEvent>,
|
||||
}
|
||||
/*
|
||||
unsafe impl Send for WebBluetoothHardware {
|
||||
}
|
||||
unsafe impl Sync for WebBluetoothHardware {
|
||||
}
|
||||
*/
|
||||
|
||||
impl WebBluetoothHardware {
|
||||
pub fn new(
|
||||
event_sender: broadcast::Sender<HardwareEvent>,
|
||||
device_event_receiver: mpsc::Receiver<WebBluetoothEvent>,
|
||||
device_command_sender: mpsc::Sender<WebBluetoothDeviceCommand>,
|
||||
) -> Self {
|
||||
Self {
|
||||
event_sender,
|
||||
device_event_receiver,
|
||||
device_command_sender,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HardwareInternal for WebBluetoothHardware {
|
||||
fn event_stream(&self) -> broadcast::Receiver<HardwareEvent> {
|
||||
self.event_sender.subscribe()
|
||||
}
|
||||
|
||||
fn disconnect(&self) -> BoxFuture<'static, Result<(), ButtplugDeviceError>> {
|
||||
Box::pin(future::ready(Ok(())))
|
||||
}
|
||||
|
||||
fn read_value(
|
||||
&self,
|
||||
msg: &HardwareReadCmd,
|
||||
) -> BoxFuture<'static, Result<HardwareReading, ButtplugDeviceError>> {
|
||||
let sender = self.device_command_sender.clone();
|
||||
let msg = msg.clone();
|
||||
Box::pin(async move {
|
||||
let fut = WebBluetoothReadResultFuture::default();
|
||||
let waker = fut.get_state_clone();
|
||||
sender
|
||||
.send(WebBluetoothDeviceCommand::Read(msg, waker))
|
||||
.await;
|
||||
fut.await
|
||||
})
|
||||
}
|
||||
|
||||
fn write_value(&self, msg: &HardwareWriteCmd) -> BoxFuture<'static, Result<(), ButtplugDeviceError>> {
|
||||
let sender = self.device_command_sender.clone();
|
||||
let msg = msg.clone();
|
||||
Box::pin(async move {
|
||||
let fut = WebBluetoothResultFuture::default();
|
||||
let waker = fut.get_state_clone();
|
||||
sender
|
||||
.send(WebBluetoothDeviceCommand::Write(msg.clone(), waker))
|
||||
.await;
|
||||
fut.await
|
||||
})
|
||||
}
|
||||
|
||||
fn subscribe(&self, msg: &HardwareSubscribeCmd) -> BoxFuture<'static, Result<(), ButtplugDeviceError>> {
|
||||
let sender = self.device_command_sender.clone();
|
||||
let msg = msg.clone();
|
||||
Box::pin(async move {
|
||||
let fut = WebBluetoothResultFuture::default();
|
||||
let waker = fut.get_state_clone();
|
||||
sender
|
||||
.send(WebBluetoothDeviceCommand::Subscribe(msg.clone(), waker))
|
||||
.await;
|
||||
fut.await
|
||||
})
|
||||
}
|
||||
|
||||
fn unsubscribe(&self, _msg: &HardwareUnsubscribeCmd) -> BoxFuture<'static, Result<(), ButtplugDeviceError>> {
|
||||
Box::pin(async move {
|
||||
error!("IMPLEMENT UNSUBSCRIBE FOR WEBBLUETOOTH WASM");
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
134
packages/buttplug/src/webbluetooth/webbluetooth_manager.rs
Normal file
134
packages/buttplug/src/webbluetooth/webbluetooth_manager.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
use super::webbluetooth_hardware::WebBluetoothHardwareConnector;
|
||||
|
||||
use buttplug::{
|
||||
core::ButtplugResultFuture,
|
||||
server::device::{
|
||||
configuration::{ProtocolCommunicationSpecifier},
|
||||
hardware::communication::{
|
||||
HardwareCommunicationManager, HardwareCommunicationManagerBuilder,
|
||||
HardwareCommunicationManagerEvent,
|
||||
},
|
||||
}
|
||||
};
|
||||
use futures::future;
|
||||
use js_sys::Array;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen_futures::{spawn_local, JsFuture};
|
||||
use web_sys::BluetoothDevice;
|
||||
use crate::create_test_dcm;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct WebBluetoothCommunicationManagerBuilder {
|
||||
}
|
||||
|
||||
impl HardwareCommunicationManagerBuilder for WebBluetoothCommunicationManagerBuilder {
|
||||
fn finish(&mut self, sender: Sender<HardwareCommunicationManagerEvent>) -> Box<dyn HardwareCommunicationManager> {
|
||||
Box::new(WebBluetoothCommunicationManager {
|
||||
sender,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WebBluetoothCommunicationManager {
|
||||
sender: Sender<HardwareCommunicationManagerEvent>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
// Use `js_namespace` here to bind `console.log(..)` instead of just
|
||||
// `log(..)`
|
||||
#[wasm_bindgen(js_namespace = console)]
|
||||
fn log(s: &str);
|
||||
}
|
||||
|
||||
impl HardwareCommunicationManager for WebBluetoothCommunicationManager {
|
||||
fn name(&self) -> &'static str {
|
||||
"WebBluetoothCommunicationManager"
|
||||
}
|
||||
|
||||
fn can_scan(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn start_scanning(&mut self) -> ButtplugResultFuture {
|
||||
info!("WebBluetooth manager scanning");
|
||||
let sender_clone = self.sender.clone();
|
||||
spawn_local(async move {
|
||||
// Build the filter block
|
||||
let nav = web_sys::window().unwrap().navigator();
|
||||
if nav.bluetooth().is_none() {
|
||||
error!("WebBluetooth is not supported on this browser");
|
||||
return;
|
||||
}
|
||||
info!("WebBluetooth supported by browser, continuing with scan.");
|
||||
// HACK: As of buttplug v5, we can't just create a HardwareCommunicationManager anymore. This is
|
||||
// using a test method to create a filled out DCM, which will work for now because there's no
|
||||
// way for anyone to add device configurations through FFI yet anyways.
|
||||
let config_manager = create_test_dcm(false);
|
||||
let options = web_sys::RequestDeviceOptions::new();
|
||||
let filters = Array::new();
|
||||
let optional_services = Array::new();
|
||||
for vals in config_manager.protocol_device_configurations().iter() {
|
||||
for config in vals.1 {
|
||||
if let ProtocolCommunicationSpecifier::BluetoothLE(btle) = &config {
|
||||
for name in btle.names() {
|
||||
let filter = web_sys::BluetoothLeScanFilterInit::new();
|
||||
if name.contains("*") {
|
||||
let mut name_clone = name.clone();
|
||||
name_clone.pop();
|
||||
filter.set_name_prefix(&name_clone);
|
||||
} else {
|
||||
filter.set_name(&name);
|
||||
}
|
||||
filters.push(&filter.into());
|
||||
}
|
||||
for (service, _) in btle.services() {
|
||||
optional_services.push(&service.to_string().into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
options.set_filters(&filters.into());
|
||||
options.set_optional_services(&optional_services.into());
|
||||
let nav = web_sys::window().unwrap().navigator();
|
||||
//nav.bluetooth().get_availability();
|
||||
//JsFuture::from(nav.bluetooth().request_device()).await;
|
||||
match JsFuture::from(nav.bluetooth().unwrap().request_device(&options)).await {
|
||||
Ok(device) => {
|
||||
let bt_device = BluetoothDevice::from(device);
|
||||
if bt_device.name().is_none() {
|
||||
return;
|
||||
}
|
||||
let name = bt_device.name().unwrap();
|
||||
let address = bt_device.id();
|
||||
let device_creator = Box::new(WebBluetoothHardwareConnector::new(bt_device));
|
||||
if sender_clone
|
||||
.send(HardwareCommunicationManagerEvent::DeviceFound {
|
||||
name,
|
||||
address,
|
||||
creator: device_creator,
|
||||
})
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
error!("Device manager receiver dropped, cannot send device found message.");
|
||||
} else {
|
||||
info!("WebBluetooth device found.");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error while trying to start bluetooth scan: {:?}", e);
|
||||
}
|
||||
};
|
||||
let _ = sender_clone
|
||||
.send(HardwareCommunicationManagerEvent::ScanningFinished)
|
||||
.await;
|
||||
});
|
||||
Box::pin(future::ready(Ok(())))
|
||||
}
|
||||
|
||||
fn stop_scanning(&mut self) -> ButtplugResultFuture {
|
||||
Box::pin(future::ready(Ok(())))
|
||||
}
|
||||
}
|
||||
13
packages/buttplug/tsconfig.json
Normal file
13
packages/buttplug/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"outDir": "dist",
|
||||
"moduleResolution": "bundler",
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
18
packages/buttplug/vite.config.ts
Normal file
18
packages/buttplug/vite.config.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { defineConfig } from "vite";
|
||||
import path from "path";
|
||||
import wasm from "vite-plugin-wasm";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [wasm()], // include wasm plugin
|
||||
build: {
|
||||
lib: {
|
||||
entry: path.resolve(__dirname, "src/index.ts"),
|
||||
name: "buttplug",
|
||||
fileName: "index",
|
||||
formats: ["es"], // this is important
|
||||
},
|
||||
minify: false, // for demo purposes
|
||||
target: "esnext", // this is important as well
|
||||
outDir: "dist",
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user