Hapbeat Python SDK
Drive Hapbeat haptic devices from Python over Wi-Fi UDP. For researchers (PsychoPy / Jupyter / ROS), media artists, and anyone prototyping haptics in Python.
A script can drive Hapbeat with a few lines. The fire side (play / stop)
and the tuning side (EventMap) are kept orthogonal and linked only by
event id.
Install
pip install hapbeat-python-sdk # core (zero dependencies, stdlib socket only)
pip install "hapbeat-python-sdk[osc]" # + generic OSC bridge (TouchOSC / Max / TD)
With pipx you get the hapbeat CLI (including the launchpad below) in an
isolated environment: pipx install hapbeat-python-sdk. Note that pipx does not make
import hapbeat available to your own scripts — for the examples/ use a
normal pip install in a venv.
Try everything from one page (launchpad)
hapbeat launchpad # opens http://127.0.0.1:7100 in your browser
A single local web page to fire events, run a metronome, a breathing pacer,
or send Morse — start and stop them live, no per-example launching. It serves
a tiny stdlib HTTP server that relays button presses to the device over UDP
(browsers can't send UDP directly). Works great with pipx install hapbeat-python-sdk.
Quick start
import hapbeat
hb = hapbeat.connect(app_name="MyExperiment") # opens the UDP socket + keep-alive
hb.play("sample-kit.sine_100hz", gain=0.3) # fire event "sample-kit.sine_100hz" at gain 0.3
hb.play("sample-kit.sine_100hz") # gain omitted -> kit baseline intensity
hb.play("sample-kit.sine_100hz", pan=-1) # pan: -1 left / 0 centre / +1 right
hb.stop("sample-kit.sine_100hz")
hb.stop_all()
hb.close()
or as a context manager:
with hapbeat.connect(app_name="MyExperiment") as hb:
hb.play("sample-kit.sine_100hz")
"sample-kit.sine_100hz" must be an event id present in the kit deployed to the
device (via Hapbeat Studio). The SDK sends
the instruction; the waveform lives in the kit on the device.
Discovery
for dev in hb.discover(timeout=1.5):
print(dev.ip, dev.address, dev.firmware_version)
How packets are sent
Commands are unicast to devices that answered a PING and use broadcast only
while no device is known. Clip streams are stricter: they start only on exact
PONG-reported endpoints and remain Deferred while none matches; stream packets
are never broadcast. Wi-Fi APs hold broadcast frames
until their next DTIM beacon (100–300 ms) whenever any client on the AP is
power-saving, which is heard as late haptics and stuttering streams; unicast is
not batched that way. The 5-second keep-alive PING keeps that table current, so
leave keepalive=True.
Pass connect(unicast=False) to always broadcast — worth it when many devices
must fire in lockstep, since one broadcast reaches them all at once while N
unicasts go out one after another.
EventMap — the tuning side (optional)
Keep per-event default gains in one place and let play("id") resolve them,
so firing code never hard-codes intensities:
em = hapbeat.EventMap.from_manifest("my-kit/my-kit-manifest.json")
hb = hapbeat.connect(event_map=em)
hb.play("sample-kit.sine_100hz") # uses the kit manifest's intensity for this event
EventMap reads the kit manifest (schema 2.0.0) intensity as the baseline
gain. You can also build one by hand: EventMap.from_dict({"sample-kit.sine_100hz": 0.5}).
Haptic file — add targeting on top of the kit
The Studio-generated manifest holds kit content (intensity / clip), but not
app-side concerns like which device/body part an event goes to. Put those
in a haptic file (an EventMap overlay that references the kit), so play(id)
resolves the target without the caller passing it:
// haptics.json
{
"kit": "kits/my-kit",
"events": {
"sample-kit.sine_100hz": { "target": "player_1/pos_neck", "gain": 0.8 },
"rain.loop": { "target": "*/pos_back" }
}
}
Targets are matched positionally: segment i of the target is compared
with segment i of the device address, * stands for one whole segment
(pos_* is compared literally — use player_1/*), and a short target
front-matches. Device addresses are always player_<N>/<position>/group_<M>,
defaulting to player_1 / group_1, so a group has to be written
*/*/group_2: plain group_2 is compared against the player slot and never
matches.
hb = hapbeat.connect(app_name="MyApp", haptics="haptics.json")
hb.play("sample-kit.sine_100hz") # goes to player_1/pos_neck at gain 0.8 — from the file
Two playback modes: command and clip
The same play(id) call branches on the manifest:
| Manifest bucket | Mode | What happens |
|---|---|---|
events |
command | the SDK sends a PLAY; the device plays its installed clip |
stream_events |
clip | the SDK reads the WAV from the kit's stream-clips/ and streams it over UDP |
Put the kit inside your project and call events by id — the per-event details (intensity, loop, command vs clip, which WAV) live in the kit, not your code:
my-app/
app.py
kits/my-kit/
my-kit-manifest.json # the "haptic file" -> EventMap
install-clips/ # command clips (flashed to the device via Studio)
stream-clips/*.wav # clip-mode WAVs the SDK streams
import hapbeat
hb = hapbeat.connect(app_name="MyApp", kit="kits/my-kit")
hb.play("sample-kit.sine_100hz") # command -> device plays its installed clip
rain = hb.play("rain.loop") # clip -> returns a StreamPlayback
rain.pan = -0.5
rain.stop() # stops only this logical source
You can also stream an ad-hoc PCM16 buffer (e.g. a synthesized stereo cue where L/R amplitude conveys direction):
left = hb.stream_pcm(pcm_bytes, sample_rate=16000, channels=2,
target="player_1/left", loop=True)
right = hb.stream_pcm(other_pcm, target="player_1/right", loop=True)
left.gain = 0.5
PCM16 input is normalized to 16 kHz stereo inside the shared endpoint mixer. Sources aimed at one endpoint are mixed into one wire session; sources aimed at different endpoints run independently. A full runnable example is in examples/clip_project/.
Generic OSC bridge
Any OSC tool (TouchOSC, Max/MSP, TouchDesigner, a DAW) can drive Hapbeat
without code. Run the bridge and send /hapbeat/play <event_id> [target] [time] [gain] [pan]:
hapbeat osc-bridge --listen 7702
See docs/osc.md for the address spec.
CLI
hapbeat scan # list devices on the LAN
hapbeat play sample-kit.sine_100hz --gain 0.3
hapbeat stop-all
hapbeat launchpad # browser UI for all of the above
Examples
Ready-to-run sample applications live in examples/: a psychophysics experiment, a breathing pacer, a haptic metronome, a live trigger pad, a task-completion notifier, and a Morse transmitter. Each is a single stdlib-only file — see examples/README.md.
For AI coding agents
Working with Claude / Cursor / Copilot? Hand your agent AGENTS.md — a single self-contained file with the SDK's model, API, project layout, and pitfalls. One file is enough to get the whole picture.
Use the Hapbeat Python SDK. Read
AGENTS.mdand follow its API and best practices.
License
MIT © Hapbeat
Metadata
Release files for hapbeat-python-sdk 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| hapbeat_python_sdk-0.3.0.tar.gz | 87.1 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| hapbeat_python_sdk-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 149.0 kB
Release files / hapbeat_python_sdk-0.3.0.tar.gz
| Download URL | hapbeat_python_sdk-0.3.0.tar.gz |
|---|---|
| Size | 87.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a13b768a88d1cd4ef1da59a22c38febbe9fd10ede6530f818257743ac8f80976
|
|
BLAKE2b-256 checksum How to use checksums |
b0eaefb82b28511a1899ce293150bf47187082296e910b8bb3d17f4b5d845927
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.
Transparency logRelease files / hapbeat_python_sdk-0.3.0-py3-none-any.whl
| Download URL | hapbeat_python_sdk-0.3.0-py3-none-any.whl |
|---|---|
| Size | 61.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2afa87492405c1673c0f0aaa848f982b144369e2e39ef039043fb155649e4731
|
|
BLAKE2b-256 checksum How to use checksums |
63e9866f83751be191af62c8a3dbf3201897422436cd82559c837daddf023298
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.
Transparency logSecurity for hapbeat-python-sdk 0.3.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for hapbeat_python_sdk-0.3.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.