A new start
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
/*!
|
||||
* 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 { IButtplugClientConnector } from "./IButtplugClientConnector";
|
||||
import { ButtplugMessage } from "../core/Messages";
|
||||
import { ButtplugBrowserWebsocketConnector } from "../utils/ButtplugBrowserWebsocketConnector";
|
||||
|
||||
export class ButtplugBrowserWebsocketClientConnector
|
||||
extends ButtplugBrowserWebsocketConnector
|
||||
implements IButtplugClientConnector
|
||||
{
|
||||
public send = (msg: ButtplugMessage): void => {
|
||||
if (!this.Connected) {
|
||||
throw new Error("ButtplugClient not connected");
|
||||
}
|
||||
this.sendMessage(msg);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*!
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { ButtplugError } from "../core/Exceptions";
|
||||
import * as Messages from "../core/Messages";
|
||||
|
||||
export class ButtplugClientConnectorException extends ButtplugError {
|
||||
public constructor(message: string) {
|
||||
super(message, Messages.ErrorClass.ERROR_UNKNOWN);
|
||||
}
|
||||
}
|
||||
401
packages/buttplug/src/client/ButtplugClientDevice.ts
Normal file
401
packages/buttplug/src/client/ButtplugClientDevice.ts
Normal file
@@ -0,0 +1,401 @@
|
||||
/*!
|
||||
* 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 * as Messages from "../core/Messages";
|
||||
import {
|
||||
ButtplugDeviceError,
|
||||
ButtplugError,
|
||||
ButtplugMessageError,
|
||||
} from "../core/Exceptions";
|
||||
import { EventEmitter } from "eventemitter3";
|
||||
import { getMessageClassFromMessage } from "../core/MessageUtils";
|
||||
|
||||
/**
|
||||
* Represents an abstract device, capable of taking certain kinds of messages.
|
||||
*/
|
||||
export class ButtplugClientDevice extends EventEmitter {
|
||||
/**
|
||||
* Return the name of the device.
|
||||
*/
|
||||
public get name(): string {
|
||||
return this._deviceInfo.DeviceName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the user set name of the device.
|
||||
*/
|
||||
public get displayName(): string | undefined {
|
||||
return this._deviceInfo.DeviceDisplayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the index of the device.
|
||||
*/
|
||||
public get index(): number {
|
||||
return this._deviceInfo.DeviceIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the index of the device.
|
||||
*/
|
||||
public get messageTimingGap(): number | undefined {
|
||||
return this._deviceInfo.DeviceMessageTimingGap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of message types the device accepts.
|
||||
*/
|
||||
public get messageAttributes(): Messages.MessageAttributes {
|
||||
return this._deviceInfo.DeviceMessages;
|
||||
}
|
||||
|
||||
public static fromMsg(
|
||||
msg: Messages.DeviceInfo,
|
||||
sendClosure: (
|
||||
device: ButtplugClientDevice,
|
||||
msg: Messages.ButtplugDeviceMessage,
|
||||
) => Promise<Messages.ButtplugMessage>,
|
||||
): ButtplugClientDevice {
|
||||
return new ButtplugClientDevice(msg, sendClosure);
|
||||
}
|
||||
|
||||
// Map of messages and their attributes (feature count, etc...)
|
||||
private allowedMsgs: Map<string, Messages.MessageAttributes> = new Map<
|
||||
string,
|
||||
Messages.MessageAttributes
|
||||
>();
|
||||
|
||||
/**
|
||||
* @param _index Index of the device, as created by the device manager.
|
||||
* @param _name Name of the device.
|
||||
* @param allowedMsgs Buttplug messages the device can receive.
|
||||
*/
|
||||
constructor(
|
||||
private _deviceInfo: Messages.DeviceInfo,
|
||||
private _sendClosure: (
|
||||
device: ButtplugClientDevice,
|
||||
msg: Messages.ButtplugDeviceMessage,
|
||||
) => Promise<Messages.ButtplugMessage>,
|
||||
) {
|
||||
super();
|
||||
_deviceInfo.DeviceMessages.update();
|
||||
}
|
||||
|
||||
public async send(
|
||||
msg: Messages.ButtplugDeviceMessage,
|
||||
): Promise<Messages.ButtplugMessage> {
|
||||
// Assume we're getting the closure from ButtplugClient, which does all of
|
||||
// the index/existence/connection/message checks for us.
|
||||
return await this._sendClosure(this, msg);
|
||||
}
|
||||
|
||||
public async sendExpectOk(
|
||||
msg: Messages.ButtplugDeviceMessage,
|
||||
): Promise<void> {
|
||||
const response = await this.send(msg);
|
||||
switch (getMessageClassFromMessage(response)) {
|
||||
case Messages.Ok:
|
||||
return;
|
||||
case Messages.Error:
|
||||
throw ButtplugError.FromError(response as Messages.Error);
|
||||
default:
|
||||
throw new ButtplugMessageError(
|
||||
`Message type ${response.constructor} not handled by SendMsgExpectOk`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async scalar(
|
||||
scalar: Messages.ScalarSubcommand | Messages.ScalarSubcommand[],
|
||||
): Promise<void> {
|
||||
if (Array.isArray(scalar)) {
|
||||
await this.sendExpectOk(new Messages.ScalarCmd(scalar, this.index));
|
||||
} else {
|
||||
await this.sendExpectOk(new Messages.ScalarCmd([scalar], this.index));
|
||||
}
|
||||
}
|
||||
|
||||
private async scalarCommandBuilder(
|
||||
speed: number | number[],
|
||||
actuator: Messages.ActuatorType,
|
||||
) {
|
||||
const scalarAttrs = this.messageAttributes.ScalarCmd?.filter(
|
||||
(x) => x.ActuatorType === actuator,
|
||||
);
|
||||
if (!scalarAttrs || scalarAttrs.length === 0) {
|
||||
throw new ButtplugDeviceError(
|
||||
`Device ${this.name} has no ${actuator} capabilities`,
|
||||
);
|
||||
}
|
||||
const cmds: Messages.ScalarSubcommand[] = [];
|
||||
if (typeof speed === "number") {
|
||||
scalarAttrs.forEach((x) =>
|
||||
cmds.push(new Messages.ScalarSubcommand(x.Index, speed, actuator)),
|
||||
);
|
||||
} else if (Array.isArray(speed)) {
|
||||
if (speed.length > scalarAttrs.length) {
|
||||
throw new ButtplugDeviceError(
|
||||
`${speed.length} commands send to a device with ${scalarAttrs.length} vibrators`,
|
||||
);
|
||||
}
|
||||
scalarAttrs.forEach((x, i) => {
|
||||
cmds.push(new Messages.ScalarSubcommand(x.Index, speed[i], actuator));
|
||||
});
|
||||
} else {
|
||||
throw new ButtplugDeviceError(
|
||||
`${actuator} can only take numbers or arrays of numbers.`,
|
||||
);
|
||||
}
|
||||
await this.scalar(cmds);
|
||||
}
|
||||
|
||||
public get vibrateAttributes(): Messages.GenericDeviceMessageAttributes[] {
|
||||
return (
|
||||
this.messageAttributes.ScalarCmd?.filter(
|
||||
(x) => x.ActuatorType === Messages.ActuatorType.Vibrate,
|
||||
) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
public async vibrate(speed: number | number[]): Promise<void> {
|
||||
await this.scalarCommandBuilder(speed, Messages.ActuatorType.Vibrate);
|
||||
}
|
||||
|
||||
public get oscillateAttributes(): Messages.GenericDeviceMessageAttributes[] {
|
||||
return (
|
||||
this.messageAttributes.ScalarCmd?.filter(
|
||||
(x) => x.ActuatorType === Messages.ActuatorType.Oscillate,
|
||||
) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
public async oscillate(speed: number | number[]): Promise<void> {
|
||||
await this.scalarCommandBuilder(speed, Messages.ActuatorType.Oscillate);
|
||||
}
|
||||
|
||||
public get rotateAttributes(): Messages.GenericDeviceMessageAttributes[] {
|
||||
return this.messageAttributes.RotateCmd ?? [];
|
||||
}
|
||||
|
||||
public async rotate(
|
||||
values: number | [number, boolean][],
|
||||
clockwise?: boolean,
|
||||
): Promise<void> {
|
||||
const rotateAttrs = this.messageAttributes.RotateCmd;
|
||||
if (!rotateAttrs || rotateAttrs.length === 0) {
|
||||
throw new ButtplugDeviceError(
|
||||
`Device ${this.name} has no Rotate capabilities`,
|
||||
);
|
||||
}
|
||||
let msg: Messages.RotateCmd;
|
||||
if (typeof values === "number") {
|
||||
msg = Messages.RotateCmd.Create(
|
||||
this.index,
|
||||
new Array(rotateAttrs.length).fill([values, clockwise]),
|
||||
);
|
||||
} else if (Array.isArray(values)) {
|
||||
msg = Messages.RotateCmd.Create(this.index, values);
|
||||
} else {
|
||||
throw new ButtplugDeviceError(
|
||||
"SendRotateCmd can only take a number and boolean, or an array of number/boolean tuples",
|
||||
);
|
||||
}
|
||||
await this.sendExpectOk(msg);
|
||||
}
|
||||
|
||||
public get linearAttributes(): Messages.GenericDeviceMessageAttributes[] {
|
||||
return this.messageAttributes.LinearCmd ?? [];
|
||||
}
|
||||
|
||||
public async linear(
|
||||
values: number | [number, number][],
|
||||
duration?: number,
|
||||
): Promise<void> {
|
||||
const linearAttrs = this.messageAttributes.LinearCmd;
|
||||
if (!linearAttrs || linearAttrs.length === 0) {
|
||||
throw new ButtplugDeviceError(
|
||||
`Device ${this.name} has no Linear capabilities`,
|
||||
);
|
||||
}
|
||||
let msg: Messages.LinearCmd;
|
||||
if (typeof values === "number") {
|
||||
msg = Messages.LinearCmd.Create(
|
||||
this.index,
|
||||
new Array(linearAttrs.length).fill([values, duration]),
|
||||
);
|
||||
} else if (Array.isArray(values)) {
|
||||
msg = Messages.LinearCmd.Create(this.index, values);
|
||||
} else {
|
||||
throw new ButtplugDeviceError(
|
||||
"SendLinearCmd can only take a number and number, or an array of number/number tuples",
|
||||
);
|
||||
}
|
||||
await this.sendExpectOk(msg);
|
||||
}
|
||||
|
||||
public async sensorRead(
|
||||
sensorIndex: number,
|
||||
sensorType: Messages.SensorType,
|
||||
): Promise<number[]> {
|
||||
const response = await this.send(
|
||||
new Messages.SensorReadCmd(this.index, sensorIndex, sensorType),
|
||||
);
|
||||
switch (getMessageClassFromMessage(response)) {
|
||||
case Messages.SensorReading:
|
||||
return (response as Messages.SensorReading).Data;
|
||||
case Messages.Error:
|
||||
throw ButtplugError.FromError(response as Messages.Error);
|
||||
default:
|
||||
throw new ButtplugMessageError(
|
||||
`Message type ${response.constructor} not handled by sensorRead`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public 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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*!
|
||||
* 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 { ButtplugBrowserWebsocketClientConnector } from "./ButtplugBrowserWebsocketClientConnector";
|
||||
import { WebSocket as NodeWebSocket } from "ws";
|
||||
|
||||
export class ButtplugNodeWebsocketClientConnector extends ButtplugBrowserWebsocketClientConnector {
|
||||
protected _websocketConstructor =
|
||||
NodeWebSocket as unknown as typeof WebSocket;
|
||||
}
|
||||
276
packages/buttplug/src/client/Client.ts
Normal file
276
packages/buttplug/src/client/Client.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
/*!
|
||||
* 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);
|
||||
};
|
||||
}
|
||||
18
packages/buttplug/src/client/IButtplugClientConnector.ts
Normal file
18
packages/buttplug/src/client/IButtplugClientConnector.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/*!
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { ButtplugMessage } from "../core/Messages";
|
||||
import { EventEmitter } from "eventemitter3";
|
||||
|
||||
export interface IButtplugClientConnector extends EventEmitter {
|
||||
connect: () => Promise<void>;
|
||||
disconnect: () => Promise<void>;
|
||||
initialize: () => Promise<void>;
|
||||
send: (msg: ButtplugMessage) => void;
|
||||
readonly Connected: boolean;
|
||||
}
|
||||
Reference in New Issue
Block a user