UNPKG

ive-connect

Version:

A universal haptic device control library for interactive experiences

300 lines (299 loc) 10.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ButtplugApi = void 0; /** * Buttplug API — high-level device management over Buttplug v4 protocol */ const events_1 = require("../../core/events"); const types_1 = require("./types"); const buttplug_ws_1 = require("./buttplug-ws"); class ButtplugApi extends events_1.EventEmitter { constructor(clientName = 'IVE-Connect') { super(); this.devices = new Map(); this.devicePreferences = new Map(); this.features = new Map(); this.isScanning = false; this.connectionState = types_1.ButtplugConnectionState.DISCONNECTED; this.clientName = clientName; this.ws = new buttplug_ws_1.ButtplugWs((type, payload) => this.onMessage(type, payload)); } getConnectionState() { return this.connectionState; } getConnectedUrl() { return this.connectedUrl; } getDevices() { return Array.from(this.devices.values()); } getIsScanning() { return this.isScanning; } getDevicePreferences() { return this.devicePreferences; } setDevicePreference(index, pref) { this.devicePreferences.set(index, pref); this.emit('devicePreferenceChanged', { deviceIndex: index, preference: pref, }); } // ── Connection ────────────────────────────────────────────────── async connect(type, serverUrl) { if (this.connectionState !== types_1.ButtplugConnectionState.DISCONNECTED) { await this.disconnect(); } if (type !== types_1.ButtplugConnectionType.WEBSOCKET || !serverUrl) { this.emit('error', 'WebSocket URL required'); return false; } try { this.connectionState = types_1.ButtplugConnectionState.CONNECTING; this.emit('connectionStateChanged', this.connectionState); await this.ws.open(serverUrl, this.clientName); this.connectedUrl = serverUrl; this.connectionState = types_1.ButtplugConnectionState.CONNECTED; this.emit('connectionStateChanged', this.connectionState); return true; } catch (e) { console.error('Buttplug connect error:', e); this.cleanup(); this.emit('error', e instanceof Error ? e.message : String(e)); return false; } } async disconnect() { this.ws.close(); this.cleanup(); return true; } // ── Scanning ──────────────────────────────────────────────────── async startScanning() { if (!this.ws.connected) return false; try { this.isScanning = true; this.emit('scanningChanged', true); await this.ws.send('StartScanning', {}); return true; } catch (e) { this.isScanning = false; this.emit('scanningChanged', false); this.emit('error', e instanceof Error ? e.message : String(e)); return false; } } async stopScanning() { if (!this.ws.connected) return false; try { await this.ws.send('StopScanning', {}); return true; } catch (_a) { this.isScanning = false; this.emit('scanningChanged', false); return false; } } // ── Device commands ───────────────────────────────────────────── async vibrateDevice(index, speed) { return this.outputCmd(index, 'Vibrate', speed); } async linearDevice(index, position, duration) { const feat = this.findFeature(index, 'HwPositionWithDuration'); if (!feat) return false; try { const value = Math.ceil(feat.maxSteps * Math.min(1, Math.max(0, position))); await this.ws.send('OutputCmd', { DeviceIndex: index, FeatureIndex: feat.featureIndex, Command: { HwPositionWithDuration: { Value: value, Duration: Math.round(duration), }, }, }); return true; } catch (_a) { return false; } } async rotateDevice(index, speed, _clockwise) { return this.outputCmd(index, 'Rotate', speed); } async oscillateDevice(index, speed, _frequency) { return this.outputCmd(index, 'Oscillate', speed); } async stopDevice(index) { const d = this.devices.get(index); if (!d) return false; try { if (d.canVibrate) await this.outputCmd(index, 'Vibrate', 0.01); else if (d.canRotate) await this.outputCmd(index, 'Rotate', 0.01); else if (d.canLinear) await this.linearDevice(index, 0.01, 500); await this.delay(100); await this.ws.send('StopCmd', { DeviceIndex: index, FeatureIndex: undefined, Inputs: true, Outputs: true, }); return true; } catch (_a) { return false; } } async stopAllDevices() { if (!this.ws.connected) return false; try { for (const d of this.devices.values()) { try { if (d.canVibrate) await this.outputCmd(d.index, 'Vibrate', 0.01); else if (d.canRotate) await this.outputCmd(d.index, 'Rotate', 0.01); else if (d.canLinear) await this.linearDevice(d.index, 0.01, 500); } catch (_a) { } } await this.delay(100); await this.ws.send('StopCmd', { DeviceIndex: undefined, FeatureIndex: undefined, Inputs: true, Outputs: true, }); return true; } catch (_b) { return false; } } // ── Internals ─────────────────────────────────────────────────── onMessage(type, payload) { switch (type) { case 'DeviceAdded': this.addDevice(payload); break; case 'DeviceRemoved': this.removeDevice(payload.DeviceIndex); break; case 'DeviceList': { // DeviceList.Devices is an object keyed by index const incoming = payload.Devices || {}; // Add new devices for (const dev of Object.values(incoming)) { this.addDevice(dev); } // Remove devices no longer in the list for (const index of this.devices.keys()) { if (!incoming.hasOwnProperty(index.toString())) { this.removeDevice(index); } } break; } case 'ScanningFinished': this.isScanning = false; this.emit('scanningChanged', false); break; } } addDevice(dev) { var _a, _b, _c; // DeviceFeatures is an object keyed by feature index const rawFeatures = dev.DeviceFeatures || {}; const parsed = []; for (const [idx, feat] of Object.entries(rawFeatures)) { const output = feat .Output; if (!output) continue; for (const [outputType, outputDef] of Object.entries(output)) { parsed.push({ featureIndex: parseInt(idx), type: outputType, maxSteps: (_c = (_b = (_a = outputDef.Value) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : outputDef.Value) !== null && _c !== void 0 ? _c : 100, }); } } this.features.set(dev.DeviceIndex, parsed); const has = (t) => parsed.some((f) => f.type === t); const info = { index: dev.DeviceIndex, name: dev.DeviceDisplayName || dev.DeviceName, canVibrate: has('Vibrate'), canLinear: has('Position') || has('HwPositionWithDuration'), canRotate: has('Rotate'), canOscillate: has('Oscillate'), }; this.devices.set(dev.DeviceIndex, info); if (!this.devicePreferences.has(dev.DeviceIndex)) { this.devicePreferences.set(dev.DeviceIndex, { enabled: true, useVibrate: info.canVibrate, useRotate: info.canRotate, useLinear: info.canLinear, useOscillate: info.canOscillate, }); } this.emit('deviceAdded', info); } removeDevice(index) { const info = this.devices.get(index); if (!info) return; this.devices.delete(index); this.features.delete(index); this.emit('deviceRemoved', info); } findFeature(deviceIndex, type) { var _a; return (_a = this.features.get(deviceIndex)) === null || _a === void 0 ? void 0 : _a.find((f) => f.type === type); } async outputCmd(deviceIndex, outputType, percent) { const feat = this.findFeature(deviceIndex, outputType); if (!feat) return false; try { const value = Math.ceil(feat.maxSteps * Math.min(1, Math.max(0, percent))); await this.ws.send('OutputCmd', { DeviceIndex: deviceIndex, FeatureIndex: feat.featureIndex, Command: { [outputType]: { Value: value } }, }); return true; } catch (_a) { return false; } } cleanup() { this.devices.clear(); this.features.clear(); this.isScanning = false; this.connectionState = types_1.ButtplugConnectionState.DISCONNECTED; this.connectedUrl = undefined; this.emit('connectionStateChanged', this.connectionState); this.emit('scanningChanged', false); } delay(ms) { return new Promise((r) => setTimeout(r, ms)); } } exports.ButtplugApi = ButtplugApi;