/*! * 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 { ButtplugClientDeviceFeature } from "./ButtplugClientDeviceFeature"; import { DeviceOutputCommand } from "./ButtplugClientDeviceCommand"; /** * Represents an abstract device, capable of taking certain kinds of messages. */ export class ButtplugClientDevice extends EventEmitter { private _features: Map; /** * 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; } public get features(): Map { return this._features; } public static fromMsg( msg: Messages.DeviceInfo, sendClosure: (msg: Messages.ButtplugMessage) => Promise, ): ButtplugClientDevice { return new ButtplugClientDevice(msg, sendClosure); } /** * @param _index Index of the device, as created by the device manager. * @param _name Name of the device. * @param allowedMsgs Buttplug messages the device can receive. */ private constructor( private _deviceInfo: Messages.DeviceInfo, private _sendClosure: (msg: Messages.ButtplugMessage) => Promise, ) { super(); this._features = new Map( Object.entries(_deviceInfo.DeviceFeatures).map(([index, v]) => [ parseInt(index), new ButtplugClientDeviceFeature( _deviceInfo.DeviceIndex, _deviceInfo.DeviceName, v, _sendClosure, ), ]), ); } public async send(msg: Messages.ButtplugMessage): Promise { // Assume we're getting the closure from ButtplugClient, which does all of // the index/existence/connection/message checks for us. return await this._sendClosure(msg); } protected sendMsgExpectOk = async (msg: Messages.ButtplugMessage): Promise => { const response = await this.send(msg); if (response.Ok !== undefined) { return; } else if (response.Error !== undefined) { throw ButtplugError.FromError(response as Messages.Error); } else { /* throw ButtplugError.LogAndError( ButtplugMessageError, this._logger, `Message ${response} not handled by SendMsgExpectOk` ); */ } }; protected isOutputValid(featureIndex: number, type: Messages.OutputType) { if (!this._deviceInfo.DeviceFeatures.hasOwnProperty(featureIndex.toString())) { throw new ButtplugDeviceError( `Feature index ${featureIndex} does not exist for device ${this.name}`, ); } if ( this._deviceInfo.DeviceFeatures[featureIndex.toString()].Outputs !== undefined && !this._deviceInfo.DeviceFeatures[featureIndex.toString()].Outputs.hasOwnProperty(type) ) { throw new ButtplugDeviceError( `Feature index ${featureIndex} does not support type ${type} for device ${this.name}`, ); } } public hasOutput(type: Messages.OutputType): boolean { return ( this._features .values() .filter((f) => f.hasOutput(type)) .toArray().length > 0 ); } public hasInput(type: Messages.InputType): boolean { return ( this._features .values() .filter((f) => f.hasInput(type)) .toArray().length > 0 ); } public async runOutput(cmd: DeviceOutputCommand): Promise { let p: Promise[] = []; for (let f of this._features.values()) { if (f.hasOutput(cmd.outputType)) { p.push(f.runOutput(cmd)); } } if (p.length == 0) { return Promise.reject(`No features with output type ${cmd.outputType}`); } await Promise.all(p); } public async stop(): Promise { await this.sendMsgExpectOk({ StopCmd: { Id: 1, DeviceIndex: this.index, FeatureIndex: undefined, Inputs: true, Outputs: true, }, }); } public async battery(): Promise { let p: Promise[] = []; for (let f of this._features.values()) { if (f.hasInput(Messages.InputType.Battery)) { // Right now, we only have one battery per device, so assume the first one we find is it. let response = await f.runInput(Messages.InputType.Battery, Messages.InputCommandType.Read); if (response === undefined) { throw new ButtplugMessageError("Got incorrect message back."); } if (response.Reading[Messages.InputType.Battery] === undefined) { throw new ButtplugMessageError("Got reading with no Battery info."); } return response.Reading[Messages.InputType.Battery].Value; } } throw new ButtplugDeviceError(`No battery present on this device.`); } public emitDisconnected() { this.emit("deviceremoved"); } }