UNPKG

ive-connect

Version:

A universal haptic device control library for interactive experiences

460 lines (459 loc) 16.1 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.ButtplugApi = void 0; /** * Buttplug API wrapper * * Handles communication with the Buttplug library */ const buttplug_1 = require("buttplug"); const buttplug_2 = require("buttplug"); const events_1 = require("../../core/events"); const types_1 = require("./types"); const DEBUG_WEBSOCKET = false; class ButtplugApi extends events_1.EventEmitter { constructor(clientName = "IVE-Connect") { super(); this.client = null; this.devices = new Map(); this.devicePreferences = new Map(); this.isScanning = false; this.connectionState = types_1.ButtplugConnectionState.DISCONNECTED; /** * Handle a device added event */ this.handleDeviceAdded = (device) => { const deviceInfo = { index: device.index, name: device.name, canVibrate: device.vibrateAttributes.length > 0, canLinear: device.messageAttributes.LinearCmd !== undefined, canRotate: device.messageAttributes.RotateCmd !== undefined, }; this.devices.set(device.index, deviceInfo); // Set default preferences if none exist if (!this.devicePreferences.has(device.index)) { this.devicePreferences.set(device.index, { enabled: true, useVibrate: deviceInfo.canVibrate, useRotate: deviceInfo.canRotate, useLinear: deviceInfo.canLinear, }); } this.emit("deviceAdded", deviceInfo); }; /** * Handle a device removed event */ this.handleDeviceRemoved = (device) => { const deviceInfo = this.devices.get(device.index); if (deviceInfo) { this.devices.delete(device.index); this.emit("deviceRemoved", deviceInfo); } }; /** * Handle scanning finished event */ this.handleScanningFinished = () => { this.isScanning = false; this.emit("scanningChanged", this.isScanning); }; /** * Handle disconnection event */ this.handleDisconnected = () => { this.cleanup(); }; this.clientName = clientName; } /** * Get the current connection state */ getConnectionState() { return this.connectionState; } /** * Get the current connected server URL if any */ getConnectedUrl() { return this.connectedUrl; } /** * Get the list of connected devices */ getDevices() { return Array.from(this.devices.values()); } /** * Get the scanning state */ getIsScanning() { return this.isScanning; } /** * Set a device preference */ setDevicePreference(deviceIndex, preference) { this.devicePreferences.set(deviceIndex, preference); this.emit("devicePreferenceChanged", { deviceIndex, preference }); } /** * Get device preferences */ getDevicePreferences() { return this.devicePreferences; } /** * Connect to a Buttplug server */ async connect(type, serverUrl) { if (this.connectionState !== types_1.ButtplugConnectionState.DISCONNECTED) { // Clean up any existing connection await this.disconnect(); } try { this.connectionState = types_1.ButtplugConnectionState.CONNECTING; this.emit("connectionStateChanged", this.connectionState); // Create a new client this.client = new buttplug_1.ButtplugClient(this.clientName); this.setupClientListeners(); let connector; if (type === types_1.ButtplugConnectionType.WEBSOCKET) { if (!serverUrl) { throw new Error("Server URL is required for WebSocket connection"); } connector = new buttplug_2.ButtplugBrowserWebsocketClientConnector(serverUrl); this.connectedUrl = serverUrl; } else { // For local connection, we'll need to import the WASM client connector dynamically // as it's only available in a browser environment const { ButtplugWasmClientConnector } = await Promise.resolve().then(() => __importStar(require("buttplug-wasm/dist/buttplug-wasm.mjs"))); connector = new ButtplugWasmClientConnector(); this.connectedUrl = "In-Browser Server"; } await this.client.connect(connector); this.connectionState = types_1.ButtplugConnectionState.CONNECTED; this.emit("connectionStateChanged", this.connectionState); return true; } catch (error) { console.error("Error connecting to Buttplug server:", error); this.connectionState = types_1.ButtplugConnectionState.DISCONNECTED; this.emit("connectionStateChanged", this.connectionState); this.emit("error", error instanceof Error ? error.message : String(error)); return false; } } /** * Disconnect from the server */ async disconnect() { if (!this.client) { return true; } try { if (this.client.connected) { await this.client.disconnect(); } } catch (error) { console.error("Error disconnecting from Buttplug server:", error); } this.cleanup(); return true; } /** * Start scanning for devices */ async startScanning() { if (!this.client || !this.client.connected) { this.emit("error", "Cannot start scanning: Not connected to a server"); return false; } try { this.isScanning = true; this.emit("scanningChanged", this.isScanning); await this.client.startScanning(); return true; } catch (error) { console.error("Error starting device scan:", error); this.isScanning = false; this.emit("scanningChanged", this.isScanning); this.emit("error", error instanceof Error ? error.message : String(error)); return false; } } /** * Stop scanning for devices */ async stopScanning() { if (!this.client || !this.client.connected) { return false; } try { await this.client.stopScanning(); // The scanningFinished event listener will handle setting the state return true; } catch (error) { console.error("Error stopping device scan:", error); this.isScanning = false; this.emit("scanningChanged", this.isScanning); this.emit("error", error instanceof Error ? error.message : String(error)); return false; } } /** * Send a vibrate command to a device */ async vibrateDevice(index, speed) { if (DEBUG_WEBSOCKET) console.log(`[BUTTPLUG-WS] Vibrate device ${index}: speed=${speed}`); const device = this.getClientDevice(index); if (!device) { this.emit("error", `No device with index ${index}`); return false; } try { await device.vibrate(speed); return true; } catch (error) { this.handleDeviceCommandError(error, "vibrate"); return false; } } /** * Send a linear command to a device */ async linearDevice(index, position, duration) { if (DEBUG_WEBSOCKET) console.log(`[BUTTPLUG-WS] Linear device ${index}: position=${position}, duration=${duration}`); const device = this.getClientDevice(index); if (!device) { this.emit("error", `No device with index ${index}`); return false; } try { await device.linear(position, duration); return true; } catch (error) { this.handleDeviceCommandError(error, "linear"); return false; } } /** * Send a rotate command to a device */ async rotateDevice(index, speed, clockwise) { if (DEBUG_WEBSOCKET) console.log(`[BUTTPLUG-WS] Rotate device ${index}: speed=${speed}, clockwise=${clockwise}`); const device = this.getClientDevice(index); if (!device) { this.emit("error", `No device with index ${index}`); return false; } try { await device.rotate(speed, clockwise); return true; } catch (error) { this.handleDeviceCommandError(error, "rotate"); return false; } } /** * Stop a specific device */ async stopDevice(index) { const device = this.getClientDevice(index); if (!device) { this.emit("error", `No device with index ${index}`); return false; } try { const deviceInfo = this.devices.get(index); // Send a gentle command before stopping to prevent device jerking if (deviceInfo) { try { if (deviceInfo.canVibrate) { await device.vibrate(0.01); } else if (deviceInfo.canRotate) { await device.rotate(0.01, true); } else if (deviceInfo.canLinear) { await device.linear(0.01, 500); } } catch (e) { console.error(`Error sending gentle command before stop:`, e); } } // Use setTimeout to ensure the gentle command has time to take effect await new Promise((resolve) => { setTimeout(async () => { try { await device.stop(); resolve(); } catch (e) { console.error(`Stop command error:`, e); resolve(); } }, 100); }); return true; } catch (error) { this.handleDeviceCommandError(error, "stop"); return false; } } /** * Stop all devices */ async stopAllDevices() { if (!this.client) { return false; } try { // First send gentle commands to all devices for (const device of this.client.devices) { const deviceInfo = this.devices.get(device.index); if (!deviceInfo) continue; try { if (deviceInfo.canVibrate) { await device.vibrate(0.01); } else if (deviceInfo.canRotate) { await device.rotate(0.01, true); } else if (deviceInfo.canLinear) { await device.linear(0.01, 500); } } catch (e) { console.error(`Error sending gentle command before stopAll:`, e); } } // Then stop all devices after a short delay await new Promise((resolve) => { setTimeout(async () => { try { const stopPromises = this.client.devices.map(async (device) => { try { await device.stop(); } catch (e) { console.error(`StopAll command error:`, e); } }); await Promise.all(stopPromises); resolve(); } catch (e) { console.error(`Error in stopAllDevices:`, e); resolve(); } }, 100); }); return true; } catch (error) { console.error("Error stopping all devices:", error); this.emit("error", error instanceof Error ? error.message : String(error)); return false; } } /** * Set up listeners for client events */ setupClientListeners() { if (!this.client) return; this.client.addListener("deviceadded", this.handleDeviceAdded); this.client.addListener("deviceremoved", this.handleDeviceRemoved); this.client.addListener("scanningfinished", this.handleScanningFinished); this.client.addListener("disconnect", this.handleDisconnected); } /** * Clean up resources when disconnecting */ cleanup() { if (this.client) { this.client.removeAllListeners(); this.client = null; } this.devices.clear(); this.isScanning = false; this.connectionState = types_1.ButtplugConnectionState.DISCONNECTED; this.connectedUrl = undefined; this.emit("connectionStateChanged", this.connectionState); this.emit("scanningChanged", this.isScanning); } /** * Get a device from the client by index */ getClientDevice(index) { if (!this.client) return undefined; return this.client.devices.find((d) => d.index === index); } /** * Handle a device command error */ handleDeviceCommandError(error, command) { if (error instanceof buttplug_1.ButtplugDeviceError) { console.error(`Device error on ${command}:`, error.message); this.emit("error", `Device error on ${command}: ${error.message}`); } else if (error instanceof buttplug_1.ButtplugError) { console.error(`Buttplug error on ${command}:`, error.message); this.emit("error", `Buttplug error on ${command}: ${error.message}`); } else { console.error(`Unknown error on ${command}:`, error); this.emit("error", `Unknown error on ${command}: ${error instanceof Error ? error.message : String(error)}`); } } } exports.ButtplugApi = ButtplugApi;