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).
npm install @hapbeat/sdkESM-only ("type": "module"). The browser path also needs the helper daemon:
pip install hapbeat-helper → run hapbeat-helper.
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();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.
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/.
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 eventThe 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 to1.0). - Clip WAVs are 16 kHz PCM16; the SDK does not resample. One stream at a time.
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.
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)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.
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.
examples/node-minimal.mjs— fire an event from Node.examples/browser-minimal.html— fire from a page (helper).examples/react-native/— an Android demo: button taps → command + streaming, direct UDP from the phone (no helper).examples/games/— a browser haptic-demo arcade (FPS + mini-games) showing the EventMap / kit-manifest / file-vs-synth router pattern. Runnpm run devand open the printed URL.
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/sdkaccordingly.
MIT © Hapbeat