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
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:
@@ -17,8 +17,10 @@
|
||||
"packageManager": "pnpm@10.19.0",
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"es5-ext",
|
||||
"esbuild",
|
||||
"svelte-preprocess",
|
||||
"vue-demi"
|
||||
"wasm-pack"
|
||||
],
|
||||
"ignoredBuiltDependencies": [
|
||||
"@tailwindcss/oxide",
|
||||
|
||||
967
packages/buttplug/Cargo.lock
generated
967
packages/buttplug/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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"] }
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
242
packages/buttplug/src/client/ButtplugClient.ts
Normal file
242
packages/buttplug/src/client/ButtplugClient.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
111
packages/buttplug/src/client/ButtplugClientDeviceCommand.ts
Normal file
111
packages/buttplug/src/client/ButtplugClientDeviceCommand.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
168
packages/buttplug/src/client/ButtplugClientDeviceFeature.ts
Normal file
168
packages/buttplug/src/client/ButtplugClientDeviceFeature.ts
Normal 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?");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 =
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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>;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
// 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;
|
||||
}
|
||||
|
||||
public update() {
|
||||
this.ScalarCmd?.forEach((x, i) => (x.Index = i));
|
||||
this.RotateCmd?.forEach((x, i) => (x.Index = i));
|
||||
this.LinearCmd?.forEach((x, i) => (x.Index = i));
|
||||
this.SensorReadCmd?.forEach((x, i) => (x.Index = i));
|
||||
this.SensorSubscribeCmd?.forEach((x, i) => (x.Index = i));
|
||||
export 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 ActuatorType {
|
||||
Unknown = "Unknown",
|
||||
Vibrate = "Vibrate",
|
||||
Rotate = "Rotate",
|
||||
Oscillate = "Oscillate",
|
||||
Constrict = "Constrict",
|
||||
Inflate = "Inflate",
|
||||
Position = "Position",
|
||||
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 enum SensorType {
|
||||
Unknown = "Unknown",
|
||||
Battery = "Battery",
|
||||
RSSI = "RSSI",
|
||||
Button = "Button",
|
||||
Pressure = "Pressure",
|
||||
// Temperature,
|
||||
// Accelerometer,
|
||||
// Gyro,
|
||||
export interface Ok {
|
||||
Id: number | undefined;
|
||||
}
|
||||
|
||||
export class GenericDeviceMessageAttributes {
|
||||
public FeatureDescriptor: string;
|
||||
public ActuatorType: ActuatorType;
|
||||
public StepCount: number;
|
||||
public Index = 0;
|
||||
constructor(data: Partial<GenericDeviceMessageAttributes>) {
|
||||
Object.assign(this, data);
|
||||
}
|
||||
}
|
||||
|
||||
export class RawDeviceMessageAttributes {
|
||||
constructor(public Endpoints: Array<string>) {}
|
||||
}
|
||||
|
||||
export class SensorDeviceMessageAttributes {
|
||||
public FeatureDescriptor: string;
|
||||
public SensorType: SensorType;
|
||||
public StepRange: Array<number>;
|
||||
public Index = 0;
|
||||
constructor(data: Partial<GenericDeviceMessageAttributes>) {
|
||||
Object.assign(this, data);
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class ButtplugMessage {
|
||||
constructor(public Id: number) {}
|
||||
|
||||
// tslint:disable-next-line:ban-types
|
||||
public get Type(): Function {
|
||||
return this.constructor;
|
||||
}
|
||||
|
||||
public toJSON(): string {
|
||||
return JSON.stringify(this.toProtocolFormat());
|
||||
}
|
||||
|
||||
public toProtocolFormat(): object {
|
||||
const jsonObj = {};
|
||||
jsonObj[(this.constructor as unknown as { Name: string }).Name] =
|
||||
instanceToPlain(this);
|
||||
return jsonObj;
|
||||
}
|
||||
|
||||
public update() {}
|
||||
}
|
||||
|
||||
export abstract class ButtplugDeviceMessage extends ButtplugMessage {
|
||||
constructor(
|
||||
public DeviceIndex: number,
|
||||
public Id: number,
|
||||
) {
|
||||
super(Id);
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class ButtplugSystemMessage extends ButtplugMessage {
|
||||
constructor(public Id: number = SYSTEM_MESSAGE_ID) {
|
||||
super(Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class Ok extends ButtplugSystemMessage {
|
||||
static Name = "Ok";
|
||||
|
||||
constructor(public Id: number = DEFAULT_MESSAGE_ID) {
|
||||
super(Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class Ping extends ButtplugMessage {
|
||||
static Name = "Ping";
|
||||
|
||||
constructor(public Id: number = DEFAULT_MESSAGE_ID) {
|
||||
super(Id);
|
||||
}
|
||||
export 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);
|
||||
export interface Error {
|
||||
ErrorMessage: string;
|
||||
ErrorCode: ErrorClass;
|
||||
Id: number | undefined;
|
||||
}
|
||||
|
||||
get Schemversion() {
|
||||
return 0;
|
||||
}
|
||||
export interface RequestDeviceList {
|
||||
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 StartScanning {
|
||||
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;
|
||||
export interface StopScanning {
|
||||
Id: number | undefined;
|
||||
}
|
||||
|
||||
public update() {
|
||||
for (const device of this.Devices) {
|
||||
device.DeviceMessages.update();
|
||||
}
|
||||
}
|
||||
export interface StopAllDevices {
|
||||
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);
|
||||
export interface ScanningFinished {
|
||||
Id: number | undefined;
|
||||
}
|
||||
|
||||
public update() {
|
||||
this.DeviceMessages.update();
|
||||
}
|
||||
export interface RequestServerInfo {
|
||||
ClientName: string;
|
||||
ProtocolVersionMajor: number;
|
||||
ProtocolVersionMinor: number;
|
||||
Id: number | undefined;
|
||||
}
|
||||
|
||||
export class DeviceRemoved extends ButtplugSystemMessage {
|
||||
static Name = "DeviceRemoved";
|
||||
|
||||
constructor(public DeviceIndex: number) {
|
||||
super();
|
||||
}
|
||||
export interface ServerInfo {
|
||||
MaxPingTime: number;
|
||||
ServerName: string;
|
||||
ProtocolVersionMajor: number;
|
||||
ProtocolVersionMinor: number;
|
||||
Id: number | undefined;
|
||||
}
|
||||
|
||||
export class RequestDeviceList extends ButtplugMessage {
|
||||
static Name = "RequestDeviceList";
|
||||
|
||||
constructor(public Id: number = DEFAULT_MESSAGE_ID) {
|
||||
super(Id);
|
||||
}
|
||||
export interface DeviceFeature {
|
||||
FeatureDescriptor: string;
|
||||
Output: { [key: string]: DeviceFeatureOutput };
|
||||
Input: { [key: string]: DeviceFeatureInput };
|
||||
FeatureIndex: number;
|
||||
}
|
||||
|
||||
export class StartScanning extends ButtplugMessage {
|
||||
static Name = "StartScanning";
|
||||
|
||||
constructor(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 StopScanning extends ButtplugMessage {
|
||||
static Name = "StopScanning";
|
||||
|
||||
constructor(public Id: number = DEFAULT_MESSAGE_ID) {
|
||||
super(Id);
|
||||
}
|
||||
export interface DeviceList {
|
||||
Devices: { [key: number]: DeviceInfo };
|
||||
Id: number | undefined;
|
||||
}
|
||||
|
||||
export class ScanningFinished extends ButtplugSystemMessage {
|
||||
static Name = "ScanningFinished";
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
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 RequestServerInfo extends ButtplugMessage {
|
||||
static Name = "RequestServerInfo";
|
||||
|
||||
constructor(
|
||||
public ClientName: string,
|
||||
public MessageVersion: number = 0,
|
||||
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 ServerInfo extends ButtplugSystemMessage {
|
||||
static Name = "ServerInfo";
|
||||
|
||||
constructor(
|
||||
public MessageVersion: number,
|
||||
public MaxPingTime: number,
|
||||
public ServerName: string,
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
export enum InputCommandType {
|
||||
Read = 'Read',
|
||||
Subscribe = 'Subscribe',
|
||||
Unsubscribe = 'Unsubscribe',
|
||||
}
|
||||
|
||||
export class StopDeviceCmd extends ButtplugDeviceMessage {
|
||||
static Name = "StopDeviceCmd";
|
||||
|
||||
constructor(
|
||||
public DeviceIndex: number = -1,
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(DeviceIndex, Id);
|
||||
}
|
||||
export interface DeviceFeatureInput {
|
||||
Value: number[];
|
||||
Command: InputCommandType[];
|
||||
}
|
||||
|
||||
export class StopAllDevices extends ButtplugMessage {
|
||||
static Name = "StopAllDevices";
|
||||
|
||||
constructor(public Id: number = DEFAULT_MESSAGE_ID) {
|
||||
super(Id);
|
||||
}
|
||||
export interface DeviceFeatureOutput {
|
||||
Value: number;
|
||||
Duration?: number;
|
||||
}
|
||||
|
||||
export class GenericMessageSubcommand {
|
||||
protected constructor(public Index: number) {}
|
||||
export interface OutputCmd {
|
||||
DeviceIndex: number;
|
||||
FeatureIndex: number;
|
||||
Command: { [key: string]: DeviceFeatureOutput };
|
||||
Id: number | undefined;
|
||||
}
|
||||
|
||||
export class ScalarSubcommand extends GenericMessageSubcommand {
|
||||
constructor(
|
||||
Index: number,
|
||||
public Scalar: number,
|
||||
public ActuatorType: ActuatorType,
|
||||
) {
|
||||
super(Index);
|
||||
}
|
||||
// Device Input Commands
|
||||
|
||||
export interface InputCmd {
|
||||
DeviceIndex: number;
|
||||
FeatureIndex: number;
|
||||
Type: InputType;
|
||||
Command: InputCommandType;
|
||||
Id: number | undefined;
|
||||
}
|
||||
|
||||
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 InputValue {
|
||||
Value: number;
|
||||
}
|
||||
|
||||
export class RotateSubcommand extends GenericMessageSubcommand {
|
||||
constructor(
|
||||
Index: number,
|
||||
public Speed: number,
|
||||
public Clockwise: boolean,
|
||||
) {
|
||||
super(Index);
|
||||
}
|
||||
export interface InputReading {
|
||||
DeviceIndex: number;
|
||||
FeatureIndex: number;
|
||||
Reading: { [key: string]: InputValue };
|
||||
Id: number | undefined;
|
||||
}
|
||||
|
||||
export class RotateCmd extends ButtplugDeviceMessage {
|
||||
static Name = "RotateCmd";
|
||||
|
||||
public static Create(
|
||||
deviceIndex: number,
|
||||
commands: [number, boolean][],
|
||||
): RotateCmd {
|
||||
const cmdList: RotateSubcommand[] = new Array<RotateSubcommand>();
|
||||
|
||||
let i = 0;
|
||||
for (const [speed, clockwise] of commands) {
|
||||
cmdList.push(new RotateSubcommand(i, speed, clockwise));
|
||||
++i;
|
||||
}
|
||||
|
||||
return new RotateCmd(cmdList, deviceIndex);
|
||||
}
|
||||
constructor(
|
||||
public Rotations: RotateSubcommand[],
|
||||
public DeviceIndex: number = -1,
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(DeviceIndex, Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class VectorSubcommand extends GenericMessageSubcommand {
|
||||
constructor(
|
||||
Index: number,
|
||||
public Position: number,
|
||||
public Duration: number,
|
||||
) {
|
||||
super(Index);
|
||||
}
|
||||
}
|
||||
|
||||
export class LinearCmd extends ButtplugDeviceMessage {
|
||||
static Name = "LinearCmd";
|
||||
|
||||
public static Create(
|
||||
deviceIndex: number,
|
||||
commands: [number, number][],
|
||||
): LinearCmd {
|
||||
const cmdList: VectorSubcommand[] = new Array<VectorSubcommand>();
|
||||
|
||||
let i = 0;
|
||||
for (const cmd of commands) {
|
||||
cmdList.push(new VectorSubcommand(i, cmd[0], cmd[1]));
|
||||
++i;
|
||||
}
|
||||
|
||||
return new LinearCmd(cmdList, deviceIndex);
|
||||
}
|
||||
constructor(
|
||||
public Vectors: VectorSubcommand[],
|
||||
public DeviceIndex: number = -1,
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(DeviceIndex, Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class SensorReadCmd extends ButtplugDeviceMessage {
|
||||
static Name = "SensorReadCmd";
|
||||
|
||||
constructor(
|
||||
public DeviceIndex: number,
|
||||
public SensorIndex: number,
|
||||
public SensorType: SensorType,
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(DeviceIndex, Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class SensorReading extends ButtplugDeviceMessage {
|
||||
static Name = "SensorReading";
|
||||
|
||||
constructor(
|
||||
public DeviceIndex: number,
|
||||
public SensorIndex: number,
|
||||
public SensorType: SensorType,
|
||||
public Data: number[],
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(DeviceIndex, Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class RawReadCmd extends ButtplugDeviceMessage {
|
||||
static Name = "RawReadCmd";
|
||||
|
||||
constructor(
|
||||
public DeviceIndex: number,
|
||||
public Endpoint: string,
|
||||
public ExpectedLength: number,
|
||||
public Timeout: number,
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(DeviceIndex, Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class RawWriteCmd extends ButtplugDeviceMessage {
|
||||
static Name = "RawWriteCmd";
|
||||
|
||||
constructor(
|
||||
public DeviceIndex: number,
|
||||
public Endpoint: string,
|
||||
public Data: Uint8Array,
|
||||
public WriteWithResponse: boolean,
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(DeviceIndex, Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class RawSubscribeCmd extends ButtplugDeviceMessage {
|
||||
static Name = "RawSubscribeCmd";
|
||||
|
||||
constructor(
|
||||
public DeviceIndex: number,
|
||||
public Endpoint: string,
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(DeviceIndex, Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class RawUnsubscribeCmd extends ButtplugDeviceMessage {
|
||||
static Name = "RawUnsubscribeCmd";
|
||||
|
||||
constructor(
|
||||
public DeviceIndex: number,
|
||||
public Endpoint: string,
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(DeviceIndex, Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class RawReading extends ButtplugDeviceMessage {
|
||||
static Name = "RawReading";
|
||||
|
||||
constructor(
|
||||
public DeviceIndex: number,
|
||||
public Endpoint: string,
|
||||
public Data: number[],
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(DeviceIndex, Id);
|
||||
}
|
||||
export interface StopCmd {
|
||||
Id: number | undefined;
|
||||
DeviceIndex: number | undefined;
|
||||
FeatureIndex: number | undefined;
|
||||
Inputs: boolean | undefined;
|
||||
Outputs: boolean | undefined;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(),
|
||||
)))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -5,9 +5,7 @@
|
||||
"outDir": "dist",
|
||||
"moduleResolution": "bundler",
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
@@ -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\//],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
});
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMessages(messages: ButtplugMessage[]) {
|
||||
messages.forEach(async (msg) => {
|
||||
await handleMessage(msg);
|
||||
});
|
||||
}
|
||||
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;
|
||||
|
||||
async function handleMessage(msg: ButtplugMessage) {
|
||||
if (msg instanceof SensorReading) {
|
||||
const device = devices[msg.DeviceIndex];
|
||||
if (msg.SensorType === SensorType.Battery) {
|
||||
device.batteryLevel = msg.Data[0];
|
||||
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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
16
pnpm-lock.yaml
generated
@@ -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: {}
|
||||
|
||||
Reference in New Issue
Block a user