Skip to content

hapbeat/hapbeat-js-sdk

Repository files navigation

Hapbeat JS/TS SDK

Drive Hapbeat haptic devices from JavaScript / TypeScript. One API, three transports:

  • Node (Electron, servers, CLIs, creative-coding) → direct Wi-Fi UDP broadcast.
  • React Native (Android / iOS apps) → direct Wi-Fi UDP broadcast via the optional react-native-udp — a phone can open a real UDP socket, so no helper is needed.
  • Browser (WebXR, three.js / Babylon.js / p5.js, React, jsPsych experiments) → relays through hapbeat-helper over a local WebSocket, because browsers cannot open raw UDP sockets.

The bundler/runtime picks the right build automatically via the package exports map.

📚 Docs: https://devtools.hapbeat.com/docs/sdk-integration/ · 🤖 AI agents: see AGENTS.md

This is the level-1 SDK: the fire side (play / stop) and the tuning side (EventMap) are orthogonal and linked only by an event id — the same design as the Hapbeat Unity SDK. You ship instructions; the haptic waveform lives in the kit on the device (authored in Hapbeat Studio).

Install

npm install @hapbeat/sdk

ESM-only ("type": "module"). The browser path also needs the helper daemon: pip install hapbeat-helper → run hapbeat-helper.

Quick start — Node

import { connect } from "@hapbeat/sdk";

const hb = await connect({ appName: "MyApp" }); // opens UDP broadcast + keep-alive
hb.play("sample-kit.sine_100hz", { gain: 0.3 });           // fire by event id (gain 0..1)
hb.play("sample-kit.sine_100hz");                          // gain omitted → kit/EventMap baseline
hb.stopAll();
await hb.close();

Quick start — Browser (needs hapbeat-helper running)

import { connect } from "@hapbeat/sdk";

const hb = await connect({ appName: "MyWebXR" }); // → ws://localhost:7703 (helper)
hb.play("sample-kit.sine_100hz", { gain: 0.5 });

"sample-kit.sine_100hz" must be an event id in the kit deployed to the device (via Hapbeat Studio). Use it in React the same way — connect() once (e.g. in an effect / a module singleton), then hb.play(...) from event handlers.

Quick start — React Native (Android / iOS, no helper)

import { connect } from "@hapbeat/sdk"; // resolves to the "react-native" build (Metro)

const hb = await connect({ appName: "MyApp" }); // direct UDP broadcast from the phone
hb.play("sample-kit.sine_100hz", { gain: 0.5 });

Install the optional native module + the required polyfill: npm install react-native-udp fast-text-encoding (RN Hermes — including 0.86 — ships TextEncoder but not TextDecoder). Also add the metro.config.js resolver and import 'fast-text-encoding' as the first import. Full setup + a runnable Android demo (command + streaming, verified on a physical device) is in examples/react-native/.

EventMap — the tuning side (optional)

Read per-event baseline intensities from the kit manifest so play("id") fires at the authored strength without hard-coding gains:

import { connect, EventMap } from "@hapbeat/sdk";

const manifest = await fetch("/my-kit/my-kit-manifest.json").then((r) => r.json());
const hb = await connect({ eventMap: EventMap.fromManifest(manifest) });
hb.play("sample-kit.sine_100hz"); // uses the manifest's intensity for this event

command vs clip — two modes, one play(id)

The kit manifest decides the mode per event; your code never changes:

manifest bucket mode what happens needs kit on device?
events command SDK sends PLAY; the device plays its installed clip yes (flash kit in Studio)
stream_events clip SDK loads the event's WAV (clipBase + clipLoader) and streams it over the wire no
  • No eventMap → everything is command mode (gain defaults to 1.0).
  • Clip WAVs are 16 kHz PCM16; the SDK does not resample. One stream at a time.

Continuous (live) streaming

For per-frame–modulated haptics (a directional tone in a game loop, a tightening rumble), open a persistent stream instead of firing discrete clips:

const live = hb.openStream({ channels: 2, sampleRate: 16000 });
function frame(pcm /* Uint8Array, your synthesized chunk */) {
  live.write(pcm);   // STREAM_BEGIN once, then chunks — no per-chunk teardown
}
// …later
live.close();

hb.streamPcm(pcm, { channels: 2 }) sends a single ad-hoc PCM16 buffer (stereo = L/R direction, since PLAY has no pan). One session at a time: a clip, streamPcm, or a new openStream ends the previous live stream.

Discovery & targeting

for (const d of await hb.discover(1500)) console.log(d.ip, d.address);

hb.play("sample-kit.sine_100hz", { target: "player_1/chest" }); // one device
hb.play("sample-kit.sine_100hz", { target: "*/chest" });         // all chest devices
hb.play("sample-kit.sine_100hz", { target: "" });                // broadcast (default)

Project layout (where the kit lives)

your-app/
├── src/ …                         your code: connect() + play(id)
└── public/ (web) or a dir (node)
    └── my-kit/
        ├── my-kit-manifest.json   ← Hapbeat Studio output; fetched → EventMap.fromManifest
        └── stream-clips/*.wav      ← clip-mode WAVs, loaded via clipBase + clipLoader

Web: serve the kit as static assets; clipBase is a URL prefix. Node: clipBase is a directory path (default clipLoader = fs.readFile). Override clipLoader to load clips from a bundle / IndexedDB.

Examples

The examples/ folder is not in the npm package (only dist ships). Clone the repo to use them: git clone https://github.com/hapbeat/hapbeat-js-sdk.

For AI coding agents

Hand your agent AGENTS.md — a single self-contained reference. Or paste:

Read https://raw.githubusercontent.com/hapbeat/hapbeat-js-sdk/master/AGENTS.md and use @hapbeat/sdk accordingly.

License

MIT © Hapbeat

About

Web/Node SDK for Hapbeat haptic devices (UDP in Node, helper WebSocket in the browser)

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors