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