Compare commits
4 Commits
fed2dd65e5
...
e744d1e40f
| Author | SHA1 | Date | |
|---|---|---|---|
| e744d1e40f | |||
| 82be8b8859 | |||
| 27d86fff8b | |||
| 6ea4ed1933 |
@@ -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,20 +6,20 @@
|
||||
* @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
|
||||
implements IButtplugClientConnector
|
||||
extends ButtplugBrowserWebsocketConnector
|
||||
implements IButtplugClientConnector
|
||||
{
|
||||
public send = (msg: ButtplugMessage): void => {
|
||||
if (!this.Connected) {
|
||||
throw new Error("ButtplugClient not connected");
|
||||
}
|
||||
this.sendMessage(msg);
|
||||
};
|
||||
public send = (msg: ButtplugMessage): void => {
|
||||
if (!this.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.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,11 +6,11 @@
|
||||
* @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) {
|
||||
super(message, Messages.ErrorClass.ERROR_UNKNOWN);
|
||||
}
|
||||
public constructor(message: string) {
|
||||
super(message, Messages.ErrorClass.ERROR_UNKNOWN);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,396 +6,160 @@
|
||||
* @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";
|
||||
ButtplugDeviceError,
|
||||
ButtplugError,
|
||||
ButtplugMessageError,
|
||||
} 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 {
|
||||
/**
|
||||
* Return the name of the device.
|
||||
*/
|
||||
public get name(): string {
|
||||
return this._deviceInfo.DeviceName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the user set name of the device.
|
||||
*/
|
||||
public get displayName(): string | undefined {
|
||||
return this._deviceInfo.DeviceDisplayName;
|
||||
}
|
||||
private _features: Map<number, ButtplugClientDeviceFeature>;
|
||||
|
||||
/**
|
||||
* Return the index of the device.
|
||||
*/
|
||||
public get index(): number {
|
||||
return this._deviceInfo.DeviceIndex;
|
||||
}
|
||||
/**
|
||||
* Return the name of the device.
|
||||
*/
|
||||
public get name(): string {
|
||||
return this._deviceInfo.DeviceName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the index of the device.
|
||||
*/
|
||||
public get messageTimingGap(): number | undefined {
|
||||
return this._deviceInfo.DeviceMessageTimingGap;
|
||||
}
|
||||
/**
|
||||
* Return the user set name of the device.
|
||||
*/
|
||||
public get displayName(): string | undefined {
|
||||
return this._deviceInfo.DeviceDisplayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of message types the device accepts.
|
||||
*/
|
||||
public get messageAttributes(): Messages.MessageAttributes {
|
||||
return this._deviceInfo.DeviceMessages;
|
||||
}
|
||||
/**
|
||||
* Return the index of the device.
|
||||
*/
|
||||
public get index(): number {
|
||||
return this._deviceInfo.DeviceIndex;
|
||||
}
|
||||
|
||||
public static fromMsg(
|
||||
msg: Messages.DeviceInfo,
|
||||
sendClosure: (
|
||||
device: ButtplugClientDevice,
|
||||
msg: Messages.ButtplugDeviceMessage,
|
||||
) => Promise<Messages.ButtplugMessage>,
|
||||
): ButtplugClientDevice {
|
||||
return new ButtplugClientDevice(msg, sendClosure);
|
||||
}
|
||||
/**
|
||||
* Return the index of the device.
|
||||
*/
|
||||
public get messageTimingGap(): number | undefined {
|
||||
return this._deviceInfo.DeviceMessageTimingGap;
|
||||
}
|
||||
|
||||
// Map of messages and their attributes (feature count, etc...)
|
||||
private allowedMsgs: Map<string, Messages.MessageAttributes> = new Map<
|
||||
string,
|
||||
Messages.MessageAttributes
|
||||
>();
|
||||
public get features(): Map<number, ButtplugClientDeviceFeature> {
|
||||
return this._features;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param _index Index of the device, as created by the device manager.
|
||||
* @param _name Name of the device.
|
||||
* @param allowedMsgs Buttplug messages the device can receive.
|
||||
*/
|
||||
constructor(
|
||||
private _deviceInfo: Messages.DeviceInfo,
|
||||
private _sendClosure: (
|
||||
device: ButtplugClientDevice,
|
||||
msg: Messages.ButtplugDeviceMessage,
|
||||
) => Promise<Messages.ButtplugMessage>,
|
||||
) {
|
||||
super();
|
||||
_deviceInfo.DeviceMessages.update();
|
||||
}
|
||||
public static fromMsg(
|
||||
msg: Messages.DeviceInfo,
|
||||
sendClosure: (
|
||||
msg: Messages.ButtplugMessage
|
||||
) => Promise<Messages.ButtplugMessage>
|
||||
): ButtplugClientDevice {
|
||||
return new ButtplugClientDevice(msg, sendClosure);
|
||||
}
|
||||
|
||||
public async send(
|
||||
msg: Messages.ButtplugDeviceMessage,
|
||||
): Promise<Messages.ButtplugMessage> {
|
||||
// Assume we're getting the closure from ButtplugClient, which does all of
|
||||
// the index/existence/connection/message checks for us.
|
||||
return await this._sendClosure(this, msg);
|
||||
}
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
private constructor(
|
||||
private _deviceInfo: Messages.DeviceInfo,
|
||||
private _sendClosure: (
|
||||
msg: Messages.ButtplugMessage
|
||||
) => Promise<Messages.ButtplugMessage>
|
||||
) {
|
||||
super();
|
||||
this._features = new Map(Object.entries(_deviceInfo.DeviceFeatures).map(([index, v]) => [parseInt(index), new ButtplugClientDeviceFeature(_deviceInfo.DeviceIndex, _deviceInfo.DeviceName, v, _sendClosure)]));
|
||||
}
|
||||
|
||||
public async sendExpectOk(
|
||||
msg: Messages.ButtplugDeviceMessage,
|
||||
): Promise<void> {
|
||||
const response = await this.send(msg);
|
||||
switch (getMessageClassFromMessage(response)) {
|
||||
case Messages.Ok:
|
||||
return;
|
||||
case Messages.Error:
|
||||
throw ButtplugError.FromError(response as Messages.Error);
|
||||
default:
|
||||
throw new ButtplugMessageError(
|
||||
`Message type ${response.constructor} not handled by SendMsgExpectOk`,
|
||||
);
|
||||
}
|
||||
}
|
||||
public async send(
|
||||
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(msg);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
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 ButtplugError.LogAndError(
|
||||
ButtplugMessageError,
|
||||
this._logger,
|
||||
`Message ${response} not handled by SendMsgExpectOk`
|
||||
);
|
||||
*/
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
public get vibrateAttributes(): Messages.GenericDeviceMessageAttributes[] {
|
||||
return (
|
||||
this.messageAttributes.ScalarCmd?.filter(
|
||||
(x) => x.ActuatorType === Messages.ActuatorType.Vibrate,
|
||||
) ?? []
|
||||
);
|
||||
}
|
||||
public hasOutput(type: Messages.OutputType): boolean {
|
||||
return this._features.values().filter((f) => f.hasOutput(type)).toArray().length > 0;
|
||||
}
|
||||
|
||||
public async vibrate(speed: number | number[]): Promise<void> {
|
||||
await this.scalarCommandBuilder(speed, Messages.ActuatorType.Vibrate);
|
||||
}
|
||||
public hasInput(type: Messages.InputType): boolean {
|
||||
return this._features.values().filter((f) => f.hasInput(type)).toArray().length > 0;
|
||||
}
|
||||
|
||||
public get oscillateAttributes(): Messages.GenericDeviceMessageAttributes[] {
|
||||
return (
|
||||
this.messageAttributes.ScalarCmd?.filter(
|
||||
(x) => x.ActuatorType === Messages.ActuatorType.Oscillate,
|
||||
) ?? []
|
||||
);
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
if (p.length == 0) {
|
||||
return Promise.reject(`No features with output type ${cmd.outputType}`);
|
||||
}
|
||||
await Promise.all(p);
|
||||
}
|
||||
|
||||
public async oscillate(speed: number | number[]): Promise<void> {
|
||||
await this.scalarCommandBuilder(speed, Messages.ActuatorType.Oscillate);
|
||||
}
|
||||
public async stop(): Promise<void> {
|
||||
await this.sendMsgExpectOk({StopCmd: { Id: 1, DeviceIndex: this.index, FeatureIndex: undefined, Inputs: true, Outputs: true}});
|
||||
}
|
||||
|
||||
public get rotateAttributes(): Messages.GenericDeviceMessageAttributes[] {
|
||||
return this.messageAttributes.RotateCmd ?? [];
|
||||
}
|
||||
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 async rotate(
|
||||
values: number | [number, boolean][],
|
||||
clockwise?: boolean,
|
||||
): Promise<void> {
|
||||
const rotateAttrs = this.messageAttributes.RotateCmd;
|
||||
if (!rotateAttrs || rotateAttrs.length === 0) {
|
||||
throw new ButtplugDeviceError(
|
||||
`Device ${this.name} has no Rotate capabilities`,
|
||||
);
|
||||
}
|
||||
let msg: Messages.RotateCmd;
|
||||
if (typeof values === "number") {
|
||||
msg = Messages.RotateCmd.Create(
|
||||
this.index,
|
||||
new Array(rotateAttrs.length).fill([values, clockwise]),
|
||||
);
|
||||
} else if (Array.isArray(values)) {
|
||||
msg = Messages.RotateCmd.Create(this.index, values);
|
||||
} else {
|
||||
throw new ButtplugDeviceError(
|
||||
"SendRotateCmd can only take a number and boolean, or an array of number/boolean tuples",
|
||||
);
|
||||
}
|
||||
await this.sendExpectOk(msg);
|
||||
}
|
||||
|
||||
public get linearAttributes(): Messages.GenericDeviceMessageAttributes[] {
|
||||
return this.messageAttributes.LinearCmd ?? [];
|
||||
}
|
||||
|
||||
public async linear(
|
||||
values: number | [number, number][],
|
||||
duration?: number,
|
||||
): Promise<void> {
|
||||
const linearAttrs = this.messageAttributes.LinearCmd;
|
||||
if (!linearAttrs || linearAttrs.length === 0) {
|
||||
throw new ButtplugDeviceError(
|
||||
`Device ${this.name} has no Linear capabilities`,
|
||||
);
|
||||
}
|
||||
let msg: Messages.LinearCmd;
|
||||
if (typeof values === "number") {
|
||||
msg = Messages.LinearCmd.Create(
|
||||
this.index,
|
||||
new Array(linearAttrs.length).fill([values, duration]),
|
||||
);
|
||||
} else if (Array.isArray(values)) {
|
||||
msg = Messages.LinearCmd.Create(this.index, values);
|
||||
} else {
|
||||
throw new ButtplugDeviceError(
|
||||
"SendLinearCmd can only take a number and number, or an array of number/number tuples",
|
||||
);
|
||||
}
|
||||
await this.sendExpectOk(msg);
|
||||
}
|
||||
|
||||
public async sensorRead(
|
||||
sensorIndex: number,
|
||||
sensorType: Messages.SensorType,
|
||||
): Promise<number[]> {
|
||||
const response = await this.send(
|
||||
new Messages.SensorReadCmd(this.index, sensorIndex, sensorType),
|
||||
);
|
||||
switch (getMessageClassFromMessage(response)) {
|
||||
case Messages.SensorReading:
|
||||
return (response as Messages.SensorReading).Data;
|
||||
case Messages.Error:
|
||||
throw ButtplugError.FromError(response as Messages.Error);
|
||||
default:
|
||||
throw new ButtplugMessageError(
|
||||
`Message type ${response.constructor} not handled by sensorRead`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public get hasBattery(): boolean {
|
||||
const batteryAttrs = this.messageAttributes.SensorReadCmd?.filter(
|
||||
(x) => x.SensorType === Messages.SensorType.Battery,
|
||||
);
|
||||
return batteryAttrs !== undefined && batteryAttrs.length > 0;
|
||||
}
|
||||
|
||||
public async battery(): Promise<number> {
|
||||
if (!this.hasBattery) {
|
||||
throw new ButtplugDeviceError(
|
||||
`Device ${this.name} has no Battery capabilities`,
|
||||
);
|
||||
}
|
||||
const batteryAttrs = this.messageAttributes.SensorReadCmd?.filter(
|
||||
(x) => x.SensorType === Messages.SensorType.Battery,
|
||||
);
|
||||
// Find the battery sensor, we'll need its index.
|
||||
const result = await this.sensorRead(
|
||||
batteryAttrs![0].Index,
|
||||
Messages.SensorType.Battery,
|
||||
);
|
||||
return result[0] / 100.0;
|
||||
}
|
||||
|
||||
public get hasRssi(): boolean {
|
||||
const rssiAttrs = this.messageAttributes.SensorReadCmd?.filter(
|
||||
(x) => x.SensorType === Messages.SensorType.RSSI,
|
||||
);
|
||||
return rssiAttrs !== undefined && rssiAttrs.length === 0;
|
||||
}
|
||||
|
||||
public async rssi(): Promise<number> {
|
||||
if (!this.hasRssi) {
|
||||
throw new ButtplugDeviceError(
|
||||
`Device ${this.name} has no RSSI capabilities`,
|
||||
);
|
||||
}
|
||||
const rssiAttrs = this.messageAttributes.SensorReadCmd?.filter(
|
||||
(x) => x.SensorType === Messages.SensorType.RSSI,
|
||||
);
|
||||
// Find the battery sensor, we'll need its index.
|
||||
const result = await this.sensorRead(
|
||||
rssiAttrs![0].Index,
|
||||
Messages.SensorType.RSSI,
|
||||
);
|
||||
return result[0];
|
||||
}
|
||||
|
||||
public async rawRead(
|
||||
endpoint: string,
|
||||
expectedLength: number,
|
||||
timeout: number,
|
||||
): Promise<Uint8Array> {
|
||||
if (!this.messageAttributes.RawReadCmd) {
|
||||
throw new ButtplugDeviceError(
|
||||
`Device ${this.name} has no raw read capabilities`,
|
||||
);
|
||||
}
|
||||
if (this.messageAttributes.RawReadCmd.Endpoints.indexOf(endpoint) === -1) {
|
||||
throw new ButtplugDeviceError(
|
||||
`Device ${this.name} has no raw readable endpoint ${endpoint}`,
|
||||
);
|
||||
}
|
||||
const response = await this.send(
|
||||
new Messages.RawReadCmd(this.index, endpoint, expectedLength, timeout),
|
||||
);
|
||||
switch (getMessageClassFromMessage(response)) {
|
||||
case Messages.RawReading:
|
||||
return new Uint8Array((response as Messages.RawReading).Data);
|
||||
case Messages.Error:
|
||||
throw ButtplugError.FromError(response as Messages.Error);
|
||||
default:
|
||||
throw new ButtplugMessageError(
|
||||
`Message type ${response.constructor} not handled by rawRead`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async rawWrite(
|
||||
endpoint: string,
|
||||
data: Uint8Array,
|
||||
writeWithResponse: boolean,
|
||||
): Promise<void> {
|
||||
if (!this.messageAttributes.RawWriteCmd) {
|
||||
throw new ButtplugDeviceError(
|
||||
`Device ${this.name} has no raw write capabilities`,
|
||||
);
|
||||
}
|
||||
if (this.messageAttributes.RawWriteCmd.Endpoints.indexOf(endpoint) === -1) {
|
||||
throw new ButtplugDeviceError(
|
||||
`Device ${this.name} has no raw writable endpoint ${endpoint}`,
|
||||
);
|
||||
}
|
||||
await this.sendExpectOk(
|
||||
new Messages.RawWriteCmd(this.index, endpoint, data, writeWithResponse),
|
||||
);
|
||||
}
|
||||
|
||||
public async rawSubscribe(endpoint: string): Promise<void> {
|
||||
if (!this.messageAttributes.RawSubscribeCmd) {
|
||||
throw new ButtplugDeviceError(
|
||||
`Device ${this.name} has no raw subscribe capabilities`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
this.messageAttributes.RawSubscribeCmd.Endpoints.indexOf(endpoint) === -1
|
||||
) {
|
||||
throw new ButtplugDeviceError(
|
||||
`Device ${this.name} has no raw subscribable endpoint ${endpoint}`,
|
||||
);
|
||||
}
|
||||
await this.sendExpectOk(new Messages.RawSubscribeCmd(this.index, endpoint));
|
||||
}
|
||||
|
||||
public async rawUnsubscribe(endpoint: string): Promise<void> {
|
||||
// This reuses raw subscribe's info.
|
||||
if (!this.messageAttributes.RawSubscribeCmd) {
|
||||
throw new ButtplugDeviceError(
|
||||
`Device ${this.name} has no raw unsubscribe capabilities`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
this.messageAttributes.RawSubscribeCmd.Endpoints.indexOf(endpoint) === -1
|
||||
) {
|
||||
throw new ButtplugDeviceError(
|
||||
`Device ${this.name} has no raw unsubscribable endpoint ${endpoint}`,
|
||||
);
|
||||
}
|
||||
await this.sendExpectOk(
|
||||
new Messages.RawUnsubscribeCmd(this.index, endpoint),
|
||||
);
|
||||
}
|
||||
|
||||
public async stop(): Promise<void> {
|
||||
await this.sendExpectOk(new Messages.StopDeviceCmd(this.index));
|
||||
}
|
||||
|
||||
public emitDisconnected() {
|
||||
this.emit("deviceremoved");
|
||||
}
|
||||
public emitDisconnected() {
|
||||
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.FeatureDescription;
|
||||
}
|
||||
|
||||
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,12 +6,12 @@
|
||||
* @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 =
|
||||
NodeWebSocket as unknown as typeof WebSocket;
|
||||
protected _websocketConstructor =
|
||||
NodeWebSocket as unknown as typeof WebSocket;
|
||||
}
|
||||
|
||||
@@ -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,13 +6,13 @@
|
||||
* @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>;
|
||||
disconnect: () => Promise<void>;
|
||||
initialize: () => Promise<void>;
|
||||
send: (msg: ButtplugMessage) => void;
|
||||
readonly Connected: boolean;
|
||||
connect: () => Promise<void>;
|
||||
disconnect: () => Promise<void>;
|
||||
initialize: () => Promise<void>;
|
||||
send: (msg: ButtplugMessage) => void;
|
||||
readonly Connected: boolean;
|
||||
}
|
||||
|
||||
@@ -6,96 +6,102 @@
|
||||
* @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 {
|
||||
return this.errorClass;
|
||||
}
|
||||
public get ErrorClass(): Messages.ErrorClass {
|
||||
return this.errorClass;
|
||||
}
|
||||
|
||||
public get InnerError(): Error | undefined {
|
||||
return this.innerError;
|
||||
}
|
||||
public get InnerError(): Error | undefined {
|
||||
return this.innerError;
|
||||
}
|
||||
|
||||
public get Id(): number | undefined {
|
||||
return this.messageId;
|
||||
}
|
||||
public get Id(): number | undefined {
|
||||
return this.messageId;
|
||||
}
|
||||
|
||||
public get ErrorMessage(): Messages.ButtplugMessage {
|
||||
return new Messages.Error(this.message, this.ErrorClass, this.Id);
|
||||
}
|
||||
public get ErrorMessage(): Messages.ButtplugMessage {
|
||||
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,
|
||||
): T {
|
||||
logger.Error(message);
|
||||
return new constructor(message, id);
|
||||
}
|
||||
public static LogAndError<T extends ButtplugError>(
|
||||
constructor: new (str: string, num: number) => T,
|
||||
logger: ButtplugLogger,
|
||||
message: string,
|
||||
id: number = Messages.SYSTEM_MESSAGE_ID
|
||||
): T {
|
||||
logger.Error(message);
|
||||
return new constructor(message, id);
|
||||
}
|
||||
|
||||
public static FromError(error: Messages.Error) {
|
||||
switch (error.ErrorCode) {
|
||||
case Messages.ErrorClass.ERROR_DEVICE:
|
||||
return new ButtplugDeviceError(error.ErrorMessage, error.Id);
|
||||
case Messages.ErrorClass.ERROR_INIT:
|
||||
return new ButtplugInitError(error.ErrorMessage, error.Id);
|
||||
case Messages.ErrorClass.ERROR_UNKNOWN:
|
||||
return new ButtplugUnknownError(error.ErrorMessage, error.Id);
|
||||
case Messages.ErrorClass.ERROR_PING:
|
||||
return new ButtplugPingError(error.ErrorMessage, error.Id);
|
||||
case Messages.ErrorClass.ERROR_MSG:
|
||||
return new ButtplugMessageError(error.ErrorMessage, error.Id);
|
||||
default:
|
||||
throw new Error(`Message type ${error.ErrorCode} not handled`);
|
||||
}
|
||||
}
|
||||
public static FromError(error: Messages.Error) {
|
||||
switch (error.ErrorCode) {
|
||||
case Messages.ErrorClass.ERROR_DEVICE:
|
||||
return new ButtplugDeviceError(error.ErrorMessage, error.Id);
|
||||
case Messages.ErrorClass.ERROR_INIT:
|
||||
return new ButtplugInitError(error.ErrorMessage, error.Id);
|
||||
case Messages.ErrorClass.ERROR_UNKNOWN:
|
||||
return new ButtplugUnknownError(error.ErrorMessage, error.Id);
|
||||
case Messages.ErrorClass.ERROR_PING:
|
||||
return new ButtplugPingError(error.ErrorMessage, error.Id);
|
||||
case Messages.ErrorClass.ERROR_MSG:
|
||||
return new ButtplugMessageError(error.ErrorMessage, error.Id);
|
||||
default:
|
||||
throw new Error(`Message type ${error.ErrorCode} not handled`);
|
||||
}
|
||||
}
|
||||
|
||||
public errorClass: Messages.ErrorClass = Messages.ErrorClass.ERROR_UNKNOWN;
|
||||
public innerError: Error | undefined;
|
||||
public messageId: number | undefined;
|
||||
public errorClass: Messages.ErrorClass = Messages.ErrorClass.ERROR_UNKNOWN;
|
||||
public innerError: Error | undefined;
|
||||
public messageId: number | undefined;
|
||||
|
||||
protected constructor(
|
||||
message: string,
|
||||
errorClass: Messages.ErrorClass,
|
||||
id: number = Messages.SYSTEM_MESSAGE_ID,
|
||||
inner?: Error,
|
||||
) {
|
||||
super(message);
|
||||
this.errorClass = errorClass;
|
||||
this.innerError = inner;
|
||||
this.messageId = id;
|
||||
}
|
||||
protected constructor(
|
||||
message: string,
|
||||
errorClass: Messages.ErrorClass,
|
||||
id: number = Messages.SYSTEM_MESSAGE_ID,
|
||||
inner?: Error
|
||||
) {
|
||||
super(message);
|
||||
this.errorClass = errorClass;
|
||||
this.innerError = inner;
|
||||
this.messageId = id;
|
||||
}
|
||||
}
|
||||
|
||||
export class ButtplugInitError extends ButtplugError {
|
||||
public constructor(message: string, id: number = Messages.SYSTEM_MESSAGE_ID) {
|
||||
super(message, Messages.ErrorClass.ERROR_INIT, id);
|
||||
}
|
||||
public constructor(message: string, id: number = Messages.SYSTEM_MESSAGE_ID) {
|
||||
super(message, Messages.ErrorClass.ERROR_INIT, id);
|
||||
}
|
||||
}
|
||||
|
||||
export class ButtplugDeviceError extends ButtplugError {
|
||||
public constructor(message: string, id: number = Messages.SYSTEM_MESSAGE_ID) {
|
||||
super(message, Messages.ErrorClass.ERROR_DEVICE, id);
|
||||
}
|
||||
public constructor(message: string, id: number = Messages.SYSTEM_MESSAGE_ID) {
|
||||
super(message, Messages.ErrorClass.ERROR_DEVICE, id);
|
||||
}
|
||||
}
|
||||
|
||||
export class ButtplugMessageError extends ButtplugError {
|
||||
public constructor(message: string, id: number = Messages.SYSTEM_MESSAGE_ID) {
|
||||
super(message, Messages.ErrorClass.ERROR_MSG, id);
|
||||
}
|
||||
public constructor(message: string, id: number = Messages.SYSTEM_MESSAGE_ID) {
|
||||
super(message, Messages.ErrorClass.ERROR_MSG, id);
|
||||
}
|
||||
}
|
||||
|
||||
export class ButtplugPingError extends ButtplugError {
|
||||
public constructor(message: string, id: number = Messages.SYSTEM_MESSAGE_ID) {
|
||||
super(message, Messages.ErrorClass.ERROR_PING, id);
|
||||
}
|
||||
public constructor(message: string, id: number = Messages.SYSTEM_MESSAGE_ID) {
|
||||
super(message, Messages.ErrorClass.ERROR_PING, id);
|
||||
}
|
||||
}
|
||||
|
||||
export class ButtplugUnknownError extends ButtplugError {
|
||||
public constructor(message: string, id: number = Messages.SYSTEM_MESSAGE_ID) {
|
||||
super(message, Messages.ErrorClass.ERROR_UNKNOWN, id);
|
||||
}
|
||||
public constructor(message: string, id: number = Messages.SYSTEM_MESSAGE_ID) {
|
||||
super(message, Messages.ErrorClass.ERROR_UNKNOWN, id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,71 +6,73 @@
|
||||
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "eventemitter3";
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
|
||||
export enum ButtplugLogLevel {
|
||||
Off,
|
||||
Error,
|
||||
Warn,
|
||||
Info,
|
||||
Debug,
|
||||
Trace,
|
||||
Off,
|
||||
Error,
|
||||
Warn,
|
||||
Info,
|
||||
Debug,
|
||||
Trace,
|
||||
}
|
||||
|
||||
/**
|
||||
* Representation of log messages for the internal logging utility.
|
||||
*/
|
||||
export class LogMessage {
|
||||
/** Timestamp for the log message */
|
||||
private timestamp: string;
|
||||
/** Timestamp for the log message */
|
||||
private timestamp: string;
|
||||
|
||||
/** Log Message */
|
||||
private logMessage: string;
|
||||
/** Log Message */
|
||||
private logMessage: string;
|
||||
|
||||
/** Log Level */
|
||||
private logLevel: ButtplugLogLevel;
|
||||
/** Log Level */
|
||||
private logLevel: ButtplugLogLevel;
|
||||
|
||||
/**
|
||||
* @param logMessage Log message.
|
||||
* @param logLevel: Log severity level.
|
||||
*/
|
||||
public constructor(logMessage: string, logLevel: ButtplugLogLevel) {
|
||||
const a = new Date();
|
||||
const hour = a.getHours();
|
||||
const min = a.getMinutes();
|
||||
const sec = a.getSeconds();
|
||||
this.timestamp = `${hour}:${min}:${sec}`;
|
||||
this.logMessage = logMessage;
|
||||
this.logLevel = logLevel;
|
||||
}
|
||||
/**
|
||||
* @param logMessage Log message.
|
||||
* @param logLevel: Log severity level.
|
||||
*/
|
||||
public constructor(logMessage: string, logLevel: ButtplugLogLevel) {
|
||||
const a = new Date();
|
||||
const hour = a.getHours();
|
||||
const min = a.getMinutes();
|
||||
const sec = a.getSeconds();
|
||||
this.timestamp = `${hour}:${min}:${sec}`;
|
||||
this.logMessage = logMessage;
|
||||
this.logLevel = logLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the log message.
|
||||
*/
|
||||
public get Message() {
|
||||
return this.logMessage;
|
||||
}
|
||||
/**
|
||||
* Returns the log message.
|
||||
*/
|
||||
public get Message() {
|
||||
return this.logMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the log message level.
|
||||
*/
|
||||
public get LogLevel() {
|
||||
return this.logLevel;
|
||||
}
|
||||
/**
|
||||
* Returns the log message level.
|
||||
*/
|
||||
public get LogLevel() {
|
||||
return this.logLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the log message timestamp.
|
||||
*/
|
||||
public get Timestamp() {
|
||||
return this.timestamp;
|
||||
}
|
||||
/**
|
||||
* Returns the log message timestamp.
|
||||
*/
|
||||
public get Timestamp() {
|
||||
return this.timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a formatted string with timestamp, level, and message.
|
||||
*/
|
||||
public get FormattedMessage() {
|
||||
return `${ButtplugLogLevel[this.logLevel]} : ${this.timestamp} : ${this.logMessage}`;
|
||||
}
|
||||
/**
|
||||
* Returns a formatted string with timestamp, level, and message.
|
||||
*/
|
||||
public get FormattedMessage() {
|
||||
return `${ButtplugLogLevel[this.logLevel]} : ${this.timestamp} : ${
|
||||
this.logMessage
|
||||
}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,117 +81,117 @@ export class LogMessage {
|
||||
* basically), and allows message logging throughout the module.
|
||||
*/
|
||||
export class ButtplugLogger extends EventEmitter {
|
||||
/** Singleton instance for the logger */
|
||||
protected static sLogger: ButtplugLogger | undefined = undefined;
|
||||
/** Sets maximum log level to log to console */
|
||||
protected maximumConsoleLogLevel: ButtplugLogLevel = ButtplugLogLevel.Off;
|
||||
/** Sets maximum log level for all log messages */
|
||||
protected maximumEventLogLevel: ButtplugLogLevel = ButtplugLogLevel.Off;
|
||||
/** Singleton instance for the logger */
|
||||
protected static sLogger: ButtplugLogger | undefined = undefined;
|
||||
/** Sets maximum log level to log to console */
|
||||
protected maximumConsoleLogLevel: ButtplugLogLevel = ButtplugLogLevel.Off;
|
||||
/** Sets maximum log level for all log messages */
|
||||
protected maximumEventLogLevel: ButtplugLogLevel = ButtplugLogLevel.Off;
|
||||
|
||||
/**
|
||||
* Returns the stored static instance of the logger, creating one if it
|
||||
* doesn't currently exist.
|
||||
*/
|
||||
public static get Logger(): ButtplugLogger {
|
||||
if (ButtplugLogger.sLogger === undefined) {
|
||||
ButtplugLogger.sLogger = new ButtplugLogger();
|
||||
}
|
||||
return this.sLogger!;
|
||||
}
|
||||
/**
|
||||
* Returns the stored static instance of the logger, creating one if it
|
||||
* doesn't currently exist.
|
||||
*/
|
||||
public static get Logger(): ButtplugLogger {
|
||||
if (ButtplugLogger.sLogger === undefined) {
|
||||
ButtplugLogger.sLogger = new ButtplugLogger();
|
||||
}
|
||||
return this.sLogger!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor. Can only be called internally since we regulate ButtplugLogger
|
||||
* ownership.
|
||||
*/
|
||||
protected constructor() {
|
||||
super();
|
||||
}
|
||||
/**
|
||||
* Constructor. Can only be called internally since we regulate ButtplugLogger
|
||||
* ownership.
|
||||
*/
|
||||
protected constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the maximum log level to output to console.
|
||||
*/
|
||||
public get MaximumConsoleLogLevel() {
|
||||
return this.maximumConsoleLogLevel;
|
||||
}
|
||||
/**
|
||||
* Set the maximum log level to output to console.
|
||||
*/
|
||||
public get MaximumConsoleLogLevel() {
|
||||
return this.maximumConsoleLogLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the maximum log level to output to console.
|
||||
*/
|
||||
public set MaximumConsoleLogLevel(buttplugLogLevel: ButtplugLogLevel) {
|
||||
this.maximumConsoleLogLevel = buttplugLogLevel;
|
||||
}
|
||||
/**
|
||||
* Get the maximum log level to output to console.
|
||||
*/
|
||||
public set MaximumConsoleLogLevel(buttplugLogLevel: ButtplugLogLevel) {
|
||||
this.maximumConsoleLogLevel = buttplugLogLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the global maximum log level
|
||||
*/
|
||||
public get MaximumEventLogLevel() {
|
||||
return this.maximumEventLogLevel;
|
||||
}
|
||||
/**
|
||||
* Set the global maximum log level
|
||||
*/
|
||||
public get MaximumEventLogLevel() {
|
||||
return this.maximumEventLogLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the global maximum log level
|
||||
*/
|
||||
public set MaximumEventLogLevel(logLevel: ButtplugLogLevel) {
|
||||
this.maximumEventLogLevel = logLevel;
|
||||
}
|
||||
/**
|
||||
* Get the global maximum log level
|
||||
*/
|
||||
public set MaximumEventLogLevel(logLevel: ButtplugLogLevel) {
|
||||
this.maximumEventLogLevel = logLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log new message at Error level.
|
||||
*/
|
||||
public Error(msg: string) {
|
||||
this.AddLogMessage(msg, ButtplugLogLevel.Error);
|
||||
}
|
||||
/**
|
||||
* Log new message at Error level.
|
||||
*/
|
||||
public Error(msg: string) {
|
||||
this.AddLogMessage(msg, ButtplugLogLevel.Error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log new message at Warn level.
|
||||
*/
|
||||
public Warn(msg: string) {
|
||||
this.AddLogMessage(msg, ButtplugLogLevel.Warn);
|
||||
}
|
||||
/**
|
||||
* Log new message at Warn level.
|
||||
*/
|
||||
public Warn(msg: string) {
|
||||
this.AddLogMessage(msg, ButtplugLogLevel.Warn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log new message at Info level.
|
||||
*/
|
||||
public Info(msg: string) {
|
||||
this.AddLogMessage(msg, ButtplugLogLevel.Info);
|
||||
}
|
||||
/**
|
||||
* Log new message at Info level.
|
||||
*/
|
||||
public Info(msg: string) {
|
||||
this.AddLogMessage(msg, ButtplugLogLevel.Info);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log new message at Debug level.
|
||||
*/
|
||||
public Debug(msg: string) {
|
||||
this.AddLogMessage(msg, ButtplugLogLevel.Debug);
|
||||
}
|
||||
/**
|
||||
* Log new message at Debug level.
|
||||
*/
|
||||
public Debug(msg: string) {
|
||||
this.AddLogMessage(msg, ButtplugLogLevel.Debug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log new message at Trace level.
|
||||
*/
|
||||
public Trace(msg: string) {
|
||||
this.AddLogMessage(msg, ButtplugLogLevel.Trace);
|
||||
}
|
||||
/**
|
||||
* Log new message at Trace level.
|
||||
*/
|
||||
public Trace(msg: string) {
|
||||
this.AddLogMessage(msg, ButtplugLogLevel.Trace);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if message should be logged, and if so, adds message to the
|
||||
* log buffer. May also print message and emit event.
|
||||
*/
|
||||
protected AddLogMessage(msg: string, level: ButtplugLogLevel) {
|
||||
// If nothing wants the log message we have, ignore it.
|
||||
if (
|
||||
level > this.maximumEventLogLevel &&
|
||||
level > this.maximumConsoleLogLevel
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const logMsg = new LogMessage(msg, level);
|
||||
// Clients and console logging may have different needs. For instance, it
|
||||
// could be that the client requests trace level, while all we want in the
|
||||
// console is info level. This makes sure the client can't also spam the
|
||||
// console.
|
||||
if (level <= this.maximumConsoleLogLevel) {
|
||||
console.log(logMsg.FormattedMessage);
|
||||
}
|
||||
if (level <= this.maximumEventLogLevel) {
|
||||
this.emit("log", logMsg);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Checks to see if message should be logged, and if so, adds message to the
|
||||
* log buffer. May also print message and emit event.
|
||||
*/
|
||||
protected AddLogMessage(msg: string, level: ButtplugLogLevel) {
|
||||
// If nothing wants the log message we have, ignore it.
|
||||
if (
|
||||
level > this.maximumEventLogLevel &&
|
||||
level > this.maximumConsoleLogLevel
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const logMsg = new LogMessage(msg, level);
|
||||
// Clients and console logging may have different needs. For instance, it
|
||||
// could be that the client requests trace level, while all we want in the
|
||||
// console is info level. This makes sure the client can't also spam the
|
||||
// console.
|
||||
if (level <= this.maximumConsoleLogLevel) {
|
||||
console.log(logMsg.FormattedMessage);
|
||||
}
|
||||
if (level <= this.maximumEventLogLevel) {
|
||||
this.emit('log', logMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,485 +7,203 @@
|
||||
*/
|
||||
|
||||
// tslint:disable:max-classes-per-file
|
||||
"use strict";
|
||||
'use strict';
|
||||
|
||||
import { instanceToPlain, Type } from "class-transformer";
|
||||
import "reflect-metadata";
|
||||
import { ButtplugMessageError } from './Exceptions';
|
||||
|
||||
export const SYSTEM_MESSAGE_ID = 0;
|
||||
export const DEFAULT_MESSAGE_ID = 1;
|
||||
export const MAX_ID = 4294967295;
|
||||
export const MESSAGE_SPEC_VERSION = 3;
|
||||
export const MESSAGE_SPEC_VERSION_MAJOR = 4;
|
||||
export const MESSAGE_SPEC_VERSION_MINOR = 0;
|
||||
|
||||
export class MessageAttributes {
|
||||
public ScalarCmd?: Array<GenericDeviceMessageAttributes>;
|
||||
public RotateCmd?: Array<GenericDeviceMessageAttributes>;
|
||||
public LinearCmd?: Array<GenericDeviceMessageAttributes>;
|
||||
public RawReadCmd?: RawDeviceMessageAttributes;
|
||||
public RawWriteCmd?: RawDeviceMessageAttributes;
|
||||
public RawSubscribeCmd?: RawDeviceMessageAttributes;
|
||||
public SensorReadCmd?: Array<SensorDeviceMessageAttributes>;
|
||||
public SensorSubscribeCmd?: Array<SensorDeviceMessageAttributes>;
|
||||
public StopDeviceCmd: {};
|
||||
|
||||
constructor(data: Partial<MessageAttributes>) {
|
||||
Object.assign(this, data);
|
||||
}
|
||||
|
||||
public update() {
|
||||
this.ScalarCmd?.forEach((x, i) => (x.Index = i));
|
||||
this.RotateCmd?.forEach((x, i) => (x.Index = i));
|
||||
this.LinearCmd?.forEach((x, i) => (x.Index = i));
|
||||
this.SensorReadCmd?.forEach((x, i) => (x.Index = i));
|
||||
this.SensorSubscribeCmd?.forEach((x, i) => (x.Index = i));
|
||||
}
|
||||
// Base message interfaces
|
||||
export interface ButtplugMessage {
|
||||
Ok?: Ok;
|
||||
Ping?: Ping;
|
||||
Error?: Error;
|
||||
RequestServerInfo?: RequestServerInfo;
|
||||
ServerInfo?: ServerInfo;
|
||||
RequestDeviceList?: RequestDeviceList;
|
||||
StartScanning?: StartScanning;
|
||||
StopScanning?: StopScanning;
|
||||
ScanningFinished?: ScanningFinished;
|
||||
StopCmd?: StopCmd;
|
||||
InputCmd?: InputCmd;
|
||||
InputReading?: InputReading;
|
||||
OutputCmd?: OutputCmd;
|
||||
DeviceList?: DeviceList;
|
||||
}
|
||||
|
||||
export enum ActuatorType {
|
||||
Unknown = "Unknown",
|
||||
Vibrate = "Vibrate",
|
||||
Rotate = "Rotate",
|
||||
Oscillate = "Oscillate",
|
||||
Constrict = "Constrict",
|
||||
Inflate = "Inflate",
|
||||
Position = "Position",
|
||||
export function msgId(msg: ButtplugMessage): number {
|
||||
for (let [_, entry] of Object.entries(msg)) {
|
||||
if (entry != undefined) {
|
||||
return entry.Id;
|
||||
}
|
||||
}
|
||||
throw new ButtplugMessageError(`Message ${msg} does not have an ID.`);
|
||||
}
|
||||
|
||||
export enum SensorType {
|
||||
Unknown = "Unknown",
|
||||
Battery = "Battery",
|
||||
RSSI = "RSSI",
|
||||
Button = "Button",
|
||||
Pressure = "Pressure",
|
||||
// Temperature,
|
||||
// Accelerometer,
|
||||
// Gyro,
|
||||
export function setMsgId(msg: ButtplugMessage, id: number) {
|
||||
for (let [_, entry] of Object.entries(msg)) {
|
||||
if (entry != undefined) {
|
||||
entry.Id = id;
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new ButtplugMessageError(`Message ${msg} does not have an ID.`);
|
||||
}
|
||||
|
||||
export class GenericDeviceMessageAttributes {
|
||||
public FeatureDescriptor: string;
|
||||
public ActuatorType: ActuatorType;
|
||||
public StepCount: number;
|
||||
public Index = 0;
|
||||
constructor(data: Partial<GenericDeviceMessageAttributes>) {
|
||||
Object.assign(this, data);
|
||||
}
|
||||
export interface Ok {
|
||||
Id: number | undefined;
|
||||
}
|
||||
|
||||
export class RawDeviceMessageAttributes {
|
||||
constructor(public Endpoints: Array<string>) {}
|
||||
}
|
||||
|
||||
export class SensorDeviceMessageAttributes {
|
||||
public FeatureDescriptor: string;
|
||||
public SensorType: SensorType;
|
||||
public StepRange: Array<number>;
|
||||
public Index = 0;
|
||||
constructor(data: Partial<GenericDeviceMessageAttributes>) {
|
||||
Object.assign(this, data);
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class ButtplugMessage {
|
||||
constructor(public Id: number) {}
|
||||
|
||||
// tslint:disable-next-line:ban-types
|
||||
public get Type(): Function {
|
||||
return this.constructor;
|
||||
}
|
||||
|
||||
public toJSON(): string {
|
||||
return JSON.stringify(this.toProtocolFormat());
|
||||
}
|
||||
|
||||
public toProtocolFormat(): object {
|
||||
const jsonObj = {};
|
||||
jsonObj[(this.constructor as unknown as { Name: string }).Name] =
|
||||
instanceToPlain(this);
|
||||
return jsonObj;
|
||||
}
|
||||
|
||||
public update() {}
|
||||
}
|
||||
|
||||
export abstract class ButtplugDeviceMessage extends ButtplugMessage {
|
||||
constructor(
|
||||
public DeviceIndex: number,
|
||||
public Id: number,
|
||||
) {
|
||||
super(Id);
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class ButtplugSystemMessage extends ButtplugMessage {
|
||||
constructor(public Id: number = SYSTEM_MESSAGE_ID) {
|
||||
super(Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class Ok extends ButtplugSystemMessage {
|
||||
static Name = "Ok";
|
||||
|
||||
constructor(public Id: number = DEFAULT_MESSAGE_ID) {
|
||||
super(Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class Ping extends ButtplugMessage {
|
||||
static Name = "Ping";
|
||||
|
||||
constructor(public Id: number = DEFAULT_MESSAGE_ID) {
|
||||
super(Id);
|
||||
}
|
||||
export interface Ping {
|
||||
Id: number | undefined;
|
||||
}
|
||||
|
||||
export enum ErrorClass {
|
||||
ERROR_UNKNOWN,
|
||||
ERROR_INIT,
|
||||
ERROR_PING,
|
||||
ERROR_MSG,
|
||||
ERROR_DEVICE,
|
||||
ERROR_UNKNOWN,
|
||||
ERROR_INIT,
|
||||
ERROR_PING,
|
||||
ERROR_MSG,
|
||||
ERROR_DEVICE,
|
||||
}
|
||||
|
||||
export class Error extends ButtplugMessage {
|
||||
static Name = "Error";
|
||||
|
||||
constructor(
|
||||
public ErrorMessage: string,
|
||||
public ErrorCode: ErrorClass = ErrorClass.ERROR_UNKNOWN,
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(Id);
|
||||
}
|
||||
|
||||
get Schemversion() {
|
||||
return 0;
|
||||
}
|
||||
export interface Error {
|
||||
ErrorMessage: string;
|
||||
ErrorCode: ErrorClass;
|
||||
Id: number | undefined;
|
||||
}
|
||||
|
||||
export class DeviceInfo {
|
||||
public DeviceIndex: number;
|
||||
public DeviceName: string;
|
||||
@Type(() => MessageAttributes)
|
||||
public DeviceMessages: MessageAttributes;
|
||||
public DeviceDisplayName?: string;
|
||||
public DeviceMessageTimingGap?: number;
|
||||
|
||||
constructor(data: Partial<DeviceInfo>) {
|
||||
Object.assign(this, data);
|
||||
}
|
||||
export interface RequestDeviceList {
|
||||
Id: number | undefined;
|
||||
}
|
||||
|
||||
export class DeviceList extends ButtplugMessage {
|
||||
static Name = "DeviceList";
|
||||
|
||||
@Type(() => DeviceInfo)
|
||||
public Devices: DeviceInfo[];
|
||||
public Id: number;
|
||||
|
||||
constructor(devices: DeviceInfo[], id: number = DEFAULT_MESSAGE_ID) {
|
||||
super(id);
|
||||
this.Devices = devices;
|
||||
this.Id = id;
|
||||
}
|
||||
|
||||
public update() {
|
||||
for (const device of this.Devices) {
|
||||
device.DeviceMessages.update();
|
||||
}
|
||||
}
|
||||
export interface StartScanning {
|
||||
Id: number | undefined;
|
||||
}
|
||||
|
||||
export class DeviceAdded extends ButtplugSystemMessage {
|
||||
static Name = "DeviceAdded";
|
||||
|
||||
public DeviceIndex: number;
|
||||
public DeviceName: string;
|
||||
@Type(() => MessageAttributes)
|
||||
public DeviceMessages: MessageAttributes;
|
||||
public DeviceDisplayName?: string;
|
||||
public DeviceMessageTimingGap?: number;
|
||||
|
||||
constructor(data: Partial<DeviceAdded>) {
|
||||
super();
|
||||
Object.assign(this, data);
|
||||
}
|
||||
|
||||
public update() {
|
||||
this.DeviceMessages.update();
|
||||
}
|
||||
export interface StopScanning {
|
||||
Id: number | undefined;
|
||||
}
|
||||
|
||||
export class DeviceRemoved extends ButtplugSystemMessage {
|
||||
static Name = "DeviceRemoved";
|
||||
|
||||
constructor(public DeviceIndex: number) {
|
||||
super();
|
||||
}
|
||||
export interface StopAllDevices {
|
||||
Id: number | undefined;
|
||||
}
|
||||
|
||||
export class RequestDeviceList extends ButtplugMessage {
|
||||
static Name = "RequestDeviceList";
|
||||
|
||||
constructor(public Id: number = DEFAULT_MESSAGE_ID) {
|
||||
super(Id);
|
||||
}
|
||||
export interface ScanningFinished {
|
||||
Id: number | undefined;
|
||||
}
|
||||
|
||||
export class StartScanning extends ButtplugMessage {
|
||||
static Name = "StartScanning";
|
||||
|
||||
constructor(public Id: number = DEFAULT_MESSAGE_ID) {
|
||||
super(Id);
|
||||
}
|
||||
export interface RequestServerInfo {
|
||||
ClientName: string;
|
||||
ProtocolVersionMajor: number;
|
||||
ProtocolVersionMinor: number;
|
||||
Id: number | undefined;
|
||||
}
|
||||
|
||||
export class StopScanning extends ButtplugMessage {
|
||||
static Name = "StopScanning";
|
||||
|
||||
constructor(public Id: number = DEFAULT_MESSAGE_ID) {
|
||||
super(Id);
|
||||
}
|
||||
export interface ServerInfo {
|
||||
MaxPingTime: number;
|
||||
ServerName: string;
|
||||
ProtocolVersionMajor: number;
|
||||
ProtocolVersionMinor: number;
|
||||
Id: number | undefined;
|
||||
}
|
||||
|
||||
export class ScanningFinished extends ButtplugSystemMessage {
|
||||
static Name = "ScanningFinished";
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
export interface DeviceFeature {
|
||||
FeatureDescription: string;
|
||||
Output: { [key: string]: DeviceFeatureOutput };
|
||||
Input: { [key: string]: DeviceFeatureInput };
|
||||
FeatureIndex: number;
|
||||
}
|
||||
|
||||
export class RequestServerInfo extends ButtplugMessage {
|
||||
static Name = "RequestServerInfo";
|
||||
|
||||
constructor(
|
||||
public ClientName: string,
|
||||
public MessageVersion: number = 0,
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(Id);
|
||||
}
|
||||
export interface DeviceInfo {
|
||||
DeviceIndex: number;
|
||||
DeviceName: string;
|
||||
DeviceFeatures: { [key: number]: DeviceFeature };
|
||||
DeviceDisplayName?: string;
|
||||
DeviceMessageTimingGap?: number;
|
||||
}
|
||||
|
||||
export class ServerInfo extends ButtplugSystemMessage {
|
||||
static Name = "ServerInfo";
|
||||
|
||||
constructor(
|
||||
public MessageVersion: number,
|
||||
public MaxPingTime: number,
|
||||
public ServerName: string,
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
export interface DeviceList {
|
||||
Devices: { [key: number]: DeviceInfo };
|
||||
Id: number | undefined;
|
||||
}
|
||||
|
||||
export class StopDeviceCmd extends ButtplugDeviceMessage {
|
||||
static Name = "StopDeviceCmd";
|
||||
|
||||
constructor(
|
||||
public DeviceIndex: number = -1,
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(DeviceIndex, Id);
|
||||
}
|
||||
export enum OutputType {
|
||||
Unknown = 'Unknown',
|
||||
Vibrate = 'Vibrate',
|
||||
Rotate = 'Rotate',
|
||||
Oscillate = 'Oscillate',
|
||||
Constrict = 'Constrict',
|
||||
Inflate = 'Inflate',
|
||||
Position = 'Position',
|
||||
HwPositionWithDuration = 'HwPositionWithDuration',
|
||||
Temperature = 'Temperature',
|
||||
Spray = 'Spray',
|
||||
Led = 'Led',
|
||||
}
|
||||
|
||||
export class StopAllDevices extends ButtplugMessage {
|
||||
static Name = "StopAllDevices";
|
||||
|
||||
constructor(public Id: number = DEFAULT_MESSAGE_ID) {
|
||||
super(Id);
|
||||
}
|
||||
export enum InputType {
|
||||
Unknown = 'Unknown',
|
||||
Battery = 'Battery',
|
||||
RSSI = 'RSSI',
|
||||
Button = 'Button',
|
||||
Pressure = 'Pressure',
|
||||
// Temperature,
|
||||
// Accelerometer,
|
||||
// Gyro,
|
||||
}
|
||||
|
||||
export class GenericMessageSubcommand {
|
||||
protected constructor(public Index: number) {}
|
||||
export enum InputCommandType {
|
||||
Read = 'Read',
|
||||
Subscribe = 'Subscribe',
|
||||
Unsubscribe = 'Unsubscribe',
|
||||
}
|
||||
|
||||
export class ScalarSubcommand extends GenericMessageSubcommand {
|
||||
constructor(
|
||||
Index: number,
|
||||
public Scalar: number,
|
||||
public ActuatorType: ActuatorType,
|
||||
) {
|
||||
super(Index);
|
||||
}
|
||||
export interface DeviceFeatureInput {
|
||||
Value: number[];
|
||||
Command: InputCommandType[];
|
||||
}
|
||||
|
||||
export class ScalarCmd extends ButtplugDeviceMessage {
|
||||
static Name = "ScalarCmd";
|
||||
|
||||
constructor(
|
||||
public Scalars: ScalarSubcommand[],
|
||||
public DeviceIndex: number = -1,
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(DeviceIndex, Id);
|
||||
}
|
||||
export interface DeviceFeatureOutput {
|
||||
Value: number;
|
||||
Duration?: number;
|
||||
}
|
||||
|
||||
export class RotateSubcommand extends GenericMessageSubcommand {
|
||||
constructor(
|
||||
Index: number,
|
||||
public Speed: number,
|
||||
public Clockwise: boolean,
|
||||
) {
|
||||
super(Index);
|
||||
}
|
||||
export interface OutputCmd {
|
||||
DeviceIndex: number;
|
||||
FeatureIndex: number;
|
||||
Command: { [key: string]: DeviceFeatureOutput };
|
||||
Id: number | undefined;
|
||||
}
|
||||
|
||||
export class RotateCmd extends ButtplugDeviceMessage {
|
||||
static Name = "RotateCmd";
|
||||
// Device Input Commands
|
||||
|
||||
public static Create(
|
||||
deviceIndex: number,
|
||||
commands: [number, boolean][],
|
||||
): RotateCmd {
|
||||
const cmdList: RotateSubcommand[] = new Array<RotateSubcommand>();
|
||||
|
||||
let i = 0;
|
||||
for (const [speed, clockwise] of commands) {
|
||||
cmdList.push(new RotateSubcommand(i, speed, clockwise));
|
||||
++i;
|
||||
}
|
||||
|
||||
return new RotateCmd(cmdList, deviceIndex);
|
||||
}
|
||||
constructor(
|
||||
public Rotations: RotateSubcommand[],
|
||||
public DeviceIndex: number = -1,
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(DeviceIndex, Id);
|
||||
}
|
||||
export interface InputCmd {
|
||||
DeviceIndex: number;
|
||||
FeatureIndex: number;
|
||||
Type: InputType;
|
||||
Command: InputCommandType;
|
||||
Id: number | undefined;
|
||||
}
|
||||
|
||||
export class VectorSubcommand extends GenericMessageSubcommand {
|
||||
constructor(
|
||||
Index: number,
|
||||
public Position: number,
|
||||
public Duration: number,
|
||||
) {
|
||||
super(Index);
|
||||
}
|
||||
export interface InputValue {
|
||||
Value: number;
|
||||
}
|
||||
|
||||
export class LinearCmd extends ButtplugDeviceMessage {
|
||||
static Name = "LinearCmd";
|
||||
|
||||
public static Create(
|
||||
deviceIndex: number,
|
||||
commands: [number, number][],
|
||||
): LinearCmd {
|
||||
const cmdList: VectorSubcommand[] = new Array<VectorSubcommand>();
|
||||
|
||||
let i = 0;
|
||||
for (const cmd of commands) {
|
||||
cmdList.push(new VectorSubcommand(i, cmd[0], cmd[1]));
|
||||
++i;
|
||||
}
|
||||
|
||||
return new LinearCmd(cmdList, deviceIndex);
|
||||
}
|
||||
constructor(
|
||||
public Vectors: VectorSubcommand[],
|
||||
public DeviceIndex: number = -1,
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(DeviceIndex, Id);
|
||||
}
|
||||
export interface InputReading {
|
||||
DeviceIndex: number;
|
||||
FeatureIndex: number;
|
||||
Reading: { [key: string]: InputValue };
|
||||
Id: number | undefined;
|
||||
}
|
||||
|
||||
export class SensorReadCmd extends ButtplugDeviceMessage {
|
||||
static Name = "SensorReadCmd";
|
||||
|
||||
constructor(
|
||||
public DeviceIndex: number,
|
||||
public SensorIndex: number,
|
||||
public SensorType: SensorType,
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(DeviceIndex, Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class SensorReading extends ButtplugDeviceMessage {
|
||||
static Name = "SensorReading";
|
||||
|
||||
constructor(
|
||||
public DeviceIndex: number,
|
||||
public SensorIndex: number,
|
||||
public SensorType: SensorType,
|
||||
public Data: number[],
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(DeviceIndex, Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class RawReadCmd extends ButtplugDeviceMessage {
|
||||
static Name = "RawReadCmd";
|
||||
|
||||
constructor(
|
||||
public DeviceIndex: number,
|
||||
public Endpoint: string,
|
||||
public ExpectedLength: number,
|
||||
public Timeout: number,
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(DeviceIndex, Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class RawWriteCmd extends ButtplugDeviceMessage {
|
||||
static Name = "RawWriteCmd";
|
||||
|
||||
constructor(
|
||||
public DeviceIndex: number,
|
||||
public Endpoint: string,
|
||||
public Data: Uint8Array,
|
||||
public WriteWithResponse: boolean,
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(DeviceIndex, Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class RawSubscribeCmd extends ButtplugDeviceMessage {
|
||||
static Name = "RawSubscribeCmd";
|
||||
|
||||
constructor(
|
||||
public DeviceIndex: number,
|
||||
public Endpoint: string,
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(DeviceIndex, Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class RawUnsubscribeCmd extends ButtplugDeviceMessage {
|
||||
static Name = "RawUnsubscribeCmd";
|
||||
|
||||
constructor(
|
||||
public DeviceIndex: number,
|
||||
public Endpoint: string,
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(DeviceIndex, Id);
|
||||
}
|
||||
}
|
||||
|
||||
export class RawReading extends ButtplugDeviceMessage {
|
||||
static Name = "RawReading";
|
||||
|
||||
constructor(
|
||||
public DeviceIndex: number,
|
||||
public Endpoint: string,
|
||||
public Data: number[],
|
||||
public Id: number = DEFAULT_MESSAGE_ID,
|
||||
) {
|
||||
super(DeviceIndex, Id);
|
||||
}
|
||||
export interface StopCmd {
|
||||
Id: number | undefined;
|
||||
DeviceIndex: number | undefined;
|
||||
FeatureIndex: number | undefined;
|
||||
Inputs: boolean | undefined;
|
||||
Outputs: boolean | undefined;
|
||||
}
|
||||
|
||||
@@ -1,88 +1,95 @@
|
||||
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
|
||||
implements IButtplugClientConnector
|
||||
extends EventEmitter
|
||||
implements IButtplugClientConnector
|
||||
{
|
||||
private static _loggingActivated = false;
|
||||
private static wasmInstance;
|
||||
private _connected: boolean = false;
|
||||
private client;
|
||||
private serverPtr;
|
||||
private static _loggingActivated = false;
|
||||
private static wasmInstance;
|
||||
private _connected: boolean = false;
|
||||
private client;
|
||||
private serverPtr;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public get Connected(): boolean {
|
||||
return this._connected;
|
||||
}
|
||||
public get Connected(): boolean {
|
||||
return this._connected;
|
||||
}
|
||||
|
||||
private static maybeLoadWasm = async () => {
|
||||
if (ButtplugWasmClientConnector.wasmInstance == undefined) {
|
||||
ButtplugWasmClientConnector.wasmInstance = await import(
|
||||
"../wasm/index.js"
|
||||
);
|
||||
}
|
||||
};
|
||||
private static maybeLoadWasm = async () => {
|
||||
if (ButtplugWasmClientConnector.wasmInstance == undefined) {
|
||||
ButtplugWasmClientConnector.wasmInstance = await import(
|
||||
'../wasm/index.js'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
public static activateLogging = async (logLevel: string = "debug") => {
|
||||
await ButtplugWasmClientConnector.maybeLoadWasm();
|
||||
if (this._loggingActivated) {
|
||||
console.log("Logging already activated, ignoring.");
|
||||
return;
|
||||
}
|
||||
console.log("Turning on logging.");
|
||||
ButtplugWasmClientConnector.wasmInstance.buttplug_activate_env_logger(
|
||||
logLevel,
|
||||
);
|
||||
};
|
||||
public static activateLogging = async (logLevel: string = 'debug') => {
|
||||
await ButtplugWasmClientConnector.maybeLoadWasm();
|
||||
if (this._loggingActivated) {
|
||||
console.log('Logging already activated, ignoring.');
|
||||
return;
|
||||
}
|
||||
console.log('Turning on logging.');
|
||||
ButtplugWasmClientConnector.wasmInstance.buttplug_activate_env_logger(
|
||||
logLevel,
|
||||
);
|
||||
};
|
||||
|
||||
public initialize = async (): Promise<void> => {};
|
||||
public initialize = async (): Promise<void> => {};
|
||||
|
||||
public connect = async (): Promise<void> => {
|
||||
await ButtplugWasmClientConnector.maybeLoadWasm();
|
||||
//ButtplugWasmClientConnector.wasmInstance.buttplug_activate_env_logger('debug');
|
||||
this.client =
|
||||
ButtplugWasmClientConnector.wasmInstance.buttplug_create_embedded_wasm_server(
|
||||
(msgs) => {
|
||||
this.emitMessage(msgs);
|
||||
},
|
||||
this.serverPtr,
|
||||
);
|
||||
this._connected = true;
|
||||
};
|
||||
public connect = async (): Promise<void> => {
|
||||
await ButtplugWasmClientConnector.maybeLoadWasm();
|
||||
this.client =
|
||||
ButtplugWasmClientConnector.wasmInstance.buttplug_create_embedded_wasm_server(
|
||||
(msgs) => {
|
||||
this.emitMessage(msgs);
|
||||
},
|
||||
this.serverPtr,
|
||||
);
|
||||
this._connected = true;
|
||||
};
|
||||
|
||||
public disconnect = async (): Promise<void> => {};
|
||||
public disconnect = async (): Promise<void> => {};
|
||||
|
||||
public send = (msg: ButtplugMessage): void => {
|
||||
ButtplugWasmClientConnector.wasmInstance.buttplug_client_send_json_message(
|
||||
this.client,
|
||||
new TextEncoder().encode("[" + msg.toJSON() + "]"),
|
||||
(output) => {
|
||||
this.emitMessage(output);
|
||||
},
|
||||
);
|
||||
};
|
||||
public send = (msg: ButtplugMessage): void => {
|
||||
ButtplugWasmClientConnector.wasmInstance.buttplug_client_send_json_message(
|
||||
this.client,
|
||||
new TextEncoder().encode('[' + JSON.stringify(msg) + ']'),
|
||||
(output) => {
|
||||
this.emitMessage(output);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
private emitMessage = (msg: Uint8Array) => {
|
||||
const str = new TextDecoder().decode(msg);
|
||||
// This needs to use buttplug-js's fromJSON, otherwise we won't resolve the message name correctly.
|
||||
this.emit("message", fromJSON(str));
|
||||
};
|
||||
private emitMessage = (msg: Uint8Array) => {
|
||||
const str = new TextDecoder().decode(msg);
|
||||
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,86 +6,83 @@
|
||||
* @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;
|
||||
protected _websocketConstructor: typeof WebSocket | null = null;
|
||||
protected _ws: WebSocket | undefined;
|
||||
protected _websocketConstructor: typeof WebSocket | null = null;
|
||||
|
||||
public constructor(private _url: string) {
|
||||
super();
|
||||
}
|
||||
public constructor(private _url: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
public get Connected(): boolean {
|
||||
return this._ws !== undefined;
|
||||
}
|
||||
public get Connected(): boolean {
|
||||
return this._ws !== undefined;
|
||||
}
|
||||
|
||||
public connect = async (): Promise<void> => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const ws = new (this._websocketConstructor ?? WebSocket)(this._url);
|
||||
const onErrorCallback = (event: Event) => {
|
||||
reject(event);
|
||||
};
|
||||
const onCloseCallback = (event: CloseEvent) => reject(event.reason);
|
||||
ws.addEventListener("open", async () => {
|
||||
this._ws = ws;
|
||||
try {
|
||||
await this.initialize();
|
||||
this._ws.addEventListener("message", (msg) => {
|
||||
this.parseIncomingMessage(msg);
|
||||
});
|
||||
this._ws.removeEventListener("close", onCloseCallback);
|
||||
this._ws.removeEventListener("error", onErrorCallback);
|
||||
this._ws.addEventListener("close", this.disconnect);
|
||||
resolve();
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
// In websockets, our error rarely tells us much, as for security reasons
|
||||
// browsers usually only throw Error Code 1006. It's up to those using this
|
||||
// library to state what the problem might be.
|
||||
public connect = async (): Promise<void> => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const ws = new (this._websocketConstructor ?? WebSocket)(this._url);
|
||||
const onErrorCallback = (event: Event) => {reject(event)}
|
||||
const onCloseCallback = (event: CloseEvent) => reject(event.reason)
|
||||
ws.addEventListener('open', async () => {
|
||||
this._ws = ws;
|
||||
try {
|
||||
await this.initialize();
|
||||
this._ws.addEventListener('message', (msg) => {
|
||||
this.parseIncomingMessage(msg);
|
||||
});
|
||||
this._ws.removeEventListener('close', onCloseCallback);
|
||||
this._ws.removeEventListener('error', onErrorCallback);
|
||||
this._ws.addEventListener('close', this.disconnect);
|
||||
resolve();
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
// In websockets, our error rarely tells us much, as for security reasons
|
||||
// browsers usually only throw Error Code 1006. It's up to those using this
|
||||
// library to state what the problem might be.
|
||||
|
||||
ws.addEventListener("error", onErrorCallback);
|
||||
ws.addEventListener("close", onCloseCallback);
|
||||
});
|
||||
};
|
||||
ws.addEventListener('error', onErrorCallback)
|
||||
ws.addEventListener('close', onCloseCallback);
|
||||
});
|
||||
};
|
||||
|
||||
public disconnect = async (): Promise<void> => {
|
||||
if (!this.Connected) {
|
||||
return;
|
||||
}
|
||||
this._ws!.close();
|
||||
this._ws = undefined;
|
||||
this.emit("disconnect");
|
||||
};
|
||||
public disconnect = async (): Promise<void> => {
|
||||
if (!this.Connected) {
|
||||
return;
|
||||
}
|
||||
this._ws!.close();
|
||||
this._ws = undefined;
|
||||
this.emit('disconnect');
|
||||
};
|
||||
|
||||
public sendMessage(msg: ButtplugMessage) {
|
||||
if (!this.Connected) {
|
||||
throw new Error("ButtplugBrowserWebsocketConnector not connected");
|
||||
}
|
||||
this._ws!.send("[" + msg.toJSON() + "]");
|
||||
}
|
||||
public sendMessage(msg: ButtplugMessage) {
|
||||
if (!this.Connected) {
|
||||
throw new Error('ButtplugBrowserWebsocketConnector not connected');
|
||||
}
|
||||
this._ws!.send('[' + JSON.stringify(msg) + ']');
|
||||
}
|
||||
|
||||
public initialize = async (): Promise<void> => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
public initialize = async (): Promise<void> => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
protected parseIncomingMessage(event: MessageEvent) {
|
||||
if (typeof event.data === "string") {
|
||||
const msgs = fromJSON(event.data);
|
||||
this.emit("message", msgs);
|
||||
} else if (event.data instanceof Blob) {
|
||||
// No-op, we only use text message types.
|
||||
}
|
||||
}
|
||||
protected parseIncomingMessage(event: MessageEvent) {
|
||||
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);
|
||||
}
|
||||
protected onReaderLoad(event: Event) {
|
||||
const msgs: ButtplugMessage[] = JSON.parse((event.target as FileReader).result as string);
|
||||
this.emit('message', msgs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,60 +6,62 @@
|
||||
* @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;
|
||||
protected _waitingMsgs: Map<
|
||||
number,
|
||||
[(val: Messages.ButtplugMessage) => void, (err: Error) => void]
|
||||
> = new Map();
|
||||
protected _counter = 1;
|
||||
protected _waitingMsgs: Map<
|
||||
number,
|
||||
[(val: Messages.ButtplugMessage) => void, (err: Error) => void]
|
||||
> = new Map();
|
||||
|
||||
public constructor(private _useCounter: boolean) {}
|
||||
public constructor(private _useCounter: boolean) {}
|
||||
|
||||
// One of the places we should actually return a promise, as we need to store
|
||||
// them while waiting for them to return across the line.
|
||||
// tslint:disable:promise-function-async
|
||||
public PrepareOutgoingMessage(
|
||||
msg: Messages.ButtplugMessage,
|
||||
): Promise<Messages.ButtplugMessage> {
|
||||
if (this._useCounter) {
|
||||
msg.Id = this._counter;
|
||||
// Always increment last, otherwise we might lose sync
|
||||
this._counter += 1;
|
||||
}
|
||||
let res;
|
||||
let rej;
|
||||
const msgPromise = new Promise<Messages.ButtplugMessage>(
|
||||
(resolve, reject) => {
|
||||
res = resolve;
|
||||
rej = reject;
|
||||
},
|
||||
);
|
||||
this._waitingMsgs.set(msg.Id, [res, rej]);
|
||||
return msgPromise;
|
||||
}
|
||||
// One of the places we should actually return a promise, as we need to store
|
||||
// them while waiting for them to return across the line.
|
||||
// tslint:disable:promise-function-async
|
||||
public PrepareOutgoingMessage(
|
||||
msg: Messages.ButtplugMessage
|
||||
): Promise<Messages.ButtplugMessage> {
|
||||
if (this._useCounter) {
|
||||
Messages.setMsgId(msg, this._counter);
|
||||
// Always increment last, otherwise we might lose sync
|
||||
this._counter += 1;
|
||||
}
|
||||
let res;
|
||||
let rej;
|
||||
const msgPromise = new Promise<Messages.ButtplugMessage>(
|
||||
(resolve, reject) => {
|
||||
res = resolve;
|
||||
rej = reject;
|
||||
}
|
||||
);
|
||||
this._waitingMsgs.set(Messages.msgId(msg), [res, rej]);
|
||||
return msgPromise;
|
||||
}
|
||||
|
||||
public ParseIncomingMessages(
|
||||
msgs: Messages.ButtplugMessage[],
|
||||
): Messages.ButtplugMessage[] {
|
||||
const noMatch: Messages.ButtplugMessage[] = [];
|
||||
for (const x of msgs) {
|
||||
if (x.Id !== Messages.SYSTEM_MESSAGE_ID && this._waitingMsgs.has(x.Id)) {
|
||||
const [res, rej] = this._waitingMsgs.get(x.Id)!;
|
||||
// If we've gotten back an error, reject the related promise using a
|
||||
// ButtplugException derived type.
|
||||
if (x.Type === Messages.Error) {
|
||||
rej(ButtplugError.FromError(x as Messages.Error));
|
||||
continue;
|
||||
}
|
||||
res(x);
|
||||
continue;
|
||||
} else {
|
||||
noMatch.push(x);
|
||||
}
|
||||
}
|
||||
return noMatch;
|
||||
}
|
||||
public ParseIncomingMessages(
|
||||
msgs: Messages.ButtplugMessage[]
|
||||
): Messages.ButtplugMessage[] {
|
||||
const noMatch: Messages.ButtplugMessage[] = [];
|
||||
for (const x of msgs) {
|
||||
let id = Messages.msgId(x);
|
||||
if (id !== Messages.SYSTEM_MESSAGE_ID && this._waitingMsgs.has(id)) {
|
||||
const [res, rej] = this._waitingMsgs.get(id)!;
|
||||
this._waitingMsgs.delete(id);
|
||||
// If we've gotten back an error, reject the related promise using a
|
||||
// ButtplugException derived type.
|
||||
if (x.Error !== undefined) {
|
||||
rej(ButtplugError.FromError(x.Error!));
|
||||
continue;
|
||||
}
|
||||
res(x);
|
||||
continue;
|
||||
} else {
|
||||
noMatch.push(x);
|
||||
}
|
||||
}
|
||||
return noMatch;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,17 @@
|
||||
use async_trait::async_trait;
|
||||
use buttplug::{
|
||||
core::{
|
||||
errors::ButtplugDeviceError,
|
||||
message::Endpoint,
|
||||
},
|
||||
server::device::{
|
||||
configuration::{BluetoothLESpecifier, ProtocolCommunicationSpecifier},
|
||||
hardware::{
|
||||
Hardware,
|
||||
HardwareConnector,
|
||||
HardwareEvent,
|
||||
HardwareInternal,
|
||||
HardwareReadCmd,
|
||||
HardwareReading,
|
||||
HardwareSpecializer,
|
||||
HardwareSubscribeCmd,
|
||||
HardwareUnsubscribeCmd,
|
||||
HardwareWriteCmd,
|
||||
},
|
||||
},
|
||||
util::future::{ButtplugFuture, ButtplugFutureStateShared},
|
||||
use buttplug_core::errors::ButtplugDeviceError;
|
||||
use buttplug_server_device_config::{BluetoothLESpecifier, Endpoint, ProtocolCommunicationSpecifier};
|
||||
use buttplug_server::device::hardware::{
|
||||
Hardware,
|
||||
HardwareConnector,
|
||||
HardwareEvent,
|
||||
HardwareInternal,
|
||||
HardwareReadCmd,
|
||||
HardwareReading,
|
||||
HardwareSpecializer,
|
||||
HardwareSubscribeCmd,
|
||||
HardwareUnsubscribeCmd,
|
||||
HardwareWriteCmd,
|
||||
};
|
||||
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::{
|
||||
HardwareCommunicationManager, HardwareCommunicationManagerBuilder,
|
||||
HardwareCommunicationManagerEvent,
|
||||
},
|
||||
}
|
||||
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\//],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -36,7 +36,8 @@
|
||||
"tailwindcss": "^4.0.0",
|
||||
"tw-animate-css": "^1.3.8",
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^7.1.4"
|
||||
"vite": "^7.1.4",
|
||||
"vite-plugin-wasm": "3.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@directus/sdk": "^20.0.3",
|
||||
|
||||
@@ -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,49 @@ async function startScanning() {
|
||||
scanning = true;
|
||||
}
|
||||
|
||||
async function onDeviceAdded(
|
||||
msg: ButtplugDeviceMessage,
|
||||
dev: ButtplugClientDevice,
|
||||
) {
|
||||
async function onDeviceAdded(dev: ButtplugClientDevice) {
|
||||
const device = convertDevice(dev);
|
||||
devices.push(device);
|
||||
|
||||
const cmds = device.info.messageAttributes.SensorReadCmd;
|
||||
|
||||
cmds?.forEach(async (cmd) => {
|
||||
await client.sendDeviceMessage(
|
||||
{ index: device.info.index },
|
||||
new SensorReadCmd(device.info.index, cmd.Index, cmd.SensorType),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function handleMessages(messages: ButtplugMessage[]) {
|
||||
messages.forEach(async (msg) => {
|
||||
await handleMessage(msg);
|
||||
});
|
||||
}
|
||||
|
||||
async function handleMessage(msg: ButtplugMessage) {
|
||||
if (msg instanceof SensorReading) {
|
||||
const device = devices[msg.DeviceIndex];
|
||||
if (msg.SensorType === SensorType.Battery) {
|
||||
device.batteryLevel = msg.Data[0];
|
||||
// Try to read battery level — access through the reactive array so Svelte detects the mutation
|
||||
const idx = devices.length - 1;
|
||||
if (device.hasBattery) {
|
||||
try {
|
||||
devices[idx].batteryLevel = await dev.battery();
|
||||
} catch (e) {
|
||||
console.warn(`Failed to read battery for ${dev.name}:`, e);
|
||||
}
|
||||
device.sensorValues[msg.Index] = msg.Data[0];
|
||||
device.lastSeen = new Date();
|
||||
} else if (msg instanceof DeviceList) {
|
||||
devices = client.devices.map(convertDevice);
|
||||
}
|
||||
}
|
||||
|
||||
function handleInputReading(msg: ButtplugMessage) {
|
||||
if (msg.InputReading === undefined) return;
|
||||
const reading = msg.InputReading;
|
||||
const device = devices.find((d) => d.info.index === reading.DeviceIndex);
|
||||
if (!device) return;
|
||||
|
||||
if (reading.Reading[InputType.Battery] !== undefined) {
|
||||
device.batteryLevel = reading.Reading[InputType.Battery].Value;
|
||||
}
|
||||
device.lastSeen = new Date();
|
||||
}
|
||||
|
||||
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 +125,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 +182,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 +332,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 +594,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
|
||||
|
||||
@@ -2,9 +2,10 @@ import path from "path";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { defineConfig } from "vite";
|
||||
import { sveltekit } from "@sveltejs/kit/vite";
|
||||
import wasm from 'vite-plugin-wasm';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit(), tailwindcss()],
|
||||
plugins: [sveltekit(), tailwindcss(), wasm()],
|
||||
resolve: {
|
||||
alias: { $lib: path.resolve("./src/lib"), "@": path.resolve("./src/lib") },
|
||||
},
|
||||
|
||||
23
pnpm-lock.yaml
generated
23
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
|
||||
@@ -145,6 +139,9 @@ importers:
|
||||
vite:
|
||||
specifier: ^7.1.4
|
||||
version: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)
|
||||
vite-plugin-wasm:
|
||||
specifier: 3.5.0
|
||||
version: 3.5.0(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0))
|
||||
|
||||
packages:
|
||||
|
||||
@@ -1650,9 +1647,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'}
|
||||
@@ -2099,11 +2093,12 @@ packages:
|
||||
glob@11.0.3:
|
||||
resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==}
|
||||
engines: {node: 20 || >=22}
|
||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
hasBin: true
|
||||
|
||||
glob@7.2.3:
|
||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||
deprecated: Glob versions prior to v9 are no longer supported
|
||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
|
||||
globals@15.15.0:
|
||||
resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==}
|
||||
@@ -2857,9 +2852,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==}
|
||||
|
||||
@@ -3124,6 +3116,7 @@ packages:
|
||||
tar@6.2.1:
|
||||
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
|
||||
engines: {node: '>=10'}
|
||||
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
|
||||
tarn@3.0.2:
|
||||
resolution: {integrity: sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==}
|
||||
@@ -4710,8 +4703,6 @@ snapshots:
|
||||
|
||||
chownr@2.0.0: {}
|
||||
|
||||
class-transformer@0.5.1: {}
|
||||
|
||||
cli-color@2.0.4:
|
||||
dependencies:
|
||||
d: 1.0.2
|
||||
@@ -5897,8 +5888,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