# @zendrex/buttplug.js @zendrex/buttplug.js is a modern TypeScript client library for the Buttplug intimate hardware protocol v4. It enables communication with Intiface Central to discover and control connected devices over WebSocket. The library provides type-safe APIs for device output control (vibration, rotation, position, oscillation, etc.), sensor input reading, and pattern-based automation. The core architecture consists of three main components: `ButtplugClient` for connection management and device discovery, `Device` for individual device control and sensor access, and `PatternEngine` for automated keyframe-based playback. The library includes built-in auto-reconnect with exponential backoff, Zod-validated protocol messages, comprehensive error handling, and both ESM and CJS output formats. ## ButtplugClient The `ButtplugClient` manages WebSocket connections to Intiface Central, handles protocol handshakes, device discovery, ping keep-alive, and event emission. It supports automatic reconnection and provides methods for scanning, stopping devices, and sending raw protocol messages. ```typescript import { ButtplugClient, consoleLogger } from "@zendrex/buttplug.js"; // Create client with auto-reconnect enabled const client = new ButtplugClient("ws://127.0.0.1:12345", { clientName: "MyApp", logger: consoleLogger, autoReconnect: true, reconnectDelay: 1000, maxReconnectDelay: 30000, maxReconnectAttempts: 10, requestTimeout: 5000, }); // Subscribe to events before connecting client.on("connected", () => { console.log("Connected to server:", client.serverInfo?.ServerName); }); client.on("deviceAdded", ({ device }) => { console.log(`Device found: ${device.displayName ?? device.name} (index: ${device.index})`); }); client.on("deviceRemoved", ({ device }) => { console.log(`Device removed: ${device.name}`); }); client.on("disconnected", ({ reason }) => { console.log(`Disconnected: ${reason}`); }); client.on("reconnecting", ({ attempt }) => { console.log(`Reconnection attempt ${attempt}...`); }); client.on("error", ({ error }) => { console.error("Client error:", error.message); }); // Connect and start scanning await client.connect(); await client.startScanning(); // Access connected state and devices console.log("Connected:", client.connected); console.log("Devices:", client.devices.map(d => d.name)); // Get a specific device by index const device = client.getDevice(0); // Request updated device list from server await client.requestDeviceList(); // Stop scanning await client.stopScanning(); // Stop all devices globally await client.stopAll(); // Graceful shutdown await client.disconnect(); client.dispose(); ``` ## Device Output Control The `Device` class provides typed methods for controlling device outputs. All intensity values are normalized to 0-1 range. Methods accept either a single value for all motors/actuators or an array for per-feature control. ```typescript import { ButtplugClient, DeviceError } from "@zendrex/buttplug.js"; const client = new ButtplugClient("ws://127.0.0.1:12345"); await client.connect(); await client.startScanning(); client.on("deviceAdded", async ({ device }) => { try { // Vibration control (single value for all motors) if (device.canOutput("Vibrate")) { await device.vibrate(0.5); // 50% intensity on all motors // Per-motor control with FeatureValue array await device.vibrate([ { index: 0, value: 0.8 }, { index: 1, value: 0.3 }, ]); } // Rotation control with direction if (device.canRotate) { await device.rotate(0.7, { clockwise: true }); // Per-motor rotation with individual directions await device.rotate([ { index: 0, speed: 0.5, clockwise: true }, { index: 1, speed: 0.8, clockwise: false }, ]); } // Linear position control (requires duration) if (device.canPosition) { await device.position(0.8, { duration: 500 }); // Move to 80% over 500ms // Per-axis position control await device.position([ { index: 0, position: 0.2, duration: 300 }, { index: 1, position: 0.9, duration: 600 }, ]); } // Oscillation control if (device.canOutput("Oscillate")) { await device.oscillate(0.6); } // Constriction control if (device.canOutput("Constrict")) { await device.constrict(0.4); } // Temperature control if (device.canOutput("Temperature")) { await device.temperature(0.7); } // LED control if (device.canOutput("Led")) { await device.led(1.0); } // Stop all features on device await device.stop(); // Stop specific feature by index await device.stop({ featureIndex: 0 }); // Stop only outputs (not inputs/sensors) await device.stop({ outputs: true, inputs: false }); } catch (err) { if (err instanceof DeviceError) { console.error(`Device ${err.deviceIndex} error:`, err.message); } } }); ``` ## Device Sensor Reading Devices may expose input sensors for battery level, signal strength, pressure, buttons, and position. The `Device` class supports one-shot reads and continuous subscriptions. ```typescript import { ButtplugClient } from "@zendrex/buttplug.js"; const client = new ButtplugClient("ws://127.0.0.1:12345"); await client.connect(); await client.startScanning(); client.on("deviceAdded", async ({ device }) => { // Check sensor capabilities console.log("Can read battery:", device.canRead("Battery")); console.log("Can subscribe RSSI:", device.canSubscribe("RSSI")); // One-shot sensor read if (device.canRead("Battery")) { const batteryLevel = await device.readSensor("Battery"); console.log(`Battery: ${batteryLevel * 100}%`); } // Read specific sensor index (for devices with multiple sensors of same type) if (device.canRead("Pressure")) { const pressure = await device.readSensor("Pressure", 0); // First pressure sensor console.log(`Pressure: ${pressure}`); } // Subscribe to continuous sensor readings if (device.canSubscribe("RSSI")) { const unsubscribe = await device.subscribeSensor("RSSI", (value) => { console.log(`RSSI update: ${value} dBm`); }); // Later: stop subscription setTimeout(async () => { await unsubscribe(); }, 10000); } // Button press subscription if (device.canSubscribe("Button")) { await device.subscribeSensor("Button", (pressed) => { console.log(`Button ${pressed ? "pressed" : "released"}`); }); } // Explicit unsubscribe by type await device.unsubscribe("RSSI", 0); }); // Global input reading events client.on("inputReading", ({ reading }) => { console.log(`Sensor reading from device ${reading.DeviceIndex}:`, reading.Reading); }); ``` ## Device Properties and Features Access device metadata and inspect available features programmatically. ```typescript import { ButtplugClient } from "@zendrex/buttplug.js"; const client = new ButtplugClient("ws://127.0.0.1:12345"); await client.connect(); await client.startScanning(); client.on("deviceAdded", ({ device }) => { // Device identification console.log("Index:", device.index); console.log("Internal name:", device.name); console.log("Display name:", device.displayName); // null if not provided console.log("Message timing gap:", device.messageTimingGap, "ms"); // Capability checks console.log("Can vibrate:", device.canOutput("Vibrate")); console.log("Can rotate:", device.canRotate); console.log("Can position:", device.canPosition); console.log("Can read battery:", device.canRead("Battery")); console.log("Can subscribe RSSI:", device.canSubscribe("RSSI")); // Detailed feature inspection const features = device.features; console.log("Output features:"); for (const output of features.outputs) { console.log(` - ${output.type} at index ${output.index}`); console.log(` Range: ${output.range[0]} - ${output.range[1]}`); console.log(` Description: ${output.descriptor}`); } console.log("Input features:"); for (const input of features.inputs) { console.log(` - ${input.type} at index ${input.index}`); console.log(` Can read: ${input.canRead}`); console.log(` Can subscribe: ${input.canSubscribe}`); } }); ``` ## Raw Output Commands Send raw protocol-level output commands for fine-grained control. ```typescript import { ButtplugClient, DeviceError } from "@zendrex/buttplug.js"; const client = new ButtplugClient("ws://127.0.0.1:12345"); await client.connect(); await client.startScanning(); client.on("deviceAdded", async ({ device }) => { try { // Raw vibration command to specific feature await device.output({ featureIndex: 0, command: { Vibrate: { Value: 0.75 } }, }); // Raw rotation with direction await device.output({ featureIndex: 0, command: { RotateWithDirection: { Value: 0.5, Clockwise: true } }, }); // Raw position with duration await device.output({ featureIndex: 0, command: { HwPositionWithDuration: { Position: 0.8, Duration: 400 } }, }); // Raw oscillation await device.output({ featureIndex: 0, command: { Oscillate: { Value: 0.6 } }, }); } catch (err) { if (err instanceof DeviceError) { console.error(`Output failed: ${err.message}`); } } }); ``` ## PatternEngine with Presets The `PatternEngine` automates device control using keyframe-based patterns. Built-in presets provide common patterns like pulse, wave, and heartbeat. ```typescript import { ButtplugClient, PatternEngine } from "@zendrex/buttplug.js"; const client = new ButtplugClient("ws://127.0.0.1:12345"); await client.connect(); const engine = new PatternEngine(client, { defaultTimeout: 1800000, // 30 minute safety timeout }); client.on("deviceAdded", async ({ device }) => { // Play a preset pattern by name const patternId = await engine.play(device, "wave", { intensity: 0.8, // Scale all values by 80% speed: 1.5, // 1.5x playback speed loop: true, // Loop indefinitely timeout: 60000, // Auto-stop after 60 seconds onComplete: (id) => console.log(`Pattern ${id} completed`), onStop: (id, reason) => console.log(`Pattern ${id} stopped: ${reason}`), }); // Play pulse pattern with limited loops const pulseId = await engine.play(device.index, "pulse", { intensity: 0.6, loop: 5, // Run 5 cycles then stop }); // Play heartbeat pattern on specific feature await engine.play(device, "heartbeat", { featureIndex: 0, intensity: 1.0, }); // Play ramp patterns (non-looping by default) await engine.play(device, "ramp_up", { intensity: 0.9 }); await engine.play(device, "surge"); // Position-based stroke pattern (for linear devices) if (device.canPosition) { await engine.play(device, "stroke", { speed: 0.8, loop: true, }); } // Stop specific pattern await engine.stop(patternId); // Stop all patterns on a device engine.stopByDevice(device.index); // Stop all patterns globally engine.stopAll(); }); // List available presets const presets = engine.listPresets(); for (const preset of presets) { console.log(`${preset.name}: ${preset.description}`); console.log(` Compatible: ${preset.compatibleOutputTypes.join(", ")}`); console.log(` Loops by default: ${preset.defaultLoop}`); } // Output: // pulse: Square wave on/off // wave: Smooth sine wave oscillation // ramp_up: Gradual increase to maximum // ramp_down: Gradual decrease to zero // heartbeat: Ba-bump heartbeat rhythm // surge: Build to peak then release // stroke: Full-range position strokes // Cleanup engine.dispose(); await client.disconnect(); ``` ## PatternEngine with Custom Keyframes Create custom patterns using keyframe tracks with precise timing and easing control. ```typescript import { ButtplugClient, PatternEngine, Track } from "@zendrex/buttplug.js"; const client = new ButtplugClient("ws://127.0.0.1:12345"); await client.connect(); const engine = new PatternEngine(client); client.on("deviceAdded", async ({ device }) => { // Custom pattern with single track const customTracks: Track[] = [ { featureIndex: 0, keyframes: [ { value: 0, duration: 0 }, // Start at 0 { value: 1, duration: 1000, easing: "easeIn" }, // Ramp to max over 1s { value: 0.5, duration: 500, easing: "easeOut" }, // Ease down to 50% { value: 0.5, duration: 200 }, // Hold at 50% { value: 0, duration: 300, easing: "linear" }, // Linear fade out ], }, ]; const patternId = await engine.play(device, customTracks, { intensity: 0.8, loop: 3, }); // Multi-track pattern for devices with multiple motors const multiTrack: Track[] = [ { featureIndex: 0, keyframes: [ { value: 0, duration: 0 }, { value: 1, duration: 500, easing: "easeInOut" }, { value: 0, duration: 500, easing: "easeInOut" }, ], }, { featureIndex: 1, keyframes: [ { value: 1, duration: 0 }, { value: 0, duration: 500, easing: "easeInOut" }, { value: 1, duration: 500, easing: "easeInOut" }, ], }, ]; await engine.play(device, multiTrack, { loop: true }); // Position track with explicit output type if (device.canPosition) { const positionTrack: Track[] = [ { featureIndex: 0, outputType: "HwPositionWithDuration", keyframes: [ { value: 0, duration: 0 }, { value: 1, duration: 800, easing: "easeInOut" }, { value: 0, duration: 800, easing: "easeInOut" }, ], }, ]; await engine.play(device, positionTrack, { loop: true }); } // Rotation track with direction const rotationTrack: Track[] = [ { featureIndex: 0, outputType: "RotateWithDirection", clockwise: true, keyframes: [ { value: 0.3, duration: 0 }, { value: 1, duration: 2000, easing: "easeIn" }, { value: 0.3, duration: 1000, easing: "easeOut" }, ], }, ]; // Using full PatternDescriptor await engine.play(device, { type: "custom", tracks: rotationTrack, intensity: 0.9, loop: true, }); // List active patterns const activePatterns = engine.list(); for (const info of activePatterns) { console.log(`Pattern ${info.id}:`); console.log(` Device: ${info.deviceIndex}`); console.log(` Features: ${info.featureIndices.join(", ")}`); console.log(` Elapsed: ${info.elapsed}ms`); } }); ``` ## Error Handling The library provides a hierarchy of typed errors for precise error handling. ```typescript import { ButtplugClient, PatternEngine, ButtplugError, ConnectionError, HandshakeError, ProtocolError, DeviceError, TimeoutError, ErrorCode, formatError, } from "@zendrex/buttplug.js"; const client = new ButtplugClient("ws://127.0.0.1:12345", { requestTimeout: 5000, }); try { await client.connect(); } catch (err) { if (err instanceof ConnectionError) { console.error("Failed to connect:", err.message); // Retry logic or fallback } else if (err instanceof HandshakeError) { console.error("Server rejected handshake:", err.message); } } client.on("error", ({ error }) => { if (error instanceof ProtocolError) { console.error(`Protocol error [${error.code}]: ${error.message}`); // Handle specific error codes switch (error.code) { case ErrorCode.PING: console.error("Ping timeout - server will halt devices"); break; case ErrorCode.DEVICE: console.error("Device-level protocol error"); break; case ErrorCode.MESSAGE: console.error("Invalid message format"); break; } } else if (error instanceof TimeoutError) { console.error(`${error.operation} timed out after ${error.timeoutMs}ms`); } }); client.on("deviceAdded", async ({ device }) => { try { await device.vibrate(0.5); } catch (err) { if (err instanceof DeviceError) { console.error(`Device ${err.deviceIndex}: ${err.message}`); } else if (err instanceof TimeoutError) { console.error(`Command timed out: ${err.operation}`); } else { // Generic error handling console.error("Unexpected error:", formatError(err)); } } }); // Pattern engine errors const engine = new PatternEngine(client); try { await engine.play(999, "wave"); // Non-existent device } catch (err) { if (err instanceof DeviceError) { console.error(`Pattern error for device ${err.deviceIndex}: ${err.message}`); } } // Check if error is any Buttplug error function handleError(err: unknown) { if (err instanceof ButtplugError) { console.error(`Buttplug error (${err.name}): ${err.message}`); if (err.cause) { console.error("Caused by:", err.cause); } } } ``` ## Logging Configure logging for debugging and diagnostics using the built-in loggers or custom implementations. ```typescript import { ButtplugClient, consoleLogger, noopLogger, Logger } from "@zendrex/buttplug.js"; // Use built-in console logger const clientWithLogs = new ButtplugClient("ws://127.0.0.1:12345", { logger: consoleLogger, }); // Use noop logger (silent, default) const silentClient = new ButtplugClient("ws://127.0.0.1:12345", { logger: noopLogger, }); // Custom logger implementation const customLogger: Logger = { debug: (msg) => console.debug(`[DEBUG] ${msg}`), info: (msg) => console.info(`[INFO] ${msg}`), warn: (msg) => console.warn(`[WARN] ${msg}`), error: (msg) => console.error(`[ERROR] ${msg}`), child: (name) => ({ debug: (msg) => console.debug(`[DEBUG][${name}] ${msg}`), info: (msg) => console.info(`[INFO][${name}] ${msg}`), warn: (msg) => console.warn(`[WARN][${name}] ${msg}`), error: (msg) => console.error(`[ERROR][${name}] ${msg}`), child: (n) => customLogger.child(`${name}:${n}`), }), }; const clientWithCustomLogger = new ButtplugClient("ws://127.0.0.1:12345", { logger: customLogger, }); ``` ## Type Definitions Key TypeScript types exported by the library for type-safe integration. ```typescript import type { // Client types ButtplugClientOptions, ClientEventMap, DeviceOutputOptions, DeviceStopOptions, // Protocol types ClientMessage, ServerMessage, ServerInfo, DeviceFeatures, OutputFeature, InputFeature, OutputType, InputType, OutputCommand, InputReading, FeatureValue, RotationValue, PositionValue, // Pattern types PatternDevice, PatternEngineClient, PatternDescriptor, PresetPattern, CustomPattern, PatternPlayOptions, PatternInfo, PresetInfo, PresetName, Track, Keyframe, Easing, StopReason, // Logger Logger, SensorCallback, } from "@zendrex/buttplug.js"; // Constants import { OUTPUT_TYPES, // Array of all output type strings INPUT_TYPES, // Array of all input type strings PRESET_NAMES, // Array of preset name strings EASING_VALUES, // Array of easing curve names EASING_FUNCTIONS, // Map of easing name to function PRESETS, // Preset definitions object ErrorCode, // Error code constants } from "@zendrex/buttplug.js"; // Usage example with types const options: ButtplugClientOptions = { clientName: "TypedApp", autoReconnect: true, reconnectDelay: 1000, }; const track: Track = { featureIndex: 0, keyframes: [ { value: 0, duration: 0 }, { value: 1, duration: 1000, easing: "easeInOut" }, ], }; const playOptions: PatternPlayOptions = { intensity: 0.8, loop: true, onStop: (id: string, reason: StopReason) => { console.log(`Pattern ${id} stopped: ${reason}`); }, }; ``` ## Summary @zendrex/buttplug.js is designed for applications requiring programmatic control of intimate hardware devices through the Buttplug protocol. Primary use cases include building interactive applications, creating synchronized experiences with media, developing accessibility tools, and prototyping device control systems. The library handles all protocol complexities including WebSocket management, device enumeration, sensor subscriptions, and timing constraints. Integration follows a straightforward pattern: instantiate `ButtplugClient` with the Intiface Central WebSocket URL, subscribe to device events, connect and scan for devices, then use the `Device` API for direct control or `PatternEngine` for automated patterns. The type-safe API with comprehensive error handling makes it suitable for production applications. Auto-reconnect with exponential backoff ensures resilience, while the pattern engine's safety timeout and drift correction provide reliable automated control. All resources are properly cleaned up via `dispose()` methods.