Skip to content

Zendrex/buttplug.js

Repository files navigation

@zendrex/buttplug.js

Type-safe Buttplug v4 client. WebSocket or WASM, Zod-validated.

Intiface Central or in-browser Web Bluetooth. Discovery, I/O, patterns.

bun add @zendrex/buttplug.js

Works with npm, pnpm, and yarn. Any runtime with WebSocket support (Node 18+, Bun, Deno, browsers). Ships ESM and CJS with .d.ts types.

Overview

  • ButtplugClient: transport, v4 handshake, scanning, device list, reconnect
  • Device: capability checks and typed output/sensor methods
  • Zod-validated protocol messages (src/protocol/schema); types inferred from schemas
  • Per-feature output ranges (typically 0100), clamped to server metadata
  • Event-driven lifecycle (device.*, connection.*, scan.*, input.reading)
  • PatternEngine (@zendrex/buttplug.js/patterns): presets and custom keyframe tracks

Prerequisites

Approach Typical use Requirements
WebSocket (default) Desktop apps, servers, scripts Intiface Central on the same machine or network (ws://127.0.0.1:12345 by default)
WASM transport Browser-only, no desktop server Chromium, HTTPS or localhost, optional peer buttplug-wasm-blob

WebSocket mode expects Intiface Central with Start Server enabled. Default endpoint: ws://127.0.0.1:12345 (or ws://localhost:12345).

Quick start

import { ButtplugClient } from "@zendrex/buttplug.js";

const client = new ButtplugClient("ws://127.0.0.1:12345");

client.on("device.added", async ({ data: { device } }) => {
  console.log(device.displayName ?? device.name);
  if (device.canOutput("Vibrate")) {
    await device.vibrate(0.5);
    setTimeout(() => device.stop(), 2000);
  }
});

await client.connect();
await client.startScanning();

Examples

Device discovery

client.on("device.added", async ({ data: { device } }) => {
  console.log(`${device.index}: ${device.name}`);
  for (const output of device.features.outputs) {
    console.log(`  ${output.type} (feature ${output.featureIndex})`);
  }
});

client.on("device.removed", ({ data: { device } }) => {
  console.log(`gone: ${device.name}`);
});

Outputs

await device.vibrate(0.75);
await device.vibrate([
  { index: 0, value: 0.4 },
  { index: 1, value: 1 },
]);

await device.rotate(0.6, { clockwise: false });
await device.position(0.8, { duration: 500 });
await device.stop();

All output values are normalized 0–1 floats; the library maps them to each feature's server-reported device range. canOutput / canRead / canSubscribe gate unsupported calls.

Sensors

const level = await device.readSensor("Battery");

const unsub = await device.subscribeSensor("RSSI", (value) => {
  console.log("RSSI", value);
});

await unsub();

Pushed sensor updates also arrive on the client as input.reading.

Patterns

@zendrex/buttplug.js/patterns

import { ButtplugClient } from "@zendrex/buttplug.js";
import { PatternEngine } from "@zendrex/buttplug.js/patterns";

const client = new ButtplugClient("ws://127.0.0.1:12345");
await client.connect();

const engine = new PatternEngine(client);

const id = await engine.play(device, "wave", {
  intensity: 0.8,
  speed: 1.5,
  loop: true,
});

const id2 = await engine.play(device, [
  {
    featureIndex: 0,
    keyframes: [
      { value: 0, duration: 0 },
      { value: 100, duration: 1000, easing: "easeIn" },
      { value: 20, duration: 500, easing: "easeOut" },
    ],
  },
], { loop: 3, intensity: 0.6 });

await engine.stop(id);
engine.stopAll();
engine.dispose();

Presets: pulse, wave, ramp_up, ramp_down, heartbeat, surge, stroke. Easings: linear, easeIn, easeOut, easeInOut, step.

WASM (browser, no Intiface)

bun add @zendrex/buttplug.js buttplug-wasm-blob
import { ButtplugClient } from "@zendrex/buttplug.js";
import { WasmTransport } from "@zendrex/buttplug.js/wasm";

const client = new ButtplugClient(new WasmTransport());
await client.connect();
await client.startScanning();

Chromium over HTTPS or localhost only. WASM guide.

Reconnect

const client = new ButtplugClient("ws://127.0.0.1:12345", {
  autoReconnect: true,
  reconnectDelay: 1000,
  maxReconnectDelay: 30_000,
  maxReconnectAttempts: 10,
});

client.on("connection.reconnected", () => {
  console.log("reconnected");
});

On reconnect the client re-handshakes, refreshes the device list, and emits connection.reconnected. PatternEngine stops patterns for devices that disappeared.

Design notes

Transport. URL string → WebSocketTransport. Custom Transport implementations are accepted in the constructor. WASM: WasmTransport from @zendrex/buttplug.js/wasm.

Validation. Schemas live in src/protocol/schema. Public types are inferred from them.

Devices. Device parses DeviceFeatures from the server descriptor. canOutput, canRead, and canSubscribe gate commands; unsupported calls throw DeviceError with deviceIndex.

Events. Emittery v2 shape: handlers get { data } (and { name, data } when needed). client.on(...) returns an unsubscribe function; client.clearListeners() and client.dispose() tear down.

Patterns. @zendrex/buttplug.js/patterns keeps the scheduler out of the main bundle. ButtplugClient implements PatternEngineClient.

Package exports

Import Provides
@zendrex/buttplug.js ButtplugClient, Device, errors, loggers, WebSocketTransport, protocol types
@zendrex/buttplug.js/patterns PatternEngine, PRESETS, PRESET_NAMES, getPresetInfo, pattern types
@zendrex/buttplug.js/wasm WasmTransport (peer: buttplug-wasm-blob)

API reference

Client

const client = new ButtplugClient(urlOrTransport, options?);

await client.connect();
await client.startScanning();
await client.stopScanning();
await client.stopAll();
await client.disconnect();
client.dispose();

client.connected;   // boolean
client.scanning;    // boolean
client.devices;     // Device[]
client.serverInfo;  // ServerInfo | null

Connection: connection.connecting, connection.connected, connection.disconnected, connection.reconnecting, connection.reconnected, connection.error

Devices: device.added, device.removed, device.updated, device.list, device.error

Scan: scan.started, scan.finished

Sensors: input.reading

Options: clientName, requestTimeout, autoPing, autoReconnect, reconnectDelay, maxReconnectDelay, maxReconnectAttempts, logger, verbose

Device outputs

Method Output type
vibrate() Vibrate
rotate() Rotate / RotateWithDirection
oscillate() Oscillate
constrict() Constrict
spray() Spray
temperature() Temperature
led() Led
position() Position / HwPositionWithDuration
stop() stops outputs and/or sensor subscriptions

OUTPUT_TYPES and INPUT_TYPES list protocol feature names the library recognizes.

Pattern engine

engine.play(deviceOrIndex, presetName, options?);
engine.play(deviceOrIndex, tracks, options?);
engine.play(deviceOrIndex, descriptor, options?);

engine.stop(patternId);
engine.stopAll();
engine.list();
engine.dispose();

Errors

All extend ButtplugError:

Class When
ConnectionError Transport failure
HandshakeError Handshake rejected
ProtocolError Server error message (code from ErrorCode)
DeviceError Missing capability or invalid pattern target (deviceIndex)
TimeoutError requestTimeout exceeded (operation, timeoutMs)

formatError(err) stringifies unknown throws safely.

Logging

Silent by default. verbose: true → console; explicit logger in options takes precedence. Wire traffic: browser DevTools → Network → WebSocket → Messages.

Docs

Site source is in docs/. Local dev:

bun run docs

Guides: getting started, devices, commands, events, WASM, patterns.

License

MIT

About

Intimate hardware control for modern JavaScript

Topics

Resources

License

Stars

Watchers

Forks

Contributors