Skip to content
EN

Getting Started

This is the SDK for driving Hapbeat from JavaScript / TypeScript (npm @hapbeat/sdk). It targets WebXR, three.js / Babylon.js, p5.js, jsPsych experiments, Electron, Node servers, and more. It separates the fire side (when and where to play) from the tuning side (what and how to play), tying them together by event id alone.

There is a single connect(), but the transport switches automatically depending on the runtime (determined by the package’s exports map).

  • Node (Electron / server / CLI / creative coding) → sends Wi-Fi UDP broadcasts directly.
  • React Native (Android / iOS mobile apps) → a phone is not sandboxed like a browser, so it can open a real UDP socket. It sends Wi-Fi UDP broadcasts directly through react-native-udp, so no hapbeat-helper is needed.
  • Browser (WebXR / three.js / p5.js / React / jsPsych) → since browsers cannot open a raw UDP socket, it relays over WebSocket (ws://localhost:7703) to the locally running hapbeat-helper.

The code is the same in every case (connect()play(id)). For the differences and constraints between transports, see Transports — Node UDP / React Native UDP / Browser helper.

npm install @hapbeat/sdk

It is ESM-only ("type": "module"). If you use the Browser path, the helper daemon is required.

pip install hapbeat-helper # install once
hapbeat-helper # and keep it running
import { connect } from "@hapbeat/sdk";
const hb = await connect({ appName: "MyApp" }); // UDP broadcast + keep-alive
hb.play("sample-kit.sine_100hz", { gain: 0.3 }); // fire by event id (gain is 0..1)
hb.play("sample-kit.sine_100hz"); // gain omitted → kit / EventMap default
hb.stopAll();
await hb.close();
  • connect() opens a UDP broadcast socket and sends keep-alives to show the app name (appName, up to 16 characters) on the device OLED.
  • play(eventId, opts) is a fire-and-forget call that sends a playback instruction. gain is 0..1 (clamped by the SDK). If omitted, the EventMap described below supplies the default (the kit’s intensity).
  • Always await hb.close() on shutdown. It tells the device the app has left and cancels any streams currently playing.

"sample-kit.sine_100hz" must be an event id contained in a kit deployed to the device (written via Hapbeat Studio). The SDK only sends instructions; the waveform lives in the kit on the device (command mode). For clip mode, where the waveform is sent from the SDK, see Command vs Clip.

The code is the same in the browser, but the helper must be running (connect() rejects if it cannot reach ws://localhost:7703).

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

The bundler picks the browser build automatically, and the helper performs the UDP broadcast on your behalf. Pass onConnectionLost to react when the helper goes down. The browser-specific constraints (clip playback reaches every device the helper knows about; targetTimeUs is ignored) are summarized in Transports — Node UDP / React Native UDP / Browser helper.

It works the same in React. Call connect() exactly once (in an effect or a module-level singleton), then just call hb.play(...) from your event handlers.

The code is the same in an Android / iOS mobile app, sending UDP directly without the helper through react-native-udp (verified on a physical Android device, RN 0.86 / Hermes).

const hb = await connect({ appName: "MyApp" });
hb.play("sample-kit.sine_100hz", { gain: 0.5 });

For setup (installing react-native-udp / fast-text-encoding, the metro.config.js resolver, and Android / iOS permissions), see Transports — Node UDP / React Native UDP / Browser helper.

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

discover(timeoutMs = 1500) collects devices via broadcast PING / PONG (not mDNS).

Separating the fire side from the tuning side (EventMap)

Section titled “Separating the fire side from the tuning side (EventMap)”

Instead of writing “haptic tuning values” such as intensity into your firing code, collect them in the kit manifest (= EventMap). play("id") resolves the defaults from there.

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"); // fires with the intensity from the kit manifest

You can swap “when to play (code)” and “how strong (kit)” independently. See EventMap reference for details.

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 to all (default)

The target resolution order is “the target at call time” → “connect()’s defaultTarget”. "" is a broadcast. For the notation (player_1/chest / */chest / group_<N>), see Address System.