A new start

This commit is contained in:
Valknar XXX
2025-10-25 22:04:41 +02:00
commit be0fc11a5c
193 changed files with 25076 additions and 0 deletions

View 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

File diff suppressed because it is too large Load Diff

View 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"
]

View 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"
}
}

View File

@@ -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);
};
}

View File

@@ -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);
}
}

View 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");
}
}

View File

@@ -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;
}

View 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);
};
}

View 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;
}

View 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);
}
}

View 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);
}
}
}

View 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;
}

View 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
View File

@@ -0,0 +1,4 @@
declare module "*.json" {
const content: string;
export default content;
}

View 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));
};
}

View 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");
}

View File

@@ -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);
}
}

View 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;
}
}

View File

@@ -0,0 +1,3 @@
export function getRandomInt(max: number) {
return Math.floor(Math.random() * Math.floor(max));
}

View File

@@ -0,0 +1,6 @@
mod webbluetooth_hardware;
mod webbluetooth_manager;
// pub use webbluetooth_hardware::{WebBluetoothHardwareConnector, WebBluetoothHardware};
pub use webbluetooth_manager::{WebBluetoothCommunicationManagerBuilder};

View 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(())
})
}
}

View 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(())))
}
}

View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"outDir": "dist",
"moduleResolution": "bundler",
"esModuleInterop": true,
"skipLibCheck": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true
},
"include": ["src"]
}

View 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",
},
});