/*! * 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 = 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 { 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 => { 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 { 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 => { 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 => { return await this.sendDeviceMessage(device, msg); }; }