ive-connect
Version:
A universal haptic device control library for interactive experiences
460 lines (459 loc) • 16.1 kB
JavaScript
"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;