feat: upgrade buttplug package to protocol v4 and WASM v10
All checks were successful
Build and Push Docker Image to Gitea / build-and-push (push) Successful in 7m30s
All checks were successful
Build and Push Docker Image to Gitea / build-and-push (push) Successful in 7m30s
Upgrade the buttplug TypeScript client from class-based v3 protocol to interface-based v4 protocol, and the Rust/WASM server from the monolithic buttplug 9.0.9 crate to the split buttplug_core/buttplug_server/ buttplug_server_device_config 10.0.0 crates. TypeScript changes: - Messages are now plain interfaces with msgId()/setMsgId() helpers - ActuatorType → OutputType, SensorType → InputType - ScalarCmd/RotateCmd/LinearCmd → OutputCmd, SensorReadCmd → InputCmd - Client.ts → ButtplugClient.ts, new DeviceCommand/DeviceFeature files - Devices getter returns Map instead of array - Removed class-transformer/reflect-metadata dependencies Rust/WASM changes: - Split imports across buttplug_core, buttplug_server, buttplug_server_device_config - Removed ButtplugServerDowngradeWrapper (use ButtplugServer directly) - Replaced ButtplugFuture/ButtplugFutureStateShared with tokio::sync::oneshot - Updated Hardware::new for new 6-arg signature - Uses git fork (valknarthing/buttplug) to fix missing wasm deps in buttplug_core Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,8 +17,10 @@
|
|||||||
"packageManager": "pnpm@10.19.0",
|
"packageManager": "pnpm@10.19.0",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
|
"es5-ext",
|
||||||
|
"esbuild",
|
||||||
"svelte-preprocess",
|
"svelte-preprocess",
|
||||||
"vue-demi"
|
"wasm-pack"
|
||||||
],
|
],
|
||||||
"ignoredBuiltDependencies": [
|
"ignoredBuiltDependencies": [
|
||||||
"@tailwindcss/oxide",
|
"@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]
|
[package]
|
||||||
name = "buttplug_wasm"
|
name = "buttplug_wasm"
|
||||||
version = "9.0.9"
|
version = "10.0.0"
|
||||||
authors = ["Nonpolynomial Labs, LLC <kyle@nonpolynomial.com>"]
|
authors = ["Nonpolynomial Labs, LLC <kyle@nonpolynomial.com>"]
|
||||||
description = "WASM Interop for the Buttplug Intimate Hardware Control Library"
|
description = "WASM Interop for the Buttplug Intimate Hardware Control Library"
|
||||||
license = "BSD-3-Clause"
|
license = "BSD-3-Clause"
|
||||||
@@ -16,9 +16,9 @@ name = "buttplug_wasm"
|
|||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
buttplug = { version = "9.0.9", default-features = false, features = ["wasm"] }
|
buttplug_core = { git = "https://github.com/valknarthing/buttplug.git", default-features = false, features = ["wasm"] }
|
||||||
# buttplug = { path = "../../../buttplug/buttplug", default-features = false, features = ["wasm"] }
|
buttplug_server = { git = "https://github.com/valknarthing/buttplug.git", default-features = false, features = ["wasm"] }
|
||||||
# buttplug_derive = { path = "../buttplug_derive" }
|
buttplug_server_device_config = { git = "https://github.com/valknarthing/buttplug.git" }
|
||||||
js-sys = "0.3.80"
|
js-sys = "0.3.80"
|
||||||
tracing-wasm = "0.2.1"
|
tracing-wasm = "0.2.1"
|
||||||
log-panics = { version = "2.1.0", features = ["with-backtrace"] }
|
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"
|
"build:wasm": "wasm-pack build --out-dir wasm --out-name index --target bundler --release"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"class-transformer": "^0.5.1",
|
|
||||||
"eventemitter3": "^5.0.1",
|
"eventemitter3": "^5.0.1",
|
||||||
"reflect-metadata": "^0.2.2",
|
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
"vite": "^7.1.4",
|
"vite": "^7.1.4",
|
||||||
"vite-plugin-wasm": "3.5.0",
|
"vite-plugin-wasm": "3.5.0",
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
"use strict";
|
'use strict';
|
||||||
|
|
||||||
import { IButtplugClientConnector } from "./IButtplugClientConnector";
|
import { IButtplugClientConnector } from './IButtplugClientConnector';
|
||||||
import { ButtplugMessage } from "../core/Messages";
|
import { ButtplugMessage } from '../core/Messages';
|
||||||
import { ButtplugBrowserWebsocketConnector } from "../utils/ButtplugBrowserWebsocketConnector";
|
import { ButtplugBrowserWebsocketConnector } from '../utils/ButtplugBrowserWebsocketConnector';
|
||||||
|
|
||||||
export class ButtplugBrowserWebsocketClientConnector
|
export class ButtplugBrowserWebsocketClientConnector
|
||||||
extends ButtplugBrowserWebsocketConnector
|
extends ButtplugBrowserWebsocketConnector
|
||||||
@@ -18,7 +18,7 @@ export class ButtplugBrowserWebsocketClientConnector
|
|||||||
{
|
{
|
||||||
public send = (msg: ButtplugMessage): void => {
|
public send = (msg: ButtplugMessage): void => {
|
||||||
if (!this.Connected) {
|
if (!this.Connected) {
|
||||||
throw new Error("ButtplugClient not connected");
|
throw new Error('ButtplugClient not connected');
|
||||||
}
|
}
|
||||||
this.sendMessage(msg);
|
this.sendMessage(msg);
|
||||||
};
|
};
|
||||||
|
|||||||
242
packages/buttplug/src/client/ButtplugClient.ts
Normal file
242
packages/buttplug/src/client/ButtplugClient.ts
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
/*!
|
||||||
|
* Buttplug JS Source Code File - Visit https://buttplug.io for more info about
|
||||||
|
* the project. Licensed under the BSD 3-Clause license. See LICENSE file in the
|
||||||
|
* project root for full license information.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import { ButtplugLogger } from '../core/Logging';
|
||||||
|
import { EventEmitter } from 'eventemitter3';
|
||||||
|
import { ButtplugClientDevice } from './ButtplugClientDevice';
|
||||||
|
import { IButtplugClientConnector } from './IButtplugClientConnector';
|
||||||
|
import { ButtplugMessageSorter } from '../utils/ButtplugMessageSorter';
|
||||||
|
import * as Messages from '../core/Messages';
|
||||||
|
import {
|
||||||
|
ButtplugError,
|
||||||
|
ButtplugInitError,
|
||||||
|
ButtplugMessageError,
|
||||||
|
} from '../core/Exceptions';
|
||||||
|
import { ButtplugClientConnectorException } from './ButtplugClientConnectorException';
|
||||||
|
|
||||||
|
export class ButtplugClient extends EventEmitter {
|
||||||
|
protected _pingTimer: NodeJS.Timeout | null = null;
|
||||||
|
protected _connector: IButtplugClientConnector | null = null;
|
||||||
|
protected _devices: Map<number, ButtplugClientDevice> = new Map();
|
||||||
|
protected _clientName: string;
|
||||||
|
protected _logger = ButtplugLogger.Logger;
|
||||||
|
protected _isScanning = false;
|
||||||
|
private _sorter: ButtplugMessageSorter = new ButtplugMessageSorter(true);
|
||||||
|
|
||||||
|
constructor(clientName = 'Generic Buttplug Client') {
|
||||||
|
super();
|
||||||
|
this._clientName = clientName;
|
||||||
|
this._logger.Debug(`ButtplugClient: Client ${clientName} created.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get connected(): boolean {
|
||||||
|
return this._connector !== null && this._connector.Connected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get devices(): Map<number, ButtplugClientDevice> {
|
||||||
|
// While this function doesn't actually send a message, if we don't have a
|
||||||
|
// connector, we shouldn't have devices.
|
||||||
|
this.checkConnector();
|
||||||
|
return this._devices;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isScanning(): boolean {
|
||||||
|
return this._isScanning;
|
||||||
|
}
|
||||||
|
|
||||||
|
public connect = async (connector: IButtplugClientConnector) => {
|
||||||
|
this._logger.Info(
|
||||||
|
`ButtplugClient: Connecting using ${connector.constructor.name}`
|
||||||
|
);
|
||||||
|
await connector.connect();
|
||||||
|
this._connector = connector;
|
||||||
|
this._connector.addListener('message', this.parseMessages);
|
||||||
|
this._connector.addListener('disconnect', this.disconnectHandler);
|
||||||
|
await this.initializeConnection();
|
||||||
|
};
|
||||||
|
|
||||||
|
public disconnect = async () => {
|
||||||
|
this._logger.Debug('ButtplugClient: Disconnect called');
|
||||||
|
this._devices.clear();
|
||||||
|
this.checkConnector();
|
||||||
|
await this.shutdownConnection();
|
||||||
|
await this._connector!.disconnect();
|
||||||
|
};
|
||||||
|
|
||||||
|
public startScanning = async () => {
|
||||||
|
this._logger.Debug('ButtplugClient: StartScanning called');
|
||||||
|
this._isScanning = true;
|
||||||
|
await this.sendMsgExpectOk({ StartScanning: { Id: 1 } });
|
||||||
|
};
|
||||||
|
|
||||||
|
public stopScanning = async () => {
|
||||||
|
this._logger.Debug('ButtplugClient: StopScanning called');
|
||||||
|
this._isScanning = false;
|
||||||
|
await this.sendMsgExpectOk({ StopScanning: { Id: 1 } });
|
||||||
|
};
|
||||||
|
|
||||||
|
public stopAllDevices = async () => {
|
||||||
|
this._logger.Debug('ButtplugClient: StopAllDevices');
|
||||||
|
await this.sendMsgExpectOk({ StopCmd: { Id: 1, DeviceIndex: undefined, FeatureIndex: undefined, Inputs: true, Outputs: true } });
|
||||||
|
};
|
||||||
|
|
||||||
|
protected disconnectHandler = () => {
|
||||||
|
this._logger.Info('ButtplugClient: Disconnect event receieved.');
|
||||||
|
this.emit('disconnect');
|
||||||
|
};
|
||||||
|
|
||||||
|
protected parseMessages = (msgs: Messages.ButtplugMessage[]) => {
|
||||||
|
const leftoverMsgs = this._sorter.ParseIncomingMessages(msgs);
|
||||||
|
for (const x of leftoverMsgs) {
|
||||||
|
if (x.DeviceList !== undefined) {
|
||||||
|
this.parseDeviceList(x as Messages.DeviceList);
|
||||||
|
break;
|
||||||
|
} else if (x.ScanningFinished !== undefined) {
|
||||||
|
this._isScanning = false;
|
||||||
|
this.emit('scanningfinished', x);
|
||||||
|
} else if (x.InputReading !== undefined) {
|
||||||
|
// TODO this should be emitted from the device or feature, not the client
|
||||||
|
this.emit('inputreading', x);
|
||||||
|
} else {
|
||||||
|
console.log(`Unhandled message: ${x}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
protected initializeConnection = async (): Promise<boolean> => {
|
||||||
|
this.checkConnector();
|
||||||
|
const msg = await this.sendMessage(
|
||||||
|
{
|
||||||
|
RequestServerInfo: {
|
||||||
|
ClientName: this._clientName,
|
||||||
|
Id: 1,
|
||||||
|
ProtocolVersionMajor: Messages.MESSAGE_SPEC_VERSION_MAJOR,
|
||||||
|
ProtocolVersionMinor: Messages.MESSAGE_SPEC_VERSION_MINOR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (msg.ServerInfo !== undefined) {
|
||||||
|
const serverinfo = msg as Messages.ServerInfo;
|
||||||
|
this._logger.Info(
|
||||||
|
`ButtplugClient: Connected to Server ${serverinfo.ServerName}`
|
||||||
|
);
|
||||||
|
// TODO: maybe store server name, do something with message template version?
|
||||||
|
const ping = serverinfo.MaxPingTime;
|
||||||
|
// If the server version is lower than the client version, the server will disconnect here.
|
||||||
|
if (ping > 0) {
|
||||||
|
/*
|
||||||
|
this._pingTimer = setInterval(async () => {
|
||||||
|
// If we've disconnected, stop trying to ping the server.
|
||||||
|
if (!this.Connected) {
|
||||||
|
await this.ShutdownConnection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.SendMessage(new Messages.Ping());
|
||||||
|
} , Math.round(ping / 2));
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
await this.requestDeviceList();
|
||||||
|
return true;
|
||||||
|
} else if (msg.Error !== undefined) {
|
||||||
|
// Disconnect and throw an exception with the error message we got back.
|
||||||
|
// This will usually only error out if we have a version mismatch that the
|
||||||
|
// server has detected.
|
||||||
|
await this._connector!.disconnect();
|
||||||
|
const err = msg.Error as Messages.Error;
|
||||||
|
throw ButtplugError.LogAndError(
|
||||||
|
ButtplugInitError,
|
||||||
|
this._logger,
|
||||||
|
`Cannot connect to server. ${err.ErrorMessage}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseDeviceList = (list: Messages.DeviceList) => {
|
||||||
|
for (let [_, d] of Object.entries(list.Devices)) {
|
||||||
|
if (!this._devices.has(d.DeviceIndex)) {
|
||||||
|
const device = ButtplugClientDevice.fromMsg(
|
||||||
|
d,
|
||||||
|
this.sendMessageClosure
|
||||||
|
);
|
||||||
|
this._logger.Debug(`ButtplugClient: Adding Device: ${device}`);
|
||||||
|
this._devices.set(d.DeviceIndex, device);
|
||||||
|
this.emit('deviceadded', device);
|
||||||
|
} else {
|
||||||
|
this._logger.Debug(`ButtplugClient: Device already added: ${d}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let [index, device] of this._devices.entries()) {
|
||||||
|
if (!list.Devices.hasOwnProperty(index.toString())) {
|
||||||
|
this._devices.delete(index);
|
||||||
|
this.emit('deviceremoved', device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected requestDeviceList = async () => {
|
||||||
|
this.checkConnector();
|
||||||
|
this._logger.Debug('ButtplugClient: ReceiveDeviceList called');
|
||||||
|
const response = (await this.sendMessage(
|
||||||
|
{
|
||||||
|
RequestDeviceList: { Id: 1 }
|
||||||
|
}
|
||||||
|
));
|
||||||
|
this.parseDeviceList(response.DeviceList!);
|
||||||
|
};
|
||||||
|
|
||||||
|
protected shutdownConnection = async () => {
|
||||||
|
await this.stopAllDevices();
|
||||||
|
if (this._pingTimer !== null) {
|
||||||
|
clearInterval(this._pingTimer);
|
||||||
|
this._pingTimer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
protected async sendMessage(
|
||||||
|
msg: Messages.ButtplugMessage
|
||||||
|
): Promise<Messages.ButtplugMessage> {
|
||||||
|
this.checkConnector();
|
||||||
|
const p = this._sorter.PrepareOutgoingMessage(msg);
|
||||||
|
await this._connector!.send(msg);
|
||||||
|
return await p;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected checkConnector() {
|
||||||
|
if (!this.connected) {
|
||||||
|
throw new ButtplugClientConnectorException(
|
||||||
|
'ButtplugClient not connected'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected sendMsgExpectOk = async (
|
||||||
|
msg: Messages.ButtplugMessage
|
||||||
|
): Promise<void> => {
|
||||||
|
const response = await this.sendMessage(msg);
|
||||||
|
if (response.Ok !== undefined) {
|
||||||
|
return;
|
||||||
|
} else if (response.Error !== undefined) {
|
||||||
|
throw ButtplugError.FromError(response as Messages.Error);
|
||||||
|
} else {
|
||||||
|
throw ButtplugError.LogAndError(
|
||||||
|
ButtplugMessageError,
|
||||||
|
this._logger,
|
||||||
|
`Message ${response} not handled by SendMsgExpectOk`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
protected sendMessageClosure = async (
|
||||||
|
msg: Messages.ButtplugMessage
|
||||||
|
): Promise<Messages.ButtplugMessage> => {
|
||||||
|
return await this.sendMessage(msg);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -6,8 +6,8 @@
|
|||||||
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ButtplugError } from "../core/Exceptions";
|
import { ButtplugError } from '../core/Exceptions';
|
||||||
import * as Messages from "../core/Messages";
|
import * as Messages from '../core/Messages';
|
||||||
|
|
||||||
export class ButtplugClientConnectorException extends ButtplugError {
|
export class ButtplugClientConnectorException extends ButtplugError {
|
||||||
public constructor(message: string) {
|
public constructor(message: string) {
|
||||||
|
|||||||
@@ -6,20 +6,24 @@
|
|||||||
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
"use strict";
|
'use strict';
|
||||||
import * as Messages from "../core/Messages";
|
import * as Messages from '../core/Messages';
|
||||||
import {
|
import {
|
||||||
ButtplugDeviceError,
|
ButtplugDeviceError,
|
||||||
ButtplugError,
|
ButtplugError,
|
||||||
ButtplugMessageError,
|
ButtplugMessageError,
|
||||||
} from "../core/Exceptions";
|
} from '../core/Exceptions';
|
||||||
import { EventEmitter } from "eventemitter3";
|
import { EventEmitter } from 'eventemitter3';
|
||||||
import { getMessageClassFromMessage } from "../core/MessageUtils";
|
import { ButtplugClientDeviceFeature } from './ButtplugClientDeviceFeature';
|
||||||
|
import { DeviceOutputCommand } from './ButtplugClientDeviceCommand';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents an abstract device, capable of taking certain kinds of messages.
|
* Represents an abstract device, capable of taking certain kinds of messages.
|
||||||
*/
|
*/
|
||||||
export class ButtplugClientDevice extends EventEmitter {
|
export class ButtplugClientDevice extends EventEmitter {
|
||||||
|
|
||||||
|
private _features: Map<number, ButtplugClientDeviceFeature>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the name of the device.
|
* Return the name of the device.
|
||||||
*/
|
*/
|
||||||
@@ -48,354 +52,114 @@ export class ButtplugClientDevice extends EventEmitter {
|
|||||||
return this._deviceInfo.DeviceMessageTimingGap;
|
return this._deviceInfo.DeviceMessageTimingGap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public get features(): Map<number, ButtplugClientDeviceFeature> {
|
||||||
* Return a list of message types the device accepts.
|
return this._features;
|
||||||
*/
|
|
||||||
public get messageAttributes(): Messages.MessageAttributes {
|
|
||||||
return this._deviceInfo.DeviceMessages;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static fromMsg(
|
public static fromMsg(
|
||||||
msg: Messages.DeviceInfo,
|
msg: Messages.DeviceInfo,
|
||||||
sendClosure: (
|
sendClosure: (
|
||||||
device: ButtplugClientDevice,
|
msg: Messages.ButtplugMessage
|
||||||
msg: Messages.ButtplugDeviceMessage,
|
) => Promise<Messages.ButtplugMessage>
|
||||||
) => Promise<Messages.ButtplugMessage>,
|
|
||||||
): ButtplugClientDevice {
|
): ButtplugClientDevice {
|
||||||
return new ButtplugClientDevice(msg, sendClosure);
|
return new ButtplugClientDevice(msg, sendClosure);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map of messages and their attributes (feature count, etc...)
|
|
||||||
private allowedMsgs: Map<string, Messages.MessageAttributes> = new Map<
|
|
||||||
string,
|
|
||||||
Messages.MessageAttributes
|
|
||||||
>();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param _index Index of the device, as created by the device manager.
|
* @param _index Index of the device, as created by the device manager.
|
||||||
* @param _name Name of the device.
|
* @param _name Name of the device.
|
||||||
* @param allowedMsgs Buttplug messages the device can receive.
|
* @param allowedMsgs Buttplug messages the device can receive.
|
||||||
*/
|
*/
|
||||||
constructor(
|
private constructor(
|
||||||
private _deviceInfo: Messages.DeviceInfo,
|
private _deviceInfo: Messages.DeviceInfo,
|
||||||
private _sendClosure: (
|
private _sendClosure: (
|
||||||
device: ButtplugClientDevice,
|
msg: Messages.ButtplugMessage
|
||||||
msg: Messages.ButtplugDeviceMessage,
|
) => Promise<Messages.ButtplugMessage>
|
||||||
) => Promise<Messages.ButtplugMessage>,
|
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
_deviceInfo.DeviceMessages.update();
|
this._features = new Map(Object.entries(_deviceInfo.DeviceFeatures).map(([index, v]) => [parseInt(index), new ButtplugClientDeviceFeature(_deviceInfo.DeviceIndex, _deviceInfo.DeviceName, v, _sendClosure)]));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async send(
|
public async send(
|
||||||
msg: Messages.ButtplugDeviceMessage,
|
msg: Messages.ButtplugMessage
|
||||||
): Promise<Messages.ButtplugMessage> {
|
): Promise<Messages.ButtplugMessage> {
|
||||||
// Assume we're getting the closure from ButtplugClient, which does all of
|
// Assume we're getting the closure from ButtplugClient, which does all of
|
||||||
// the index/existence/connection/message checks for us.
|
// the index/existence/connection/message checks for us.
|
||||||
return await this._sendClosure(this, msg);
|
return await this._sendClosure(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async sendExpectOk(
|
protected sendMsgExpectOk = async (
|
||||||
msg: Messages.ButtplugDeviceMessage,
|
msg: Messages.ButtplugMessage
|
||||||
): Promise<void> {
|
): Promise<void> => {
|
||||||
const response = await this.send(msg);
|
const response = await this.send(msg);
|
||||||
switch (getMessageClassFromMessage(response)) {
|
if (response.Ok !== undefined) {
|
||||||
case Messages.Ok:
|
|
||||||
return;
|
return;
|
||||||
case Messages.Error:
|
} else if (response.Error !== undefined) {
|
||||||
throw ButtplugError.FromError(response as Messages.Error);
|
throw ButtplugError.FromError(response as Messages.Error);
|
||||||
default:
|
|
||||||
throw new ButtplugMessageError(
|
|
||||||
`Message type ${response.constructor} not handled by SendMsgExpectOk`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async scalar(
|
|
||||||
scalar: Messages.ScalarSubcommand | Messages.ScalarSubcommand[],
|
|
||||||
): Promise<void> {
|
|
||||||
if (Array.isArray(scalar)) {
|
|
||||||
await this.sendExpectOk(new Messages.ScalarCmd(scalar, this.index));
|
|
||||||
} else {
|
} else {
|
||||||
await this.sendExpectOk(new Messages.ScalarCmd([scalar], this.index));
|
/*
|
||||||
|
throw ButtplugError.LogAndError(
|
||||||
|
ButtplugMessageError,
|
||||||
|
this._logger,
|
||||||
|
`Message ${response} not handled by SendMsgExpectOk`
|
||||||
|
);
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
protected isOutputValid(featureIndex: number, type: Messages.OutputType) {
|
||||||
|
if (!this._deviceInfo.DeviceFeatures.hasOwnProperty(featureIndex.toString())) {
|
||||||
|
throw new ButtplugDeviceError(`Feature index ${featureIndex} does not exist for device ${this.name}`);
|
||||||
|
}
|
||||||
|
if (this._deviceInfo.DeviceFeatures[featureIndex.toString()].Outputs !== undefined && !this._deviceInfo.DeviceFeatures[featureIndex.toString()].Outputs.hasOwnProperty(type)) {
|
||||||
|
throw new ButtplugDeviceError(`Feature index ${featureIndex} does not support type ${type} for device ${this.name}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async scalarCommandBuilder(
|
public hasOutput(type: Messages.OutputType): boolean {
|
||||||
speed: number | number[],
|
return this._features.values().filter((f) => f.hasOutput(type)).toArray().length > 0;
|
||||||
actuator: Messages.ActuatorType,
|
|
||||||
) {
|
|
||||||
const scalarAttrs = this.messageAttributes.ScalarCmd?.filter(
|
|
||||||
(x) => x.ActuatorType === actuator,
|
|
||||||
);
|
|
||||||
if (!scalarAttrs || scalarAttrs.length === 0) {
|
|
||||||
throw new ButtplugDeviceError(
|
|
||||||
`Device ${this.name} has no ${actuator} capabilities`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const cmds: Messages.ScalarSubcommand[] = [];
|
|
||||||
if (typeof speed === "number") {
|
|
||||||
scalarAttrs.forEach((x) =>
|
|
||||||
cmds.push(new Messages.ScalarSubcommand(x.Index, speed, actuator)),
|
|
||||||
);
|
|
||||||
} else if (Array.isArray(speed)) {
|
|
||||||
if (speed.length > scalarAttrs.length) {
|
|
||||||
throw new ButtplugDeviceError(
|
|
||||||
`${speed.length} commands send to a device with ${scalarAttrs.length} vibrators`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
scalarAttrs.forEach((x, i) => {
|
|
||||||
cmds.push(new Messages.ScalarSubcommand(x.Index, speed[i], actuator));
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw new ButtplugDeviceError(
|
|
||||||
`${actuator} can only take numbers or arrays of numbers.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await this.scalar(cmds);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get vibrateAttributes(): Messages.GenericDeviceMessageAttributes[] {
|
public hasInput(type: Messages.InputType): boolean {
|
||||||
return (
|
return this._features.values().filter((f) => f.hasInput(type)).toArray().length > 0;
|
||||||
this.messageAttributes.ScalarCmd?.filter(
|
|
||||||
(x) => x.ActuatorType === Messages.ActuatorType.Vibrate,
|
|
||||||
) ?? []
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async vibrate(speed: number | number[]): Promise<void> {
|
public async runOutput(cmd: DeviceOutputCommand): Promise<void> {
|
||||||
await this.scalarCommandBuilder(speed, Messages.ActuatorType.Vibrate);
|
let p: Promise<void>[] = [];
|
||||||
}
|
for (let f of this._features.values()) {
|
||||||
|
if (f.hasOutput(cmd.outputType)) {
|
||||||
public get oscillateAttributes(): Messages.GenericDeviceMessageAttributes[] {
|
p.push(f.runOutput(cmd));
|
||||||
return (
|
|
||||||
this.messageAttributes.ScalarCmd?.filter(
|
|
||||||
(x) => x.ActuatorType === Messages.ActuatorType.Oscillate,
|
|
||||||
) ?? []
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async oscillate(speed: number | number[]): Promise<void> {
|
|
||||||
await this.scalarCommandBuilder(speed, Messages.ActuatorType.Oscillate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public get rotateAttributes(): Messages.GenericDeviceMessageAttributes[] {
|
|
||||||
return this.messageAttributes.RotateCmd ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public async rotate(
|
|
||||||
values: number | [number, boolean][],
|
|
||||||
clockwise?: boolean,
|
|
||||||
): Promise<void> {
|
|
||||||
const rotateAttrs = this.messageAttributes.RotateCmd;
|
|
||||||
if (!rotateAttrs || rotateAttrs.length === 0) {
|
|
||||||
throw new ButtplugDeviceError(
|
|
||||||
`Device ${this.name} has no Rotate capabilities`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let msg: Messages.RotateCmd;
|
|
||||||
if (typeof values === "number") {
|
|
||||||
msg = Messages.RotateCmd.Create(
|
|
||||||
this.index,
|
|
||||||
new Array(rotateAttrs.length).fill([values, clockwise]),
|
|
||||||
);
|
|
||||||
} else if (Array.isArray(values)) {
|
|
||||||
msg = Messages.RotateCmd.Create(this.index, values);
|
|
||||||
} else {
|
|
||||||
throw new ButtplugDeviceError(
|
|
||||||
"SendRotateCmd can only take a number and boolean, or an array of number/boolean tuples",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await this.sendExpectOk(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
public get linearAttributes(): Messages.GenericDeviceMessageAttributes[] {
|
|
||||||
return this.messageAttributes.LinearCmd ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public async linear(
|
|
||||||
values: number | [number, number][],
|
|
||||||
duration?: number,
|
|
||||||
): Promise<void> {
|
|
||||||
const linearAttrs = this.messageAttributes.LinearCmd;
|
|
||||||
if (!linearAttrs || linearAttrs.length === 0) {
|
|
||||||
throw new ButtplugDeviceError(
|
|
||||||
`Device ${this.name} has no Linear capabilities`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let msg: Messages.LinearCmd;
|
|
||||||
if (typeof values === "number") {
|
|
||||||
msg = Messages.LinearCmd.Create(
|
|
||||||
this.index,
|
|
||||||
new Array(linearAttrs.length).fill([values, duration]),
|
|
||||||
);
|
|
||||||
} else if (Array.isArray(values)) {
|
|
||||||
msg = Messages.LinearCmd.Create(this.index, values);
|
|
||||||
} else {
|
|
||||||
throw new ButtplugDeviceError(
|
|
||||||
"SendLinearCmd can only take a number and number, or an array of number/number tuples",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await this.sendExpectOk(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async sensorRead(
|
|
||||||
sensorIndex: number,
|
|
||||||
sensorType: Messages.SensorType,
|
|
||||||
): Promise<number[]> {
|
|
||||||
const response = await this.send(
|
|
||||||
new Messages.SensorReadCmd(this.index, sensorIndex, sensorType),
|
|
||||||
);
|
|
||||||
switch (getMessageClassFromMessage(response)) {
|
|
||||||
case Messages.SensorReading:
|
|
||||||
return (response as Messages.SensorReading).Data;
|
|
||||||
case Messages.Error:
|
|
||||||
throw ButtplugError.FromError(response as Messages.Error);
|
|
||||||
default:
|
|
||||||
throw new ButtplugMessageError(
|
|
||||||
`Message type ${response.constructor} not handled by sensorRead`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (p.length == 0) {
|
||||||
public get hasBattery(): boolean {
|
return Promise.reject(`No features with output type ${cmd.outputType}`);
|
||||||
const batteryAttrs = this.messageAttributes.SensorReadCmd?.filter(
|
|
||||||
(x) => x.SensorType === Messages.SensorType.Battery,
|
|
||||||
);
|
|
||||||
return batteryAttrs !== undefined && batteryAttrs.length > 0;
|
|
||||||
}
|
}
|
||||||
|
await Promise.all(p);
|
||||||
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> {
|
public async stop(): Promise<void> {
|
||||||
await this.sendExpectOk(new Messages.StopDeviceCmd(this.index));
|
await this.sendMsgExpectOk({StopCmd: { Id: 1, DeviceIndex: this.index, FeatureIndex: undefined, Inputs: true, Outputs: true}});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async battery(): Promise<number> {
|
||||||
|
let p: Promise<void>[] = [];
|
||||||
|
for (let f of this._features.values()) {
|
||||||
|
if (f.hasInput(Messages.InputType.Battery)) {
|
||||||
|
// Right now, we only have one battery per device, so assume the first one we find is it.
|
||||||
|
let response = await f.runInput(Messages.InputType.Battery, Messages.InputCommandType.Read);
|
||||||
|
if (response === undefined) {
|
||||||
|
throw new ButtplugMessageError("Got incorrect message back.");
|
||||||
|
}
|
||||||
|
if (response.Reading[Messages.InputType.Battery] === undefined) {
|
||||||
|
throw new ButtplugMessageError("Got reading with no Battery info.");
|
||||||
|
}
|
||||||
|
return response.Reading[Messages.InputType.Battery].Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new ButtplugDeviceError(`No battery present on this device.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public emitDisconnected() {
|
public emitDisconnected() {
|
||||||
this.emit("deviceremoved");
|
this.emit('deviceremoved');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
111
packages/buttplug/src/client/ButtplugClientDeviceCommand.ts
Normal file
111
packages/buttplug/src/client/ButtplugClientDeviceCommand.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { ButtplugDeviceError } from "../core/Exceptions";
|
||||||
|
import { OutputType } from "../core/Messages";
|
||||||
|
|
||||||
|
class PercentOrSteps {
|
||||||
|
private _percent: number | undefined;
|
||||||
|
private _steps: number | undefined;
|
||||||
|
|
||||||
|
public get percent() {
|
||||||
|
return this._percent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get steps() {
|
||||||
|
return this._steps;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static createSteps(s: number): PercentOrSteps {
|
||||||
|
let v = new PercentOrSteps;
|
||||||
|
v._steps = s;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static createPercent(p: number): PercentOrSteps {
|
||||||
|
if (p < 0 || p > 1.0) {
|
||||||
|
throw new ButtplugDeviceError(`Percent value ${p} is not in the range 0.0 <= x <= 1.0`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let v = new PercentOrSteps;
|
||||||
|
v._percent = p;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DeviceOutputCommand {
|
||||||
|
public constructor(
|
||||||
|
private _outputType: OutputType,
|
||||||
|
private _value: PercentOrSteps,
|
||||||
|
private _duration?: number,
|
||||||
|
)
|
||||||
|
{}
|
||||||
|
|
||||||
|
public get outputType() {
|
||||||
|
return this._outputType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get value() {
|
||||||
|
return this._value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get duration() {
|
||||||
|
return this._duration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DeviceOutputValueConstructor {
|
||||||
|
public constructor(
|
||||||
|
private _outputType: OutputType)
|
||||||
|
{}
|
||||||
|
|
||||||
|
public steps(steps: number): DeviceOutputCommand {
|
||||||
|
return new DeviceOutputCommand(this._outputType, PercentOrSteps.createSteps(steps), undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
public percent(percent: number): DeviceOutputCommand {
|
||||||
|
return new DeviceOutputCommand(this._outputType, PercentOrSteps.createPercent(percent), undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DeviceOutputPositionWithDurationConstructor {
|
||||||
|
public steps(steps: number, duration: number): DeviceOutputCommand {
|
||||||
|
return new DeviceOutputCommand(OutputType.Position, PercentOrSteps.createSteps(steps), duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
public percent(percent: number, duration: number): DeviceOutputCommand {
|
||||||
|
return new DeviceOutputCommand(OutputType.HwPositionWithDuration, PercentOrSteps.createPercent(percent), duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DeviceOutput {
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
public static get Vibrate() {
|
||||||
|
return new DeviceOutputValueConstructor(OutputType.Vibrate);
|
||||||
|
}
|
||||||
|
public static get Rotate() {
|
||||||
|
return new DeviceOutputValueConstructor(OutputType.Rotate);
|
||||||
|
}
|
||||||
|
public static get Oscillate() {
|
||||||
|
return new DeviceOutputValueConstructor(OutputType.Oscillate);
|
||||||
|
}
|
||||||
|
public static get Constrict() {
|
||||||
|
return new DeviceOutputValueConstructor(OutputType.Constrict);
|
||||||
|
}
|
||||||
|
public static get Inflate() {
|
||||||
|
return new DeviceOutputValueConstructor(OutputType.Inflate);
|
||||||
|
}
|
||||||
|
public static get Temperature() {
|
||||||
|
return new DeviceOutputValueConstructor(OutputType.Temperature);
|
||||||
|
}
|
||||||
|
public static get Led() {
|
||||||
|
return new DeviceOutputValueConstructor(OutputType.Led);
|
||||||
|
}
|
||||||
|
public static get Spray() {
|
||||||
|
return new DeviceOutputValueConstructor(OutputType.Spray);
|
||||||
|
}
|
||||||
|
public static get Position() {
|
||||||
|
return new DeviceOutputValueConstructor(OutputType.Position);
|
||||||
|
}
|
||||||
|
public static get PositionWithDuration() {
|
||||||
|
return new DeviceOutputPositionWithDurationConstructor();
|
||||||
|
}
|
||||||
|
}
|
||||||
168
packages/buttplug/src/client/ButtplugClientDeviceFeature.ts
Normal file
168
packages/buttplug/src/client/ButtplugClientDeviceFeature.ts
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import { ButtplugDeviceError, ButtplugError, ButtplugMessageError } from "../core/Exceptions";
|
||||||
|
import * as Messages from "../core/Messages";
|
||||||
|
import { DeviceOutputCommand } from "./ButtplugClientDeviceCommand";
|
||||||
|
|
||||||
|
export class ButtplugClientDeviceFeature {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private _deviceIndex: number,
|
||||||
|
private _deviceName: string,
|
||||||
|
private _feature: Messages.DeviceFeature,
|
||||||
|
private _sendClosure: (
|
||||||
|
msg: Messages.ButtplugMessage
|
||||||
|
) => Promise<Messages.ButtplugMessage>) {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected send = async (msg: Messages.ButtplugMessage): Promise<Messages.ButtplugMessage> => {
|
||||||
|
return await this._sendClosure(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected sendMsgExpectOk = async (
|
||||||
|
msg: Messages.ButtplugMessage
|
||||||
|
): Promise<void> => {
|
||||||
|
const response = await this.send(msg);
|
||||||
|
if (response.Ok !== undefined) {
|
||||||
|
return;
|
||||||
|
} else if (response.Error !== undefined) {
|
||||||
|
throw ButtplugError.FromError(response as Messages.Error);
|
||||||
|
} else {
|
||||||
|
throw new ButtplugMessageError("Expected Ok or Error, and didn't get either!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
protected isOutputValid(type: Messages.OutputType) {
|
||||||
|
if (this._feature.Output !== undefined && !this._feature.Output.hasOwnProperty(type)) {
|
||||||
|
throw new ButtplugDeviceError(`Feature index ${this._feature.FeatureIndex} does not support type ${type} for device ${this._deviceName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected isInputValid(type: Messages.InputType) {
|
||||||
|
if (this._feature.Input !== undefined && !this._feature.Input.hasOwnProperty(type)) {
|
||||||
|
throw new ButtplugDeviceError(`Feature index ${this._feature.FeatureIndex} does not support type ${type} for device ${this._deviceName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async sendOutputCmd(command: DeviceOutputCommand): Promise<void> {
|
||||||
|
// Make sure the requested feature is valid
|
||||||
|
this.isOutputValid(command.outputType);
|
||||||
|
if (command.value === undefined) {
|
||||||
|
throw new ButtplugDeviceError(`${command.outputType} requires value defined`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let type = command.outputType;
|
||||||
|
let duration: undefined | number = undefined;
|
||||||
|
if (type == Messages.OutputType.HwPositionWithDuration) {
|
||||||
|
if (command.duration === undefined) {
|
||||||
|
throw new ButtplugDeviceError("PositionWithDuration requires duration defined");
|
||||||
|
}
|
||||||
|
duration = command.duration;
|
||||||
|
}
|
||||||
|
let value: number;
|
||||||
|
let p = command.value;
|
||||||
|
if (p.percent === undefined) {
|
||||||
|
// TODO Check step limits here
|
||||||
|
value = command.value.steps!;
|
||||||
|
} else {
|
||||||
|
value = Math.ceil(this._feature.Output[type]!.Value![1] * p.percent);
|
||||||
|
}
|
||||||
|
let newCommand: Messages.DeviceFeatureOutput = { Value: value, Duration: duration };
|
||||||
|
let outCommand = {};
|
||||||
|
outCommand[type.toString()] = newCommand;
|
||||||
|
|
||||||
|
let cmd: Messages.ButtplugMessage = {
|
||||||
|
OutputCmd: {
|
||||||
|
Id: 1,
|
||||||
|
DeviceIndex: this._deviceIndex,
|
||||||
|
FeatureIndex: this._feature.FeatureIndex,
|
||||||
|
Command: outCommand
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await this.sendMsgExpectOk(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get featureDescriptor(): string {
|
||||||
|
return this._feature.FeatureDescriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get featureIndex(): number {
|
||||||
|
return this._feature.FeatureIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get outputTypes(): Messages.OutputType[] {
|
||||||
|
if (this._feature.Output === undefined) return [];
|
||||||
|
return Object.keys(this._feature.Output) as Messages.OutputType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
public get inputTypes(): Messages.InputType[] {
|
||||||
|
if (this._feature.Input === undefined) return [];
|
||||||
|
return Object.keys(this._feature.Input) as Messages.InputType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
public outputMaxValue(type: Messages.OutputType): number {
|
||||||
|
if (this._feature.Output === undefined || this._feature.Output[type] === undefined) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const val = this._feature.Output[type]!.Value;
|
||||||
|
// Value can arrive as number[] [min, max] from server or as number
|
||||||
|
if (Array.isArray(val)) {
|
||||||
|
return val[val.length - 1];
|
||||||
|
}
|
||||||
|
return val as number;
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasOutput(type: Messages.OutputType): boolean {
|
||||||
|
if (this._feature.Output !== undefined) {
|
||||||
|
return this._feature.Output.hasOwnProperty(type.toString());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasInput(type: Messages.InputType): boolean {
|
||||||
|
if (this._feature.Input !== undefined) {
|
||||||
|
return this._feature.Input.hasOwnProperty(type.toString());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async runOutput(cmd: DeviceOutputCommand): Promise<void> {
|
||||||
|
if (this._feature.Output !== undefined && this._feature.Output.hasOwnProperty(cmd.outputType.toString())) {
|
||||||
|
return this.sendOutputCmd(cmd);
|
||||||
|
}
|
||||||
|
throw new ButtplugDeviceError(`Output type ${cmd.outputType} not supported by feature.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async runInput(inputType: Messages.InputType, inputCommand: Messages.InputCommandType): Promise<Messages.InputReading | undefined> {
|
||||||
|
// Make sure the requested feature is valid
|
||||||
|
this.isInputValid(inputType);
|
||||||
|
let inputAttributes = this._feature.Input[inputType];
|
||||||
|
console.log(this._feature.Input);
|
||||||
|
if ((inputCommand === Messages.InputCommandType.Unsubscribe && !inputAttributes.Command.includes(Messages.InputCommandType.Subscribe)) && !inputAttributes.Command.includes(inputCommand)) {
|
||||||
|
throw new ButtplugDeviceError(`${inputType} does not support command ${inputCommand}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let cmd: Messages.ButtplugMessage = {
|
||||||
|
InputCmd: {
|
||||||
|
Id: 1,
|
||||||
|
DeviceIndex: this._deviceIndex,
|
||||||
|
FeatureIndex: this._feature.FeatureIndex,
|
||||||
|
Type: inputType,
|
||||||
|
Command: inputCommand,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (inputCommand == Messages.InputCommandType.Read) {
|
||||||
|
const response = await this.send(cmd);
|
||||||
|
if (response.InputReading !== undefined) {
|
||||||
|
return response.InputReading;
|
||||||
|
} else if (response.Error !== undefined) {
|
||||||
|
throw ButtplugError.FromError(response as Messages.Error);
|
||||||
|
} else {
|
||||||
|
throw new ButtplugMessageError("Expected InputReading or Error, and didn't get either!");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`Sending subscribe message: ${JSON.stringify(cmd)}`);
|
||||||
|
await this.sendMsgExpectOk(cmd);
|
||||||
|
console.log("Got back ok?");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,10 +6,10 @@
|
|||||||
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
"use strict";
|
'use strict';
|
||||||
|
|
||||||
import { ButtplugBrowserWebsocketClientConnector } from "./ButtplugBrowserWebsocketClientConnector";
|
import { ButtplugBrowserWebsocketClientConnector } from './ButtplugBrowserWebsocketClientConnector';
|
||||||
import { WebSocket as NodeWebSocket } from "ws";
|
import { WebSocket as NodeWebSocket } from 'ws';
|
||||||
|
|
||||||
export class ButtplugNodeWebsocketClientConnector extends ButtplugBrowserWebsocketClientConnector {
|
export class ButtplugNodeWebsocketClientConnector extends ButtplugBrowserWebsocketClientConnector {
|
||||||
protected _websocketConstructor =
|
protected _websocketConstructor =
|
||||||
|
|||||||
@@ -1,276 +0,0 @@
|
|||||||
/*!
|
|
||||||
* Buttplug JS Source Code File - Visit https://buttplug.io for more info about
|
|
||||||
* the project. Licensed under the BSD 3-Clause license. See LICENSE file in the
|
|
||||||
* project root for full license information.
|
|
||||||
*
|
|
||||||
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
import { ButtplugLogger } from "../core/Logging";
|
|
||||||
import { EventEmitter } from "eventemitter3";
|
|
||||||
import { ButtplugClientDevice } from "./ButtplugClientDevice";
|
|
||||||
import { IButtplugClientConnector } from "./IButtplugClientConnector";
|
|
||||||
import { ButtplugMessageSorter } from "../utils/ButtplugMessageSorter";
|
|
||||||
|
|
||||||
import * as Messages from "../core/Messages";
|
|
||||||
import {
|
|
||||||
ButtplugDeviceError,
|
|
||||||
ButtplugError,
|
|
||||||
ButtplugInitError,
|
|
||||||
ButtplugMessageError,
|
|
||||||
} from "../core/Exceptions";
|
|
||||||
import { ButtplugClientConnectorException } from "./ButtplugClientConnectorException";
|
|
||||||
import { getMessageClassFromMessage } from "../core/MessageUtils";
|
|
||||||
|
|
||||||
export class ButtplugClient extends EventEmitter {
|
|
||||||
protected _pingTimer: NodeJS.Timeout | null = null;
|
|
||||||
protected _connector: IButtplugClientConnector | null = null;
|
|
||||||
protected _devices: Map<number, ButtplugClientDevice> = new Map();
|
|
||||||
protected _clientName: string;
|
|
||||||
protected _logger = ButtplugLogger.Logger;
|
|
||||||
protected _isScanning = false;
|
|
||||||
private _sorter: ButtplugMessageSorter = new ButtplugMessageSorter(true);
|
|
||||||
|
|
||||||
constructor(clientName = "Generic Buttplug Client") {
|
|
||||||
super();
|
|
||||||
this._clientName = clientName;
|
|
||||||
this._logger.Debug(`ButtplugClient: Client ${clientName} created.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
public get connected(): boolean {
|
|
||||||
return this._connector !== null && this._connector.Connected;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get devices(): ButtplugClientDevice[] {
|
|
||||||
// While this function doesn't actually send a message, if we don't have a
|
|
||||||
// connector, we shouldn't have devices.
|
|
||||||
this.checkConnector();
|
|
||||||
const devices: ButtplugClientDevice[] = [];
|
|
||||||
this._devices.forEach((d) => {
|
|
||||||
devices.push(d);
|
|
||||||
});
|
|
||||||
return devices;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get isScanning(): boolean {
|
|
||||||
return this._isScanning;
|
|
||||||
}
|
|
||||||
|
|
||||||
public connect = async (connector: IButtplugClientConnector) => {
|
|
||||||
this._logger.Info(
|
|
||||||
`ButtplugClient: Connecting using ${connector.constructor.name}`,
|
|
||||||
);
|
|
||||||
await connector.connect();
|
|
||||||
this._connector = connector;
|
|
||||||
this._connector.addListener("message", this.parseMessages);
|
|
||||||
this._connector.addListener("disconnect", this.disconnectHandler);
|
|
||||||
await this.initializeConnection();
|
|
||||||
};
|
|
||||||
|
|
||||||
public disconnect = async () => {
|
|
||||||
this._logger.Debug("ButtplugClient: Disconnect called");
|
|
||||||
this.checkConnector();
|
|
||||||
await this.shutdownConnection();
|
|
||||||
await this._connector!.disconnect();
|
|
||||||
};
|
|
||||||
|
|
||||||
public startScanning = async () => {
|
|
||||||
this._logger.Debug("ButtplugClient: StartScanning called");
|
|
||||||
this._isScanning = true;
|
|
||||||
await this.sendMsgExpectOk(new Messages.StartScanning());
|
|
||||||
};
|
|
||||||
|
|
||||||
public stopScanning = async () => {
|
|
||||||
this._logger.Debug("ButtplugClient: StopScanning called");
|
|
||||||
this._isScanning = false;
|
|
||||||
await this.sendMsgExpectOk(new Messages.StopScanning());
|
|
||||||
};
|
|
||||||
|
|
||||||
public stopAllDevices = async () => {
|
|
||||||
this._logger.Debug("ButtplugClient: StopAllDevices");
|
|
||||||
await this.sendMsgExpectOk(new Messages.StopAllDevices());
|
|
||||||
};
|
|
||||||
|
|
||||||
private async sendDeviceMessage(
|
|
||||||
device: ButtplugClientDevice,
|
|
||||||
deviceMsg: Messages.ButtplugDeviceMessage,
|
|
||||||
): Promise<Messages.ButtplugMessage> {
|
|
||||||
this.checkConnector();
|
|
||||||
const dev = this._devices.get(device.index);
|
|
||||||
if (dev === undefined) {
|
|
||||||
throw ButtplugError.LogAndError(
|
|
||||||
ButtplugDeviceError,
|
|
||||||
this._logger,
|
|
||||||
`Device ${device.index} not available.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
deviceMsg.DeviceIndex = device.index;
|
|
||||||
return await this.sendMessage(deviceMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected disconnectHandler = () => {
|
|
||||||
this._logger.Info("ButtplugClient: Disconnect event receieved.");
|
|
||||||
this.emit("disconnect");
|
|
||||||
};
|
|
||||||
|
|
||||||
protected parseMessages = (msgs: Messages.ButtplugMessage[]) => {
|
|
||||||
const leftoverMsgs = this._sorter.ParseIncomingMessages(msgs);
|
|
||||||
for (const x of leftoverMsgs) {
|
|
||||||
switch (getMessageClassFromMessage(x)) {
|
|
||||||
case Messages.DeviceAdded: {
|
|
||||||
const addedMsg = x as Messages.DeviceAdded;
|
|
||||||
const addedDevice = ButtplugClientDevice.fromMsg(
|
|
||||||
addedMsg,
|
|
||||||
this.sendDeviceMessageClosure,
|
|
||||||
);
|
|
||||||
this._devices.set(addedMsg.DeviceIndex, addedDevice);
|
|
||||||
this.emit("deviceadded", addedMsg, addedDevice);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case Messages.DeviceRemoved: {
|
|
||||||
const removedMsg = x as Messages.DeviceRemoved;
|
|
||||||
if (this._devices.has(removedMsg.DeviceIndex)) {
|
|
||||||
const removedDevice = this._devices.get(removedMsg.DeviceIndex);
|
|
||||||
removedDevice?.emitDisconnected();
|
|
||||||
this._devices.delete(removedMsg.DeviceIndex);
|
|
||||||
this.emit("deviceremoved", removedMsg, removedDevice);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case Messages.ScanningFinished:
|
|
||||||
this._isScanning = false;
|
|
||||||
this.emit("scanningfinished", x);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
protected initializeConnection = async (): Promise<boolean> => {
|
|
||||||
this.checkConnector();
|
|
||||||
const msg = await this.sendMessage(
|
|
||||||
new Messages.RequestServerInfo(
|
|
||||||
this._clientName,
|
|
||||||
Messages.MESSAGE_SPEC_VERSION,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
switch (getMessageClassFromMessage(msg)) {
|
|
||||||
case Messages.ServerInfo: {
|
|
||||||
const serverinfo = msg as Messages.ServerInfo;
|
|
||||||
this._logger.Info(
|
|
||||||
`ButtplugClient: Connected to Server ${serverinfo.ServerName}`,
|
|
||||||
);
|
|
||||||
// TODO: maybe store server name, do something with message template version?
|
|
||||||
const ping = serverinfo.MaxPingTime;
|
|
||||||
if (serverinfo.MessageVersion < Messages.MESSAGE_SPEC_VERSION) {
|
|
||||||
// Disconnect and throw an exception explaining the version mismatch problem.
|
|
||||||
await this._connector!.disconnect();
|
|
||||||
throw ButtplugError.LogAndError(
|
|
||||||
ButtplugInitError,
|
|
||||||
this._logger,
|
|
||||||
`Server protocol version ${serverinfo.MessageVersion} is older than client protocol version ${Messages.MESSAGE_SPEC_VERSION}. Please update server.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (ping > 0) {
|
|
||||||
/*
|
|
||||||
this._pingTimer = setInterval(async () => {
|
|
||||||
// If we've disconnected, stop trying to ping the server.
|
|
||||||
if (!this.Connected) {
|
|
||||||
await this.ShutdownConnection();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await this.SendMessage(new Messages.Ping());
|
|
||||||
} , Math.round(ping / 2));
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
await this.requestDeviceList();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case Messages.Error: {
|
|
||||||
// Disconnect and throw an exception with the error message we got back.
|
|
||||||
// This will usually only error out if we have a version mismatch that the
|
|
||||||
// server has detected.
|
|
||||||
await this._connector!.disconnect();
|
|
||||||
const err = msg as Messages.Error;
|
|
||||||
throw ButtplugError.LogAndError(
|
|
||||||
ButtplugInitError,
|
|
||||||
this._logger,
|
|
||||||
`Cannot connect to server. ${err.ErrorMessage}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
protected requestDeviceList = async () => {
|
|
||||||
this.checkConnector();
|
|
||||||
this._logger.Debug("ButtplugClient: ReceiveDeviceList called");
|
|
||||||
const deviceList = (await this.sendMessage(
|
|
||||||
new Messages.RequestDeviceList(),
|
|
||||||
)) as Messages.DeviceList;
|
|
||||||
deviceList.Devices.forEach((d) => {
|
|
||||||
if (!this._devices.has(d.DeviceIndex)) {
|
|
||||||
const device = ButtplugClientDevice.fromMsg(
|
|
||||||
d,
|
|
||||||
this.sendDeviceMessageClosure,
|
|
||||||
);
|
|
||||||
this._logger.Debug(`ButtplugClient: Adding Device: ${device}`);
|
|
||||||
this._devices.set(d.DeviceIndex, device);
|
|
||||||
this.emit("deviceadded", device);
|
|
||||||
} else {
|
|
||||||
this._logger.Debug(`ButtplugClient: Device already added: ${d}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
protected shutdownConnection = async () => {
|
|
||||||
await this.stopAllDevices();
|
|
||||||
if (this._pingTimer !== null) {
|
|
||||||
clearInterval(this._pingTimer);
|
|
||||||
this._pingTimer = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
protected async sendMessage(
|
|
||||||
msg: Messages.ButtplugMessage,
|
|
||||||
): Promise<Messages.ButtplugMessage> {
|
|
||||||
this.checkConnector();
|
|
||||||
const p = this._sorter.PrepareOutgoingMessage(msg);
|
|
||||||
await this._connector!.send(msg);
|
|
||||||
return await p;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected checkConnector() {
|
|
||||||
if (!this.connected) {
|
|
||||||
throw new ButtplugClientConnectorException(
|
|
||||||
"ButtplugClient not connected",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected sendMsgExpectOk = async (
|
|
||||||
msg: Messages.ButtplugMessage,
|
|
||||||
): Promise<void> => {
|
|
||||||
const response = await this.sendMessage(msg);
|
|
||||||
switch (getMessageClassFromMessage(response)) {
|
|
||||||
case Messages.Ok:
|
|
||||||
return;
|
|
||||||
case Messages.Error:
|
|
||||||
throw ButtplugError.FromError(response as Messages.Error);
|
|
||||||
default:
|
|
||||||
throw ButtplugError.LogAndError(
|
|
||||||
ButtplugMessageError,
|
|
||||||
this._logger,
|
|
||||||
`Message type ${getMessageClassFromMessage(response)!.constructor} not handled by SendMsgExpectOk`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
protected sendDeviceMessageClosure = async (
|
|
||||||
device: ButtplugClientDevice,
|
|
||||||
msg: Messages.ButtplugDeviceMessage,
|
|
||||||
): Promise<Messages.ButtplugMessage> => {
|
|
||||||
return await this.sendDeviceMessage(device, msg);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -6,8 +6,8 @@
|
|||||||
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ButtplugMessage } from "../core/Messages";
|
import { ButtplugMessage } from '../core/Messages';
|
||||||
import { EventEmitter } from "eventemitter3";
|
import { EventEmitter } from 'eventemitter3';
|
||||||
|
|
||||||
export interface IButtplugClientConnector extends EventEmitter {
|
export interface IButtplugClientConnector extends EventEmitter {
|
||||||
connect: () => Promise<void>;
|
connect: () => Promise<void>;
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as Messages from "./Messages";
|
import * as Messages from './Messages';
|
||||||
import { ButtplugLogger } from "./Logging";
|
import { ButtplugLogger } from './Logging';
|
||||||
|
|
||||||
export class ButtplugError extends Error {
|
export class ButtplugError extends Error {
|
||||||
public get ErrorClass(): Messages.ErrorClass {
|
public get ErrorClass(): Messages.ErrorClass {
|
||||||
@@ -23,14 +23,20 @@ export class ButtplugError extends Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public get ErrorMessage(): Messages.ButtplugMessage {
|
public get ErrorMessage(): Messages.ButtplugMessage {
|
||||||
return new Messages.Error(this.message, this.ErrorClass, this.Id);
|
return {
|
||||||
|
Error: {
|
||||||
|
Id: this.Id,
|
||||||
|
ErrorCode: this.ErrorClass,
|
||||||
|
ErrorMessage: this.message
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LogAndError<T extends ButtplugError>(
|
public static LogAndError<T extends ButtplugError>(
|
||||||
constructor: new (str: string, num: number) => T,
|
constructor: new (str: string, num: number) => T,
|
||||||
logger: ButtplugLogger,
|
logger: ButtplugLogger,
|
||||||
message: string,
|
message: string,
|
||||||
id: number = Messages.SYSTEM_MESSAGE_ID,
|
id: number = Messages.SYSTEM_MESSAGE_ID
|
||||||
): T {
|
): T {
|
||||||
logger.Error(message);
|
logger.Error(message);
|
||||||
return new constructor(message, id);
|
return new constructor(message, id);
|
||||||
@@ -61,7 +67,7 @@ export class ButtplugError extends Error {
|
|||||||
message: string,
|
message: string,
|
||||||
errorClass: Messages.ErrorClass,
|
errorClass: Messages.ErrorClass,
|
||||||
id: number = Messages.SYSTEM_MESSAGE_ID,
|
id: number = Messages.SYSTEM_MESSAGE_ID,
|
||||||
inner?: Error,
|
inner?: Error
|
||||||
) {
|
) {
|
||||||
super(message);
|
super(message);
|
||||||
this.errorClass = errorClass;
|
this.errorClass = errorClass;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from "eventemitter3";
|
import { EventEmitter } from 'eventemitter3';
|
||||||
|
|
||||||
export enum ButtplugLogLevel {
|
export enum ButtplugLogLevel {
|
||||||
Off,
|
Off,
|
||||||
@@ -69,7 +69,9 @@ export class LogMessage {
|
|||||||
* Returns a formatted string with timestamp, level, and message.
|
* Returns a formatted string with timestamp, level, and message.
|
||||||
*/
|
*/
|
||||||
public get FormattedMessage() {
|
public get FormattedMessage() {
|
||||||
return `${ButtplugLogLevel[this.logLevel]} : ${this.timestamp} : ${this.logMessage}`;
|
return `${ButtplugLogLevel[this.logLevel]} : ${this.timestamp} : ${
|
||||||
|
this.logMessage
|
||||||
|
}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,7 +191,7 @@ export class ButtplugLogger extends EventEmitter {
|
|||||||
console.log(logMsg.FormattedMessage);
|
console.log(logMsg.FormattedMessage);
|
||||||
}
|
}
|
||||||
if (level <= this.maximumEventLogLevel) {
|
if (level <= this.maximumEventLogLevel) {
|
||||||
this.emit("log", logMsg);
|
this.emit('log', logMsg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
/*!
|
|
||||||
* Buttplug JS Source Code File - Visit https://buttplug.io for more info about
|
|
||||||
* the project. Licensed under the BSD 3-Clause license. See LICENSE file in the
|
|
||||||
* project root for full license information.
|
|
||||||
*
|
|
||||||
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
"use strict";
|
|
||||||
import { plainToInstance } from "class-transformer";
|
|
||||||
import * as Messages from "./Messages";
|
|
||||||
|
|
||||||
function getMessageClass(
|
|
||||||
type: string,
|
|
||||||
): (new (...args: unknown[]) => Messages.ButtplugMessage) | null {
|
|
||||||
for (const value of Object.values(Messages)) {
|
|
||||||
if (typeof value === "function" && "Name" in value && value.Name === type) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMessageClassFromMessage(
|
|
||||||
msg: Messages.ButtplugMessage,
|
|
||||||
): (new (...args: unknown[]) => Messages.ButtplugMessage) | null {
|
|
||||||
// Making the bold assumption all message classes have the Name static. Should define a
|
|
||||||
// requirement for this in the abstract class.
|
|
||||||
return getMessageClass(Object.getPrototypeOf(msg).constructor.Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fromJSON(str): Messages.ButtplugMessage[] {
|
|
||||||
const msgarray: object[] = JSON.parse(str);
|
|
||||||
const msgs: Messages.ButtplugMessage[] = [];
|
|
||||||
for (const x of Array.from(msgarray)) {
|
|
||||||
const type = Object.getOwnPropertyNames(x)[0];
|
|
||||||
const cls = getMessageClass(type);
|
|
||||||
if (cls) {
|
|
||||||
const msg = plainToInstance<Messages.ButtplugMessage, unknown>(
|
|
||||||
cls,
|
|
||||||
x[type],
|
|
||||||
);
|
|
||||||
msg.update();
|
|
||||||
msgs.push(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return msgs;
|
|
||||||
}
|
|
||||||
@@ -7,136 +7,59 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// tslint:disable:max-classes-per-file
|
// tslint:disable:max-classes-per-file
|
||||||
"use strict";
|
'use strict';
|
||||||
|
|
||||||
import { instanceToPlain, Type } from "class-transformer";
|
import { ButtplugMessageError } from './Exceptions';
|
||||||
import "reflect-metadata";
|
|
||||||
|
|
||||||
export const SYSTEM_MESSAGE_ID = 0;
|
export const SYSTEM_MESSAGE_ID = 0;
|
||||||
export const DEFAULT_MESSAGE_ID = 1;
|
export const DEFAULT_MESSAGE_ID = 1;
|
||||||
export const MAX_ID = 4294967295;
|
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 {
|
// Base message interfaces
|
||||||
public ScalarCmd?: Array<GenericDeviceMessageAttributes>;
|
export interface ButtplugMessage {
|
||||||
public RotateCmd?: Array<GenericDeviceMessageAttributes>;
|
Ok?: Ok;
|
||||||
public LinearCmd?: Array<GenericDeviceMessageAttributes>;
|
Ping?: Ping;
|
||||||
public RawReadCmd?: RawDeviceMessageAttributes;
|
Error?: Error;
|
||||||
public RawWriteCmd?: RawDeviceMessageAttributes;
|
RequestServerInfo?: RequestServerInfo;
|
||||||
public RawSubscribeCmd?: RawDeviceMessageAttributes;
|
ServerInfo?: ServerInfo;
|
||||||
public SensorReadCmd?: Array<SensorDeviceMessageAttributes>;
|
RequestDeviceList?: RequestDeviceList;
|
||||||
public SensorSubscribeCmd?: Array<SensorDeviceMessageAttributes>;
|
StartScanning?: StartScanning;
|
||||||
public StopDeviceCmd: {};
|
StopScanning?: StopScanning;
|
||||||
|
ScanningFinished?: ScanningFinished;
|
||||||
constructor(data: Partial<MessageAttributes>) {
|
StopCmd?: StopCmd;
|
||||||
Object.assign(this, data);
|
InputCmd?: InputCmd;
|
||||||
}
|
InputReading?: InputReading;
|
||||||
|
OutputCmd?: OutputCmd;
|
||||||
public update() {
|
DeviceList?: DeviceList;
|
||||||
this.ScalarCmd?.forEach((x, i) => (x.Index = i));
|
|
||||||
this.RotateCmd?.forEach((x, i) => (x.Index = i));
|
|
||||||
this.LinearCmd?.forEach((x, i) => (x.Index = i));
|
|
||||||
this.SensorReadCmd?.forEach((x, i) => (x.Index = i));
|
|
||||||
this.SensorSubscribeCmd?.forEach((x, i) => (x.Index = i));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ActuatorType {
|
export function msgId(msg: ButtplugMessage): number {
|
||||||
Unknown = "Unknown",
|
for (let [_, entry] of Object.entries(msg)) {
|
||||||
Vibrate = "Vibrate",
|
if (entry != undefined) {
|
||||||
Rotate = "Rotate",
|
return entry.Id;
|
||||||
Oscillate = "Oscillate",
|
}
|
||||||
Constrict = "Constrict",
|
}
|
||||||
Inflate = "Inflate",
|
throw new ButtplugMessageError(`Message ${msg} does not have an ID.`);
|
||||||
Position = "Position",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SensorType {
|
export function setMsgId(msg: ButtplugMessage, id: number) {
|
||||||
Unknown = "Unknown",
|
for (let [_, entry] of Object.entries(msg)) {
|
||||||
Battery = "Battery",
|
if (entry != undefined) {
|
||||||
RSSI = "RSSI",
|
entry.Id = id;
|
||||||
Button = "Button",
|
return;
|
||||||
Pressure = "Pressure",
|
}
|
||||||
// Temperature,
|
}
|
||||||
// Accelerometer,
|
throw new ButtplugMessageError(`Message ${msg} does not have an ID.`);
|
||||||
// Gyro,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GenericDeviceMessageAttributes {
|
export interface Ok {
|
||||||
public FeatureDescriptor: string;
|
Id: number | undefined;
|
||||||
public ActuatorType: ActuatorType;
|
|
||||||
public StepCount: number;
|
|
||||||
public Index = 0;
|
|
||||||
constructor(data: Partial<GenericDeviceMessageAttributes>) {
|
|
||||||
Object.assign(this, data);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RawDeviceMessageAttributes {
|
export interface Ping {
|
||||||
constructor(public Endpoints: Array<string>) {}
|
Id: number | undefined;
|
||||||
}
|
|
||||||
|
|
||||||
export class SensorDeviceMessageAttributes {
|
|
||||||
public FeatureDescriptor: string;
|
|
||||||
public SensorType: SensorType;
|
|
||||||
public StepRange: Array<number>;
|
|
||||||
public Index = 0;
|
|
||||||
constructor(data: Partial<GenericDeviceMessageAttributes>) {
|
|
||||||
Object.assign(this, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export abstract class ButtplugMessage {
|
|
||||||
constructor(public Id: number) {}
|
|
||||||
|
|
||||||
// tslint:disable-next-line:ban-types
|
|
||||||
public get Type(): Function {
|
|
||||||
return this.constructor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public toJSON(): string {
|
|
||||||
return JSON.stringify(this.toProtocolFormat());
|
|
||||||
}
|
|
||||||
|
|
||||||
public toProtocolFormat(): object {
|
|
||||||
const jsonObj = {};
|
|
||||||
jsonObj[(this.constructor as unknown as { Name: string }).Name] =
|
|
||||||
instanceToPlain(this);
|
|
||||||
return jsonObj;
|
|
||||||
}
|
|
||||||
|
|
||||||
public update() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export abstract class ButtplugDeviceMessage extends ButtplugMessage {
|
|
||||||
constructor(
|
|
||||||
public DeviceIndex: number,
|
|
||||||
public Id: number,
|
|
||||||
) {
|
|
||||||
super(Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export abstract class ButtplugSystemMessage extends ButtplugMessage {
|
|
||||||
constructor(public Id: number = SYSTEM_MESSAGE_ID) {
|
|
||||||
super(Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Ok extends ButtplugSystemMessage {
|
|
||||||
static Name = "Ok";
|
|
||||||
|
|
||||||
constructor(public Id: number = DEFAULT_MESSAGE_ID) {
|
|
||||||
super(Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Ping extends ButtplugMessage {
|
|
||||||
static Name = "Ping";
|
|
||||||
|
|
||||||
constructor(public Id: number = DEFAULT_MESSAGE_ID) {
|
|
||||||
super(Id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ErrorClass {
|
export enum ErrorClass {
|
||||||
@@ -147,345 +70,140 @@ export enum ErrorClass {
|
|||||||
ERROR_DEVICE,
|
ERROR_DEVICE,
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Error extends ButtplugMessage {
|
export interface Error {
|
||||||
static Name = "Error";
|
ErrorMessage: string;
|
||||||
|
ErrorCode: ErrorClass;
|
||||||
constructor(
|
Id: number | undefined;
|
||||||
public ErrorMessage: string,
|
|
||||||
public ErrorCode: ErrorClass = ErrorClass.ERROR_UNKNOWN,
|
|
||||||
public Id: number = DEFAULT_MESSAGE_ID,
|
|
||||||
) {
|
|
||||||
super(Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
get Schemversion() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DeviceInfo {
|
export interface RequestDeviceList {
|
||||||
public DeviceIndex: number;
|
Id: number | undefined;
|
||||||
public DeviceName: string;
|
|
||||||
@Type(() => MessageAttributes)
|
|
||||||
public DeviceMessages: MessageAttributes;
|
|
||||||
public DeviceDisplayName?: string;
|
|
||||||
public DeviceMessageTimingGap?: number;
|
|
||||||
|
|
||||||
constructor(data: Partial<DeviceInfo>) {
|
|
||||||
Object.assign(this, data);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DeviceList extends ButtplugMessage {
|
export interface StartScanning {
|
||||||
static Name = "DeviceList";
|
Id: number | undefined;
|
||||||
|
|
||||||
@Type(() => DeviceInfo)
|
|
||||||
public Devices: DeviceInfo[];
|
|
||||||
public Id: number;
|
|
||||||
|
|
||||||
constructor(devices: DeviceInfo[], id: number = DEFAULT_MESSAGE_ID) {
|
|
||||||
super(id);
|
|
||||||
this.Devices = devices;
|
|
||||||
this.Id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public update() {
|
|
||||||
for (const device of this.Devices) {
|
|
||||||
device.DeviceMessages.update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DeviceAdded extends ButtplugSystemMessage {
|
export interface StopScanning {
|
||||||
static Name = "DeviceAdded";
|
Id: number | undefined;
|
||||||
|
|
||||||
public DeviceIndex: number;
|
|
||||||
public DeviceName: string;
|
|
||||||
@Type(() => MessageAttributes)
|
|
||||||
public DeviceMessages: MessageAttributes;
|
|
||||||
public DeviceDisplayName?: string;
|
|
||||||
public DeviceMessageTimingGap?: number;
|
|
||||||
|
|
||||||
constructor(data: Partial<DeviceAdded>) {
|
|
||||||
super();
|
|
||||||
Object.assign(this, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public update() {
|
|
||||||
this.DeviceMessages.update();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DeviceRemoved extends ButtplugSystemMessage {
|
export interface StopAllDevices {
|
||||||
static Name = "DeviceRemoved";
|
Id: number | undefined;
|
||||||
|
|
||||||
constructor(public DeviceIndex: number) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RequestDeviceList extends ButtplugMessage {
|
export interface ScanningFinished {
|
||||||
static Name = "RequestDeviceList";
|
Id: number | undefined;
|
||||||
|
|
||||||
constructor(public Id: number = DEFAULT_MESSAGE_ID) {
|
|
||||||
super(Id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StartScanning extends ButtplugMessage {
|
export interface RequestServerInfo {
|
||||||
static Name = "StartScanning";
|
ClientName: string;
|
||||||
|
ProtocolVersionMajor: number;
|
||||||
constructor(public Id: number = DEFAULT_MESSAGE_ID) {
|
ProtocolVersionMinor: number;
|
||||||
super(Id);
|
Id: number | undefined;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StopScanning extends ButtplugMessage {
|
export interface ServerInfo {
|
||||||
static Name = "StopScanning";
|
MaxPingTime: number;
|
||||||
|
ServerName: string;
|
||||||
constructor(public Id: number = DEFAULT_MESSAGE_ID) {
|
ProtocolVersionMajor: number;
|
||||||
super(Id);
|
ProtocolVersionMinor: number;
|
||||||
}
|
Id: number | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ScanningFinished extends ButtplugSystemMessage {
|
export interface DeviceFeature {
|
||||||
static Name = "ScanningFinished";
|
FeatureDescriptor: string;
|
||||||
|
Output: { [key: string]: DeviceFeatureOutput };
|
||||||
constructor() {
|
Input: { [key: string]: DeviceFeatureInput };
|
||||||
super();
|
FeatureIndex: number;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RequestServerInfo extends ButtplugMessage {
|
export interface DeviceInfo {
|
||||||
static Name = "RequestServerInfo";
|
DeviceIndex: number;
|
||||||
|
DeviceName: string;
|
||||||
constructor(
|
DeviceFeatures: { [key: number]: DeviceFeature };
|
||||||
public ClientName: string,
|
DeviceDisplayName?: string;
|
||||||
public MessageVersion: number = 0,
|
DeviceMessageTimingGap?: number;
|
||||||
public Id: number = DEFAULT_MESSAGE_ID,
|
|
||||||
) {
|
|
||||||
super(Id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ServerInfo extends ButtplugSystemMessage {
|
export interface DeviceList {
|
||||||
static Name = "ServerInfo";
|
Devices: { [key: number]: DeviceInfo };
|
||||||
|
Id: number | undefined;
|
||||||
constructor(
|
|
||||||
public MessageVersion: number,
|
|
||||||
public MaxPingTime: number,
|
|
||||||
public ServerName: string,
|
|
||||||
public Id: number = DEFAULT_MESSAGE_ID,
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StopDeviceCmd extends ButtplugDeviceMessage {
|
export enum OutputType {
|
||||||
static Name = "StopDeviceCmd";
|
Unknown = 'Unknown',
|
||||||
|
Vibrate = 'Vibrate',
|
||||||
constructor(
|
Rotate = 'Rotate',
|
||||||
public DeviceIndex: number = -1,
|
Oscillate = 'Oscillate',
|
||||||
public Id: number = DEFAULT_MESSAGE_ID,
|
Constrict = 'Constrict',
|
||||||
) {
|
Inflate = 'Inflate',
|
||||||
super(DeviceIndex, Id);
|
Position = 'Position',
|
||||||
}
|
HwPositionWithDuration = 'HwPositionWithDuration',
|
||||||
|
Temperature = 'Temperature',
|
||||||
|
Spray = 'Spray',
|
||||||
|
Led = 'Led',
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StopAllDevices extends ButtplugMessage {
|
export enum InputType {
|
||||||
static Name = "StopAllDevices";
|
Unknown = 'Unknown',
|
||||||
|
Battery = 'Battery',
|
||||||
constructor(public Id: number = DEFAULT_MESSAGE_ID) {
|
RSSI = 'RSSI',
|
||||||
super(Id);
|
Button = 'Button',
|
||||||
}
|
Pressure = 'Pressure',
|
||||||
|
// Temperature,
|
||||||
|
// Accelerometer,
|
||||||
|
// Gyro,
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GenericMessageSubcommand {
|
export enum InputCommandType {
|
||||||
protected constructor(public Index: number) {}
|
Read = 'Read',
|
||||||
|
Subscribe = 'Subscribe',
|
||||||
|
Unsubscribe = 'Unsubscribe',
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ScalarSubcommand extends GenericMessageSubcommand {
|
export interface DeviceFeatureInput {
|
||||||
constructor(
|
Value: number[];
|
||||||
Index: number,
|
Command: InputCommandType[];
|
||||||
public Scalar: number,
|
|
||||||
public ActuatorType: ActuatorType,
|
|
||||||
) {
|
|
||||||
super(Index);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ScalarCmd extends ButtplugDeviceMessage {
|
export interface DeviceFeatureOutput {
|
||||||
static Name = "ScalarCmd";
|
Value: number;
|
||||||
|
Duration?: number;
|
||||||
constructor(
|
|
||||||
public Scalars: ScalarSubcommand[],
|
|
||||||
public DeviceIndex: number = -1,
|
|
||||||
public Id: number = DEFAULT_MESSAGE_ID,
|
|
||||||
) {
|
|
||||||
super(DeviceIndex, Id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RotateSubcommand extends GenericMessageSubcommand {
|
export interface OutputCmd {
|
||||||
constructor(
|
DeviceIndex: number;
|
||||||
Index: number,
|
FeatureIndex: number;
|
||||||
public Speed: number,
|
Command: { [key: string]: DeviceFeatureOutput };
|
||||||
public Clockwise: boolean,
|
Id: number | undefined;
|
||||||
) {
|
|
||||||
super(Index);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RotateCmd extends ButtplugDeviceMessage {
|
// Device Input Commands
|
||||||
static Name = "RotateCmd";
|
|
||||||
|
|
||||||
public static Create(
|
export interface InputCmd {
|
||||||
deviceIndex: number,
|
DeviceIndex: number;
|
||||||
commands: [number, boolean][],
|
FeatureIndex: number;
|
||||||
): RotateCmd {
|
Type: InputType;
|
||||||
const cmdList: RotateSubcommand[] = new Array<RotateSubcommand>();
|
Command: InputCommandType;
|
||||||
|
Id: number | undefined;
|
||||||
let i = 0;
|
|
||||||
for (const [speed, clockwise] of commands) {
|
|
||||||
cmdList.push(new RotateSubcommand(i, speed, clockwise));
|
|
||||||
++i;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new RotateCmd(cmdList, deviceIndex);
|
|
||||||
}
|
|
||||||
constructor(
|
|
||||||
public Rotations: RotateSubcommand[],
|
|
||||||
public DeviceIndex: number = -1,
|
|
||||||
public Id: number = DEFAULT_MESSAGE_ID,
|
|
||||||
) {
|
|
||||||
super(DeviceIndex, Id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class VectorSubcommand extends GenericMessageSubcommand {
|
export interface InputValue {
|
||||||
constructor(
|
Value: number;
|
||||||
Index: number,
|
|
||||||
public Position: number,
|
|
||||||
public Duration: number,
|
|
||||||
) {
|
|
||||||
super(Index);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LinearCmd extends ButtplugDeviceMessage {
|
export interface InputReading {
|
||||||
static Name = "LinearCmd";
|
DeviceIndex: number;
|
||||||
|
FeatureIndex: number;
|
||||||
public static Create(
|
Reading: { [key: string]: InputValue };
|
||||||
deviceIndex: number,
|
Id: number | undefined;
|
||||||
commands: [number, number][],
|
|
||||||
): LinearCmd {
|
|
||||||
const cmdList: VectorSubcommand[] = new Array<VectorSubcommand>();
|
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
for (const cmd of commands) {
|
|
||||||
cmdList.push(new VectorSubcommand(i, cmd[0], cmd[1]));
|
|
||||||
++i;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new LinearCmd(cmdList, deviceIndex);
|
|
||||||
}
|
|
||||||
constructor(
|
|
||||||
public Vectors: VectorSubcommand[],
|
|
||||||
public DeviceIndex: number = -1,
|
|
||||||
public Id: number = DEFAULT_MESSAGE_ID,
|
|
||||||
) {
|
|
||||||
super(DeviceIndex, Id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SensorReadCmd extends ButtplugDeviceMessage {
|
export interface StopCmd {
|
||||||
static Name = "SensorReadCmd";
|
Id: number | undefined;
|
||||||
|
DeviceIndex: number | undefined;
|
||||||
constructor(
|
FeatureIndex: number | undefined;
|
||||||
public DeviceIndex: number,
|
Inputs: boolean | undefined;
|
||||||
public SensorIndex: number,
|
Outputs: boolean | undefined;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,27 @@
|
|||||||
import { ButtplugMessage } from "./core/Messages";
|
/*!
|
||||||
import { IButtplugClientConnector } from "./client/IButtplugClientConnector";
|
* Buttplug JS Source Code File - Visit https://buttplug.io for more info about
|
||||||
import { fromJSON } from "./core/MessageUtils";
|
* the project. Licensed under the BSD 3-Clause license. See LICENSE file in the
|
||||||
import { EventEmitter } from "eventemitter3";
|
* project root for full license information.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
export * from "./client/Client";
|
import { ButtplugMessage } from './core/Messages';
|
||||||
export * from "./client/ButtplugClientDevice";
|
import { IButtplugClientConnector } from './client/IButtplugClientConnector';
|
||||||
export * from "./client/ButtplugBrowserWebsocketClientConnector";
|
import { EventEmitter } from 'eventemitter3';
|
||||||
export * from "./client/ButtplugNodeWebsocketClientConnector";
|
|
||||||
export * from "./client/ButtplugClientConnectorException";
|
export * from './client/ButtplugClient';
|
||||||
export * from "./utils/ButtplugMessageSorter";
|
export * from './client/ButtplugClientDevice';
|
||||||
export * from "./client/IButtplugClientConnector";
|
export * from './client/ButtplugBrowserWebsocketClientConnector';
|
||||||
export * from "./core/Messages";
|
export * from './client/ButtplugNodeWebsocketClientConnector';
|
||||||
export * from "./core/MessageUtils";
|
export * from './client/ButtplugClientConnectorException';
|
||||||
export * from "./core/Logging";
|
export * from './utils/ButtplugMessageSorter';
|
||||||
export * from "./core/Exceptions";
|
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
|
export class ButtplugWasmClientConnector
|
||||||
extends EventEmitter
|
extends EventEmitter
|
||||||
@@ -36,18 +44,18 @@ export class ButtplugWasmClientConnector
|
|||||||
private static maybeLoadWasm = async () => {
|
private static maybeLoadWasm = async () => {
|
||||||
if (ButtplugWasmClientConnector.wasmInstance == undefined) {
|
if (ButtplugWasmClientConnector.wasmInstance == undefined) {
|
||||||
ButtplugWasmClientConnector.wasmInstance = await import(
|
ButtplugWasmClientConnector.wasmInstance = await import(
|
||||||
"../wasm/index.js"
|
'../wasm/index.js'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public static activateLogging = async (logLevel: string = "debug") => {
|
public static activateLogging = async (logLevel: string = 'debug') => {
|
||||||
await ButtplugWasmClientConnector.maybeLoadWasm();
|
await ButtplugWasmClientConnector.maybeLoadWasm();
|
||||||
if (this._loggingActivated) {
|
if (this._loggingActivated) {
|
||||||
console.log("Logging already activated, ignoring.");
|
console.log('Logging already activated, ignoring.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log("Turning on logging.");
|
console.log('Turning on logging.');
|
||||||
ButtplugWasmClientConnector.wasmInstance.buttplug_activate_env_logger(
|
ButtplugWasmClientConnector.wasmInstance.buttplug_activate_env_logger(
|
||||||
logLevel,
|
logLevel,
|
||||||
);
|
);
|
||||||
@@ -57,7 +65,6 @@ export class ButtplugWasmClientConnector
|
|||||||
|
|
||||||
public connect = async (): Promise<void> => {
|
public connect = async (): Promise<void> => {
|
||||||
await ButtplugWasmClientConnector.maybeLoadWasm();
|
await ButtplugWasmClientConnector.maybeLoadWasm();
|
||||||
//ButtplugWasmClientConnector.wasmInstance.buttplug_activate_env_logger('debug');
|
|
||||||
this.client =
|
this.client =
|
||||||
ButtplugWasmClientConnector.wasmInstance.buttplug_create_embedded_wasm_server(
|
ButtplugWasmClientConnector.wasmInstance.buttplug_create_embedded_wasm_server(
|
||||||
(msgs) => {
|
(msgs) => {
|
||||||
@@ -73,7 +80,7 @@ export class ButtplugWasmClientConnector
|
|||||||
public send = (msg: ButtplugMessage): void => {
|
public send = (msg: ButtplugMessage): void => {
|
||||||
ButtplugWasmClientConnector.wasmInstance.buttplug_client_send_json_message(
|
ButtplugWasmClientConnector.wasmInstance.buttplug_client_send_json_message(
|
||||||
this.client,
|
this.client,
|
||||||
new TextEncoder().encode("[" + msg.toJSON() + "]"),
|
new TextEncoder().encode('[' + JSON.stringify(msg) + ']'),
|
||||||
(output) => {
|
(output) => {
|
||||||
this.emitMessage(output);
|
this.emitMessage(output);
|
||||||
},
|
},
|
||||||
@@ -82,7 +89,7 @@ export class ButtplugWasmClientConnector
|
|||||||
|
|
||||||
private emitMessage = (msg: Uint8Array) => {
|
private emitMessage = (msg: Uint8Array) => {
|
||||||
const str = new TextDecoder().decode(msg);
|
const str = new TextDecoder().decode(msg);
|
||||||
// This needs to use buttplug-js's fromJSON, otherwise we won't resolve the message name correctly.
|
const msgs: ButtplugMessage[] = JSON.parse(str);
|
||||||
this.emit("message", fromJSON(str));
|
this.emit('message', msgs);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,16 @@ mod webbluetooth;
|
|||||||
use js_sys;
|
use js_sys;
|
||||||
use tokio_stream::StreamExt;
|
use tokio_stream::StreamExt;
|
||||||
use crate::webbluetooth::{WebBluetoothCommunicationManagerBuilder};
|
use crate::webbluetooth::{WebBluetoothCommunicationManagerBuilder};
|
||||||
use buttplug::{
|
use buttplug_core::{
|
||||||
core::message::{ButtplugServerMessageCurrent,serializer::vec_to_protocol_json},
|
message::{ButtplugServerMessageCurrent, BUTTPLUG_CURRENT_API_MAJOR_VERSION, serializer::{ButtplugSerializedMessage, ButtplugMessageSerializer}},
|
||||||
server::{ButtplugServerBuilder,ButtplugServerDowngradeWrapper,device::{ServerDeviceManagerBuilder,configuration::{DeviceConfigurationManager}}},
|
util::async_manager,
|
||||||
util::async_manager, core::message::{BUTTPLUG_CURRENT_MESSAGE_SPEC_VERSION, ButtplugServerMessageVariant, serializer::{ButtplugSerializedMessage, ButtplugMessageSerializer, ButtplugServerJSONSerializer}},
|
|
||||||
util::device_configuration::load_protocol_configs
|
|
||||||
};
|
};
|
||||||
|
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 FFICallback = js_sys::Function;
|
||||||
type FFICallbackContext = u32;
|
type FFICallbackContext = u32;
|
||||||
@@ -33,16 +37,17 @@ use wasm_bindgen::prelude::*;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use js_sys::Uint8Array;
|
use js_sys::Uint8Array;
|
||||||
|
|
||||||
pub type ButtplugWASMServer = Arc<ButtplugServerDowngradeWrapper>;
|
pub type ButtplugWASMServer = Arc<ButtplugServer>;
|
||||||
|
|
||||||
pub fn send_server_message(
|
pub fn send_server_message(
|
||||||
message: &ButtplugServerMessageCurrent,
|
message: &ButtplugServerMessageCurrent,
|
||||||
callback: &FFICallback,
|
callback: &FFICallback,
|
||||||
) {
|
) {
|
||||||
let msg_array = [message.clone()];
|
let serializer = ButtplugServerJSONSerializer::default();
|
||||||
let json_msg = vec_to_protocol_json(&msg_array);
|
serializer.force_message_version(&BUTTPLUG_CURRENT_API_MAJOR_VERSION);
|
||||||
let buf = json_msg.as_bytes();
|
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 this = JsValue::null();
|
||||||
let uint8buf = unsafe { Uint8Array::new(&Uint8Array::view(buf)) };
|
let uint8buf = unsafe { Uint8Array::new(&Uint8Array::view(buf)) };
|
||||||
callback.call1(&this, &JsValue::from(uint8buf));
|
callback.call1(&this, &JsValue::from(uint8buf));
|
||||||
@@ -50,10 +55,9 @@ pub fn send_server_message(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[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)
|
load_protocol_configs(&None, &None, false)
|
||||||
.expect("If this fails, the whole library goes with it.")
|
.expect("If this fails, the whole library goes with it.")
|
||||||
.allow_raw_messages(allow_raw_messages)
|
|
||||||
.finish()
|
.finish()
|
||||||
.expect("If this fails, the whole library goes with it.")
|
.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);
|
let mut sdm = ServerDeviceManagerBuilder::new(dcm);
|
||||||
sdm.comm_manager(WebBluetoothCommunicationManagerBuilder::default());
|
sdm.comm_manager(WebBluetoothCommunicationManagerBuilder::default());
|
||||||
let builder = ButtplugServerBuilder::new(sdm.finish().unwrap());
|
let builder = ButtplugServerBuilder::new(sdm.finish().unwrap());
|
||||||
let server = builder.finish().unwrap();
|
let server = Arc::new(builder.finish().unwrap());
|
||||||
let wrapper = Arc::new(ButtplugServerDowngradeWrapper::new(server));
|
let event_stream = server.server_version_event_stream();
|
||||||
let event_stream = wrapper.server_version_event_stream();
|
|
||||||
let callback = callback.clone();
|
let callback = callback.clone();
|
||||||
async_manager::spawn(async move {
|
async_manager::spawn(async move {
|
||||||
pin_mut!(event_stream);
|
pin_mut!(event_stream);
|
||||||
while let Some(message) = event_stream.next().await {
|
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]
|
#[no_mangle]
|
||||||
@@ -106,15 +109,18 @@ pub fn buttplug_client_send_json_message(
|
|||||||
};
|
};
|
||||||
let callback = callback.clone();
|
let callback = callback.clone();
|
||||||
let serializer = ButtplugServerJSONSerializer::default();
|
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();
|
let input_msg = serializer.deserialize(&ButtplugSerializedMessage::Text(std::str::from_utf8(buf).unwrap().to_owned())).unwrap();
|
||||||
async_manager::spawn(async move {
|
async_manager::spawn(async move {
|
||||||
let msg = input_msg[0].clone();
|
let msg = input_msg[0].clone();
|
||||||
let response = server.parse_message(msg).await.unwrap();
|
let response = server.parse_message(msg).await.unwrap();
|
||||||
if let ButtplugServerMessageVariant::V3(response) = response {
|
let json_msg = serializer.serialize(&[response]);
|
||||||
send_server_message(&response, &callback);
|
if let ButtplugSerializedMessage::Text(json) = json_msg {
|
||||||
|
let buf = json.as_bytes();
|
||||||
|
let this = JsValue::null();
|
||||||
|
let uint8buf = unsafe { Uint8Array::new(&Uint8Array::view(buf)) };
|
||||||
|
callback.call1(&this, &JsValue::from(uint8buf));
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,10 @@
|
|||||||
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
"use strict";
|
'use strict';
|
||||||
|
|
||||||
import { EventEmitter } from "eventemitter3";
|
import { EventEmitter } from 'eventemitter3';
|
||||||
import { ButtplugMessage } from "../core/Messages";
|
import { ButtplugMessage } from '../core/Messages';
|
||||||
import { fromJSON } from "../core/MessageUtils";
|
|
||||||
|
|
||||||
export class ButtplugBrowserWebsocketConnector extends EventEmitter {
|
export class ButtplugBrowserWebsocketConnector extends EventEmitter {
|
||||||
protected _ws: WebSocket | undefined;
|
protected _ws: WebSocket | undefined;
|
||||||
@@ -27,20 +26,18 @@ export class ButtplugBrowserWebsocketConnector extends EventEmitter {
|
|||||||
public connect = async (): Promise<void> => {
|
public connect = async (): Promise<void> => {
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
const ws = new (this._websocketConstructor ?? WebSocket)(this._url);
|
const ws = new (this._websocketConstructor ?? WebSocket)(this._url);
|
||||||
const onErrorCallback = (event: Event) => {
|
const onErrorCallback = (event: Event) => {reject(event)}
|
||||||
reject(event);
|
const onCloseCallback = (event: CloseEvent) => reject(event.reason)
|
||||||
};
|
ws.addEventListener('open', async () => {
|
||||||
const onCloseCallback = (event: CloseEvent) => reject(event.reason);
|
|
||||||
ws.addEventListener("open", async () => {
|
|
||||||
this._ws = ws;
|
this._ws = ws;
|
||||||
try {
|
try {
|
||||||
await this.initialize();
|
await this.initialize();
|
||||||
this._ws.addEventListener("message", (msg) => {
|
this._ws.addEventListener('message', (msg) => {
|
||||||
this.parseIncomingMessage(msg);
|
this.parseIncomingMessage(msg);
|
||||||
});
|
});
|
||||||
this._ws.removeEventListener("close", onCloseCallback);
|
this._ws.removeEventListener('close', onCloseCallback);
|
||||||
this._ws.removeEventListener("error", onErrorCallback);
|
this._ws.removeEventListener('error', onErrorCallback);
|
||||||
this._ws.addEventListener("close", this.disconnect);
|
this._ws.addEventListener('close', this.disconnect);
|
||||||
resolve();
|
resolve();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reject(e);
|
reject(e);
|
||||||
@@ -50,8 +47,8 @@ export class ButtplugBrowserWebsocketConnector extends EventEmitter {
|
|||||||
// browsers usually only throw Error Code 1006. It's up to those using this
|
// browsers usually only throw Error Code 1006. It's up to those using this
|
||||||
// library to state what the problem might be.
|
// library to state what the problem might be.
|
||||||
|
|
||||||
ws.addEventListener("error", onErrorCallback);
|
ws.addEventListener('error', onErrorCallback)
|
||||||
ws.addEventListener("close", onCloseCallback);
|
ws.addEventListener('close', onCloseCallback);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -61,14 +58,14 @@ export class ButtplugBrowserWebsocketConnector extends EventEmitter {
|
|||||||
}
|
}
|
||||||
this._ws!.close();
|
this._ws!.close();
|
||||||
this._ws = undefined;
|
this._ws = undefined;
|
||||||
this.emit("disconnect");
|
this.emit('disconnect');
|
||||||
};
|
};
|
||||||
|
|
||||||
public sendMessage(msg: ButtplugMessage) {
|
public sendMessage(msg: ButtplugMessage) {
|
||||||
if (!this.Connected) {
|
if (!this.Connected) {
|
||||||
throw new Error("ButtplugBrowserWebsocketConnector not connected");
|
throw new Error('ButtplugBrowserWebsocketConnector not connected');
|
||||||
}
|
}
|
||||||
this._ws!.send("[" + msg.toJSON() + "]");
|
this._ws!.send('[' + JSON.stringify(msg) + ']');
|
||||||
}
|
}
|
||||||
|
|
||||||
public initialize = async (): Promise<void> => {
|
public initialize = async (): Promise<void> => {
|
||||||
@@ -76,16 +73,16 @@ export class ButtplugBrowserWebsocketConnector extends EventEmitter {
|
|||||||
};
|
};
|
||||||
|
|
||||||
protected parseIncomingMessage(event: MessageEvent) {
|
protected parseIncomingMessage(event: MessageEvent) {
|
||||||
if (typeof event.data === "string") {
|
if (typeof event.data === 'string') {
|
||||||
const msgs = fromJSON(event.data);
|
const msgs: ButtplugMessage[] = JSON.parse(event.data);
|
||||||
this.emit("message", msgs);
|
this.emit('message', msgs);
|
||||||
} else if (event.data instanceof Blob) {
|
} else if (event.data instanceof Blob) {
|
||||||
// No-op, we only use text message types.
|
// No-op, we only use text message types.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onReaderLoad(event: Event) {
|
protected onReaderLoad(event: Event) {
|
||||||
const msgs = fromJSON((event.target as FileReader).result);
|
const msgs: ButtplugMessage[] = JSON.parse((event.target as FileReader).result as string);
|
||||||
this.emit("message", msgs);
|
this.emit('message', msgs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as Messages from "../core/Messages";
|
import * as Messages from '../core/Messages';
|
||||||
import { ButtplugError } from "../core/Exceptions";
|
import { ButtplugError } from '../core/Exceptions';
|
||||||
|
|
||||||
export class ButtplugMessageSorter {
|
export class ButtplugMessageSorter {
|
||||||
protected _counter = 1;
|
protected _counter = 1;
|
||||||
@@ -22,10 +22,10 @@ export class ButtplugMessageSorter {
|
|||||||
// them while waiting for them to return across the line.
|
// them while waiting for them to return across the line.
|
||||||
// tslint:disable:promise-function-async
|
// tslint:disable:promise-function-async
|
||||||
public PrepareOutgoingMessage(
|
public PrepareOutgoingMessage(
|
||||||
msg: Messages.ButtplugMessage,
|
msg: Messages.ButtplugMessage
|
||||||
): Promise<Messages.ButtplugMessage> {
|
): Promise<Messages.ButtplugMessage> {
|
||||||
if (this._useCounter) {
|
if (this._useCounter) {
|
||||||
msg.Id = this._counter;
|
Messages.setMsgId(msg, this._counter);
|
||||||
// Always increment last, otherwise we might lose sync
|
// Always increment last, otherwise we might lose sync
|
||||||
this._counter += 1;
|
this._counter += 1;
|
||||||
}
|
}
|
||||||
@@ -35,23 +35,24 @@ export class ButtplugMessageSorter {
|
|||||||
(resolve, reject) => {
|
(resolve, reject) => {
|
||||||
res = resolve;
|
res = resolve;
|
||||||
rej = reject;
|
rej = reject;
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
this._waitingMsgs.set(msg.Id, [res, rej]);
|
this._waitingMsgs.set(Messages.msgId(msg), [res, rej]);
|
||||||
return msgPromise;
|
return msgPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ParseIncomingMessages(
|
public ParseIncomingMessages(
|
||||||
msgs: Messages.ButtplugMessage[],
|
msgs: Messages.ButtplugMessage[]
|
||||||
): Messages.ButtplugMessage[] {
|
): Messages.ButtplugMessage[] {
|
||||||
const noMatch: Messages.ButtplugMessage[] = [];
|
const noMatch: Messages.ButtplugMessage[] = [];
|
||||||
for (const x of msgs) {
|
for (const x of msgs) {
|
||||||
if (x.Id !== Messages.SYSTEM_MESSAGE_ID && this._waitingMsgs.has(x.Id)) {
|
let id = Messages.msgId(x);
|
||||||
const [res, rej] = this._waitingMsgs.get(x.Id)!;
|
if (id !== Messages.SYSTEM_MESSAGE_ID && this._waitingMsgs.has(id)) {
|
||||||
|
const [res, rej] = this._waitingMsgs.get(id)!;
|
||||||
// If we've gotten back an error, reject the related promise using a
|
// If we've gotten back an error, reject the related promise using a
|
||||||
// ButtplugException derived type.
|
// ButtplugException derived type.
|
||||||
if (x.Type === Messages.Error) {
|
if (x.Error !== undefined) {
|
||||||
rej(ButtplugError.FromError(x as Messages.Error));
|
rej(ButtplugError.FromError(x.Error!));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
res(x);
|
res(x);
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use buttplug::{
|
use buttplug_core::errors::ButtplugDeviceError;
|
||||||
core::{
|
use buttplug_server_device_config::{BluetoothLESpecifier, Endpoint, ProtocolCommunicationSpecifier};
|
||||||
errors::ButtplugDeviceError,
|
use buttplug_server::device::hardware::{
|
||||||
message::Endpoint,
|
|
||||||
},
|
|
||||||
server::device::{
|
|
||||||
configuration::{BluetoothLESpecifier, ProtocolCommunicationSpecifier},
|
|
||||||
hardware::{
|
|
||||||
Hardware,
|
Hardware,
|
||||||
HardwareConnector,
|
HardwareConnector,
|
||||||
HardwareEvent,
|
HardwareEvent,
|
||||||
@@ -17,9 +12,6 @@ use buttplug::{
|
|||||||
HardwareSubscribeCmd,
|
HardwareSubscribeCmd,
|
||||||
HardwareUnsubscribeCmd,
|
HardwareUnsubscribeCmd,
|
||||||
HardwareWriteCmd,
|
HardwareWriteCmd,
|
||||||
},
|
|
||||||
},
|
|
||||||
util::future::{ButtplugFuture, ButtplugFutureStateShared},
|
|
||||||
};
|
};
|
||||||
use futures::future::{self, BoxFuture};
|
use futures::future::{self, BoxFuture};
|
||||||
use js_sys::{DataView, Uint8Array};
|
use js_sys::{DataView, Uint8Array};
|
||||||
@@ -28,7 +20,7 @@ use std::{
|
|||||||
convert::TryFrom,
|
convert::TryFrom,
|
||||||
fmt::{self, Debug},
|
fmt::{self, Debug},
|
||||||
};
|
};
|
||||||
use tokio::sync::{broadcast, mpsc};
|
use tokio::sync::{broadcast, mpsc, oneshot};
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
use wasm_bindgen_futures::{spawn_local, JsFuture};
|
use wasm_bindgen_futures::{spawn_local, JsFuture};
|
||||||
@@ -41,9 +33,6 @@ use web_sys::{
|
|||||||
MessageEvent,
|
MessageEvent,
|
||||||
};
|
};
|
||||||
|
|
||||||
type WebBluetoothResultFuture = ButtplugFuture<Result<(), ButtplugDeviceError>>;
|
|
||||||
type WebBluetoothReadResultFuture = ButtplugFuture<Result<HardwareReading, ButtplugDeviceError>>;
|
|
||||||
|
|
||||||
struct BluetoothDeviceWrapper {
|
struct BluetoothDeviceWrapper {
|
||||||
pub device: BluetoothDevice
|
pub device: BluetoothDevice
|
||||||
}
|
}
|
||||||
@@ -179,7 +168,7 @@ impl HardwareSpecializer for WebBluetoothHardwareSpecializer {
|
|||||||
receiver,
|
receiver,
|
||||||
command_sender,
|
command_sender,
|
||||||
));
|
));
|
||||||
Ok(Hardware::new(&name, &address, &[], device_impl))
|
Ok(Hardware::new(&name, &address, &[], &None, false, device_impl))
|
||||||
}
|
}
|
||||||
WebBluetoothEvent::Disconnected => Err(
|
WebBluetoothEvent::Disconnected => Err(
|
||||||
ButtplugDeviceError::DeviceCommunicationError(
|
ButtplugDeviceError::DeviceCommunicationError(
|
||||||
@@ -202,19 +191,19 @@ pub enum WebBluetoothEvent {
|
|||||||
pub enum WebBluetoothDeviceCommand {
|
pub enum WebBluetoothDeviceCommand {
|
||||||
Write(
|
Write(
|
||||||
HardwareWriteCmd,
|
HardwareWriteCmd,
|
||||||
ButtplugFutureStateShared<Result<(), ButtplugDeviceError>>,
|
oneshot::Sender<Result<(), ButtplugDeviceError>>,
|
||||||
),
|
),
|
||||||
Read(
|
Read(
|
||||||
HardwareReadCmd,
|
HardwareReadCmd,
|
||||||
ButtplugFutureStateShared<Result<HardwareReading, ButtplugDeviceError>>,
|
oneshot::Sender<Result<HardwareReading, ButtplugDeviceError>>,
|
||||||
),
|
),
|
||||||
Subscribe(
|
Subscribe(
|
||||||
HardwareSubscribeCmd,
|
HardwareSubscribeCmd,
|
||||||
ButtplugFutureStateShared<Result<(), ButtplugDeviceError>>,
|
oneshot::Sender<Result<(), ButtplugDeviceError>>,
|
||||||
),
|
),
|
||||||
Unsubscribe(
|
Unsubscribe(
|
||||||
HardwareUnsubscribeCmd,
|
HardwareUnsubscribeCmd,
|
||||||
ButtplugFutureStateShared<Result<(), ButtplugDeviceError>>,
|
oneshot::Sender<Result<(), ButtplugDeviceError>>,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,7 +276,7 @@ async fn run_webbluetooth_loop(
|
|||||||
.await;
|
.await;
|
||||||
while let Some(msg) = device_command_receiver.recv().await {
|
while let Some(msg) = device_command_receiver.recv().await {
|
||||||
match msg {
|
match msg {
|
||||||
WebBluetoothDeviceCommand::Write(write_cmd, waker) => {
|
WebBluetoothDeviceCommand::Write(write_cmd, sender) => {
|
||||||
debug!("Writing to endpoint {:?}", write_cmd.endpoint());
|
debug!("Writing to endpoint {:?}", write_cmd.endpoint());
|
||||||
let chr = char_map.get(&write_cmd.endpoint()).unwrap().clone();
|
let chr = char_map.get(&write_cmd.endpoint()).unwrap().clone();
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
@@ -295,10 +284,10 @@ async fn run_webbluetooth_loop(
|
|||||||
JsFuture::from(chr.write_value_with_u8_array(&uint8buf).unwrap())
|
JsFuture::from(chr.write_value_with_u8_array(&uint8buf).unwrap())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.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());
|
debug!("Writing to endpoint {:?}", read_cmd.endpoint());
|
||||||
let chr = char_map.get(&read_cmd.endpoint()).unwrap().clone();
|
let chr = char_map.get(&read_cmd.endpoint()).unwrap().clone();
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
@@ -307,10 +296,10 @@ async fn run_webbluetooth_loop(
|
|||||||
let mut body = vec![0; data_view.byte_length() as usize];
|
let mut body = vec![0; data_view.byte_length() as usize];
|
||||||
Uint8Array::new(&data_view).copy_to(&mut body[..]);
|
Uint8Array::new(&data_view).copy_to(&mut body[..]);
|
||||||
let reading = HardwareReading::new(read_cmd.endpoint(), &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());
|
debug!("Subscribing to endpoint {:?}", subscribe_cmd.endpoint());
|
||||||
let chr = char_map.get(&subscribe_cmd.endpoint()).unwrap().clone();
|
let chr = char_map.get(&subscribe_cmd.endpoint()).unwrap().clone();
|
||||||
let ep = subscribe_cmd.endpoint();
|
let ep = subscribe_cmd.endpoint();
|
||||||
@@ -335,10 +324,10 @@ async fn run_webbluetooth_loop(
|
|||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
JsFuture::from(chr.start_notifications()).await.unwrap();
|
JsFuture::from(chr.start_notifications()).await.unwrap();
|
||||||
debug!("Endpoint subscribed");
|
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!");
|
debug!("run_webbluetooth_loop exited!");
|
||||||
@@ -388,12 +377,13 @@ impl HardwareInternal for WebBluetoothHardware {
|
|||||||
let sender = self.device_command_sender.clone();
|
let sender = self.device_command_sender.clone();
|
||||||
let msg = msg.clone();
|
let msg = msg.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let fut = WebBluetoothReadResultFuture::default();
|
let (tx, rx) = oneshot::channel();
|
||||||
let waker = fut.get_state_clone();
|
let _ = sender
|
||||||
sender
|
.send(WebBluetoothDeviceCommand::Read(msg, tx))
|
||||||
.send(WebBluetoothDeviceCommand::Read(msg, waker))
|
|
||||||
.await;
|
.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 sender = self.device_command_sender.clone();
|
||||||
let msg = msg.clone();
|
let msg = msg.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let fut = WebBluetoothResultFuture::default();
|
let (tx, rx) = oneshot::channel();
|
||||||
let waker = fut.get_state_clone();
|
let _ = sender
|
||||||
sender
|
.send(WebBluetoothDeviceCommand::Write(msg, tx))
|
||||||
.send(WebBluetoothDeviceCommand::Write(msg.clone(), waker))
|
|
||||||
.await;
|
.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 sender = self.device_command_sender.clone();
|
||||||
let msg = msg.clone();
|
let msg = msg.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let fut = WebBluetoothResultFuture::default();
|
let (tx, rx) = oneshot::channel();
|
||||||
let waker = fut.get_state_clone();
|
let _ = sender
|
||||||
sender
|
.send(WebBluetoothDeviceCommand::Subscribe(msg, tx))
|
||||||
.send(WebBluetoothDeviceCommand::Subscribe(msg.clone(), waker))
|
|
||||||
.await;
|
.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 super::webbluetooth_hardware::WebBluetoothHardwareConnector;
|
||||||
|
|
||||||
use buttplug::{
|
use buttplug_core::ButtplugResultFuture;
|
||||||
core::ButtplugResultFuture,
|
use buttplug_server_device_config::ProtocolCommunicationSpecifier;
|
||||||
server::device::{
|
use buttplug_server::device::hardware::communication::{
|
||||||
configuration::{ProtocolCommunicationSpecifier},
|
|
||||||
hardware::communication::{
|
|
||||||
HardwareCommunicationManager, HardwareCommunicationManagerBuilder,
|
HardwareCommunicationManager, HardwareCommunicationManagerBuilder,
|
||||||
HardwareCommunicationManagerEvent,
|
HardwareCommunicationManagerEvent,
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
use futures::future;
|
use futures::future;
|
||||||
use js_sys::Array;
|
use js_sys::Array;
|
||||||
@@ -69,8 +65,8 @@ impl HardwareCommunicationManager for WebBluetoothCommunicationManager {
|
|||||||
let options = web_sys::RequestDeviceOptions::new();
|
let options = web_sys::RequestDeviceOptions::new();
|
||||||
let filters = Array::new();
|
let filters = Array::new();
|
||||||
let optional_services = Array::new();
|
let optional_services = Array::new();
|
||||||
for vals in config_manager.protocol_device_configurations().iter() {
|
for vals in config_manager.base_communication_specifiers().iter() {
|
||||||
for config in vals.1 {
|
for config in vals.1.iter() {
|
||||||
if let ProtocolCommunicationSpecifier::BluetoothLE(btle) = &config {
|
if let ProtocolCommunicationSpecifier::BluetoothLE(btle) = &config {
|
||||||
for name in btle.names() {
|
for name in btle.names() {
|
||||||
let filter = web_sys::BluetoothLeScanFilterInit::new();
|
let filter = web_sys::BluetoothLeScanFilterInit::new();
|
||||||
|
|||||||
@@ -5,9 +5,7 @@
|
|||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true
|
||||||
"emitDecoratorMetadata": true,
|
|
||||||
"experimentalDecorators": true
|
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,5 +14,8 @@ export default defineConfig({
|
|||||||
minify: false, // for demo purposes
|
minify: false, // for demo purposes
|
||||||
target: "esnext", // this is important as well
|
target: "esnext", // this is important as well
|
||||||
outDir: "dist",
|
outDir: "dist",
|
||||||
|
rollupOptions: {
|
||||||
|
external: [/\.\/wasm\//, /\.\.\/wasm\//],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { Label } from "$lib/components/ui/label";
|
|||||||
import { Card, CardContent, CardHeader } from "$lib/components/ui/card";
|
import { Card, CardContent, CardHeader } from "$lib/components/ui/card";
|
||||||
import type { BluetoothDevice } from "$lib/types";
|
import type { BluetoothDevice } from "$lib/types";
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
import { ActuatorType } from "@sexy.pivoine.art/buttplug";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
device: BluetoothDevice;
|
device: BluetoothDevice;
|
||||||
@@ -16,7 +15,7 @@ interface Props {
|
|||||||
let { device, onChange, onStop }: Props = $props();
|
let { device, onChange, onStop }: Props = $props();
|
||||||
|
|
||||||
function getBatteryColor(level: number) {
|
function getBatteryColor(level: number) {
|
||||||
if (!device.info.hasBattery) {
|
if (!device.hasBattery) {
|
||||||
return "text-gray-400";
|
return "text-gray-400";
|
||||||
}
|
}
|
||||||
if (level > 60) return "text-green-400";
|
if (level > 60) return "text-green-400";
|
||||||
@@ -25,7 +24,7 @@ function getBatteryColor(level: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getBatteryBgColor(level: number) {
|
function getBatteryBgColor(level: number) {
|
||||||
if (!device.info.hasBattery) {
|
if (!device.hasBattery) {
|
||||||
return "bg-gray-400/20";
|
return "bg-gray-400/20";
|
||||||
}
|
}
|
||||||
if (level > 60) return "bg-green-400/20";
|
if (level > 60) return "bg-green-400/20";
|
||||||
@@ -34,17 +33,13 @@ function getBatteryBgColor(level: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getScalarAnimations() {
|
function getScalarAnimations() {
|
||||||
const cmds: [{ ActuatorType: typeof ActuatorType }] =
|
return device.actuators
|
||||||
device.info.messageAttributes.ScalarCmd;
|
.filter((a) => a.value > 0)
|
||||||
return cmds
|
.map((a) => `animate-${a.outputType.toLowerCase()}`);
|
||||||
.filter((_, i: number) => !!device.actuatorValues[i])
|
|
||||||
.map(({ ActuatorType }) => `animate-${ActuatorType.toLowerCase()}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isActive() {
|
function isActive() {
|
||||||
const cmds: [{ ActuatorType: typeof ActuatorType }] =
|
return device.actuators.some((a) => a.value > 0);
|
||||||
device.info.messageAttributes.ScalarCmd;
|
|
||||||
return cmds.some((_, i: number) => !!device.actuatorValues[i]);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -119,7 +114,7 @@ function isActive() {
|
|||||||
></span>
|
></span>
|
||||||
<span class="text-sm text-muted-foreground">{$_("device_card.battery")}</span>
|
<span class="text-sm text-muted-foreground">{$_("device_card.battery")}</span>
|
||||||
</div>
|
</div>
|
||||||
{#if device.info.hasBattery}
|
{#if device.hasBattery}
|
||||||
<span class="text-sm font-medium {getBatteryColor(device.batteryLevel)}">
|
<span class="text-sm font-medium {getBatteryColor(device.batteryLevel)}">
|
||||||
{device.batteryLevel}%
|
{device.batteryLevel}%
|
||||||
</span>
|
</span>
|
||||||
@@ -144,19 +139,19 @@ function isActive() {
|
|||||||
</div> -->
|
</div> -->
|
||||||
|
|
||||||
<!-- Action Button -->
|
<!-- Action Button -->
|
||||||
{#each device.info.messageAttributes.ScalarCmd as scalarCmd}
|
{#each device.actuators as actuator, idx}
|
||||||
<div class="space-y-2">
|
<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
|
)}</Label
|
||||||
>
|
>
|
||||||
<Slider
|
<Slider
|
||||||
id={`device-${device.info.index}-${scalarCmd.Index}`}
|
id={`device-${device.info.index}-${actuator.featureIndex}-${actuator.outputType}`}
|
||||||
type="single"
|
type="single"
|
||||||
value={device.actuatorValues[scalarCmd.Index]}
|
value={actuator.value}
|
||||||
onValueChange={(val) => onChange(scalarCmd.Index, val)}
|
onValueChange={(val) => onChange(idx, val)}
|
||||||
max={scalarCmd.StepCount}
|
max={actuator.maxSteps}
|
||||||
step={1}
|
step={1}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -108,12 +108,20 @@ export interface Stats {
|
|||||||
viewers_count: number;
|
viewers_count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DeviceActuator {
|
||||||
|
featureIndex: number;
|
||||||
|
outputType: string;
|
||||||
|
maxSteps: number;
|
||||||
|
descriptor: string;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface BluetoothDevice {
|
export interface BluetoothDevice {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
actuatorValues: number[];
|
actuators: DeviceActuator[];
|
||||||
sensorValues: number[];
|
|
||||||
batteryLevel: number;
|
batteryLevel: number;
|
||||||
|
hasBattery: boolean;
|
||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
lastSeen: Date;
|
lastSeen: Date;
|
||||||
info: ButtplugClientDevice;
|
info: ButtplugClientDevice;
|
||||||
|
|||||||
@@ -3,18 +3,13 @@ import { _ } from "svelte-i18n";
|
|||||||
import Meta from "$lib/components/meta/meta.svelte";
|
import Meta from "$lib/components/meta/meta.svelte";
|
||||||
import {
|
import {
|
||||||
ButtplugClient,
|
ButtplugClient,
|
||||||
ButtplugMessage,
|
|
||||||
ButtplugWasmClientConnector,
|
ButtplugWasmClientConnector,
|
||||||
DeviceList,
|
|
||||||
SensorReadCmd,
|
|
||||||
StopDeviceCmd,
|
|
||||||
SensorReading,
|
|
||||||
ScalarCmd,
|
|
||||||
ScalarSubcommand,
|
|
||||||
ButtplugDeviceMessage,
|
|
||||||
ButtplugClientDevice,
|
ButtplugClientDevice,
|
||||||
SensorType,
|
OutputType,
|
||||||
|
InputType,
|
||||||
|
DeviceOutputValueConstructor,
|
||||||
} from "@sexy.pivoine.art/buttplug";
|
} from "@sexy.pivoine.art/buttplug";
|
||||||
|
import type { ButtplugMessage } from "@sexy.pivoine.art/buttplug";
|
||||||
import Button from "$lib/components/ui/button/button.svelte";
|
import Button from "$lib/components/ui/button/button.svelte";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
@@ -50,11 +45,12 @@ async function init() {
|
|||||||
// await ButtplugWasmClientConnector.activateLogging("info");
|
// await ButtplugWasmClientConnector.activateLogging("info");
|
||||||
await client.connect(connector);
|
await client.connect(connector);
|
||||||
client.on("deviceadded", onDeviceAdded);
|
client.on("deviceadded", onDeviceAdded);
|
||||||
client.on("deviceremoved", (msg: ButtplugDeviceMessage) =>
|
client.on("deviceremoved", (dev: ButtplugClientDevice) => {
|
||||||
devices.splice(msg.DeviceIndex, 1),
|
const idx = devices.findIndex((d) => d.info.index === dev.index);
|
||||||
);
|
if (idx !== -1) devices.splice(idx, 1);
|
||||||
|
});
|
||||||
client.on("scanningfinished", () => (scanning = false));
|
client.on("scanningfinished", () => (scanning = false));
|
||||||
connector.on("message", handleMessages);
|
client.on("inputreading", handleInputReading);
|
||||||
connected = client.connected;
|
connected = client.connected;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,65 +59,48 @@ async function startScanning() {
|
|||||||
scanning = true;
|
scanning = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onDeviceAdded(
|
async function onDeviceAdded(dev: ButtplugClientDevice) {
|
||||||
msg: ButtplugDeviceMessage,
|
|
||||||
dev: ButtplugClientDevice,
|
|
||||||
) {
|
|
||||||
const device = convertDevice(dev);
|
const device = convertDevice(dev);
|
||||||
devices.push(device);
|
devices.push(device);
|
||||||
|
|
||||||
const cmds = device.info.messageAttributes.SensorReadCmd;
|
// Try to read battery level
|
||||||
|
if (device.hasBattery) {
|
||||||
cmds?.forEach(async (cmd) => {
|
try {
|
||||||
await client.sendDeviceMessage(
|
device.batteryLevel = await dev.battery();
|
||||||
{ index: device.info.index },
|
} catch (e) {
|
||||||
new SensorReadCmd(device.info.index, cmd.Index, cmd.SensorType),
|
console.warn(`Failed to read battery for ${dev.name}:`, e);
|
||||||
);
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleMessages(messages: ButtplugMessage[]) {
|
function handleInputReading(msg: ButtplugMessage) {
|
||||||
messages.forEach(async (msg) => {
|
if (msg.InputReading === undefined) return;
|
||||||
await handleMessage(msg);
|
const reading = msg.InputReading;
|
||||||
});
|
const device = devices.find((d) => d.info.index === reading.DeviceIndex);
|
||||||
}
|
if (!device) return;
|
||||||
|
|
||||||
async function handleMessage(msg: ButtplugMessage) {
|
if (reading.Reading[InputType.Battery] !== undefined) {
|
||||||
if (msg instanceof SensorReading) {
|
device.batteryLevel = reading.Reading[InputType.Battery].Value;
|
||||||
const device = devices[msg.DeviceIndex];
|
|
||||||
if (msg.SensorType === SensorType.Battery) {
|
|
||||||
device.batteryLevel = msg.Data[0];
|
|
||||||
}
|
}
|
||||||
device.sensorValues[msg.Index] = msg.Data[0];
|
|
||||||
device.lastSeen = new Date();
|
device.lastSeen = new Date();
|
||||||
} else if (msg instanceof DeviceList) {
|
|
||||||
devices = client.devices.map(convertDevice);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleChange(
|
async function handleChange(
|
||||||
device: BluetoothDevice,
|
device: BluetoothDevice,
|
||||||
scalarIndex: number,
|
actuatorIdx: number,
|
||||||
value: number,
|
value: number,
|
||||||
) {
|
) {
|
||||||
const vibrateCmd = device.info.messageAttributes.ScalarCmd[scalarIndex];
|
const actuator = device.actuators[actuatorIdx];
|
||||||
await client.sendDeviceMessage(
|
const feature = device.info.features.get(actuator.featureIndex);
|
||||||
{ index: device.info.index },
|
if (!feature) return;
|
||||||
new ScalarCmd(
|
|
||||||
[
|
actuator.value = value;
|
||||||
new ScalarSubcommand(
|
const outputType = actuator.outputType as OutputType;
|
||||||
vibrateCmd.Index,
|
await feature.runOutput(new DeviceOutputValueConstructor(outputType).steps(value));
|
||||||
(device.actuatorValues[scalarIndex] = value),
|
|
||||||
vibrateCmd.ActuatorType,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
device.info.index,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Capture event if recording
|
// Capture event if recording
|
||||||
if (isRecording && recordingStartTime) {
|
if (isRecording && recordingStartTime) {
|
||||||
captureEvent(device, scalarIndex, value);
|
captureEvent(device, actuatorIdx, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,44 +124,51 @@ function stopRecording() {
|
|||||||
|
|
||||||
function captureEvent(
|
function captureEvent(
|
||||||
device: BluetoothDevice,
|
device: BluetoothDevice,
|
||||||
scalarIndex: number,
|
actuatorIdx: number,
|
||||||
value: number,
|
value: number,
|
||||||
) {
|
) {
|
||||||
if (!recordingStartTime) return;
|
if (!recordingStartTime) return;
|
||||||
|
|
||||||
const timestamp = performance.now() - recordingStartTime;
|
const timestamp = performance.now() - recordingStartTime;
|
||||||
const scalarCmd = device.info.messageAttributes.ScalarCmd[scalarIndex];
|
const actuator = device.actuators[actuatorIdx];
|
||||||
|
|
||||||
recordedEvents.push({
|
recordedEvents.push({
|
||||||
timestamp,
|
timestamp,
|
||||||
deviceIndex: device.info.index,
|
deviceIndex: device.info.index,
|
||||||
deviceName: device.name,
|
deviceName: device.name,
|
||||||
actuatorIndex: scalarIndex,
|
actuatorIndex: actuatorIdx,
|
||||||
actuatorType: scalarCmd.ActuatorType,
|
actuatorType: actuator.outputType,
|
||||||
value: (value / scalarCmd.StepCount) * 100, // Normalize to 0-100
|
value: (value / actuator.maxSteps) * 100, // Normalize to 0-100
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleStop(device: BluetoothDevice) {
|
async function handleStop(device: BluetoothDevice) {
|
||||||
await client.sendDeviceMessage(
|
await device.info.stop();
|
||||||
{ index: device.info.index },
|
device.actuators.forEach((a) => (a.value = 0));
|
||||||
new StopDeviceCmd(device.info.index),
|
|
||||||
);
|
|
||||||
device.actuatorValues = device.info.messageAttributes.ScalarCmd.map(() => 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertDevice(device: ButtplugClientDevice): BluetoothDevice {
|
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 {
|
return {
|
||||||
id: device.index as string,
|
id: String(device.index),
|
||||||
name: device.name as string,
|
name: device.name,
|
||||||
|
actuators,
|
||||||
batteryLevel: 0,
|
batteryLevel: 0,
|
||||||
|
hasBattery: device.hasInput(InputType.Battery),
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
lastSeen: new Date(),
|
lastSeen: new Date(),
|
||||||
sensorValues: device.messageAttributes.SensorReadCmd
|
|
||||||
? device.messageAttributes.SensorReadCmd.map(() => 0)
|
|
||||||
: [],
|
|
||||||
actuatorValues: device.messageAttributes.ScalarCmd.map(() => 0),
|
|
||||||
info: device,
|
info: device,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -195,7 +181,7 @@ async function handleSaveRecording(data: {
|
|||||||
const deviceInfo: DeviceInfo[] = devices.map((d) => ({
|
const deviceInfo: DeviceInfo[] = devices.map((d) => ({
|
||||||
name: d.name,
|
name: d.name,
|
||||||
index: d.info.index,
|
index: d.info.index,
|
||||||
capabilities: d.info.messageAttributes.ScalarCmd.map((cmd) => cmd.ActuatorType),
|
capabilities: d.actuators.map((a) => a.outputType),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -345,37 +331,26 @@ function executeEvent(event: RecordedEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find matching actuator by type
|
// Find matching actuator by type
|
||||||
const scalarCmd = device.info.messageAttributes.ScalarCmd.find(
|
const actuator = device.actuators.find(
|
||||||
cmd => cmd.ActuatorType === event.actuatorType
|
(a) => a.outputType === event.actuatorType,
|
||||||
);
|
);
|
||||||
if (!scalarCmd) {
|
if (!actuator) {
|
||||||
console.warn(`Actuator type ${event.actuatorType} not found on ${device.name}`);
|
console.warn(`Actuator type ${event.actuatorType} not found on ${device.name}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert normalized value (0-100) back to device scale
|
// 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
|
// Send command to device via feature
|
||||||
client.sendDeviceMessage(
|
const feature = device.info.features.get(actuator.featureIndex);
|
||||||
{ index: device.info.index },
|
if (feature) {
|
||||||
new ScalarCmd(
|
const outputType = actuator.outputType as OutputType;
|
||||||
[
|
feature.runOutput(new DeviceOutputValueConstructor(outputType).steps(deviceValue));
|
||||||
new ScalarSubcommand(
|
}
|
||||||
scalarCmd.Index,
|
|
||||||
deviceValue,
|
|
||||||
scalarCmd.ActuatorType,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
device.info.index,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update UI
|
// Update UI
|
||||||
const scalarIndex = device.info.messageAttributes.ScalarCmd.indexOf(scalarCmd);
|
actuator.value = deviceValue;
|
||||||
if (scalarIndex !== -1) {
|
|
||||||
device.actuatorValues[scalarIndex] = deviceValue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function seek(percentage: number) {
|
function seek(percentage: number) {
|
||||||
@@ -618,7 +593,7 @@ onMount(() => {
|
|||||||
deviceInfo={devices.map((d) => ({
|
deviceInfo={devices.map((d) => ({
|
||||||
name: d.name,
|
name: d.name,
|
||||||
index: d.info.index,
|
index: d.info.index,
|
||||||
capabilities: d.info.messageAttributes.ScalarCmd.map((cmd) => cmd.ActuatorType),
|
capabilities: d.actuators.map((a) => a.outputType),
|
||||||
}))}
|
}))}
|
||||||
duration={recordingDuration}
|
duration={recordingDuration}
|
||||||
onSave={handleSaveRecording}
|
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
|
// Check if a connected device is compatible with a recorded device
|
||||||
function isCompatible(recordedDevice: DeviceInfo, connectedDevice: BluetoothDevice): boolean {
|
function isCompatible(recordedDevice: DeviceInfo, connectedDevice: BluetoothDevice): boolean {
|
||||||
const connectedActuators = connectedDevice.info.messageAttributes.ScalarCmd.map(
|
const connectedActuators = connectedDevice.actuators.map(
|
||||||
cmd => cmd.ActuatorType
|
(a) => a.outputType,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if all required actuator types from recording exist on connected device
|
// Check if all required actuator types from recording exist on connected device
|
||||||
|
|||||||
@@ -8,6 +8,14 @@ export default defineConfig({
|
|||||||
resolve: {
|
resolve: {
|
||||||
alias: { $lib: path.resolve("./src/lib"), "@": path.resolve("./src/lib") },
|
alias: { $lib: path.resolve("./src/lib"), "@": path.resolve("./src/lib") },
|
||||||
},
|
},
|
||||||
|
ssr: {
|
||||||
|
noExternal: ["@sexy.pivoine.art/buttplug"],
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
external: [/\/wasm\/index\.js/],
|
||||||
|
},
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
proxy: {
|
proxy: {
|
||||||
|
|||||||
16
pnpm-lock.yaml
generated
16
pnpm-lock.yaml
generated
@@ -23,15 +23,9 @@ importers:
|
|||||||
|
|
||||||
packages/buttplug:
|
packages/buttplug:
|
||||||
dependencies:
|
dependencies:
|
||||||
class-transformer:
|
|
||||||
specifier: ^0.5.1
|
|
||||||
version: 0.5.1
|
|
||||||
eventemitter3:
|
eventemitter3:
|
||||||
specifier: ^5.0.1
|
specifier: ^5.0.1
|
||||||
version: 5.0.1
|
version: 5.0.1
|
||||||
reflect-metadata:
|
|
||||||
specifier: ^0.2.2
|
|
||||||
version: 0.2.2
|
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.9.2
|
specifier: ^5.9.2
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
@@ -1650,9 +1644,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
|
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
class-transformer@0.5.1:
|
|
||||||
resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==}
|
|
||||||
|
|
||||||
cli-color@2.0.4:
|
cli-color@2.0.4:
|
||||||
resolution: {integrity: sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==}
|
resolution: {integrity: sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==}
|
||||||
engines: {node: '>=0.10'}
|
engines: {node: '>=0.10'}
|
||||||
@@ -2857,9 +2848,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==}
|
resolution: {integrity: sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
reflect-metadata@0.2.2:
|
|
||||||
resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
|
|
||||||
|
|
||||||
relative-time-format@1.1.11:
|
relative-time-format@1.1.11:
|
||||||
resolution: {integrity: sha512-TH+oV/w77hjaB9xCzoFYJ/Icmr/12+02IAoCI/YGS2UBTbjCbBjHGEBxGnVy4EJvOR1qadGzyFRI6hGaJJG93Q==}
|
resolution: {integrity: sha512-TH+oV/w77hjaB9xCzoFYJ/Icmr/12+02IAoCI/YGS2UBTbjCbBjHGEBxGnVy4EJvOR1qadGzyFRI6hGaJJG93Q==}
|
||||||
|
|
||||||
@@ -4710,8 +4698,6 @@ snapshots:
|
|||||||
|
|
||||||
chownr@2.0.0: {}
|
chownr@2.0.0: {}
|
||||||
|
|
||||||
class-transformer@0.5.1: {}
|
|
||||||
|
|
||||||
cli-color@2.0.4:
|
cli-color@2.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
d: 1.0.2
|
d: 1.0.2
|
||||||
@@ -5897,8 +5883,6 @@ snapshots:
|
|||||||
|
|
||||||
reduce-flatten@2.0.0: {}
|
reduce-flatten@2.0.0: {}
|
||||||
|
|
||||||
reflect-metadata@0.2.2: {}
|
|
||||||
|
|
||||||
relative-time-format@1.1.11: {}
|
relative-time-format@1.1.11: {}
|
||||||
|
|
||||||
resolve-from@4.0.0: {}
|
resolve-from@4.0.0: {}
|
||||||
|
|||||||
Reference in New Issue
Block a user