feat: upgrade buttplug package to protocol v4 and WASM v10
All checks were successful
Build and Push Docker Image to Gitea / build-and-push (push) Successful in 7m30s

Upgrade the buttplug TypeScript client from class-based v3 protocol to
interface-based v4 protocol, and the Rust/WASM server from the monolithic
buttplug 9.0.9 crate to the split buttplug_core/buttplug_server/
buttplug_server_device_config 10.0.0 crates.

TypeScript changes:
- Messages are now plain interfaces with msgId()/setMsgId() helpers
- ActuatorType → OutputType, SensorType → InputType
- ScalarCmd/RotateCmd/LinearCmd → OutputCmd, SensorReadCmd → InputCmd
- Client.ts → ButtplugClient.ts, new DeviceCommand/DeviceFeature files
- Devices getter returns Map instead of array
- Removed class-transformer/reflect-metadata dependencies

Rust/WASM changes:
- Split imports across buttplug_core, buttplug_server, buttplug_server_device_config
- Removed ButtplugServerDowngradeWrapper (use ButtplugServer directly)
- Replaced ButtplugFuture/ButtplugFutureStateShared with tokio::sync::oneshot
- Updated Hardware::new for new 6-arg signature
- Uses git fork (valknarthing/buttplug) to fix missing wasm deps in buttplug_core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-06 14:46:47 +01:00
parent fed2dd65e5
commit 6ea4ed1933
31 changed files with 1763 additions and 2441 deletions

View File

@@ -17,8 +17,10 @@
"packageManager": "pnpm@10.19.0",
"pnpm": {
"onlyBuiltDependencies": [
"es5-ext",
"esbuild",
"svelte-preprocess",
"vue-demi"
"wasm-pack"
],
"ignoredBuiltDependencies": [
"@tailwindcss/oxide",

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "buttplug_wasm"
version = "9.0.9"
version = "10.0.0"
authors = ["Nonpolynomial Labs, LLC <kyle@nonpolynomial.com>"]
description = "WASM Interop for the Buttplug Intimate Hardware Control Library"
license = "BSD-3-Clause"
@@ -16,9 +16,9 @@ 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" }
buttplug_core = { git = "https://github.com/valknarthing/buttplug.git", default-features = false, features = ["wasm"] }
buttplug_server = { git = "https://github.com/valknarthing/buttplug.git", default-features = false, features = ["wasm"] }
buttplug_server_device_config = { git = "https://github.com/valknarthing/buttplug.git" }
js-sys = "0.3.80"
tracing-wasm = "0.2.1"
log-panics = { version = "2.1.0", features = ["with-backtrace"] }

View File

@@ -13,9 +13,7 @@
"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",

View File

@@ -6,11 +6,11 @@
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
*/
"use strict";
'use strict';
import { IButtplugClientConnector } from "./IButtplugClientConnector";
import { ButtplugMessage } from "../core/Messages";
import { ButtplugBrowserWebsocketConnector } from "../utils/ButtplugBrowserWebsocketConnector";
import { IButtplugClientConnector } from './IButtplugClientConnector';
import { ButtplugMessage } from '../core/Messages';
import { ButtplugBrowserWebsocketConnector } from '../utils/ButtplugBrowserWebsocketConnector';
export class ButtplugBrowserWebsocketClientConnector
extends ButtplugBrowserWebsocketConnector
@@ -18,7 +18,7 @@ export class ButtplugBrowserWebsocketClientConnector
{
public send = (msg: ButtplugMessage): void => {
if (!this.Connected) {
throw new Error("ButtplugClient not connected");
throw new Error('ButtplugClient not connected');
}
this.sendMessage(msg);
};

View File

@@ -0,0 +1,242 @@
/*!
* 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 {
ButtplugError,
ButtplugInitError,
ButtplugMessageError,
} from '../core/Exceptions';
import { ButtplugClientConnectorException } from './ButtplugClientConnectorException';
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(): Map<number, ButtplugClientDevice> {
// While this function doesn't actually send a message, if we don't have a
// connector, we shouldn't have devices.
this.checkConnector();
return this._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._devices.clear();
this.checkConnector();
await this.shutdownConnection();
await this._connector!.disconnect();
};
public startScanning = async () => {
this._logger.Debug('ButtplugClient: StartScanning called');
this._isScanning = true;
await this.sendMsgExpectOk({ StartScanning: { Id: 1 } });
};
public stopScanning = async () => {
this._logger.Debug('ButtplugClient: StopScanning called');
this._isScanning = false;
await this.sendMsgExpectOk({ StopScanning: { Id: 1 } });
};
public stopAllDevices = async () => {
this._logger.Debug('ButtplugClient: StopAllDevices');
await this.sendMsgExpectOk({ StopCmd: { Id: 1, DeviceIndex: undefined, FeatureIndex: undefined, Inputs: true, Outputs: true } });
};
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) {
if (x.DeviceList !== undefined) {
this.parseDeviceList(x as Messages.DeviceList);
break;
} else if (x.ScanningFinished !== undefined) {
this._isScanning = false;
this.emit('scanningfinished', x);
} else if (x.InputReading !== undefined) {
// TODO this should be emitted from the device or feature, not the client
this.emit('inputreading', x);
} else {
console.log(`Unhandled message: ${x}`);
}
}
};
protected initializeConnection = async (): Promise<boolean> => {
this.checkConnector();
const msg = await this.sendMessage(
{
RequestServerInfo: {
ClientName: this._clientName,
Id: 1,
ProtocolVersionMajor: Messages.MESSAGE_SPEC_VERSION_MAJOR,
ProtocolVersionMinor: Messages.MESSAGE_SPEC_VERSION_MINOR
}
}
);
if (msg.ServerInfo !== undefined) {
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 the server version is lower than the client version, the server will disconnect here.
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;
} else if (msg.Error !== undefined) {
// 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.Error as Messages.Error;
throw ButtplugError.LogAndError(
ButtplugInitError,
this._logger,
`Cannot connect to server. ${err.ErrorMessage}`
);
}
return false;
}
private parseDeviceList = (list: Messages.DeviceList) => {
for (let [_, d] of Object.entries(list.Devices)) {
if (!this._devices.has(d.DeviceIndex)) {
const device = ButtplugClientDevice.fromMsg(
d,
this.sendMessageClosure
);
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}`);
}
}
for (let [index, device] of this._devices.entries()) {
if (!list.Devices.hasOwnProperty(index.toString())) {
this._devices.delete(index);
this.emit('deviceremoved', device);
}
}
}
protected requestDeviceList = async () => {
this.checkConnector();
this._logger.Debug('ButtplugClient: ReceiveDeviceList called');
const response = (await this.sendMessage(
{
RequestDeviceList: { Id: 1 }
}
));
this.parseDeviceList(response.DeviceList!);
};
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);
if (response.Ok !== undefined) {
return;
} else if (response.Error !== undefined) {
throw ButtplugError.FromError(response as Messages.Error);
} else {
throw ButtplugError.LogAndError(
ButtplugMessageError,
this._logger,
`Message ${response} not handled by SendMsgExpectOk`
);
}
};
protected sendMessageClosure = async (
msg: Messages.ButtplugMessage
): Promise<Messages.ButtplugMessage> => {
return await this.sendMessage(msg);
};
}

View File

@@ -6,8 +6,8 @@
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
*/
import { ButtplugError } from "../core/Exceptions";
import * as Messages from "../core/Messages";
import { ButtplugError } from '../core/Exceptions';
import * as Messages from '../core/Messages';
export class ButtplugClientConnectorException extends ButtplugError {
public constructor(message: string) {

View File

@@ -6,20 +6,24 @@
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
*/
"use strict";
import * as Messages from "../core/Messages";
'use strict';
import * as Messages from '../core/Messages';
import {
ButtplugDeviceError,
ButtplugError,
ButtplugMessageError,
} from "../core/Exceptions";
import { EventEmitter } from "eventemitter3";
import { getMessageClassFromMessage } from "../core/MessageUtils";
} from '../core/Exceptions';
import { EventEmitter } from 'eventemitter3';
import { ButtplugClientDeviceFeature } from './ButtplugClientDeviceFeature';
import { DeviceOutputCommand } from './ButtplugClientDeviceCommand';
/**
* Represents an abstract device, capable of taking certain kinds of messages.
*/
export class ButtplugClientDevice extends EventEmitter {
private _features: Map<number, ButtplugClientDeviceFeature>;
/**
* Return the name of the device.
*/
@@ -48,354 +52,114 @@ export class ButtplugClientDevice extends EventEmitter {
return this._deviceInfo.DeviceMessageTimingGap;
}
/**
* Return a list of message types the device accepts.
*/
public get messageAttributes(): Messages.MessageAttributes {
return this._deviceInfo.DeviceMessages;
public get features(): Map<number, ButtplugClientDeviceFeature> {
return this._features;
}
public static fromMsg(
msg: Messages.DeviceInfo,
sendClosure: (
device: ButtplugClientDevice,
msg: Messages.ButtplugDeviceMessage,
) => Promise<Messages.ButtplugMessage>,
msg: Messages.ButtplugMessage
) => 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 constructor(
private _deviceInfo: Messages.DeviceInfo,
private _sendClosure: (
device: ButtplugClientDevice,
msg: Messages.ButtplugDeviceMessage,
) => Promise<Messages.ButtplugMessage>,
msg: Messages.ButtplugMessage
) => Promise<Messages.ButtplugMessage>
) {
super();
_deviceInfo.DeviceMessages.update();
this._features = new Map(Object.entries(_deviceInfo.DeviceFeatures).map(([index, v]) => [parseInt(index), new ButtplugClientDeviceFeature(_deviceInfo.DeviceIndex, _deviceInfo.DeviceName, v, _sendClosure)]));
}
public async send(
msg: Messages.ButtplugDeviceMessage,
msg: Messages.ButtplugMessage
): 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);
return await this._sendClosure(msg);
}
public async sendExpectOk(
msg: Messages.ButtplugDeviceMessage,
): Promise<void> {
protected sendMsgExpectOk = async (
msg: Messages.ButtplugMessage
): Promise<void> => {
const response = await this.send(msg);
switch (getMessageClassFromMessage(response)) {
case Messages.Ok:
if (response.Ok !== undefined) {
return;
case Messages.Error:
} else if (response.Error !== undefined) {
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));
/*
throw ButtplugError.LogAndError(
ButtplugMessageError,
this._logger,
`Message ${response} not handled by SendMsgExpectOk`
);
*/
}
};
protected isOutputValid(featureIndex: number, type: Messages.OutputType) {
if (!this._deviceInfo.DeviceFeatures.hasOwnProperty(featureIndex.toString())) {
throw new ButtplugDeviceError(`Feature index ${featureIndex} does not exist for device ${this.name}`);
}
if (this._deviceInfo.DeviceFeatures[featureIndex.toString()].Outputs !== undefined && !this._deviceInfo.DeviceFeatures[featureIndex.toString()].Outputs.hasOwnProperty(type)) {
throw new ButtplugDeviceError(`Feature index ${featureIndex} does not support type ${type} for device ${this.name}`);
}
}
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 hasOutput(type: Messages.OutputType): boolean {
return this._features.values().filter((f) => f.hasOutput(type)).toArray().length > 0;
}
public get vibrateAttributes(): Messages.GenericDeviceMessageAttributes[] {
return (
this.messageAttributes.ScalarCmd?.filter(
(x) => x.ActuatorType === Messages.ActuatorType.Vibrate,
) ?? []
);
public hasInput(type: Messages.InputType): boolean {
return this._features.values().filter((f) => f.hasInput(type)).toArray().length > 0;
}
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 async runOutput(cmd: DeviceOutputCommand): Promise<void> {
let p: Promise<void>[] = [];
for (let f of this._features.values()) {
if (f.hasOutput(cmd.outputType)) {
p.push(f.runOutput(cmd));
}
}
public get hasBattery(): boolean {
const batteryAttrs = this.messageAttributes.SensorReadCmd?.filter(
(x) => x.SensorType === Messages.SensorType.Battery,
);
return batteryAttrs !== undefined && batteryAttrs.length > 0;
if (p.length == 0) {
return Promise.reject(`No features with output type ${cmd.outputType}`);
}
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),
);
await Promise.all(p);
}
public async stop(): Promise<void> {
await this.sendExpectOk(new Messages.StopDeviceCmd(this.index));
await this.sendMsgExpectOk({StopCmd: { Id: 1, DeviceIndex: this.index, FeatureIndex: undefined, Inputs: true, Outputs: true}});
}
public async battery(): Promise<number> {
let p: Promise<void>[] = [];
for (let f of this._features.values()) {
if (f.hasInput(Messages.InputType.Battery)) {
// Right now, we only have one battery per device, so assume the first one we find is it.
let response = await f.runInput(Messages.InputType.Battery, Messages.InputCommandType.Read);
if (response === undefined) {
throw new ButtplugMessageError("Got incorrect message back.");
}
if (response.Reading[Messages.InputType.Battery] === undefined) {
throw new ButtplugMessageError("Got reading with no Battery info.");
}
return response.Reading[Messages.InputType.Battery].Value;
}
}
throw new ButtplugDeviceError(`No battery present on this device.`);
}
public emitDisconnected() {
this.emit("deviceremoved");
this.emit('deviceremoved');
}
}

View File

@@ -0,0 +1,111 @@
import { ButtplugDeviceError } from "../core/Exceptions";
import { OutputType } from "../core/Messages";
class PercentOrSteps {
private _percent: number | undefined;
private _steps: number | undefined;
public get percent() {
return this._percent;
}
public get steps() {
return this._steps;
}
public static createSteps(s: number): PercentOrSteps {
let v = new PercentOrSteps;
v._steps = s;
return v;
}
public static createPercent(p: number): PercentOrSteps {
if (p < 0 || p > 1.0) {
throw new ButtplugDeviceError(`Percent value ${p} is not in the range 0.0 <= x <= 1.0`);
}
let v = new PercentOrSteps;
v._percent = p;
return v;
}
}
export class DeviceOutputCommand {
public constructor(
private _outputType: OutputType,
private _value: PercentOrSteps,
private _duration?: number,
)
{}
public get outputType() {
return this._outputType;
}
public get value() {
return this._value;
}
public get duration() {
return this._duration;
}
}
export class DeviceOutputValueConstructor {
public constructor(
private _outputType: OutputType)
{}
public steps(steps: number): DeviceOutputCommand {
return new DeviceOutputCommand(this._outputType, PercentOrSteps.createSteps(steps), undefined);
}
public percent(percent: number): DeviceOutputCommand {
return new DeviceOutputCommand(this._outputType, PercentOrSteps.createPercent(percent), undefined);
}
}
export class DeviceOutputPositionWithDurationConstructor {
public steps(steps: number, duration: number): DeviceOutputCommand {
return new DeviceOutputCommand(OutputType.Position, PercentOrSteps.createSteps(steps), duration);
}
public percent(percent: number, duration: number): DeviceOutputCommand {
return new DeviceOutputCommand(OutputType.HwPositionWithDuration, PercentOrSteps.createPercent(percent), duration);
}
}
export class DeviceOutput {
private constructor() {}
public static get Vibrate() {
return new DeviceOutputValueConstructor(OutputType.Vibrate);
}
public static get Rotate() {
return new DeviceOutputValueConstructor(OutputType.Rotate);
}
public static get Oscillate() {
return new DeviceOutputValueConstructor(OutputType.Oscillate);
}
public static get Constrict() {
return new DeviceOutputValueConstructor(OutputType.Constrict);
}
public static get Inflate() {
return new DeviceOutputValueConstructor(OutputType.Inflate);
}
public static get Temperature() {
return new DeviceOutputValueConstructor(OutputType.Temperature);
}
public static get Led() {
return new DeviceOutputValueConstructor(OutputType.Led);
}
public static get Spray() {
return new DeviceOutputValueConstructor(OutputType.Spray);
}
public static get Position() {
return new DeviceOutputValueConstructor(OutputType.Position);
}
public static get PositionWithDuration() {
return new DeviceOutputPositionWithDurationConstructor();
}
}

View File

@@ -0,0 +1,168 @@
import { ButtplugDeviceError, ButtplugError, ButtplugMessageError } from "../core/Exceptions";
import * as Messages from "../core/Messages";
import { DeviceOutputCommand } from "./ButtplugClientDeviceCommand";
export class ButtplugClientDeviceFeature {
constructor(
private _deviceIndex: number,
private _deviceName: string,
private _feature: Messages.DeviceFeature,
private _sendClosure: (
msg: Messages.ButtplugMessage
) => Promise<Messages.ButtplugMessage>) {
}
protected send = async (msg: Messages.ButtplugMessage): Promise<Messages.ButtplugMessage> => {
return await this._sendClosure(msg);
}
protected sendMsgExpectOk = async (
msg: Messages.ButtplugMessage
): Promise<void> => {
const response = await this.send(msg);
if (response.Ok !== undefined) {
return;
} else if (response.Error !== undefined) {
throw ButtplugError.FromError(response as Messages.Error);
} else {
throw new ButtplugMessageError("Expected Ok or Error, and didn't get either!");
}
};
protected isOutputValid(type: Messages.OutputType) {
if (this._feature.Output !== undefined && !this._feature.Output.hasOwnProperty(type)) {
throw new ButtplugDeviceError(`Feature index ${this._feature.FeatureIndex} does not support type ${type} for device ${this._deviceName}`);
}
}
protected isInputValid(type: Messages.InputType) {
if (this._feature.Input !== undefined && !this._feature.Input.hasOwnProperty(type)) {
throw new ButtplugDeviceError(`Feature index ${this._feature.FeatureIndex} does not support type ${type} for device ${this._deviceName}`);
}
}
protected async sendOutputCmd(command: DeviceOutputCommand): Promise<void> {
// Make sure the requested feature is valid
this.isOutputValid(command.outputType);
if (command.value === undefined) {
throw new ButtplugDeviceError(`${command.outputType} requires value defined`);
}
let type = command.outputType;
let duration: undefined | number = undefined;
if (type == Messages.OutputType.HwPositionWithDuration) {
if (command.duration === undefined) {
throw new ButtplugDeviceError("PositionWithDuration requires duration defined");
}
duration = command.duration;
}
let value: number;
let p = command.value;
if (p.percent === undefined) {
// TODO Check step limits here
value = command.value.steps!;
} else {
value = Math.ceil(this._feature.Output[type]!.Value![1] * p.percent);
}
let newCommand: Messages.DeviceFeatureOutput = { Value: value, Duration: duration };
let outCommand = {};
outCommand[type.toString()] = newCommand;
let cmd: Messages.ButtplugMessage = {
OutputCmd: {
Id: 1,
DeviceIndex: this._deviceIndex,
FeatureIndex: this._feature.FeatureIndex,
Command: outCommand
}
};
await this.sendMsgExpectOk(cmd);
}
public get featureDescriptor(): string {
return this._feature.FeatureDescriptor;
}
public get featureIndex(): number {
return this._feature.FeatureIndex;
}
public get outputTypes(): Messages.OutputType[] {
if (this._feature.Output === undefined) return [];
return Object.keys(this._feature.Output) as Messages.OutputType[];
}
public get inputTypes(): Messages.InputType[] {
if (this._feature.Input === undefined) return [];
return Object.keys(this._feature.Input) as Messages.InputType[];
}
public outputMaxValue(type: Messages.OutputType): number {
if (this._feature.Output === undefined || this._feature.Output[type] === undefined) {
return 0;
}
const val = this._feature.Output[type]!.Value;
// Value can arrive as number[] [min, max] from server or as number
if (Array.isArray(val)) {
return val[val.length - 1];
}
return val as number;
}
public hasOutput(type: Messages.OutputType): boolean {
if (this._feature.Output !== undefined) {
return this._feature.Output.hasOwnProperty(type.toString());
}
return false;
}
public hasInput(type: Messages.InputType): boolean {
if (this._feature.Input !== undefined) {
return this._feature.Input.hasOwnProperty(type.toString());
}
return false;
}
public async runOutput(cmd: DeviceOutputCommand): Promise<void> {
if (this._feature.Output !== undefined && this._feature.Output.hasOwnProperty(cmd.outputType.toString())) {
return this.sendOutputCmd(cmd);
}
throw new ButtplugDeviceError(`Output type ${cmd.outputType} not supported by feature.`);
}
public async runInput(inputType: Messages.InputType, inputCommand: Messages.InputCommandType): Promise<Messages.InputReading | undefined> {
// Make sure the requested feature is valid
this.isInputValid(inputType);
let inputAttributes = this._feature.Input[inputType];
console.log(this._feature.Input);
if ((inputCommand === Messages.InputCommandType.Unsubscribe && !inputAttributes.Command.includes(Messages.InputCommandType.Subscribe)) && !inputAttributes.Command.includes(inputCommand)) {
throw new ButtplugDeviceError(`${inputType} does not support command ${inputCommand}`);
}
let cmd: Messages.ButtplugMessage = {
InputCmd: {
Id: 1,
DeviceIndex: this._deviceIndex,
FeatureIndex: this._feature.FeatureIndex,
Type: inputType,
Command: inputCommand,
}
};
if (inputCommand == Messages.InputCommandType.Read) {
const response = await this.send(cmd);
if (response.InputReading !== undefined) {
return response.InputReading;
} else if (response.Error !== undefined) {
throw ButtplugError.FromError(response as Messages.Error);
} else {
throw new ButtplugMessageError("Expected InputReading or Error, and didn't get either!");
}
} else {
console.log(`Sending subscribe message: ${JSON.stringify(cmd)}`);
await this.sendMsgExpectOk(cmd);
console.log("Got back ok?");
}
}
}

View File

@@ -6,10 +6,10 @@
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
*/
"use strict";
'use strict';
import { ButtplugBrowserWebsocketClientConnector } from "./ButtplugBrowserWebsocketClientConnector";
import { WebSocket as NodeWebSocket } from "ws";
import { ButtplugBrowserWebsocketClientConnector } from './ButtplugBrowserWebsocketClientConnector';
import { WebSocket as NodeWebSocket } from 'ws';
export class ButtplugNodeWebsocketClientConnector extends ButtplugBrowserWebsocketClientConnector {
protected _websocketConstructor =

View File

@@ -1,276 +0,0 @@
/*!
* 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

@@ -6,8 +6,8 @@
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
*/
import { ButtplugMessage } from "../core/Messages";
import { EventEmitter } from "eventemitter3";
import { ButtplugMessage } from '../core/Messages';
import { EventEmitter } from 'eventemitter3';
export interface IButtplugClientConnector extends EventEmitter {
connect: () => Promise<void>;

View File

@@ -6,8 +6,8 @@
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
*/
import * as Messages from "./Messages";
import { ButtplugLogger } from "./Logging";
import * as Messages from './Messages';
import { ButtplugLogger } from './Logging';
export class ButtplugError extends Error {
public get ErrorClass(): Messages.ErrorClass {
@@ -23,14 +23,20 @@ export class ButtplugError extends Error {
}
public get ErrorMessage(): Messages.ButtplugMessage {
return new Messages.Error(this.message, this.ErrorClass, this.Id);
return {
Error: {
Id: this.Id,
ErrorCode: this.ErrorClass,
ErrorMessage: this.message
}
}
}
public static LogAndError<T extends ButtplugError>(
constructor: new (str: string, num: number) => T,
logger: ButtplugLogger,
message: string,
id: number = Messages.SYSTEM_MESSAGE_ID,
id: number = Messages.SYSTEM_MESSAGE_ID
): T {
logger.Error(message);
return new constructor(message, id);
@@ -61,7 +67,7 @@ export class ButtplugError extends Error {
message: string,
errorClass: Messages.ErrorClass,
id: number = Messages.SYSTEM_MESSAGE_ID,
inner?: Error,
inner?: Error
) {
super(message);
this.errorClass = errorClass;

View File

@@ -6,7 +6,7 @@
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
*/
import { EventEmitter } from "eventemitter3";
import { EventEmitter } from 'eventemitter3';
export enum ButtplugLogLevel {
Off,
@@ -69,7 +69,9 @@ export class LogMessage {
* Returns a formatted string with timestamp, level, and message.
*/
public get FormattedMessage() {
return `${ButtplugLogLevel[this.logLevel]} : ${this.timestamp} : ${this.logMessage}`;
return `${ButtplugLogLevel[this.logLevel]} : ${this.timestamp} : ${
this.logMessage
}`;
}
}
@@ -189,7 +191,7 @@ export class ButtplugLogger extends EventEmitter {
console.log(logMsg.FormattedMessage);
}
if (level <= this.maximumEventLogLevel) {
this.emit("log", logMsg);
this.emit('log', logMsg);
}
}
}

View File

@@ -1,48 +0,0 @@
/*!
* 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

@@ -7,136 +7,59 @@
*/
// tslint:disable:max-classes-per-file
"use strict";
'use strict';
import { instanceToPlain, Type } from "class-transformer";
import "reflect-metadata";
import { ButtplugMessageError } from './Exceptions';
export const SYSTEM_MESSAGE_ID = 0;
export const DEFAULT_MESSAGE_ID = 1;
export const MAX_ID = 4294967295;
export const MESSAGE_SPEC_VERSION = 3;
export const MESSAGE_SPEC_VERSION_MAJOR = 4;
export const MESSAGE_SPEC_VERSION_MINOR = 0;
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));
}
// Base message interfaces
export interface ButtplugMessage {
Ok?: Ok;
Ping?: Ping;
Error?: Error;
RequestServerInfo?: RequestServerInfo;
ServerInfo?: ServerInfo;
RequestDeviceList?: RequestDeviceList;
StartScanning?: StartScanning;
StopScanning?: StopScanning;
ScanningFinished?: ScanningFinished;
StopCmd?: StopCmd;
InputCmd?: InputCmd;
InputReading?: InputReading;
OutputCmd?: OutputCmd;
DeviceList?: DeviceList;
}
export enum ActuatorType {
Unknown = "Unknown",
Vibrate = "Vibrate",
Rotate = "Rotate",
Oscillate = "Oscillate",
Constrict = "Constrict",
Inflate = "Inflate",
Position = "Position",
export function msgId(msg: ButtplugMessage): number {
for (let [_, entry] of Object.entries(msg)) {
if (entry != undefined) {
return entry.Id;
}
}
throw new ButtplugMessageError(`Message ${msg} does not have an ID.`);
}
export enum SensorType {
Unknown = "Unknown",
Battery = "Battery",
RSSI = "RSSI",
Button = "Button",
Pressure = "Pressure",
// Temperature,
// Accelerometer,
// Gyro,
export function setMsgId(msg: ButtplugMessage, id: number) {
for (let [_, entry] of Object.entries(msg)) {
if (entry != undefined) {
entry.Id = id;
return;
}
}
throw new ButtplugMessageError(`Message ${msg} does not have an ID.`);
}
export class GenericDeviceMessageAttributes {
public FeatureDescriptor: string;
public ActuatorType: ActuatorType;
public StepCount: number;
public Index = 0;
constructor(data: Partial<GenericDeviceMessageAttributes>) {
Object.assign(this, data);
}
export interface Ok {
Id: number | undefined;
}
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 interface Ping {
Id: number | undefined;
}
export enum ErrorClass {
@@ -147,345 +70,140 @@ export enum ErrorClass {
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 interface Error {
ErrorMessage: string;
ErrorCode: ErrorClass;
Id: number | undefined;
}
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 interface RequestDeviceList {
Id: number | undefined;
}
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 interface StartScanning {
Id: number | undefined;
}
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 interface StopScanning {
Id: number | undefined;
}
export class DeviceRemoved extends ButtplugSystemMessage {
static Name = "DeviceRemoved";
constructor(public DeviceIndex: number) {
super();
}
export interface StopAllDevices {
Id: number | undefined;
}
export class RequestDeviceList extends ButtplugMessage {
static Name = "RequestDeviceList";
constructor(public Id: number = DEFAULT_MESSAGE_ID) {
super(Id);
}
export interface ScanningFinished {
Id: number | undefined;
}
export class StartScanning extends ButtplugMessage {
static Name = "StartScanning";
constructor(public Id: number = DEFAULT_MESSAGE_ID) {
super(Id);
}
export interface RequestServerInfo {
ClientName: string;
ProtocolVersionMajor: number;
ProtocolVersionMinor: number;
Id: number | undefined;
}
export class StopScanning extends ButtplugMessage {
static Name = "StopScanning";
constructor(public Id: number = DEFAULT_MESSAGE_ID) {
super(Id);
}
export interface ServerInfo {
MaxPingTime: number;
ServerName: string;
ProtocolVersionMajor: number;
ProtocolVersionMinor: number;
Id: number | undefined;
}
export class ScanningFinished extends ButtplugSystemMessage {
static Name = "ScanningFinished";
constructor() {
super();
}
export interface DeviceFeature {
FeatureDescriptor: string;
Output: { [key: string]: DeviceFeatureOutput };
Input: { [key: string]: DeviceFeatureInput };
FeatureIndex: number;
}
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 interface DeviceInfo {
DeviceIndex: number;
DeviceName: string;
DeviceFeatures: { [key: number]: DeviceFeature };
DeviceDisplayName?: string;
DeviceMessageTimingGap?: number;
}
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 interface DeviceList {
Devices: { [key: number]: DeviceInfo };
Id: number | undefined;
}
export class StopDeviceCmd extends ButtplugDeviceMessage {
static Name = "StopDeviceCmd";
constructor(
public DeviceIndex: number = -1,
public Id: number = DEFAULT_MESSAGE_ID,
) {
super(DeviceIndex, Id);
}
export enum OutputType {
Unknown = 'Unknown',
Vibrate = 'Vibrate',
Rotate = 'Rotate',
Oscillate = 'Oscillate',
Constrict = 'Constrict',
Inflate = 'Inflate',
Position = 'Position',
HwPositionWithDuration = 'HwPositionWithDuration',
Temperature = 'Temperature',
Spray = 'Spray',
Led = 'Led',
}
export class StopAllDevices extends ButtplugMessage {
static Name = "StopAllDevices";
constructor(public Id: number = DEFAULT_MESSAGE_ID) {
super(Id);
}
export enum InputType {
Unknown = 'Unknown',
Battery = 'Battery',
RSSI = 'RSSI',
Button = 'Button',
Pressure = 'Pressure',
// Temperature,
// Accelerometer,
// Gyro,
}
export class GenericMessageSubcommand {
protected constructor(public Index: number) {}
export enum InputCommandType {
Read = 'Read',
Subscribe = 'Subscribe',
Unsubscribe = 'Unsubscribe',
}
export class ScalarSubcommand extends GenericMessageSubcommand {
constructor(
Index: number,
public Scalar: number,
public ActuatorType: ActuatorType,
) {
super(Index);
}
export interface DeviceFeatureInput {
Value: number[];
Command: InputCommandType[];
}
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 interface DeviceFeatureOutput {
Value: number;
Duration?: number;
}
export class RotateSubcommand extends GenericMessageSubcommand {
constructor(
Index: number,
public Speed: number,
public Clockwise: boolean,
) {
super(Index);
}
export interface OutputCmd {
DeviceIndex: number;
FeatureIndex: number;
Command: { [key: string]: DeviceFeatureOutput };
Id: number | undefined;
}
export class RotateCmd extends ButtplugDeviceMessage {
static Name = "RotateCmd";
// Device Input Commands
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 interface InputCmd {
DeviceIndex: number;
FeatureIndex: number;
Type: InputType;
Command: InputCommandType;
Id: number | undefined;
}
export class VectorSubcommand extends GenericMessageSubcommand {
constructor(
Index: number,
public Position: number,
public Duration: number,
) {
super(Index);
}
export interface InputValue {
Value: number;
}
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 interface InputReading {
DeviceIndex: number;
FeatureIndex: number;
Reading: { [key: string]: InputValue };
Id: number | undefined;
}
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);
}
export interface StopCmd {
Id: number | undefined;
DeviceIndex: number | undefined;
FeatureIndex: number | undefined;
Inputs: boolean | undefined;
Outputs: boolean | undefined;
}

View File

@@ -1,19 +1,27 @@
import { ButtplugMessage } from "./core/Messages";
import { IButtplugClientConnector } from "./client/IButtplugClientConnector";
import { fromJSON } from "./core/MessageUtils";
import { EventEmitter } from "eventemitter3";
/*!
* 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.
*/
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";
import { ButtplugMessage } from './core/Messages';
import { IButtplugClientConnector } from './client/IButtplugClientConnector';
import { EventEmitter } from 'eventemitter3';
export * from './client/ButtplugClient';
export * from './client/ButtplugClientDevice';
export * from './client/ButtplugBrowserWebsocketClientConnector';
export * from './client/ButtplugNodeWebsocketClientConnector';
export * from './client/ButtplugClientConnectorException';
export * from './utils/ButtplugMessageSorter';
export * from './client/ButtplugClientDeviceCommand';
export * from './client/ButtplugClientDeviceFeature';
export * from './client/IButtplugClientConnector';
export * from './core/Messages';
export * from './core/Logging';
export * from './core/Exceptions';
export class ButtplugWasmClientConnector
extends EventEmitter
@@ -36,18 +44,18 @@ export class ButtplugWasmClientConnector
private static maybeLoadWasm = async () => {
if (ButtplugWasmClientConnector.wasmInstance == undefined) {
ButtplugWasmClientConnector.wasmInstance = await import(
"../wasm/index.js"
'../wasm/index.js'
);
}
};
public static activateLogging = async (logLevel: string = "debug") => {
public static activateLogging = async (logLevel: string = 'debug') => {
await ButtplugWasmClientConnector.maybeLoadWasm();
if (this._loggingActivated) {
console.log("Logging already activated, ignoring.");
console.log('Logging already activated, ignoring.');
return;
}
console.log("Turning on logging.");
console.log('Turning on logging.');
ButtplugWasmClientConnector.wasmInstance.buttplug_activate_env_logger(
logLevel,
);
@@ -57,7 +65,6 @@ export class ButtplugWasmClientConnector
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) => {
@@ -73,7 +80,7 @@ export class ButtplugWasmClientConnector
public send = (msg: ButtplugMessage): void => {
ButtplugWasmClientConnector.wasmInstance.buttplug_client_send_json_message(
this.client,
new TextEncoder().encode("[" + msg.toJSON() + "]"),
new TextEncoder().encode('[' + JSON.stringify(msg) + ']'),
(output) => {
this.emitMessage(output);
},
@@ -82,7 +89,7 @@ export class ButtplugWasmClientConnector
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));
const msgs: ButtplugMessage[] = JSON.parse(str);
this.emit('message', msgs);
};
}

View File

@@ -8,12 +8,16 @@ 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
use buttplug_core::{
message::{ButtplugServerMessageCurrent, BUTTPLUG_CURRENT_API_MAJOR_VERSION, serializer::{ButtplugSerializedMessage, ButtplugMessageSerializer}},
util::async_manager,
};
use buttplug_server::{
ButtplugServerBuilder, ButtplugServer,
device::ServerDeviceManagerBuilder,
message::{ButtplugServerMessageVariant, serializer::ButtplugServerJSONSerializer},
};
use buttplug_server_device_config::{DeviceConfigurationManager, load_protocol_configs};
type FFICallback = js_sys::Function;
type FFICallbackContext = u32;
@@ -33,16 +37,17 @@ use wasm_bindgen::prelude::*;
use std::sync::Arc;
use js_sys::Uint8Array;
pub type ButtplugWASMServer = Arc<ButtplugServerDowngradeWrapper>;
pub type ButtplugWASMServer = Arc<ButtplugServer>;
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 serializer = ButtplugServerJSONSerializer::default();
serializer.force_message_version(&BUTTPLUG_CURRENT_API_MAJOR_VERSION);
let json_msg = serializer.serialize(&[ButtplugServerMessageVariant::V4(message.clone())]);
if let ButtplugSerializedMessage::Text(json) = json_msg {
let buf = json.as_bytes();
let this = JsValue::null();
let uint8buf = unsafe { Uint8Array::new(&Uint8Array::view(buf)) };
callback.call1(&this, &JsValue::from(uint8buf));
@@ -50,10 +55,9 @@ pub fn send_server_message(
}
#[no_mangle]
pub fn create_test_dcm(allow_raw_messages: bool) -> DeviceConfigurationManager {
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.")
}
@@ -68,18 +72,17 @@ pub fn buttplug_create_embedded_wasm_server(
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 server = Arc::new(builder.finish().unwrap());
let event_stream = server.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);
send_server_message(&message, &callback);
}
});
Box::into_raw(Box::new(wrapper))
Box::into_raw(Box::new(server))
}
#[no_mangle]
@@ -106,15 +109,18 @@ pub fn buttplug_client_send_json_message(
};
let callback = callback.clone();
let serializer = ButtplugServerJSONSerializer::default();
serializer.force_message_version(&BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION);
serializer.force_message_version(&BUTTPLUG_CURRENT_API_MAJOR_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);
let json_msg = serializer.serialize(&[response]);
if let ButtplugSerializedMessage::Text(json) = json_msg {
let buf = json.as_bytes();
let this = JsValue::null();
let uint8buf = unsafe { Uint8Array::new(&Uint8Array::view(buf)) };
callback.call1(&this, &JsValue::from(uint8buf));
}
});
}

View File

@@ -6,11 +6,10 @@
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
*/
"use strict";
'use strict';
import { EventEmitter } from "eventemitter3";
import { ButtplugMessage } from "../core/Messages";
import { fromJSON } from "../core/MessageUtils";
import { EventEmitter } from 'eventemitter3';
import { ButtplugMessage } from '../core/Messages';
export class ButtplugBrowserWebsocketConnector extends EventEmitter {
protected _ws: WebSocket | undefined;
@@ -27,20 +26,18 @@ export class ButtplugBrowserWebsocketConnector extends EventEmitter {
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 () => {
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._ws.addEventListener('message', (msg) => {
this.parseIncomingMessage(msg);
});
this._ws.removeEventListener("close", onCloseCallback);
this._ws.removeEventListener("error", onErrorCallback);
this._ws.addEventListener("close", this.disconnect);
this._ws.removeEventListener('close', onCloseCallback);
this._ws.removeEventListener('error', onErrorCallback);
this._ws.addEventListener('close', this.disconnect);
resolve();
} catch (e) {
reject(e);
@@ -50,8 +47,8 @@ export class ButtplugBrowserWebsocketConnector extends EventEmitter {
// 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);
ws.addEventListener('error', onErrorCallback)
ws.addEventListener('close', onCloseCallback);
});
};
@@ -61,14 +58,14 @@ export class ButtplugBrowserWebsocketConnector extends EventEmitter {
}
this._ws!.close();
this._ws = undefined;
this.emit("disconnect");
this.emit('disconnect');
};
public sendMessage(msg: ButtplugMessage) {
if (!this.Connected) {
throw new Error("ButtplugBrowserWebsocketConnector not connected");
throw new Error('ButtplugBrowserWebsocketConnector not connected');
}
this._ws!.send("[" + msg.toJSON() + "]");
this._ws!.send('[' + JSON.stringify(msg) + ']');
}
public initialize = async (): Promise<void> => {
@@ -76,16 +73,16 @@ export class ButtplugBrowserWebsocketConnector extends EventEmitter {
};
protected parseIncomingMessage(event: MessageEvent) {
if (typeof event.data === "string") {
const msgs = fromJSON(event.data);
this.emit("message", msgs);
if (typeof event.data === 'string') {
const msgs: ButtplugMessage[] = JSON.parse(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);
const msgs: ButtplugMessage[] = JSON.parse((event.target as FileReader).result as string);
this.emit('message', msgs);
}
}

View File

@@ -6,8 +6,8 @@
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
*/
import * as Messages from "../core/Messages";
import { ButtplugError } from "../core/Exceptions";
import * as Messages from '../core/Messages';
import { ButtplugError } from '../core/Exceptions';
export class ButtplugMessageSorter {
protected _counter = 1;
@@ -22,10 +22,10 @@ export class ButtplugMessageSorter {
// them while waiting for them to return across the line.
// tslint:disable:promise-function-async
public PrepareOutgoingMessage(
msg: Messages.ButtplugMessage,
msg: Messages.ButtplugMessage
): Promise<Messages.ButtplugMessage> {
if (this._useCounter) {
msg.Id = this._counter;
Messages.setMsgId(msg, this._counter);
// Always increment last, otherwise we might lose sync
this._counter += 1;
}
@@ -35,23 +35,24 @@ export class ButtplugMessageSorter {
(resolve, reject) => {
res = resolve;
rej = reject;
},
}
);
this._waitingMsgs.set(msg.Id, [res, rej]);
this._waitingMsgs.set(Messages.msgId(msg), [res, rej]);
return msgPromise;
}
public ParseIncomingMessages(
msgs: Messages.ButtplugMessage[],
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)!;
let id = Messages.msgId(x);
if (id !== Messages.SYSTEM_MESSAGE_ID && this._waitingMsgs.has(id)) {
const [res, rej] = this._waitingMsgs.get(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));
if (x.Error !== undefined) {
rej(ButtplugError.FromError(x.Error!));
continue;
}
res(x);

View File

@@ -1,12 +1,7 @@
use async_trait::async_trait;
use buttplug::{
core::{
errors::ButtplugDeviceError,
message::Endpoint,
},
server::device::{
configuration::{BluetoothLESpecifier, ProtocolCommunicationSpecifier},
hardware::{
use buttplug_core::errors::ButtplugDeviceError;
use buttplug_server_device_config::{BluetoothLESpecifier, Endpoint, ProtocolCommunicationSpecifier};
use buttplug_server::device::hardware::{
Hardware,
HardwareConnector,
HardwareEvent,
@@ -17,9 +12,6 @@ use buttplug::{
HardwareSubscribeCmd,
HardwareUnsubscribeCmd,
HardwareWriteCmd,
},
},
util::future::{ButtplugFuture, ButtplugFutureStateShared},
};
use futures::future::{self, BoxFuture};
use js_sys::{DataView, Uint8Array};
@@ -28,7 +20,7 @@ use std::{
convert::TryFrom,
fmt::{self, Debug},
};
use tokio::sync::{broadcast, mpsc};
use tokio::sync::{broadcast, mpsc, oneshot};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::{spawn_local, JsFuture};
@@ -41,9 +33,6 @@ use web_sys::{
MessageEvent,
};
type WebBluetoothResultFuture = ButtplugFuture<Result<(), ButtplugDeviceError>>;
type WebBluetoothReadResultFuture = ButtplugFuture<Result<HardwareReading, ButtplugDeviceError>>;
struct BluetoothDeviceWrapper {
pub device: BluetoothDevice
}
@@ -179,7 +168,7 @@ impl HardwareSpecializer for WebBluetoothHardwareSpecializer {
receiver,
command_sender,
));
Ok(Hardware::new(&name, &address, &[], device_impl))
Ok(Hardware::new(&name, &address, &[], &None, false, device_impl))
}
WebBluetoothEvent::Disconnected => Err(
ButtplugDeviceError::DeviceCommunicationError(
@@ -202,19 +191,19 @@ pub enum WebBluetoothEvent {
pub enum WebBluetoothDeviceCommand {
Write(
HardwareWriteCmd,
ButtplugFutureStateShared<Result<(), ButtplugDeviceError>>,
oneshot::Sender<Result<(), ButtplugDeviceError>>,
),
Read(
HardwareReadCmd,
ButtplugFutureStateShared<Result<HardwareReading, ButtplugDeviceError>>,
oneshot::Sender<Result<HardwareReading, ButtplugDeviceError>>,
),
Subscribe(
HardwareSubscribeCmd,
ButtplugFutureStateShared<Result<(), ButtplugDeviceError>>,
oneshot::Sender<Result<(), ButtplugDeviceError>>,
),
Unsubscribe(
HardwareUnsubscribeCmd,
ButtplugFutureStateShared<Result<(), ButtplugDeviceError>>,
oneshot::Sender<Result<(), ButtplugDeviceError>>,
),
}
@@ -287,7 +276,7 @@ async fn run_webbluetooth_loop(
.await;
while let Some(msg) = device_command_receiver.recv().await {
match msg {
WebBluetoothDeviceCommand::Write(write_cmd, waker) => {
WebBluetoothDeviceCommand::Write(write_cmd, sender) => {
debug!("Writing to endpoint {:?}", write_cmd.endpoint());
let chr = char_map.get(&write_cmd.endpoint()).unwrap().clone();
spawn_local(async move {
@@ -295,10 +284,10 @@ async fn run_webbluetooth_loop(
JsFuture::from(chr.write_value_with_u8_array(&uint8buf).unwrap())
.await
.unwrap();
waker.set_reply(Ok(()));
let _ = sender.send(Ok(()));
});
}
WebBluetoothDeviceCommand::Read(read_cmd, waker) => {
WebBluetoothDeviceCommand::Read(read_cmd, sender) => {
debug!("Writing to endpoint {:?}", read_cmd.endpoint());
let chr = char_map.get(&read_cmd.endpoint()).unwrap().clone();
spawn_local(async move {
@@ -307,10 +296,10 @@ async fn run_webbluetooth_loop(
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));
let _ = sender.send(Ok(reading));
});
}
WebBluetoothDeviceCommand::Subscribe(subscribe_cmd, waker) => {
WebBluetoothDeviceCommand::Subscribe(subscribe_cmd, sender) => {
debug!("Subscribing to endpoint {:?}", subscribe_cmd.endpoint());
let chr = char_map.get(&subscribe_cmd.endpoint()).unwrap().clone();
let ep = subscribe_cmd.endpoint();
@@ -335,10 +324,10 @@ async fn run_webbluetooth_loop(
spawn_local(async move {
JsFuture::from(chr.start_notifications()).await.unwrap();
debug!("Endpoint subscribed");
waker.set_reply(Ok(()));
let _ = sender.send(Ok(()));
});
}
WebBluetoothDeviceCommand::Unsubscribe(_unsubscribe_cmd, _waker) => {}
WebBluetoothDeviceCommand::Unsubscribe(_unsubscribe_cmd, _sender) => {}
}
}
debug!("run_webbluetooth_loop exited!");
@@ -388,12 +377,13 @@ impl HardwareInternal for WebBluetoothHardware {
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))
let (tx, rx) = oneshot::channel();
let _ = sender
.send(WebBluetoothDeviceCommand::Read(msg, tx))
.await;
fut.await
rx.await.unwrap_or(Err(ButtplugDeviceError::DeviceCommunicationError(
"Device command channel closed".to_string(),
)))
})
}
@@ -401,12 +391,13 @@ impl HardwareInternal for WebBluetoothHardware {
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))
let (tx, rx) = oneshot::channel();
let _ = sender
.send(WebBluetoothDeviceCommand::Write(msg, tx))
.await;
fut.await
rx.await.unwrap_or(Err(ButtplugDeviceError::DeviceCommunicationError(
"Device command channel closed".to_string(),
)))
})
}
@@ -414,12 +405,13 @@ impl HardwareInternal for WebBluetoothHardware {
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))
let (tx, rx) = oneshot::channel();
let _ = sender
.send(WebBluetoothDeviceCommand::Subscribe(msg, tx))
.await;
fut.await
rx.await.unwrap_or(Err(ButtplugDeviceError::DeviceCommunicationError(
"Device command channel closed".to_string(),
)))
})
}

View File

@@ -1,14 +1,10 @@
use super::webbluetooth_hardware::WebBluetoothHardwareConnector;
use buttplug::{
core::ButtplugResultFuture,
server::device::{
configuration::{ProtocolCommunicationSpecifier},
hardware::communication::{
use buttplug_core::ButtplugResultFuture;
use buttplug_server_device_config::ProtocolCommunicationSpecifier;
use buttplug_server::device::hardware::communication::{
HardwareCommunicationManager, HardwareCommunicationManagerBuilder,
HardwareCommunicationManagerEvent,
},
}
};
use futures::future;
use js_sys::Array;
@@ -69,8 +65,8 @@ impl HardwareCommunicationManager for WebBluetoothCommunicationManager {
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 {
for vals in config_manager.base_communication_specifiers().iter() {
for config in vals.1.iter() {
if let ProtocolCommunicationSpecifier::BluetoothLE(btle) = &config {
for name in btle.names() {
let filter = web_sys::BluetoothLeScanFilterInit::new();

View File

@@ -5,9 +5,7 @@
"outDir": "dist",
"moduleResolution": "bundler",
"esModuleInterop": true,
"skipLibCheck": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true
"skipLibCheck": true
},
"include": ["src"]
}

View File

@@ -14,5 +14,8 @@ export default defineConfig({
minify: false, // for demo purposes
target: "esnext", // this is important as well
outDir: "dist",
rollupOptions: {
external: [/\.\/wasm\//, /\.\.\/wasm\//],
},
},
});

View File

@@ -5,7 +5,6 @@ import { Label } from "$lib/components/ui/label";
import { Card, CardContent, CardHeader } from "$lib/components/ui/card";
import type { BluetoothDevice } from "$lib/types";
import { _ } from "svelte-i18n";
import { ActuatorType } from "@sexy.pivoine.art/buttplug";
interface Props {
device: BluetoothDevice;
@@ -16,7 +15,7 @@ interface Props {
let { device, onChange, onStop }: Props = $props();
function getBatteryColor(level: number) {
if (!device.info.hasBattery) {
if (!device.hasBattery) {
return "text-gray-400";
}
if (level > 60) return "text-green-400";
@@ -25,7 +24,7 @@ function getBatteryColor(level: number) {
}
function getBatteryBgColor(level: number) {
if (!device.info.hasBattery) {
if (!device.hasBattery) {
return "bg-gray-400/20";
}
if (level > 60) return "bg-green-400/20";
@@ -34,17 +33,13 @@ function getBatteryBgColor(level: number) {
}
function getScalarAnimations() {
const cmds: [{ ActuatorType: typeof ActuatorType }] =
device.info.messageAttributes.ScalarCmd;
return cmds
.filter((_, i: number) => !!device.actuatorValues[i])
.map(({ ActuatorType }) => `animate-${ActuatorType.toLowerCase()}`);
return device.actuators
.filter((a) => a.value > 0)
.map((a) => `animate-${a.outputType.toLowerCase()}`);
}
function isActive() {
const cmds: [{ ActuatorType: typeof ActuatorType }] =
device.info.messageAttributes.ScalarCmd;
return cmds.some((_, i: number) => !!device.actuatorValues[i]);
return device.actuators.some((a) => a.value > 0);
}
</script>
@@ -119,7 +114,7 @@ function isActive() {
></span>
<span class="text-sm text-muted-foreground">{$_("device_card.battery")}</span>
</div>
{#if device.info.hasBattery}
{#if device.hasBattery}
<span class="text-sm font-medium {getBatteryColor(device.batteryLevel)}">
{device.batteryLevel}%
</span>
@@ -144,19 +139,19 @@ function isActive() {
</div> -->
<!-- Action Button -->
{#each device.info.messageAttributes.ScalarCmd as scalarCmd}
{#each device.actuators as actuator, idx}
<div class="space-y-2">
<Label for={`device-${device.info.index}-${scalarCmd.Index}`}
<Label for={`device-${device.info.index}-${actuator.featureIndex}-${actuator.outputType}`}
>{$_(
`device_card.actuator_types.${scalarCmd.ActuatorType.toLowerCase()}`,
`device_card.actuator_types.${actuator.outputType.toLowerCase()}`,
)}</Label
>
<Slider
id={`device-${device.info.index}-${scalarCmd.Index}`}
id={`device-${device.info.index}-${actuator.featureIndex}-${actuator.outputType}`}
type="single"
value={device.actuatorValues[scalarCmd.Index]}
onValueChange={(val) => onChange(scalarCmd.Index, val)}
max={scalarCmd.StepCount}
value={actuator.value}
onValueChange={(val) => onChange(idx, val)}
max={actuator.maxSteps}
step={1}
/>
</div>

View File

@@ -108,12 +108,20 @@ export interface Stats {
viewers_count: number;
}
export interface DeviceActuator {
featureIndex: number;
outputType: string;
maxSteps: number;
descriptor: string;
value: number;
}
export interface BluetoothDevice {
id: string;
name: string;
actuatorValues: number[];
sensorValues: number[];
actuators: DeviceActuator[];
batteryLevel: number;
hasBattery: boolean;
isConnected: boolean;
lastSeen: Date;
info: ButtplugClientDevice;

View File

@@ -3,18 +3,13 @@ import { _ } from "svelte-i18n";
import Meta from "$lib/components/meta/meta.svelte";
import {
ButtplugClient,
ButtplugMessage,
ButtplugWasmClientConnector,
DeviceList,
SensorReadCmd,
StopDeviceCmd,
SensorReading,
ScalarCmd,
ScalarSubcommand,
ButtplugDeviceMessage,
ButtplugClientDevice,
SensorType,
OutputType,
InputType,
DeviceOutputValueConstructor,
} from "@sexy.pivoine.art/buttplug";
import type { ButtplugMessage } from "@sexy.pivoine.art/buttplug";
import Button from "$lib/components/ui/button/button.svelte";
import { onMount } from "svelte";
import { goto } from "$app/navigation";
@@ -50,11 +45,12 @@ async function init() {
// await ButtplugWasmClientConnector.activateLogging("info");
await client.connect(connector);
client.on("deviceadded", onDeviceAdded);
client.on("deviceremoved", (msg: ButtplugDeviceMessage) =>
devices.splice(msg.DeviceIndex, 1),
);
client.on("deviceremoved", (dev: ButtplugClientDevice) => {
const idx = devices.findIndex((d) => d.info.index === dev.index);
if (idx !== -1) devices.splice(idx, 1);
});
client.on("scanningfinished", () => (scanning = false));
connector.on("message", handleMessages);
client.on("inputreading", handleInputReading);
connected = client.connected;
}
@@ -63,65 +59,48 @@ async function startScanning() {
scanning = true;
}
async function onDeviceAdded(
msg: ButtplugDeviceMessage,
dev: ButtplugClientDevice,
) {
async function onDeviceAdded(dev: ButtplugClientDevice) {
const device = convertDevice(dev);
devices.push(device);
const cmds = device.info.messageAttributes.SensorReadCmd;
cmds?.forEach(async (cmd) => {
await client.sendDeviceMessage(
{ index: device.info.index },
new SensorReadCmd(device.info.index, cmd.Index, cmd.SensorType),
);
});
}
async function handleMessages(messages: ButtplugMessage[]) {
messages.forEach(async (msg) => {
await handleMessage(msg);
});
}
async function handleMessage(msg: ButtplugMessage) {
if (msg instanceof SensorReading) {
const device = devices[msg.DeviceIndex];
if (msg.SensorType === SensorType.Battery) {
device.batteryLevel = msg.Data[0];
// Try to read battery level
if (device.hasBattery) {
try {
device.batteryLevel = await dev.battery();
} catch (e) {
console.warn(`Failed to read battery for ${dev.name}:`, e);
}
}
}
function handleInputReading(msg: ButtplugMessage) {
if (msg.InputReading === undefined) return;
const reading = msg.InputReading;
const device = devices.find((d) => d.info.index === reading.DeviceIndex);
if (!device) return;
if (reading.Reading[InputType.Battery] !== undefined) {
device.batteryLevel = reading.Reading[InputType.Battery].Value;
}
device.sensorValues[msg.Index] = msg.Data[0];
device.lastSeen = new Date();
} else if (msg instanceof DeviceList) {
devices = client.devices.map(convertDevice);
}
}
async function handleChange(
device: BluetoothDevice,
scalarIndex: number,
actuatorIdx: number,
value: number,
) {
const vibrateCmd = device.info.messageAttributes.ScalarCmd[scalarIndex];
await client.sendDeviceMessage(
{ index: device.info.index },
new ScalarCmd(
[
new ScalarSubcommand(
vibrateCmd.Index,
(device.actuatorValues[scalarIndex] = value),
vibrateCmd.ActuatorType,
),
],
device.info.index,
),
);
const actuator = device.actuators[actuatorIdx];
const feature = device.info.features.get(actuator.featureIndex);
if (!feature) return;
actuator.value = value;
const outputType = actuator.outputType as OutputType;
await feature.runOutput(new DeviceOutputValueConstructor(outputType).steps(value));
// Capture event if recording
if (isRecording && recordingStartTime) {
captureEvent(device, scalarIndex, value);
captureEvent(device, actuatorIdx, value);
}
}
@@ -145,44 +124,51 @@ function stopRecording() {
function captureEvent(
device: BluetoothDevice,
scalarIndex: number,
actuatorIdx: number,
value: number,
) {
if (!recordingStartTime) return;
const timestamp = performance.now() - recordingStartTime;
const scalarCmd = device.info.messageAttributes.ScalarCmd[scalarIndex];
const actuator = device.actuators[actuatorIdx];
recordedEvents.push({
timestamp,
deviceIndex: device.info.index,
deviceName: device.name,
actuatorIndex: scalarIndex,
actuatorType: scalarCmd.ActuatorType,
value: (value / scalarCmd.StepCount) * 100, // Normalize to 0-100
actuatorIndex: actuatorIdx,
actuatorType: actuator.outputType,
value: (value / actuator.maxSteps) * 100, // Normalize to 0-100
});
}
async function handleStop(device: BluetoothDevice) {
await client.sendDeviceMessage(
{ index: device.info.index },
new StopDeviceCmd(device.info.index),
);
device.actuatorValues = device.info.messageAttributes.ScalarCmd.map(() => 0);
await device.info.stop();
device.actuators.forEach((a) => (a.value = 0));
}
function convertDevice(device: ButtplugClientDevice): BluetoothDevice {
console.log(device);
const actuators: import("$lib/types").DeviceActuator[] = [];
for (const [, feature] of device.features) {
for (const outputType of feature.outputTypes) {
actuators.push({
featureIndex: feature.featureIndex,
outputType,
maxSteps: feature.outputMaxValue(outputType),
descriptor: feature.featureDescriptor,
value: 0,
});
}
}
return {
id: device.index as string,
name: device.name as string,
id: String(device.index),
name: device.name,
actuators,
batteryLevel: 0,
hasBattery: device.hasInput(InputType.Battery),
isConnected: true,
lastSeen: new Date(),
sensorValues: device.messageAttributes.SensorReadCmd
? device.messageAttributes.SensorReadCmd.map(() => 0)
: [],
actuatorValues: device.messageAttributes.ScalarCmd.map(() => 0),
info: device,
};
}
@@ -195,7 +181,7 @@ async function handleSaveRecording(data: {
const deviceInfo: DeviceInfo[] = devices.map((d) => ({
name: d.name,
index: d.info.index,
capabilities: d.info.messageAttributes.ScalarCmd.map((cmd) => cmd.ActuatorType),
capabilities: d.actuators.map((a) => a.outputType),
}));
try {
@@ -345,37 +331,26 @@ function executeEvent(event: RecordedEvent) {
}
// Find matching actuator by type
const scalarCmd = device.info.messageAttributes.ScalarCmd.find(
cmd => cmd.ActuatorType === event.actuatorType
const actuator = device.actuators.find(
(a) => a.outputType === event.actuatorType,
);
if (!scalarCmd) {
if (!actuator) {
console.warn(`Actuator type ${event.actuatorType} not found on ${device.name}`);
return;
}
// Convert normalized value (0-100) back to device scale
const deviceValue = (event.value / 100) * scalarCmd.StepCount;
const deviceValue = Math.round((event.value / 100) * actuator.maxSteps);
// Send command to device
client.sendDeviceMessage(
{ index: device.info.index },
new ScalarCmd(
[
new ScalarSubcommand(
scalarCmd.Index,
deviceValue,
scalarCmd.ActuatorType,
),
],
device.info.index,
),
);
// Send command to device via feature
const feature = device.info.features.get(actuator.featureIndex);
if (feature) {
const outputType = actuator.outputType as OutputType;
feature.runOutput(new DeviceOutputValueConstructor(outputType).steps(deviceValue));
}
// Update UI
const scalarIndex = device.info.messageAttributes.ScalarCmd.indexOf(scalarCmd);
if (scalarIndex !== -1) {
device.actuatorValues[scalarIndex] = deviceValue;
}
actuator.value = deviceValue;
}
function seek(percentage: number) {
@@ -618,7 +593,7 @@ onMount(() => {
deviceInfo={devices.map((d) => ({
name: d.name,
index: d.info.index,
capabilities: d.info.messageAttributes.ScalarCmd.map((cmd) => cmd.ActuatorType),
capabilities: d.actuators.map((a) => a.outputType),
}))}
duration={recordingDuration}
onSave={handleSaveRecording}

View File

@@ -19,8 +19,8 @@ let mappings = $state<Map<string, BluetoothDevice>>(new Map());
// Check if a connected device is compatible with a recorded device
function isCompatible(recordedDevice: DeviceInfo, connectedDevice: BluetoothDevice): boolean {
const connectedActuators = connectedDevice.info.messageAttributes.ScalarCmd.map(
cmd => cmd.ActuatorType
const connectedActuators = connectedDevice.actuators.map(
(a) => a.outputType,
);
// Check if all required actuator types from recording exist on connected device

View File

@@ -8,6 +8,14 @@ export default defineConfig({
resolve: {
alias: { $lib: path.resolve("./src/lib"), "@": path.resolve("./src/lib") },
},
ssr: {
noExternal: ["@sexy.pivoine.art/buttplug"],
},
build: {
rollupOptions: {
external: [/\/wasm\/index\.js/],
},
},
server: {
port: 3000,
proxy: {

16
pnpm-lock.yaml generated
View File

@@ -23,15 +23,9 @@ importers:
packages/buttplug:
dependencies:
class-transformer:
specifier: ^0.5.1
version: 0.5.1
eventemitter3:
specifier: ^5.0.1
version: 5.0.1
reflect-metadata:
specifier: ^0.2.2
version: 0.2.2
typescript:
specifier: ^5.9.2
version: 5.9.3
@@ -1650,9 +1644,6 @@ packages:
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
engines: {node: '>=10'}
class-transformer@0.5.1:
resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==}
cli-color@2.0.4:
resolution: {integrity: sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==}
engines: {node: '>=0.10'}
@@ -2857,9 +2848,6 @@ packages:
resolution: {integrity: sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==}
engines: {node: '>=6'}
reflect-metadata@0.2.2:
resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
relative-time-format@1.1.11:
resolution: {integrity: sha512-TH+oV/w77hjaB9xCzoFYJ/Icmr/12+02IAoCI/YGS2UBTbjCbBjHGEBxGnVy4EJvOR1qadGzyFRI6hGaJJG93Q==}
@@ -4710,8 +4698,6 @@ snapshots:
chownr@2.0.0: {}
class-transformer@0.5.1: {}
cli-color@2.0.4:
dependencies:
d: 1.0.2
@@ -5897,8 +5883,6 @@ snapshots:
reduce-flatten@2.0.0: {}
reflect-metadata@0.2.2: {}
relative-time-format@1.1.11: {}
resolve-from@4.0.0: {}