Compare commits
4 Commits
fed2dd65e5
...
e744d1e40f
| Author | SHA1 | Date | |
|---|---|---|---|
| e744d1e40f | |||
| 82be8b8859 | |||
| 27d86fff8b | |||
| 6ea4ed1933 |
@@ -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,20 +6,20 @@
|
|||||||
* @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
|
||||||
implements IButtplugClientConnector
|
implements IButtplugClientConnector
|
||||||
{
|
{
|
||||||
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.DeviceList!);
|
||||||
|
break;
|
||||||
|
} else if (x.ScanningFinished !== undefined) {
|
||||||
|
this._isScanning = false;
|
||||||
|
this.emit('scanningfinished', x);
|
||||||
|
} else if (x.InputReading !== undefined) {
|
||||||
|
// TODO this should be emitted from the device or feature, not the client
|
||||||
|
this.emit('inputreading', x);
|
||||||
|
} else {
|
||||||
|
console.log(`Unhandled message: ${x}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
protected initializeConnection = async (): Promise<boolean> => {
|
||||||
|
this.checkConnector();
|
||||||
|
const msg = await this.sendMessage(
|
||||||
|
{
|
||||||
|
RequestServerInfo: {
|
||||||
|
ClientName: this._clientName,
|
||||||
|
Id: 1,
|
||||||
|
ProtocolVersionMajor: Messages.MESSAGE_SPEC_VERSION_MAJOR,
|
||||||
|
ProtocolVersionMinor: Messages.MESSAGE_SPEC_VERSION_MINOR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (msg.ServerInfo !== undefined) {
|
||||||
|
const serverinfo = msg as Messages.ServerInfo;
|
||||||
|
this._logger.Info(
|
||||||
|
`ButtplugClient: Connected to Server ${serverinfo.ServerName}`
|
||||||
|
);
|
||||||
|
// TODO: maybe store server name, do something with message template version?
|
||||||
|
const ping = serverinfo.MaxPingTime;
|
||||||
|
// If the server version is lower than the client version, the server will disconnect here.
|
||||||
|
if (ping > 0) {
|
||||||
|
/*
|
||||||
|
this._pingTimer = setInterval(async () => {
|
||||||
|
// If we've disconnected, stop trying to ping the server.
|
||||||
|
if (!this.Connected) {
|
||||||
|
await this.ShutdownConnection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.SendMessage(new Messages.Ping());
|
||||||
|
} , Math.round(ping / 2));
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
await this.requestDeviceList();
|
||||||
|
return true;
|
||||||
|
} else if (msg.Error !== undefined) {
|
||||||
|
// Disconnect and throw an exception with the error message we got back.
|
||||||
|
// This will usually only error out if we have a version mismatch that the
|
||||||
|
// server has detected.
|
||||||
|
await this._connector!.disconnect();
|
||||||
|
const err = msg.Error as Messages.Error;
|
||||||
|
throw ButtplugError.LogAndError(
|
||||||
|
ButtplugInitError,
|
||||||
|
this._logger,
|
||||||
|
`Cannot connect to server. ${err.ErrorMessage}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseDeviceList = (list: Messages.DeviceList) => {
|
||||||
|
for (let [_, d] of Object.entries(list.Devices)) {
|
||||||
|
if (!this._devices.has(d.DeviceIndex)) {
|
||||||
|
const device = ButtplugClientDevice.fromMsg(
|
||||||
|
d,
|
||||||
|
this.sendMessageClosure
|
||||||
|
);
|
||||||
|
this._logger.Debug(`ButtplugClient: Adding Device: ${device}`);
|
||||||
|
this._devices.set(d.DeviceIndex, device);
|
||||||
|
this.emit('deviceadded', device);
|
||||||
|
} else {
|
||||||
|
this._logger.Debug(`ButtplugClient: Device already added: ${d}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let [index, device] of this._devices.entries()) {
|
||||||
|
if (!list.Devices.hasOwnProperty(index.toString())) {
|
||||||
|
this._devices.delete(index);
|
||||||
|
this.emit('deviceremoved', device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected requestDeviceList = async () => {
|
||||||
|
this.checkConnector();
|
||||||
|
this._logger.Debug('ButtplugClient: ReceiveDeviceList called');
|
||||||
|
const response = (await this.sendMessage(
|
||||||
|
{
|
||||||
|
RequestDeviceList: { Id: 1 }
|
||||||
|
}
|
||||||
|
));
|
||||||
|
this.parseDeviceList(response.DeviceList!);
|
||||||
|
};
|
||||||
|
|
||||||
|
protected shutdownConnection = async () => {
|
||||||
|
await this.stopAllDevices();
|
||||||
|
if (this._pingTimer !== null) {
|
||||||
|
clearInterval(this._pingTimer);
|
||||||
|
this._pingTimer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
protected async sendMessage(
|
||||||
|
msg: Messages.ButtplugMessage
|
||||||
|
): Promise<Messages.ButtplugMessage> {
|
||||||
|
this.checkConnector();
|
||||||
|
const p = this._sorter.PrepareOutgoingMessage(msg);
|
||||||
|
await this._connector!.send(msg);
|
||||||
|
return await p;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected checkConnector() {
|
||||||
|
if (!this.connected) {
|
||||||
|
throw new ButtplugClientConnectorException(
|
||||||
|
'ButtplugClient not connected'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected sendMsgExpectOk = async (
|
||||||
|
msg: Messages.ButtplugMessage
|
||||||
|
): Promise<void> => {
|
||||||
|
const response = await this.sendMessage(msg);
|
||||||
|
if (response.Ok !== undefined) {
|
||||||
|
return;
|
||||||
|
} else if (response.Error !== undefined) {
|
||||||
|
throw ButtplugError.FromError(response as Messages.Error);
|
||||||
|
} else {
|
||||||
|
throw ButtplugError.LogAndError(
|
||||||
|
ButtplugMessageError,
|
||||||
|
this._logger,
|
||||||
|
`Message ${response} not handled by SendMsgExpectOk`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
protected sendMessageClosure = async (
|
||||||
|
msg: Messages.ButtplugMessage
|
||||||
|
): Promise<Messages.ButtplugMessage> => {
|
||||||
|
return await this.sendMessage(msg);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -6,11 +6,11 @@
|
|||||||
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
* @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) {
|
||||||
super(message, Messages.ErrorClass.ERROR_UNKNOWN);
|
super(message, Messages.ErrorClass.ERROR_UNKNOWN);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,396 +6,160 @@
|
|||||||
* @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 {
|
||||||
/**
|
|
||||||
* Return the name of the device.
|
|
||||||
*/
|
|
||||||
public get name(): string {
|
|
||||||
return this._deviceInfo.DeviceName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
private _features: Map<number, ButtplugClientDeviceFeature>;
|
||||||
* Return the user set name of the device.
|
|
||||||
*/
|
|
||||||
public get displayName(): string | undefined {
|
|
||||||
return this._deviceInfo.DeviceDisplayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the index of the device.
|
* Return the name of the device.
|
||||||
*/
|
*/
|
||||||
public get index(): number {
|
public get name(): string {
|
||||||
return this._deviceInfo.DeviceIndex;
|
return this._deviceInfo.DeviceName;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the index of the device.
|
* Return the user set name of the device.
|
||||||
*/
|
*/
|
||||||
public get messageTimingGap(): number | undefined {
|
public get displayName(): string | undefined {
|
||||||
return this._deviceInfo.DeviceMessageTimingGap;
|
return this._deviceInfo.DeviceDisplayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a list of message types the device accepts.
|
* Return the index of the device.
|
||||||
*/
|
*/
|
||||||
public get messageAttributes(): Messages.MessageAttributes {
|
public get index(): number {
|
||||||
return this._deviceInfo.DeviceMessages;
|
return this._deviceInfo.DeviceIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static fromMsg(
|
/**
|
||||||
msg: Messages.DeviceInfo,
|
* Return the index of the device.
|
||||||
sendClosure: (
|
*/
|
||||||
device: ButtplugClientDevice,
|
public get messageTimingGap(): number | undefined {
|
||||||
msg: Messages.ButtplugDeviceMessage,
|
return this._deviceInfo.DeviceMessageTimingGap;
|
||||||
) => Promise<Messages.ButtplugMessage>,
|
}
|
||||||
): ButtplugClientDevice {
|
|
||||||
return new ButtplugClientDevice(msg, sendClosure);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map of messages and their attributes (feature count, etc...)
|
public get features(): Map<number, ButtplugClientDeviceFeature> {
|
||||||
private allowedMsgs: Map<string, Messages.MessageAttributes> = new Map<
|
return this._features;
|
||||||
string,
|
}
|
||||||
Messages.MessageAttributes
|
|
||||||
>();
|
|
||||||
|
|
||||||
/**
|
public static fromMsg(
|
||||||
* @param _index Index of the device, as created by the device manager.
|
msg: Messages.DeviceInfo,
|
||||||
* @param _name Name of the device.
|
sendClosure: (
|
||||||
* @param allowedMsgs Buttplug messages the device can receive.
|
msg: Messages.ButtplugMessage
|
||||||
*/
|
) => Promise<Messages.ButtplugMessage>
|
||||||
constructor(
|
): ButtplugClientDevice {
|
||||||
private _deviceInfo: Messages.DeviceInfo,
|
return new ButtplugClientDevice(msg, sendClosure);
|
||||||
private _sendClosure: (
|
}
|
||||||
device: ButtplugClientDevice,
|
|
||||||
msg: Messages.ButtplugDeviceMessage,
|
|
||||||
) => Promise<Messages.ButtplugMessage>,
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
_deviceInfo.DeviceMessages.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async send(
|
/**
|
||||||
msg: Messages.ButtplugDeviceMessage,
|
* @param _index Index of the device, as created by the device manager.
|
||||||
): Promise<Messages.ButtplugMessage> {
|
* @param _name Name of the device.
|
||||||
// Assume we're getting the closure from ButtplugClient, which does all of
|
* @param allowedMsgs Buttplug messages the device can receive.
|
||||||
// the index/existence/connection/message checks for us.
|
*/
|
||||||
return await this._sendClosure(this, msg);
|
private constructor(
|
||||||
}
|
private _deviceInfo: Messages.DeviceInfo,
|
||||||
|
private _sendClosure: (
|
||||||
|
msg: Messages.ButtplugMessage
|
||||||
|
) => Promise<Messages.ButtplugMessage>
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
this._features = new Map(Object.entries(_deviceInfo.DeviceFeatures).map(([index, v]) => [parseInt(index), new ButtplugClientDeviceFeature(_deviceInfo.DeviceIndex, _deviceInfo.DeviceName, v, _sendClosure)]));
|
||||||
|
}
|
||||||
|
|
||||||
public async sendExpectOk(
|
public async send(
|
||||||
msg: Messages.ButtplugDeviceMessage,
|
msg: Messages.ButtplugMessage
|
||||||
): Promise<void> {
|
): Promise<Messages.ButtplugMessage> {
|
||||||
const response = await this.send(msg);
|
// Assume we're getting the closure from ButtplugClient, which does all of
|
||||||
switch (getMessageClassFromMessage(response)) {
|
// the index/existence/connection/message checks for us.
|
||||||
case Messages.Ok:
|
return await this._sendClosure(msg);
|
||||||
return;
|
}
|
||||||
case Messages.Error:
|
|
||||||
throw ButtplugError.FromError(response as Messages.Error);
|
|
||||||
default:
|
|
||||||
throw new ButtplugMessageError(
|
|
||||||
`Message type ${response.constructor} not handled by SendMsgExpectOk`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async scalar(
|
protected sendMsgExpectOk = async (
|
||||||
scalar: Messages.ScalarSubcommand | Messages.ScalarSubcommand[],
|
msg: Messages.ButtplugMessage
|
||||||
): Promise<void> {
|
): Promise<void> => {
|
||||||
if (Array.isArray(scalar)) {
|
const response = await this.send(msg);
|
||||||
await this.sendExpectOk(new Messages.ScalarCmd(scalar, this.index));
|
if (response.Ok !== undefined) {
|
||||||
} else {
|
return;
|
||||||
await this.sendExpectOk(new Messages.ScalarCmd([scalar], this.index));
|
} else if (response.Error !== undefined) {
|
||||||
}
|
throw ButtplugError.FromError(response as Messages.Error);
|
||||||
}
|
} else {
|
||||||
|
/*
|
||||||
|
throw ButtplugError.LogAndError(
|
||||||
|
ButtplugMessageError,
|
||||||
|
this._logger,
|
||||||
|
`Message ${response} not handled by SendMsgExpectOk`
|
||||||
|
);
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private async scalarCommandBuilder(
|
protected isOutputValid(featureIndex: number, type: Messages.OutputType) {
|
||||||
speed: number | number[],
|
if (!this._deviceInfo.DeviceFeatures.hasOwnProperty(featureIndex.toString())) {
|
||||||
actuator: Messages.ActuatorType,
|
throw new ButtplugDeviceError(`Feature index ${featureIndex} does not exist for device ${this.name}`);
|
||||||
) {
|
}
|
||||||
const scalarAttrs = this.messageAttributes.ScalarCmd?.filter(
|
if (this._deviceInfo.DeviceFeatures[featureIndex.toString()].Outputs !== undefined && !this._deviceInfo.DeviceFeatures[featureIndex.toString()].Outputs.hasOwnProperty(type)) {
|
||||||
(x) => x.ActuatorType === actuator,
|
throw new ButtplugDeviceError(`Feature index ${featureIndex} does not support type ${type} for device ${this.name}`);
|
||||||
);
|
}
|
||||||
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 hasOutput(type: Messages.OutputType): boolean {
|
||||||
return (
|
return this._features.values().filter((f) => f.hasOutput(type)).toArray().length > 0;
|
||||||
this.messageAttributes.ScalarCmd?.filter(
|
}
|
||||||
(x) => x.ActuatorType === Messages.ActuatorType.Vibrate,
|
|
||||||
) ?? []
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async vibrate(speed: number | number[]): Promise<void> {
|
public hasInput(type: Messages.InputType): boolean {
|
||||||
await this.scalarCommandBuilder(speed, Messages.ActuatorType.Vibrate);
|
return this._features.values().filter((f) => f.hasInput(type)).toArray().length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get oscillateAttributes(): Messages.GenericDeviceMessageAttributes[] {
|
public async runOutput(cmd: DeviceOutputCommand): Promise<void> {
|
||||||
return (
|
let p: Promise<void>[] = [];
|
||||||
this.messageAttributes.ScalarCmd?.filter(
|
for (let f of this._features.values()) {
|
||||||
(x) => x.ActuatorType === Messages.ActuatorType.Oscillate,
|
if (f.hasOutput(cmd.outputType)) {
|
||||||
) ?? []
|
p.push(f.runOutput(cmd));
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
|
if (p.length == 0) {
|
||||||
|
return Promise.reject(`No features with output type ${cmd.outputType}`);
|
||||||
|
}
|
||||||
|
await Promise.all(p);
|
||||||
|
}
|
||||||
|
|
||||||
public async oscillate(speed: number | number[]): Promise<void> {
|
public async stop(): Promise<void> {
|
||||||
await this.scalarCommandBuilder(speed, Messages.ActuatorType.Oscillate);
|
await this.sendMsgExpectOk({StopCmd: { Id: 1, DeviceIndex: this.index, FeatureIndex: undefined, Inputs: true, Outputs: true}});
|
||||||
}
|
}
|
||||||
|
|
||||||
public get rotateAttributes(): Messages.GenericDeviceMessageAttributes[] {
|
public async battery(): Promise<number> {
|
||||||
return this.messageAttributes.RotateCmd ?? [];
|
let p: Promise<void>[] = [];
|
||||||
}
|
for (let f of this._features.values()) {
|
||||||
|
if (f.hasInput(Messages.InputType.Battery)) {
|
||||||
|
// Right now, we only have one battery per device, so assume the first one we find is it.
|
||||||
|
let response = await f.runInput(Messages.InputType.Battery, Messages.InputCommandType.Read);
|
||||||
|
if (response === undefined) {
|
||||||
|
throw new ButtplugMessageError("Got incorrect message back.");
|
||||||
|
}
|
||||||
|
if (response.Reading[Messages.InputType.Battery] === undefined) {
|
||||||
|
throw new ButtplugMessageError("Got reading with no Battery info.");
|
||||||
|
}
|
||||||
|
return response.Reading[Messages.InputType.Battery].Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new ButtplugDeviceError(`No battery present on this device.`);
|
||||||
|
}
|
||||||
|
|
||||||
public async rotate(
|
public emitDisconnected() {
|
||||||
values: number | [number, boolean][],
|
this.emit('deviceremoved');
|
||||||
clockwise?: boolean,
|
}
|
||||||
): Promise<void> {
|
|
||||||
const rotateAttrs = this.messageAttributes.RotateCmd;
|
|
||||||
if (!rotateAttrs || rotateAttrs.length === 0) {
|
|
||||||
throw new ButtplugDeviceError(
|
|
||||||
`Device ${this.name} has no Rotate capabilities`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let msg: Messages.RotateCmd;
|
|
||||||
if (typeof values === "number") {
|
|
||||||
msg = Messages.RotateCmd.Create(
|
|
||||||
this.index,
|
|
||||||
new Array(rotateAttrs.length).fill([values, clockwise]),
|
|
||||||
);
|
|
||||||
} else if (Array.isArray(values)) {
|
|
||||||
msg = Messages.RotateCmd.Create(this.index, values);
|
|
||||||
} else {
|
|
||||||
throw new ButtplugDeviceError(
|
|
||||||
"SendRotateCmd can only take a number and boolean, or an array of number/boolean tuples",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await this.sendExpectOk(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
public get linearAttributes(): Messages.GenericDeviceMessageAttributes[] {
|
|
||||||
return this.messageAttributes.LinearCmd ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public async linear(
|
|
||||||
values: number | [number, number][],
|
|
||||||
duration?: number,
|
|
||||||
): Promise<void> {
|
|
||||||
const linearAttrs = this.messageAttributes.LinearCmd;
|
|
||||||
if (!linearAttrs || linearAttrs.length === 0) {
|
|
||||||
throw new ButtplugDeviceError(
|
|
||||||
`Device ${this.name} has no Linear capabilities`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let msg: Messages.LinearCmd;
|
|
||||||
if (typeof values === "number") {
|
|
||||||
msg = Messages.LinearCmd.Create(
|
|
||||||
this.index,
|
|
||||||
new Array(linearAttrs.length).fill([values, duration]),
|
|
||||||
);
|
|
||||||
} else if (Array.isArray(values)) {
|
|
||||||
msg = Messages.LinearCmd.Create(this.index, values);
|
|
||||||
} else {
|
|
||||||
throw new ButtplugDeviceError(
|
|
||||||
"SendLinearCmd can only take a number and number, or an array of number/number tuples",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await this.sendExpectOk(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async sensorRead(
|
|
||||||
sensorIndex: number,
|
|
||||||
sensorType: Messages.SensorType,
|
|
||||||
): Promise<number[]> {
|
|
||||||
const response = await this.send(
|
|
||||||
new Messages.SensorReadCmd(this.index, sensorIndex, sensorType),
|
|
||||||
);
|
|
||||||
switch (getMessageClassFromMessage(response)) {
|
|
||||||
case Messages.SensorReading:
|
|
||||||
return (response as Messages.SensorReading).Data;
|
|
||||||
case Messages.Error:
|
|
||||||
throw ButtplugError.FromError(response as Messages.Error);
|
|
||||||
default:
|
|
||||||
throw new ButtplugMessageError(
|
|
||||||
`Message type ${response.constructor} not handled by sensorRead`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public get hasBattery(): boolean {
|
|
||||||
const batteryAttrs = this.messageAttributes.SensorReadCmd?.filter(
|
|
||||||
(x) => x.SensorType === Messages.SensorType.Battery,
|
|
||||||
);
|
|
||||||
return batteryAttrs !== undefined && batteryAttrs.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async battery(): Promise<number> {
|
|
||||||
if (!this.hasBattery) {
|
|
||||||
throw new ButtplugDeviceError(
|
|
||||||
`Device ${this.name} has no Battery capabilities`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const batteryAttrs = this.messageAttributes.SensorReadCmd?.filter(
|
|
||||||
(x) => x.SensorType === Messages.SensorType.Battery,
|
|
||||||
);
|
|
||||||
// Find the battery sensor, we'll need its index.
|
|
||||||
const result = await this.sensorRead(
|
|
||||||
batteryAttrs![0].Index,
|
|
||||||
Messages.SensorType.Battery,
|
|
||||||
);
|
|
||||||
return result[0] / 100.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get hasRssi(): boolean {
|
|
||||||
const rssiAttrs = this.messageAttributes.SensorReadCmd?.filter(
|
|
||||||
(x) => x.SensorType === Messages.SensorType.RSSI,
|
|
||||||
);
|
|
||||||
return rssiAttrs !== undefined && rssiAttrs.length === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async rssi(): Promise<number> {
|
|
||||||
if (!this.hasRssi) {
|
|
||||||
throw new ButtplugDeviceError(
|
|
||||||
`Device ${this.name} has no RSSI capabilities`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const rssiAttrs = this.messageAttributes.SensorReadCmd?.filter(
|
|
||||||
(x) => x.SensorType === Messages.SensorType.RSSI,
|
|
||||||
);
|
|
||||||
// Find the battery sensor, we'll need its index.
|
|
||||||
const result = await this.sensorRead(
|
|
||||||
rssiAttrs![0].Index,
|
|
||||||
Messages.SensorType.RSSI,
|
|
||||||
);
|
|
||||||
return result[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
public async rawRead(
|
|
||||||
endpoint: string,
|
|
||||||
expectedLength: number,
|
|
||||||
timeout: number,
|
|
||||||
): Promise<Uint8Array> {
|
|
||||||
if (!this.messageAttributes.RawReadCmd) {
|
|
||||||
throw new ButtplugDeviceError(
|
|
||||||
`Device ${this.name} has no raw read capabilities`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (this.messageAttributes.RawReadCmd.Endpoints.indexOf(endpoint) === -1) {
|
|
||||||
throw new ButtplugDeviceError(
|
|
||||||
`Device ${this.name} has no raw readable endpoint ${endpoint}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const response = await this.send(
|
|
||||||
new Messages.RawReadCmd(this.index, endpoint, expectedLength, timeout),
|
|
||||||
);
|
|
||||||
switch (getMessageClassFromMessage(response)) {
|
|
||||||
case Messages.RawReading:
|
|
||||||
return new Uint8Array((response as Messages.RawReading).Data);
|
|
||||||
case Messages.Error:
|
|
||||||
throw ButtplugError.FromError(response as Messages.Error);
|
|
||||||
default:
|
|
||||||
throw new ButtplugMessageError(
|
|
||||||
`Message type ${response.constructor} not handled by rawRead`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async rawWrite(
|
|
||||||
endpoint: string,
|
|
||||||
data: Uint8Array,
|
|
||||||
writeWithResponse: boolean,
|
|
||||||
): Promise<void> {
|
|
||||||
if (!this.messageAttributes.RawWriteCmd) {
|
|
||||||
throw new ButtplugDeviceError(
|
|
||||||
`Device ${this.name} has no raw write capabilities`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (this.messageAttributes.RawWriteCmd.Endpoints.indexOf(endpoint) === -1) {
|
|
||||||
throw new ButtplugDeviceError(
|
|
||||||
`Device ${this.name} has no raw writable endpoint ${endpoint}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await this.sendExpectOk(
|
|
||||||
new Messages.RawWriteCmd(this.index, endpoint, data, writeWithResponse),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async rawSubscribe(endpoint: string): Promise<void> {
|
|
||||||
if (!this.messageAttributes.RawSubscribeCmd) {
|
|
||||||
throw new ButtplugDeviceError(
|
|
||||||
`Device ${this.name} has no raw subscribe capabilities`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
this.messageAttributes.RawSubscribeCmd.Endpoints.indexOf(endpoint) === -1
|
|
||||||
) {
|
|
||||||
throw new ButtplugDeviceError(
|
|
||||||
`Device ${this.name} has no raw subscribable endpoint ${endpoint}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await this.sendExpectOk(new Messages.RawSubscribeCmd(this.index, endpoint));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async rawUnsubscribe(endpoint: string): Promise<void> {
|
|
||||||
// This reuses raw subscribe's info.
|
|
||||||
if (!this.messageAttributes.RawSubscribeCmd) {
|
|
||||||
throw new ButtplugDeviceError(
|
|
||||||
`Device ${this.name} has no raw unsubscribe capabilities`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
this.messageAttributes.RawSubscribeCmd.Endpoints.indexOf(endpoint) === -1
|
|
||||||
) {
|
|
||||||
throw new ButtplugDeviceError(
|
|
||||||
`Device ${this.name} has no raw unsubscribable endpoint ${endpoint}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await this.sendExpectOk(
|
|
||||||
new Messages.RawUnsubscribeCmd(this.index, endpoint),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async stop(): Promise<void> {
|
|
||||||
await this.sendExpectOk(new Messages.StopDeviceCmd(this.index));
|
|
||||||
}
|
|
||||||
|
|
||||||
public emitDisconnected() {
|
|
||||||
this.emit("deviceremoved");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
111
packages/buttplug/src/client/ButtplugClientDeviceCommand.ts
Normal file
111
packages/buttplug/src/client/ButtplugClientDeviceCommand.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { ButtplugDeviceError } from "../core/Exceptions";
|
||||||
|
import { OutputType } from "../core/Messages";
|
||||||
|
|
||||||
|
class PercentOrSteps {
|
||||||
|
private _percent: number | undefined;
|
||||||
|
private _steps: number | undefined;
|
||||||
|
|
||||||
|
public get percent() {
|
||||||
|
return this._percent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get steps() {
|
||||||
|
return this._steps;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static createSteps(s: number): PercentOrSteps {
|
||||||
|
let v = new PercentOrSteps;
|
||||||
|
v._steps = s;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static createPercent(p: number): PercentOrSteps {
|
||||||
|
if (p < 0 || p > 1.0) {
|
||||||
|
throw new ButtplugDeviceError(`Percent value ${p} is not in the range 0.0 <= x <= 1.0`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let v = new PercentOrSteps;
|
||||||
|
v._percent = p;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DeviceOutputCommand {
|
||||||
|
public constructor(
|
||||||
|
private _outputType: OutputType,
|
||||||
|
private _value: PercentOrSteps,
|
||||||
|
private _duration?: number,
|
||||||
|
)
|
||||||
|
{}
|
||||||
|
|
||||||
|
public get outputType() {
|
||||||
|
return this._outputType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get value() {
|
||||||
|
return this._value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get duration() {
|
||||||
|
return this._duration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DeviceOutputValueConstructor {
|
||||||
|
public constructor(
|
||||||
|
private _outputType: OutputType)
|
||||||
|
{}
|
||||||
|
|
||||||
|
public steps(steps: number): DeviceOutputCommand {
|
||||||
|
return new DeviceOutputCommand(this._outputType, PercentOrSteps.createSteps(steps), undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
public percent(percent: number): DeviceOutputCommand {
|
||||||
|
return new DeviceOutputCommand(this._outputType, PercentOrSteps.createPercent(percent), undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DeviceOutputPositionWithDurationConstructor {
|
||||||
|
public steps(steps: number, duration: number): DeviceOutputCommand {
|
||||||
|
return new DeviceOutputCommand(OutputType.Position, PercentOrSteps.createSteps(steps), duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
public percent(percent: number, duration: number): DeviceOutputCommand {
|
||||||
|
return new DeviceOutputCommand(OutputType.HwPositionWithDuration, PercentOrSteps.createPercent(percent), duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DeviceOutput {
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
public static get Vibrate() {
|
||||||
|
return new DeviceOutputValueConstructor(OutputType.Vibrate);
|
||||||
|
}
|
||||||
|
public static get Rotate() {
|
||||||
|
return new DeviceOutputValueConstructor(OutputType.Rotate);
|
||||||
|
}
|
||||||
|
public static get Oscillate() {
|
||||||
|
return new DeviceOutputValueConstructor(OutputType.Oscillate);
|
||||||
|
}
|
||||||
|
public static get Constrict() {
|
||||||
|
return new DeviceOutputValueConstructor(OutputType.Constrict);
|
||||||
|
}
|
||||||
|
public static get Inflate() {
|
||||||
|
return new DeviceOutputValueConstructor(OutputType.Inflate);
|
||||||
|
}
|
||||||
|
public static get Temperature() {
|
||||||
|
return new DeviceOutputValueConstructor(OutputType.Temperature);
|
||||||
|
}
|
||||||
|
public static get Led() {
|
||||||
|
return new DeviceOutputValueConstructor(OutputType.Led);
|
||||||
|
}
|
||||||
|
public static get Spray() {
|
||||||
|
return new DeviceOutputValueConstructor(OutputType.Spray);
|
||||||
|
}
|
||||||
|
public static get Position() {
|
||||||
|
return new DeviceOutputValueConstructor(OutputType.Position);
|
||||||
|
}
|
||||||
|
public static get PositionWithDuration() {
|
||||||
|
return new DeviceOutputPositionWithDurationConstructor();
|
||||||
|
}
|
||||||
|
}
|
||||||
168
packages/buttplug/src/client/ButtplugClientDeviceFeature.ts
Normal file
168
packages/buttplug/src/client/ButtplugClientDeviceFeature.ts
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import { ButtplugDeviceError, ButtplugError, ButtplugMessageError } from "../core/Exceptions";
|
||||||
|
import * as Messages from "../core/Messages";
|
||||||
|
import { DeviceOutputCommand } from "./ButtplugClientDeviceCommand";
|
||||||
|
|
||||||
|
export class ButtplugClientDeviceFeature {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private _deviceIndex: number,
|
||||||
|
private _deviceName: string,
|
||||||
|
private _feature: Messages.DeviceFeature,
|
||||||
|
private _sendClosure: (
|
||||||
|
msg: Messages.ButtplugMessage
|
||||||
|
) => Promise<Messages.ButtplugMessage>) {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected send = async (msg: Messages.ButtplugMessage): Promise<Messages.ButtplugMessage> => {
|
||||||
|
return await this._sendClosure(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected sendMsgExpectOk = async (
|
||||||
|
msg: Messages.ButtplugMessage
|
||||||
|
): Promise<void> => {
|
||||||
|
const response = await this.send(msg);
|
||||||
|
if (response.Ok !== undefined) {
|
||||||
|
return;
|
||||||
|
} else if (response.Error !== undefined) {
|
||||||
|
throw ButtplugError.FromError(response as Messages.Error);
|
||||||
|
} else {
|
||||||
|
throw new ButtplugMessageError("Expected Ok or Error, and didn't get either!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
protected isOutputValid(type: Messages.OutputType) {
|
||||||
|
if (this._feature.Output !== undefined && !this._feature.Output.hasOwnProperty(type)) {
|
||||||
|
throw new ButtplugDeviceError(`Feature index ${this._feature.FeatureIndex} does not support type ${type} for device ${this._deviceName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected isInputValid(type: Messages.InputType) {
|
||||||
|
if (this._feature.Input !== undefined && !this._feature.Input.hasOwnProperty(type)) {
|
||||||
|
throw new ButtplugDeviceError(`Feature index ${this._feature.FeatureIndex} does not support type ${type} for device ${this._deviceName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async sendOutputCmd(command: DeviceOutputCommand): Promise<void> {
|
||||||
|
// Make sure the requested feature is valid
|
||||||
|
this.isOutputValid(command.outputType);
|
||||||
|
if (command.value === undefined) {
|
||||||
|
throw new ButtplugDeviceError(`${command.outputType} requires value defined`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let type = command.outputType;
|
||||||
|
let duration: undefined | number = undefined;
|
||||||
|
if (type == Messages.OutputType.HwPositionWithDuration) {
|
||||||
|
if (command.duration === undefined) {
|
||||||
|
throw new ButtplugDeviceError("PositionWithDuration requires duration defined");
|
||||||
|
}
|
||||||
|
duration = command.duration;
|
||||||
|
}
|
||||||
|
let value: number;
|
||||||
|
let p = command.value;
|
||||||
|
if (p.percent === undefined) {
|
||||||
|
// TODO Check step limits here
|
||||||
|
value = command.value.steps!;
|
||||||
|
} else {
|
||||||
|
value = Math.ceil(this._feature.Output[type]!.Value![1] * p.percent);
|
||||||
|
}
|
||||||
|
let newCommand: Messages.DeviceFeatureOutput = { Value: value, Duration: duration };
|
||||||
|
let outCommand = {};
|
||||||
|
outCommand[type.toString()] = newCommand;
|
||||||
|
|
||||||
|
let cmd: Messages.ButtplugMessage = {
|
||||||
|
OutputCmd: {
|
||||||
|
Id: 1,
|
||||||
|
DeviceIndex: this._deviceIndex,
|
||||||
|
FeatureIndex: this._feature.FeatureIndex,
|
||||||
|
Command: outCommand
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await this.sendMsgExpectOk(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get featureDescriptor(): string {
|
||||||
|
return this._feature.FeatureDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get featureIndex(): number {
|
||||||
|
return this._feature.FeatureIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get outputTypes(): Messages.OutputType[] {
|
||||||
|
if (this._feature.Output === undefined) return [];
|
||||||
|
return Object.keys(this._feature.Output) as Messages.OutputType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
public get inputTypes(): Messages.InputType[] {
|
||||||
|
if (this._feature.Input === undefined) return [];
|
||||||
|
return Object.keys(this._feature.Input) as Messages.InputType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
public outputMaxValue(type: Messages.OutputType): number {
|
||||||
|
if (this._feature.Output === undefined || this._feature.Output[type] === undefined) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const val = this._feature.Output[type]!.Value;
|
||||||
|
// Value can arrive as number[] [min, max] from server or as number
|
||||||
|
if (Array.isArray(val)) {
|
||||||
|
return val[val.length - 1];
|
||||||
|
}
|
||||||
|
return val as number;
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasOutput(type: Messages.OutputType): boolean {
|
||||||
|
if (this._feature.Output !== undefined) {
|
||||||
|
return this._feature.Output.hasOwnProperty(type.toString());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasInput(type: Messages.InputType): boolean {
|
||||||
|
if (this._feature.Input !== undefined) {
|
||||||
|
return this._feature.Input.hasOwnProperty(type.toString());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async runOutput(cmd: DeviceOutputCommand): Promise<void> {
|
||||||
|
if (this._feature.Output !== undefined && this._feature.Output.hasOwnProperty(cmd.outputType.toString())) {
|
||||||
|
return this.sendOutputCmd(cmd);
|
||||||
|
}
|
||||||
|
throw new ButtplugDeviceError(`Output type ${cmd.outputType} not supported by feature.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async runInput(inputType: Messages.InputType, inputCommand: Messages.InputCommandType): Promise<Messages.InputReading | undefined> {
|
||||||
|
// Make sure the requested feature is valid
|
||||||
|
this.isInputValid(inputType);
|
||||||
|
let inputAttributes = this._feature.Input[inputType];
|
||||||
|
console.log(this._feature.Input);
|
||||||
|
if ((inputCommand === Messages.InputCommandType.Unsubscribe && !inputAttributes.Command.includes(Messages.InputCommandType.Subscribe)) && !inputAttributes.Command.includes(inputCommand)) {
|
||||||
|
throw new ButtplugDeviceError(`${inputType} does not support command ${inputCommand}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let cmd: Messages.ButtplugMessage = {
|
||||||
|
InputCmd: {
|
||||||
|
Id: 1,
|
||||||
|
DeviceIndex: this._deviceIndex,
|
||||||
|
FeatureIndex: this._feature.FeatureIndex,
|
||||||
|
Type: inputType,
|
||||||
|
Command: inputCommand,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (inputCommand == Messages.InputCommandType.Read) {
|
||||||
|
const response = await this.send(cmd);
|
||||||
|
if (response.InputReading !== undefined) {
|
||||||
|
return response.InputReading;
|
||||||
|
} else if (response.Error !== undefined) {
|
||||||
|
throw ButtplugError.FromError(response as Messages.Error);
|
||||||
|
} else {
|
||||||
|
throw new ButtplugMessageError("Expected InputReading or Error, and didn't get either!");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`Sending subscribe message: ${JSON.stringify(cmd)}`);
|
||||||
|
await this.sendMsgExpectOk(cmd);
|
||||||
|
console.log("Got back ok?");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,12 +6,12 @@
|
|||||||
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
* @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 =
|
||||||
NodeWebSocket as unknown as typeof WebSocket;
|
NodeWebSocket as unknown as typeof WebSocket;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,276 +0,0 @@
|
|||||||
/*!
|
|
||||||
* Buttplug JS Source Code File - Visit https://buttplug.io for more info about
|
|
||||||
* the project. Licensed under the BSD 3-Clause license. See LICENSE file in the
|
|
||||||
* project root for full license information.
|
|
||||||
*
|
|
||||||
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
import { ButtplugLogger } from "../core/Logging";
|
|
||||||
import { EventEmitter } from "eventemitter3";
|
|
||||||
import { ButtplugClientDevice } from "./ButtplugClientDevice";
|
|
||||||
import { IButtplugClientConnector } from "./IButtplugClientConnector";
|
|
||||||
import { ButtplugMessageSorter } from "../utils/ButtplugMessageSorter";
|
|
||||||
|
|
||||||
import * as Messages from "../core/Messages";
|
|
||||||
import {
|
|
||||||
ButtplugDeviceError,
|
|
||||||
ButtplugError,
|
|
||||||
ButtplugInitError,
|
|
||||||
ButtplugMessageError,
|
|
||||||
} from "../core/Exceptions";
|
|
||||||
import { ButtplugClientConnectorException } from "./ButtplugClientConnectorException";
|
|
||||||
import { getMessageClassFromMessage } from "../core/MessageUtils";
|
|
||||||
|
|
||||||
export class ButtplugClient extends EventEmitter {
|
|
||||||
protected _pingTimer: NodeJS.Timeout | null = null;
|
|
||||||
protected _connector: IButtplugClientConnector | null = null;
|
|
||||||
protected _devices: Map<number, ButtplugClientDevice> = new Map();
|
|
||||||
protected _clientName: string;
|
|
||||||
protected _logger = ButtplugLogger.Logger;
|
|
||||||
protected _isScanning = false;
|
|
||||||
private _sorter: ButtplugMessageSorter = new ButtplugMessageSorter(true);
|
|
||||||
|
|
||||||
constructor(clientName = "Generic Buttplug Client") {
|
|
||||||
super();
|
|
||||||
this._clientName = clientName;
|
|
||||||
this._logger.Debug(`ButtplugClient: Client ${clientName} created.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
public get connected(): boolean {
|
|
||||||
return this._connector !== null && this._connector.Connected;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get devices(): ButtplugClientDevice[] {
|
|
||||||
// While this function doesn't actually send a message, if we don't have a
|
|
||||||
// connector, we shouldn't have devices.
|
|
||||||
this.checkConnector();
|
|
||||||
const devices: ButtplugClientDevice[] = [];
|
|
||||||
this._devices.forEach((d) => {
|
|
||||||
devices.push(d);
|
|
||||||
});
|
|
||||||
return devices;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get isScanning(): boolean {
|
|
||||||
return this._isScanning;
|
|
||||||
}
|
|
||||||
|
|
||||||
public connect = async (connector: IButtplugClientConnector) => {
|
|
||||||
this._logger.Info(
|
|
||||||
`ButtplugClient: Connecting using ${connector.constructor.name}`,
|
|
||||||
);
|
|
||||||
await connector.connect();
|
|
||||||
this._connector = connector;
|
|
||||||
this._connector.addListener("message", this.parseMessages);
|
|
||||||
this._connector.addListener("disconnect", this.disconnectHandler);
|
|
||||||
await this.initializeConnection();
|
|
||||||
};
|
|
||||||
|
|
||||||
public disconnect = async () => {
|
|
||||||
this._logger.Debug("ButtplugClient: Disconnect called");
|
|
||||||
this.checkConnector();
|
|
||||||
await this.shutdownConnection();
|
|
||||||
await this._connector!.disconnect();
|
|
||||||
};
|
|
||||||
|
|
||||||
public startScanning = async () => {
|
|
||||||
this._logger.Debug("ButtplugClient: StartScanning called");
|
|
||||||
this._isScanning = true;
|
|
||||||
await this.sendMsgExpectOk(new Messages.StartScanning());
|
|
||||||
};
|
|
||||||
|
|
||||||
public stopScanning = async () => {
|
|
||||||
this._logger.Debug("ButtplugClient: StopScanning called");
|
|
||||||
this._isScanning = false;
|
|
||||||
await this.sendMsgExpectOk(new Messages.StopScanning());
|
|
||||||
};
|
|
||||||
|
|
||||||
public stopAllDevices = async () => {
|
|
||||||
this._logger.Debug("ButtplugClient: StopAllDevices");
|
|
||||||
await this.sendMsgExpectOk(new Messages.StopAllDevices());
|
|
||||||
};
|
|
||||||
|
|
||||||
private async sendDeviceMessage(
|
|
||||||
device: ButtplugClientDevice,
|
|
||||||
deviceMsg: Messages.ButtplugDeviceMessage,
|
|
||||||
): Promise<Messages.ButtplugMessage> {
|
|
||||||
this.checkConnector();
|
|
||||||
const dev = this._devices.get(device.index);
|
|
||||||
if (dev === undefined) {
|
|
||||||
throw ButtplugError.LogAndError(
|
|
||||||
ButtplugDeviceError,
|
|
||||||
this._logger,
|
|
||||||
`Device ${device.index} not available.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
deviceMsg.DeviceIndex = device.index;
|
|
||||||
return await this.sendMessage(deviceMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected disconnectHandler = () => {
|
|
||||||
this._logger.Info("ButtplugClient: Disconnect event receieved.");
|
|
||||||
this.emit("disconnect");
|
|
||||||
};
|
|
||||||
|
|
||||||
protected parseMessages = (msgs: Messages.ButtplugMessage[]) => {
|
|
||||||
const leftoverMsgs = this._sorter.ParseIncomingMessages(msgs);
|
|
||||||
for (const x of leftoverMsgs) {
|
|
||||||
switch (getMessageClassFromMessage(x)) {
|
|
||||||
case Messages.DeviceAdded: {
|
|
||||||
const addedMsg = x as Messages.DeviceAdded;
|
|
||||||
const addedDevice = ButtplugClientDevice.fromMsg(
|
|
||||||
addedMsg,
|
|
||||||
this.sendDeviceMessageClosure,
|
|
||||||
);
|
|
||||||
this._devices.set(addedMsg.DeviceIndex, addedDevice);
|
|
||||||
this.emit("deviceadded", addedMsg, addedDevice);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case Messages.DeviceRemoved: {
|
|
||||||
const removedMsg = x as Messages.DeviceRemoved;
|
|
||||||
if (this._devices.has(removedMsg.DeviceIndex)) {
|
|
||||||
const removedDevice = this._devices.get(removedMsg.DeviceIndex);
|
|
||||||
removedDevice?.emitDisconnected();
|
|
||||||
this._devices.delete(removedMsg.DeviceIndex);
|
|
||||||
this.emit("deviceremoved", removedMsg, removedDevice);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case Messages.ScanningFinished:
|
|
||||||
this._isScanning = false;
|
|
||||||
this.emit("scanningfinished", x);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
protected initializeConnection = async (): Promise<boolean> => {
|
|
||||||
this.checkConnector();
|
|
||||||
const msg = await this.sendMessage(
|
|
||||||
new Messages.RequestServerInfo(
|
|
||||||
this._clientName,
|
|
||||||
Messages.MESSAGE_SPEC_VERSION,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
switch (getMessageClassFromMessage(msg)) {
|
|
||||||
case Messages.ServerInfo: {
|
|
||||||
const serverinfo = msg as Messages.ServerInfo;
|
|
||||||
this._logger.Info(
|
|
||||||
`ButtplugClient: Connected to Server ${serverinfo.ServerName}`,
|
|
||||||
);
|
|
||||||
// TODO: maybe store server name, do something with message template version?
|
|
||||||
const ping = serverinfo.MaxPingTime;
|
|
||||||
if (serverinfo.MessageVersion < Messages.MESSAGE_SPEC_VERSION) {
|
|
||||||
// Disconnect and throw an exception explaining the version mismatch problem.
|
|
||||||
await this._connector!.disconnect();
|
|
||||||
throw ButtplugError.LogAndError(
|
|
||||||
ButtplugInitError,
|
|
||||||
this._logger,
|
|
||||||
`Server protocol version ${serverinfo.MessageVersion} is older than client protocol version ${Messages.MESSAGE_SPEC_VERSION}. Please update server.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (ping > 0) {
|
|
||||||
/*
|
|
||||||
this._pingTimer = setInterval(async () => {
|
|
||||||
// If we've disconnected, stop trying to ping the server.
|
|
||||||
if (!this.Connected) {
|
|
||||||
await this.ShutdownConnection();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await this.SendMessage(new Messages.Ping());
|
|
||||||
} , Math.round(ping / 2));
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
await this.requestDeviceList();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case Messages.Error: {
|
|
||||||
// Disconnect and throw an exception with the error message we got back.
|
|
||||||
// This will usually only error out if we have a version mismatch that the
|
|
||||||
// server has detected.
|
|
||||||
await this._connector!.disconnect();
|
|
||||||
const err = msg as Messages.Error;
|
|
||||||
throw ButtplugError.LogAndError(
|
|
||||||
ButtplugInitError,
|
|
||||||
this._logger,
|
|
||||||
`Cannot connect to server. ${err.ErrorMessage}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
protected requestDeviceList = async () => {
|
|
||||||
this.checkConnector();
|
|
||||||
this._logger.Debug("ButtplugClient: ReceiveDeviceList called");
|
|
||||||
const deviceList = (await this.sendMessage(
|
|
||||||
new Messages.RequestDeviceList(),
|
|
||||||
)) as Messages.DeviceList;
|
|
||||||
deviceList.Devices.forEach((d) => {
|
|
||||||
if (!this._devices.has(d.DeviceIndex)) {
|
|
||||||
const device = ButtplugClientDevice.fromMsg(
|
|
||||||
d,
|
|
||||||
this.sendDeviceMessageClosure,
|
|
||||||
);
|
|
||||||
this._logger.Debug(`ButtplugClient: Adding Device: ${device}`);
|
|
||||||
this._devices.set(d.DeviceIndex, device);
|
|
||||||
this.emit("deviceadded", device);
|
|
||||||
} else {
|
|
||||||
this._logger.Debug(`ButtplugClient: Device already added: ${d}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
protected shutdownConnection = async () => {
|
|
||||||
await this.stopAllDevices();
|
|
||||||
if (this._pingTimer !== null) {
|
|
||||||
clearInterval(this._pingTimer);
|
|
||||||
this._pingTimer = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
protected async sendMessage(
|
|
||||||
msg: Messages.ButtplugMessage,
|
|
||||||
): Promise<Messages.ButtplugMessage> {
|
|
||||||
this.checkConnector();
|
|
||||||
const p = this._sorter.PrepareOutgoingMessage(msg);
|
|
||||||
await this._connector!.send(msg);
|
|
||||||
return await p;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected checkConnector() {
|
|
||||||
if (!this.connected) {
|
|
||||||
throw new ButtplugClientConnectorException(
|
|
||||||
"ButtplugClient not connected",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected sendMsgExpectOk = async (
|
|
||||||
msg: Messages.ButtplugMessage,
|
|
||||||
): Promise<void> => {
|
|
||||||
const response = await this.sendMessage(msg);
|
|
||||||
switch (getMessageClassFromMessage(response)) {
|
|
||||||
case Messages.Ok:
|
|
||||||
return;
|
|
||||||
case Messages.Error:
|
|
||||||
throw ButtplugError.FromError(response as Messages.Error);
|
|
||||||
default:
|
|
||||||
throw ButtplugError.LogAndError(
|
|
||||||
ButtplugMessageError,
|
|
||||||
this._logger,
|
|
||||||
`Message type ${getMessageClassFromMessage(response)!.constructor} not handled by SendMsgExpectOk`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
protected sendDeviceMessageClosure = async (
|
|
||||||
device: ButtplugClientDevice,
|
|
||||||
msg: Messages.ButtplugDeviceMessage,
|
|
||||||
): Promise<Messages.ButtplugMessage> => {
|
|
||||||
return await this.sendDeviceMessage(device, msg);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -6,13 +6,13 @@
|
|||||||
* @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved.
|
* @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>;
|
||||||
disconnect: () => Promise<void>;
|
disconnect: () => Promise<void>;
|
||||||
initialize: () => Promise<void>;
|
initialize: () => Promise<void>;
|
||||||
send: (msg: ButtplugMessage) => void;
|
send: (msg: ButtplugMessage) => void;
|
||||||
readonly Connected: boolean;
|
readonly Connected: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,96 +6,102 @@
|
|||||||
* @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 {
|
||||||
return this.errorClass;
|
return this.errorClass;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get InnerError(): Error | undefined {
|
public get InnerError(): Error | undefined {
|
||||||
return this.innerError;
|
return this.innerError;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get Id(): number | undefined {
|
public get Id(): number | undefined {
|
||||||
return this.messageId;
|
return this.messageId;
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static FromError(error: Messages.Error) {
|
public static FromError(error: Messages.Error) {
|
||||||
switch (error.ErrorCode) {
|
switch (error.ErrorCode) {
|
||||||
case Messages.ErrorClass.ERROR_DEVICE:
|
case Messages.ErrorClass.ERROR_DEVICE:
|
||||||
return new ButtplugDeviceError(error.ErrorMessage, error.Id);
|
return new ButtplugDeviceError(error.ErrorMessage, error.Id);
|
||||||
case Messages.ErrorClass.ERROR_INIT:
|
case Messages.ErrorClass.ERROR_INIT:
|
||||||
return new ButtplugInitError(error.ErrorMessage, error.Id);
|
return new ButtplugInitError(error.ErrorMessage, error.Id);
|
||||||
case Messages.ErrorClass.ERROR_UNKNOWN:
|
case Messages.ErrorClass.ERROR_UNKNOWN:
|
||||||
return new ButtplugUnknownError(error.ErrorMessage, error.Id);
|
return new ButtplugUnknownError(error.ErrorMessage, error.Id);
|
||||||
case Messages.ErrorClass.ERROR_PING:
|
case Messages.ErrorClass.ERROR_PING:
|
||||||
return new ButtplugPingError(error.ErrorMessage, error.Id);
|
return new ButtplugPingError(error.ErrorMessage, error.Id);
|
||||||
case Messages.ErrorClass.ERROR_MSG:
|
case Messages.ErrorClass.ERROR_MSG:
|
||||||
return new ButtplugMessageError(error.ErrorMessage, error.Id);
|
return new ButtplugMessageError(error.ErrorMessage, error.Id);
|
||||||
default:
|
default:
|
||||||
throw new Error(`Message type ${error.ErrorCode} not handled`);
|
throw new Error(`Message type ${error.ErrorCode} not handled`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public errorClass: Messages.ErrorClass = Messages.ErrorClass.ERROR_UNKNOWN;
|
public errorClass: Messages.ErrorClass = Messages.ErrorClass.ERROR_UNKNOWN;
|
||||||
public innerError: Error | undefined;
|
public innerError: Error | undefined;
|
||||||
public messageId: number | undefined;
|
public messageId: number | undefined;
|
||||||
|
|
||||||
protected constructor(
|
protected constructor(
|
||||||
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;
|
||||||
this.innerError = inner;
|
this.innerError = inner;
|
||||||
this.messageId = id;
|
this.messageId = id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ButtplugInitError extends ButtplugError {
|
export class ButtplugInitError extends ButtplugError {
|
||||||
public constructor(message: string, id: number = Messages.SYSTEM_MESSAGE_ID) {
|
public constructor(message: string, id: number = Messages.SYSTEM_MESSAGE_ID) {
|
||||||
super(message, Messages.ErrorClass.ERROR_INIT, id);
|
super(message, Messages.ErrorClass.ERROR_INIT, id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ButtplugDeviceError extends ButtplugError {
|
export class ButtplugDeviceError extends ButtplugError {
|
||||||
public constructor(message: string, id: number = Messages.SYSTEM_MESSAGE_ID) {
|
public constructor(message: string, id: number = Messages.SYSTEM_MESSAGE_ID) {
|
||||||
super(message, Messages.ErrorClass.ERROR_DEVICE, id);
|
super(message, Messages.ErrorClass.ERROR_DEVICE, id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ButtplugMessageError extends ButtplugError {
|
export class ButtplugMessageError extends ButtplugError {
|
||||||
public constructor(message: string, id: number = Messages.SYSTEM_MESSAGE_ID) {
|
public constructor(message: string, id: number = Messages.SYSTEM_MESSAGE_ID) {
|
||||||
super(message, Messages.ErrorClass.ERROR_MSG, id);
|
super(message, Messages.ErrorClass.ERROR_MSG, id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ButtplugPingError extends ButtplugError {
|
export class ButtplugPingError extends ButtplugError {
|
||||||
public constructor(message: string, id: number = Messages.SYSTEM_MESSAGE_ID) {
|
public constructor(message: string, id: number = Messages.SYSTEM_MESSAGE_ID) {
|
||||||
super(message, Messages.ErrorClass.ERROR_PING, id);
|
super(message, Messages.ErrorClass.ERROR_PING, id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ButtplugUnknownError extends ButtplugError {
|
export class ButtplugUnknownError extends ButtplugError {
|
||||||
public constructor(message: string, id: number = Messages.SYSTEM_MESSAGE_ID) {
|
public constructor(message: string, id: number = Messages.SYSTEM_MESSAGE_ID) {
|
||||||
super(message, Messages.ErrorClass.ERROR_UNKNOWN, id);
|
super(message, Messages.ErrorClass.ERROR_UNKNOWN, id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,71 +6,73 @@
|
|||||||
* @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,
|
||||||
Error,
|
Error,
|
||||||
Warn,
|
Warn,
|
||||||
Info,
|
Info,
|
||||||
Debug,
|
Debug,
|
||||||
Trace,
|
Trace,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Representation of log messages for the internal logging utility.
|
* Representation of log messages for the internal logging utility.
|
||||||
*/
|
*/
|
||||||
export class LogMessage {
|
export class LogMessage {
|
||||||
/** Timestamp for the log message */
|
/** Timestamp for the log message */
|
||||||
private timestamp: string;
|
private timestamp: string;
|
||||||
|
|
||||||
/** Log Message */
|
/** Log Message */
|
||||||
private logMessage: string;
|
private logMessage: string;
|
||||||
|
|
||||||
/** Log Level */
|
/** Log Level */
|
||||||
private logLevel: ButtplugLogLevel;
|
private logLevel: ButtplugLogLevel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param logMessage Log message.
|
* @param logMessage Log message.
|
||||||
* @param logLevel: Log severity level.
|
* @param logLevel: Log severity level.
|
||||||
*/
|
*/
|
||||||
public constructor(logMessage: string, logLevel: ButtplugLogLevel) {
|
public constructor(logMessage: string, logLevel: ButtplugLogLevel) {
|
||||||
const a = new Date();
|
const a = new Date();
|
||||||
const hour = a.getHours();
|
const hour = a.getHours();
|
||||||
const min = a.getMinutes();
|
const min = a.getMinutes();
|
||||||
const sec = a.getSeconds();
|
const sec = a.getSeconds();
|
||||||
this.timestamp = `${hour}:${min}:${sec}`;
|
this.timestamp = `${hour}:${min}:${sec}`;
|
||||||
this.logMessage = logMessage;
|
this.logMessage = logMessage;
|
||||||
this.logLevel = logLevel;
|
this.logLevel = logLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the log message.
|
* Returns the log message.
|
||||||
*/
|
*/
|
||||||
public get Message() {
|
public get Message() {
|
||||||
return this.logMessage;
|
return this.logMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the log message level.
|
* Returns the log message level.
|
||||||
*/
|
*/
|
||||||
public get LogLevel() {
|
public get LogLevel() {
|
||||||
return this.logLevel;
|
return this.logLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the log message timestamp.
|
* Returns the log message timestamp.
|
||||||
*/
|
*/
|
||||||
public get Timestamp() {
|
public get Timestamp() {
|
||||||
return this.timestamp;
|
return this.timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
|
}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -79,117 +81,117 @@ export class LogMessage {
|
|||||||
* basically), and allows message logging throughout the module.
|
* basically), and allows message logging throughout the module.
|
||||||
*/
|
*/
|
||||||
export class ButtplugLogger extends EventEmitter {
|
export class ButtplugLogger extends EventEmitter {
|
||||||
/** Singleton instance for the logger */
|
/** Singleton instance for the logger */
|
||||||
protected static sLogger: ButtplugLogger | undefined = undefined;
|
protected static sLogger: ButtplugLogger | undefined = undefined;
|
||||||
/** Sets maximum log level to log to console */
|
/** Sets maximum log level to log to console */
|
||||||
protected maximumConsoleLogLevel: ButtplugLogLevel = ButtplugLogLevel.Off;
|
protected maximumConsoleLogLevel: ButtplugLogLevel = ButtplugLogLevel.Off;
|
||||||
/** Sets maximum log level for all log messages */
|
/** Sets maximum log level for all log messages */
|
||||||
protected maximumEventLogLevel: ButtplugLogLevel = ButtplugLogLevel.Off;
|
protected maximumEventLogLevel: ButtplugLogLevel = ButtplugLogLevel.Off;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the stored static instance of the logger, creating one if it
|
* Returns the stored static instance of the logger, creating one if it
|
||||||
* doesn't currently exist.
|
* doesn't currently exist.
|
||||||
*/
|
*/
|
||||||
public static get Logger(): ButtplugLogger {
|
public static get Logger(): ButtplugLogger {
|
||||||
if (ButtplugLogger.sLogger === undefined) {
|
if (ButtplugLogger.sLogger === undefined) {
|
||||||
ButtplugLogger.sLogger = new ButtplugLogger();
|
ButtplugLogger.sLogger = new ButtplugLogger();
|
||||||
}
|
}
|
||||||
return this.sLogger!;
|
return this.sLogger!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor. Can only be called internally since we regulate ButtplugLogger
|
* Constructor. Can only be called internally since we regulate ButtplugLogger
|
||||||
* ownership.
|
* ownership.
|
||||||
*/
|
*/
|
||||||
protected constructor() {
|
protected constructor() {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the maximum log level to output to console.
|
* Set the maximum log level to output to console.
|
||||||
*/
|
*/
|
||||||
public get MaximumConsoleLogLevel() {
|
public get MaximumConsoleLogLevel() {
|
||||||
return this.maximumConsoleLogLevel;
|
return this.maximumConsoleLogLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the maximum log level to output to console.
|
* Get the maximum log level to output to console.
|
||||||
*/
|
*/
|
||||||
public set MaximumConsoleLogLevel(buttplugLogLevel: ButtplugLogLevel) {
|
public set MaximumConsoleLogLevel(buttplugLogLevel: ButtplugLogLevel) {
|
||||||
this.maximumConsoleLogLevel = buttplugLogLevel;
|
this.maximumConsoleLogLevel = buttplugLogLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the global maximum log level
|
* Set the global maximum log level
|
||||||
*/
|
*/
|
||||||
public get MaximumEventLogLevel() {
|
public get MaximumEventLogLevel() {
|
||||||
return this.maximumEventLogLevel;
|
return this.maximumEventLogLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the global maximum log level
|
* Get the global maximum log level
|
||||||
*/
|
*/
|
||||||
public set MaximumEventLogLevel(logLevel: ButtplugLogLevel) {
|
public set MaximumEventLogLevel(logLevel: ButtplugLogLevel) {
|
||||||
this.maximumEventLogLevel = logLevel;
|
this.maximumEventLogLevel = logLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log new message at Error level.
|
* Log new message at Error level.
|
||||||
*/
|
*/
|
||||||
public Error(msg: string) {
|
public Error(msg: string) {
|
||||||
this.AddLogMessage(msg, ButtplugLogLevel.Error);
|
this.AddLogMessage(msg, ButtplugLogLevel.Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log new message at Warn level.
|
* Log new message at Warn level.
|
||||||
*/
|
*/
|
||||||
public Warn(msg: string) {
|
public Warn(msg: string) {
|
||||||
this.AddLogMessage(msg, ButtplugLogLevel.Warn);
|
this.AddLogMessage(msg, ButtplugLogLevel.Warn);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log new message at Info level.
|
* Log new message at Info level.
|
||||||
*/
|
*/
|
||||||
public Info(msg: string) {
|
public Info(msg: string) {
|
||||||
this.AddLogMessage(msg, ButtplugLogLevel.Info);
|
this.AddLogMessage(msg, ButtplugLogLevel.Info);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log new message at Debug level.
|
* Log new message at Debug level.
|
||||||
*/
|
*/
|
||||||
public Debug(msg: string) {
|
public Debug(msg: string) {
|
||||||
this.AddLogMessage(msg, ButtplugLogLevel.Debug);
|
this.AddLogMessage(msg, ButtplugLogLevel.Debug);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log new message at Trace level.
|
* Log new message at Trace level.
|
||||||
*/
|
*/
|
||||||
public Trace(msg: string) {
|
public Trace(msg: string) {
|
||||||
this.AddLogMessage(msg, ButtplugLogLevel.Trace);
|
this.AddLogMessage(msg, ButtplugLogLevel.Trace);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks to see if message should be logged, and if so, adds message to the
|
* Checks to see if message should be logged, and if so, adds message to the
|
||||||
* log buffer. May also print message and emit event.
|
* log buffer. May also print message and emit event.
|
||||||
*/
|
*/
|
||||||
protected AddLogMessage(msg: string, level: ButtplugLogLevel) {
|
protected AddLogMessage(msg: string, level: ButtplugLogLevel) {
|
||||||
// If nothing wants the log message we have, ignore it.
|
// If nothing wants the log message we have, ignore it.
|
||||||
if (
|
if (
|
||||||
level > this.maximumEventLogLevel &&
|
level > this.maximumEventLogLevel &&
|
||||||
level > this.maximumConsoleLogLevel
|
level > this.maximumConsoleLogLevel
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const logMsg = new LogMessage(msg, level);
|
const logMsg = new LogMessage(msg, level);
|
||||||
// Clients and console logging may have different needs. For instance, it
|
// Clients and console logging may have different needs. For instance, it
|
||||||
// could be that the client requests trace level, while all we want in the
|
// could be that the client requests trace level, while all we want in the
|
||||||
// console is info level. This makes sure the client can't also spam the
|
// console is info level. This makes sure the client can't also spam the
|
||||||
// console.
|
// console.
|
||||||
if (level <= this.maximumConsoleLogLevel) {
|
if (level <= this.maximumConsoleLogLevel) {
|
||||||
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,485 +7,203 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// 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 {
|
||||||
ERROR_UNKNOWN,
|
ERROR_UNKNOWN,
|
||||||
ERROR_INIT,
|
ERROR_INIT,
|
||||||
ERROR_PING,
|
ERROR_PING,
|
||||||
ERROR_MSG,
|
ERROR_MSG,
|
||||||
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";
|
FeatureDescription: 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,88 +1,95 @@
|
|||||||
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
|
||||||
implements IButtplugClientConnector
|
implements IButtplugClientConnector
|
||||||
{
|
{
|
||||||
private static _loggingActivated = false;
|
private static _loggingActivated = false;
|
||||||
private static wasmInstance;
|
private static wasmInstance;
|
||||||
private _connected: boolean = false;
|
private _connected: boolean = false;
|
||||||
private client;
|
private client;
|
||||||
private serverPtr;
|
private serverPtr;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
public get Connected(): boolean {
|
public get Connected(): boolean {
|
||||||
return this._connected;
|
return this._connected;
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
public initialize = async (): Promise<void> => {};
|
public initialize = async (): Promise<void> => {};
|
||||||
|
|
||||||
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) => {
|
this.emitMessage(msgs);
|
||||||
this.emitMessage(msgs);
|
},
|
||||||
},
|
this.serverPtr,
|
||||||
this.serverPtr,
|
);
|
||||||
);
|
this._connected = true;
|
||||||
this._connected = true;
|
};
|
||||||
};
|
|
||||||
|
|
||||||
public disconnect = async (): Promise<void> => {};
|
public disconnect = async (): Promise<void> => {};
|
||||||
|
|
||||||
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);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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,86 +6,83 @@
|
|||||||
* @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;
|
||||||
protected _websocketConstructor: typeof WebSocket | null = null;
|
protected _websocketConstructor: typeof WebSocket | null = null;
|
||||||
|
|
||||||
public constructor(private _url: string) {
|
public constructor(private _url: string) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
public get Connected(): boolean {
|
public get Connected(): boolean {
|
||||||
return this._ws !== undefined;
|
return this._ws !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
this._ws = ws;
|
||||||
ws.addEventListener("open", async () => {
|
try {
|
||||||
this._ws = ws;
|
await this.initialize();
|
||||||
try {
|
this._ws.addEventListener('message', (msg) => {
|
||||||
await this.initialize();
|
this.parseIncomingMessage(msg);
|
||||||
this._ws.addEventListener("message", (msg) => {
|
});
|
||||||
this.parseIncomingMessage(msg);
|
this._ws.removeEventListener('close', onCloseCallback);
|
||||||
});
|
this._ws.removeEventListener('error', onErrorCallback);
|
||||||
this._ws.removeEventListener("close", onCloseCallback);
|
this._ws.addEventListener('close', this.disconnect);
|
||||||
this._ws.removeEventListener("error", onErrorCallback);
|
resolve();
|
||||||
this._ws.addEventListener("close", this.disconnect);
|
} catch (e) {
|
||||||
resolve();
|
reject(e);
|
||||||
} catch (e) {
|
}
|
||||||
reject(e);
|
});
|
||||||
}
|
// In websockets, our error rarely tells us much, as for security reasons
|
||||||
});
|
// browsers usually only throw Error Code 1006. It's up to those using this
|
||||||
// In websockets, our error rarely tells us much, as for security reasons
|
// library to state what the problem might be.
|
||||||
// browsers usually only throw Error Code 1006. It's up to those using this
|
|
||||||
// library to state what the problem might be.
|
|
||||||
|
|
||||||
ws.addEventListener("error", onErrorCallback);
|
ws.addEventListener('error', onErrorCallback)
|
||||||
ws.addEventListener("close", onCloseCallback);
|
ws.addEventListener('close', onCloseCallback);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
public disconnect = async (): Promise<void> => {
|
public disconnect = async (): Promise<void> => {
|
||||||
if (!this.Connected) {
|
if (!this.Connected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
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> => {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
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,60 +6,62 @@
|
|||||||
* @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;
|
||||||
protected _waitingMsgs: Map<
|
protected _waitingMsgs: Map<
|
||||||
number,
|
number,
|
||||||
[(val: Messages.ButtplugMessage) => void, (err: Error) => void]
|
[(val: Messages.ButtplugMessage) => void, (err: Error) => void]
|
||||||
> = new Map();
|
> = new Map();
|
||||||
|
|
||||||
public constructor(private _useCounter: boolean) {}
|
public constructor(private _useCounter: boolean) {}
|
||||||
|
|
||||||
// One of the places we should actually return a promise, as we need to store
|
// One of the places we should actually return a promise, as we need to store
|
||||||
// them while waiting for them to return across the line.
|
// 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;
|
||||||
}
|
}
|
||||||
let res;
|
let res;
|
||||||
let rej;
|
let rej;
|
||||||
const msgPromise = new Promise<Messages.ButtplugMessage>(
|
const msgPromise = new Promise<Messages.ButtplugMessage>(
|
||||||
(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)) {
|
||||||
// If we've gotten back an error, reject the related promise using a
|
const [res, rej] = this._waitingMsgs.get(id)!;
|
||||||
// ButtplugException derived type.
|
this._waitingMsgs.delete(id);
|
||||||
if (x.Type === Messages.Error) {
|
// If we've gotten back an error, reject the related promise using a
|
||||||
rej(ButtplugError.FromError(x as Messages.Error));
|
// ButtplugException derived type.
|
||||||
continue;
|
if (x.Error !== undefined) {
|
||||||
}
|
rej(ButtplugError.FromError(x.Error!));
|
||||||
res(x);
|
continue;
|
||||||
continue;
|
}
|
||||||
} else {
|
res(x);
|
||||||
noMatch.push(x);
|
continue;
|
||||||
}
|
} else {
|
||||||
}
|
noMatch.push(x);
|
||||||
return noMatch;
|
}
|
||||||
}
|
}
|
||||||
|
return noMatch;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,17 @@
|
|||||||
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,
|
Hardware,
|
||||||
},
|
HardwareConnector,
|
||||||
server::device::{
|
HardwareEvent,
|
||||||
configuration::{BluetoothLESpecifier, ProtocolCommunicationSpecifier},
|
HardwareInternal,
|
||||||
hardware::{
|
HardwareReadCmd,
|
||||||
Hardware,
|
HardwareReading,
|
||||||
HardwareConnector,
|
HardwareSpecializer,
|
||||||
HardwareEvent,
|
HardwareSubscribeCmd,
|
||||||
HardwareInternal,
|
HardwareUnsubscribeCmd,
|
||||||
HardwareReadCmd,
|
HardwareWriteCmd,
|
||||||
HardwareReading,
|
|
||||||
HardwareSpecializer,
|
|
||||||
HardwareSubscribeCmd,
|
|
||||||
HardwareUnsubscribeCmd,
|
|
||||||
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},
|
HardwareCommunicationManager, HardwareCommunicationManagerBuilder,
|
||||||
hardware::communication::{
|
HardwareCommunicationManagerEvent,
|
||||||
HardwareCommunicationManager, HardwareCommunicationManagerBuilder,
|
|
||||||
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\//],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,7 +36,8 @@
|
|||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
"tw-animate-css": "^1.3.8",
|
"tw-animate-css": "^1.3.8",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
"vite": "^7.1.4"
|
"vite": "^7.1.4",
|
||||||
|
"vite-plugin-wasm": "3.5.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@directus/sdk": "^20.0.3",
|
"@directus/sdk": "^20.0.3",
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { Label } from "$lib/components/ui/label";
|
|||||||
import { Card, CardContent, CardHeader } from "$lib/components/ui/card";
|
import { 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,49 @@ 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 — access through the reactive array so Svelte detects the mutation
|
||||||
|
const idx = devices.length - 1;
|
||||||
cmds?.forEach(async (cmd) => {
|
if (device.hasBattery) {
|
||||||
await client.sendDeviceMessage(
|
try {
|
||||||
{ index: device.info.index },
|
devices[idx].batteryLevel = await dev.battery();
|
||||||
new SensorReadCmd(device.info.index, cmd.Index, cmd.SensorType),
|
} catch (e) {
|
||||||
);
|
console.warn(`Failed to read battery for ${dev.name}:`, e);
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleMessages(messages: ButtplugMessage[]) {
|
|
||||||
messages.forEach(async (msg) => {
|
|
||||||
await handleMessage(msg);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleMessage(msg: ButtplugMessage) {
|
|
||||||
if (msg instanceof SensorReading) {
|
|
||||||
const device = devices[msg.DeviceIndex];
|
|
||||||
if (msg.SensorType === SensorType.Battery) {
|
|
||||||
device.batteryLevel = msg.Data[0];
|
|
||||||
}
|
}
|
||||||
device.sensorValues[msg.Index] = msg.Data[0];
|
|
||||||
device.lastSeen = new Date();
|
|
||||||
} else if (msg instanceof DeviceList) {
|
|
||||||
devices = client.devices.map(convertDevice);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleInputReading(msg: ButtplugMessage) {
|
||||||
|
if (msg.InputReading === undefined) return;
|
||||||
|
const reading = msg.InputReading;
|
||||||
|
const device = devices.find((d) => d.info.index === reading.DeviceIndex);
|
||||||
|
if (!device) return;
|
||||||
|
|
||||||
|
if (reading.Reading[InputType.Battery] !== undefined) {
|
||||||
|
device.batteryLevel = reading.Reading[InputType.Battery].Value;
|
||||||
|
}
|
||||||
|
device.lastSeen = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
async function handleChange(
|
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 +125,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 +182,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 +332,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 +594,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
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import path from "path";
|
|||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import { sveltekit } from "@sveltejs/kit/vite";
|
import { sveltekit } from "@sveltejs/kit/vite";
|
||||||
|
import wasm from 'vite-plugin-wasm';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [sveltekit(), tailwindcss()],
|
plugins: [sveltekit(), tailwindcss(), wasm()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: { $lib: path.resolve("./src/lib"), "@": path.resolve("./src/lib") },
|
alias: { $lib: path.resolve("./src/lib"), "@": path.resolve("./src/lib") },
|
||||||
},
|
},
|
||||||
|
|||||||
23
pnpm-lock.yaml
generated
23
pnpm-lock.yaml
generated
@@ -23,15 +23,9 @@ importers:
|
|||||||
|
|
||||||
packages/buttplug:
|
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
|
||||||
@@ -145,6 +139,9 @@ importers:
|
|||||||
vite:
|
vite:
|
||||||
specifier: ^7.1.4
|
specifier: ^7.1.4
|
||||||
version: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)
|
version: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)
|
||||||
|
vite-plugin-wasm:
|
||||||
|
specifier: 3.5.0
|
||||||
|
version: 3.5.0(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0))
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -1650,9 +1647,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'}
|
||||||
@@ -2099,11 +2093,12 @@ packages:
|
|||||||
glob@11.0.3:
|
glob@11.0.3:
|
||||||
resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==}
|
resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
|
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
glob@7.2.3:
|
glob@7.2.3:
|
||||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||||
deprecated: Glob versions prior to v9 are no longer supported
|
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||||
|
|
||||||
globals@15.15.0:
|
globals@15.15.0:
|
||||||
resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==}
|
resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==}
|
||||||
@@ -2857,9 +2852,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==}
|
||||||
|
|
||||||
@@ -3124,6 +3116,7 @@ packages:
|
|||||||
tar@6.2.1:
|
tar@6.2.1:
|
||||||
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
|
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||||
|
|
||||||
tarn@3.0.2:
|
tarn@3.0.2:
|
||||||
resolution: {integrity: sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==}
|
resolution: {integrity: sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==}
|
||||||
@@ -4710,8 +4703,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 +5888,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