hapbeat-python-sdk 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. hapbeat_python_sdk-0.1.0/LICENSE +21 -0
  2. hapbeat_python_sdk-0.1.0/PKG-INFO +214 -0
  3. hapbeat_python_sdk-0.1.0/README.md +181 -0
  4. hapbeat_python_sdk-0.1.0/pyproject.toml +52 -0
  5. hapbeat_python_sdk-0.1.0/setup.cfg +4 -0
  6. hapbeat_python_sdk-0.1.0/src/hapbeat/__init__.py +49 -0
  7. hapbeat_python_sdk-0.1.0/src/hapbeat/__main__.py +8 -0
  8. hapbeat_python_sdk-0.1.0/src/hapbeat/cli.py +124 -0
  9. hapbeat_python_sdk-0.1.0/src/hapbeat/client.py +152 -0
  10. hapbeat_python_sdk-0.1.0/src/hapbeat/clip.py +121 -0
  11. hapbeat_python_sdk-0.1.0/src/hapbeat/eventmap.py +203 -0
  12. hapbeat_python_sdk-0.1.0/src/hapbeat/hapbeat.py +446 -0
  13. hapbeat_python_sdk-0.1.0/src/hapbeat/launchpad.py +659 -0
  14. hapbeat_python_sdk-0.1.0/src/hapbeat/osc.py +126 -0
  15. hapbeat_python_sdk-0.1.0/src/hapbeat/protocol.py +253 -0
  16. hapbeat_python_sdk-0.1.0/src/hapbeat/wav.py +51 -0
  17. hapbeat_python_sdk-0.1.0/src/hapbeat_python_sdk.egg-info/PKG-INFO +214 -0
  18. hapbeat_python_sdk-0.1.0/src/hapbeat_python_sdk.egg-info/SOURCES.txt +26 -0
  19. hapbeat_python_sdk-0.1.0/src/hapbeat_python_sdk.egg-info/dependency_links.txt +1 -0
  20. hapbeat_python_sdk-0.1.0/src/hapbeat_python_sdk.egg-info/entry_points.txt +2 -0
  21. hapbeat_python_sdk-0.1.0/src/hapbeat_python_sdk.egg-info/requires.txt +6 -0
  22. hapbeat_python_sdk-0.1.0/src/hapbeat_python_sdk.egg-info/top_level.txt +1 -0
  23. hapbeat_python_sdk-0.1.0/tests/test_clip.py +204 -0
  24. hapbeat_python_sdk-0.1.0/tests/test_examples.py +283 -0
  25. hapbeat_python_sdk-0.1.0/tests/test_hapbeat.py +96 -0
  26. hapbeat_python_sdk-0.1.0/tests/test_launchpad.py +180 -0
  27. hapbeat_python_sdk-0.1.0/tests/test_overlay_osc.py +145 -0
  28. hapbeat_python_sdk-0.1.0/tests/test_protocol.py +100 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hapbeat
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,214 @@
1
+ Metadata-Version: 2.4
2
+ Name: hapbeat-python-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for driving Hapbeat haptic devices over Wi-Fi UDP
5
+ Author-email: Hapbeat <yus988@hapbeat.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://devtools.hapbeat.com/
8
+ Project-URL: Documentation, https://devtools.hapbeat.com/docs/sdk-integration/
9
+ Project-URL: Repository, https://github.com/hapbeat/hapbeat-python-sdk
10
+ Project-URL: Issues, https://github.com/hapbeat/hapbeat-python-sdk/issues
11
+ Keywords: hapbeat,haptics,udp,osc,vr,research,psychopy
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Multimedia
23
+ Classifier: Topic :: Scientific/Engineering :: Human Machine Interfaces
24
+ Classifier: Topic :: System :: Hardware
25
+ Requires-Python: >=3.10
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Provides-Extra: osc
29
+ Requires-Dist: python-osc>=1.8; extra == "osc"
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=8; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ # Hapbeat Python SDK
35
+
36
+ Drive [Hapbeat](https://hapbeat.com) haptic devices from Python over Wi-Fi UDP.
37
+ For researchers (PsychoPy / Jupyter / ROS), media artists, and anyone
38
+ prototyping haptics in Python.
39
+
40
+ > **📚 Docs**: <https://devtools.hapbeat.com/docs/sdk-integration/>
41
+
42
+ A script can drive Hapbeat with a few lines. The fire side (`play` / `stop`)
43
+ and the tuning side (`EventMap`) are kept orthogonal and linked only by
44
+ event id.
45
+
46
+ ## Install
47
+
48
+ ```bash
49
+ pip install hapbeat-python-sdk # core (zero dependencies, stdlib socket only)
50
+ pip install "hapbeat-python-sdk[osc]" # + generic OSC bridge (TouchOSC / Max / TD)
51
+ ```
52
+
53
+ With **pipx** you get the `hapbeat` CLI (including the launchpad below) in an
54
+ isolated environment: `pipx install hapbeat-python-sdk`. Note that pipx does *not* make
55
+ `import hapbeat` available to your own scripts — for the `examples/` use a
56
+ normal `pip install` in a venv.
57
+
58
+ ## Try everything from one page (launchpad)
59
+
60
+ ```bash
61
+ hapbeat launchpad # opens http://127.0.0.1:7100 in your browser
62
+ ```
63
+
64
+ A single local web page to fire events, run a metronome, a breathing pacer,
65
+ or send Morse — start and stop them live, no per-example launching. It serves
66
+ a tiny stdlib HTTP server that relays button presses to the device over UDP
67
+ (browsers can't send UDP directly). Works great with `pipx install hapbeat-python-sdk`.
68
+
69
+ ## Quick start
70
+
71
+ ```python
72
+ import hapbeat
73
+
74
+ hb = hapbeat.connect(app_name="MyExperiment") # opens UDP broadcast + keep-alive
75
+ hb.play("impact.hit", gain=0.3) # fire event "impact.hit" at gain 0.3
76
+ hb.play("impact.hit") # gain omitted -> kit baseline intensity
77
+ hb.stop("impact.hit")
78
+ hb.stop_all()
79
+ hb.close()
80
+ ```
81
+
82
+ or as a context manager:
83
+
84
+ ```python
85
+ with hapbeat.connect(app_name="MyExperiment") as hb:
86
+ hb.play("impact.hit")
87
+ ```
88
+
89
+ `"impact.hit"` must be an event id present in the **kit deployed to the
90
+ device** (via [Hapbeat Studio](https://devtools.hapbeat.com)). The SDK sends
91
+ the *instruction*; the waveform lives in the kit on the device.
92
+
93
+ ## Discovery
94
+
95
+ ```python
96
+ for dev in hb.discover(timeout=1.5):
97
+ print(dev.ip, dev.address, dev.firmware_version)
98
+ ```
99
+
100
+ ## EventMap — the tuning side (optional)
101
+
102
+ Keep per-event default gains in one place and let `play("id")` resolve them,
103
+ so firing code never hard-codes intensities:
104
+
105
+ ```python
106
+ em = hapbeat.EventMap.from_manifest("my-kit/my-kit-manifest.json")
107
+ hb = hapbeat.connect(event_map=em)
108
+ hb.play("impact.hit") # uses the kit manifest's intensity for this event
109
+ ```
110
+
111
+ `EventMap` reads the kit manifest (schema 2.0.0) `intensity` as the baseline
112
+ gain. You can also build one by hand: `EventMap.from_dict({"impact.hit": 0.5})`.
113
+
114
+ ### Haptic file — add targeting on top of the kit
115
+
116
+ The Studio-generated manifest holds kit content (intensity / clip), but not
117
+ app-side concerns like **which device/body part** an event goes to. Put those
118
+ in a *haptic file* (an EventMap overlay that references the kit), so `play(id)`
119
+ resolves the target without the caller passing it:
120
+
121
+ ```json
122
+ // haptics.json
123
+ {
124
+ "kit": "kits/my-kit",
125
+ "events": {
126
+ "impact.hit": { "target": "player_1/chest", "gain": 0.8 },
127
+ "rain.loop": { "target": "*/back" }
128
+ }
129
+ }
130
+ ```
131
+
132
+ ```python
133
+ hb = hapbeat.connect(app_name="MyApp", haptics="haptics.json")
134
+ hb.play("impact.hit") # goes to player_1/chest at gain 0.8 — from the file
135
+ ```
136
+
137
+ ## Two playback modes: command and clip
138
+
139
+ The same `play(id)` call branches on the manifest:
140
+
141
+ | Manifest bucket | Mode | What happens |
142
+ |---|---|---|
143
+ | `events` | **command** | the SDK sends a PLAY; the **device** plays its installed clip |
144
+ | `stream_events` | **clip** | the SDK reads the WAV from the kit's `stream-clips/` and **streams** it over UDP |
145
+
146
+ Put the kit inside your project and call events by id — the per-event details
147
+ (intensity, loop, command vs clip, which WAV) live in the kit, not your code:
148
+
149
+ ```
150
+ my-app/
151
+ app.py
152
+ kits/my-kit/
153
+ my-kit-manifest.json # the "haptic file" -> EventMap
154
+ install-clips/ # command clips (flashed to the device via Studio)
155
+ stream-clips/*.wav # clip-mode WAVs the SDK streams
156
+ ```
157
+
158
+ ```python
159
+ import hapbeat
160
+ hb = hapbeat.connect(app_name="MyApp", kit="kits/my-kit")
161
+ hb.play("impact.hit") # command -> device plays its installed clip
162
+ hb.play("rain.loop") # clip -> SDK streams stream-clips/<wav> over UDP
163
+ hb.stop("rain.loop") # ends the active stream
164
+ ```
165
+
166
+ You can also stream an ad-hoc PCM16 buffer (e.g. a synthesized stereo cue where
167
+ L/R amplitude conveys direction):
168
+
169
+ ```python
170
+ hb.stream_pcm(pcm_bytes, sample_rate=16000, channels=2)
171
+ ```
172
+
173
+ Author clips as **16 kHz mono PCM16** (the device plays at 16 kHz; the SDK does
174
+ not resample). A full runnable example is in
175
+ [examples/clip_project/](examples/clip_project/).
176
+
177
+ ## Generic OSC bridge
178
+
179
+ Any OSC tool (TouchOSC, Max/MSP, TouchDesigner, a DAW) can drive Hapbeat
180
+ without code. Run the bridge and send `/hapbeat/play <event_id> [target] [time] [gain]`:
181
+
182
+ ```bash
183
+ hapbeat osc-bridge --listen 7702
184
+ ```
185
+
186
+ See [docs/osc.md](docs/osc.md) for the address spec.
187
+
188
+ ## CLI
189
+
190
+ ```bash
191
+ hapbeat scan # list devices on the LAN
192
+ hapbeat play impact.hit --gain 0.3
193
+ hapbeat stop-all
194
+ hapbeat launchpad # browser UI for all of the above
195
+ ```
196
+
197
+ ## Examples
198
+
199
+ Ready-to-run sample applications live in [examples/](examples/):
200
+ a psychophysics experiment, a breathing pacer, a haptic metronome,
201
+ a live trigger pad, a task-completion notifier, and a Morse transmitter.
202
+ Each is a single stdlib-only file — see [examples/README.md](examples/README.md).
203
+
204
+ ## For AI coding agents
205
+
206
+ Working with Claude / Cursor / Copilot? Hand your agent [AGENTS.md](AGENTS.md) —
207
+ a single self-contained file with the SDK's model, API, project layout, and
208
+ pitfalls. One file is enough to get the whole picture.
209
+
210
+ > Use the Hapbeat Python SDK. Read `AGENTS.md` and follow its API and best practices.
211
+
212
+ ## License
213
+
214
+ MIT © Hapbeat
@@ -0,0 +1,181 @@
1
+ # Hapbeat Python SDK
2
+
3
+ Drive [Hapbeat](https://hapbeat.com) haptic devices from Python over Wi-Fi UDP.
4
+ For researchers (PsychoPy / Jupyter / ROS), media artists, and anyone
5
+ prototyping haptics in Python.
6
+
7
+ > **📚 Docs**: <https://devtools.hapbeat.com/docs/sdk-integration/>
8
+
9
+ A script can drive Hapbeat with a few lines. The fire side (`play` / `stop`)
10
+ and the tuning side (`EventMap`) are kept orthogonal and linked only by
11
+ event id.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ pip install hapbeat-python-sdk # core (zero dependencies, stdlib socket only)
17
+ pip install "hapbeat-python-sdk[osc]" # + generic OSC bridge (TouchOSC / Max / TD)
18
+ ```
19
+
20
+ With **pipx** you get the `hapbeat` CLI (including the launchpad below) in an
21
+ isolated environment: `pipx install hapbeat-python-sdk`. Note that pipx does *not* make
22
+ `import hapbeat` available to your own scripts — for the `examples/` use a
23
+ normal `pip install` in a venv.
24
+
25
+ ## Try everything from one page (launchpad)
26
+
27
+ ```bash
28
+ hapbeat launchpad # opens http://127.0.0.1:7100 in your browser
29
+ ```
30
+
31
+ A single local web page to fire events, run a metronome, a breathing pacer,
32
+ or send Morse — start and stop them live, no per-example launching. It serves
33
+ a tiny stdlib HTTP server that relays button presses to the device over UDP
34
+ (browsers can't send UDP directly). Works great with `pipx install hapbeat-python-sdk`.
35
+
36
+ ## Quick start
37
+
38
+ ```python
39
+ import hapbeat
40
+
41
+ hb = hapbeat.connect(app_name="MyExperiment") # opens UDP broadcast + keep-alive
42
+ hb.play("impact.hit", gain=0.3) # fire event "impact.hit" at gain 0.3
43
+ hb.play("impact.hit") # gain omitted -> kit baseline intensity
44
+ hb.stop("impact.hit")
45
+ hb.stop_all()
46
+ hb.close()
47
+ ```
48
+
49
+ or as a context manager:
50
+
51
+ ```python
52
+ with hapbeat.connect(app_name="MyExperiment") as hb:
53
+ hb.play("impact.hit")
54
+ ```
55
+
56
+ `"impact.hit"` must be an event id present in the **kit deployed to the
57
+ device** (via [Hapbeat Studio](https://devtools.hapbeat.com)). The SDK sends
58
+ the *instruction*; the waveform lives in the kit on the device.
59
+
60
+ ## Discovery
61
+
62
+ ```python
63
+ for dev in hb.discover(timeout=1.5):
64
+ print(dev.ip, dev.address, dev.firmware_version)
65
+ ```
66
+
67
+ ## EventMap — the tuning side (optional)
68
+
69
+ Keep per-event default gains in one place and let `play("id")` resolve them,
70
+ so firing code never hard-codes intensities:
71
+
72
+ ```python
73
+ em = hapbeat.EventMap.from_manifest("my-kit/my-kit-manifest.json")
74
+ hb = hapbeat.connect(event_map=em)
75
+ hb.play("impact.hit") # uses the kit manifest's intensity for this event
76
+ ```
77
+
78
+ `EventMap` reads the kit manifest (schema 2.0.0) `intensity` as the baseline
79
+ gain. You can also build one by hand: `EventMap.from_dict({"impact.hit": 0.5})`.
80
+
81
+ ### Haptic file — add targeting on top of the kit
82
+
83
+ The Studio-generated manifest holds kit content (intensity / clip), but not
84
+ app-side concerns like **which device/body part** an event goes to. Put those
85
+ in a *haptic file* (an EventMap overlay that references the kit), so `play(id)`
86
+ resolves the target without the caller passing it:
87
+
88
+ ```json
89
+ // haptics.json
90
+ {
91
+ "kit": "kits/my-kit",
92
+ "events": {
93
+ "impact.hit": { "target": "player_1/chest", "gain": 0.8 },
94
+ "rain.loop": { "target": "*/back" }
95
+ }
96
+ }
97
+ ```
98
+
99
+ ```python
100
+ hb = hapbeat.connect(app_name="MyApp", haptics="haptics.json")
101
+ hb.play("impact.hit") # goes to player_1/chest at gain 0.8 — from the file
102
+ ```
103
+
104
+ ## Two playback modes: command and clip
105
+
106
+ The same `play(id)` call branches on the manifest:
107
+
108
+ | Manifest bucket | Mode | What happens |
109
+ |---|---|---|
110
+ | `events` | **command** | the SDK sends a PLAY; the **device** plays its installed clip |
111
+ | `stream_events` | **clip** | the SDK reads the WAV from the kit's `stream-clips/` and **streams** it over UDP |
112
+
113
+ Put the kit inside your project and call events by id — the per-event details
114
+ (intensity, loop, command vs clip, which WAV) live in the kit, not your code:
115
+
116
+ ```
117
+ my-app/
118
+ app.py
119
+ kits/my-kit/
120
+ my-kit-manifest.json # the "haptic file" -> EventMap
121
+ install-clips/ # command clips (flashed to the device via Studio)
122
+ stream-clips/*.wav # clip-mode WAVs the SDK streams
123
+ ```
124
+
125
+ ```python
126
+ import hapbeat
127
+ hb = hapbeat.connect(app_name="MyApp", kit="kits/my-kit")
128
+ hb.play("impact.hit") # command -> device plays its installed clip
129
+ hb.play("rain.loop") # clip -> SDK streams stream-clips/<wav> over UDP
130
+ hb.stop("rain.loop") # ends the active stream
131
+ ```
132
+
133
+ You can also stream an ad-hoc PCM16 buffer (e.g. a synthesized stereo cue where
134
+ L/R amplitude conveys direction):
135
+
136
+ ```python
137
+ hb.stream_pcm(pcm_bytes, sample_rate=16000, channels=2)
138
+ ```
139
+
140
+ Author clips as **16 kHz mono PCM16** (the device plays at 16 kHz; the SDK does
141
+ not resample). A full runnable example is in
142
+ [examples/clip_project/](examples/clip_project/).
143
+
144
+ ## Generic OSC bridge
145
+
146
+ Any OSC tool (TouchOSC, Max/MSP, TouchDesigner, a DAW) can drive Hapbeat
147
+ without code. Run the bridge and send `/hapbeat/play <event_id> [target] [time] [gain]`:
148
+
149
+ ```bash
150
+ hapbeat osc-bridge --listen 7702
151
+ ```
152
+
153
+ See [docs/osc.md](docs/osc.md) for the address spec.
154
+
155
+ ## CLI
156
+
157
+ ```bash
158
+ hapbeat scan # list devices on the LAN
159
+ hapbeat play impact.hit --gain 0.3
160
+ hapbeat stop-all
161
+ hapbeat launchpad # browser UI for all of the above
162
+ ```
163
+
164
+ ## Examples
165
+
166
+ Ready-to-run sample applications live in [examples/](examples/):
167
+ a psychophysics experiment, a breathing pacer, a haptic metronome,
168
+ a live trigger pad, a task-completion notifier, and a Morse transmitter.
169
+ Each is a single stdlib-only file — see [examples/README.md](examples/README.md).
170
+
171
+ ## For AI coding agents
172
+
173
+ Working with Claude / Cursor / Copilot? Hand your agent [AGENTS.md](AGENTS.md) —
174
+ a single self-contained file with the SDK's model, API, project layout, and
175
+ pitfalls. One file is enough to get the whole picture.
176
+
177
+ > Use the Hapbeat Python SDK. Read `AGENTS.md` and follow its API and best practices.
178
+
179
+ ## License
180
+
181
+ MIT © Hapbeat
@@ -0,0 +1,52 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ # Distribution name (pip/pipx). The import package stays `import hapbeat`.
7
+ name = "hapbeat-python-sdk"
8
+ version = "0.1.0"
9
+ description = "Python SDK for driving Hapbeat haptic devices over Wi-Fi UDP"
10
+ readme = "README.md"
11
+ requires-python = ">=3.10"
12
+ license = { text = "MIT" }
13
+ authors = [{ name = "Hapbeat", email = "yus988@hapbeat.com" }]
14
+ keywords = ["hapbeat", "haptics", "udp", "osc", "vr", "research", "psychopy"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Intended Audience :: Science/Research",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Operating System :: OS Independent",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Topic :: Multimedia",
27
+ "Topic :: Scientific/Engineering :: Human Machine Interfaces",
28
+ "Topic :: System :: Hardware",
29
+ ]
30
+ # Core level-1 has zero runtime dependencies — stdlib socket only.
31
+ dependencies = []
32
+
33
+ [project.optional-dependencies]
34
+ # Generic OSC bridge (TouchOSC / Max / TouchDesigner / DAWs).
35
+ osc = ["python-osc>=1.8"]
36
+ dev = ["pytest>=8"]
37
+
38
+ [project.urls]
39
+ Homepage = "https://devtools.hapbeat.com/"
40
+ Documentation = "https://devtools.hapbeat.com/docs/sdk-integration/"
41
+ Repository = "https://github.com/hapbeat/hapbeat-python-sdk"
42
+ Issues = "https://github.com/hapbeat/hapbeat-python-sdk/issues"
43
+
44
+ [project.scripts]
45
+ hapbeat = "hapbeat.cli:main"
46
+
47
+ [tool.setuptools.packages.find]
48
+ where = ["src"]
49
+
50
+ [tool.pytest.ini_options]
51
+ testpaths = ["tests"]
52
+ pythonpath = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,49 @@
1
+ """Hapbeat SDK for Python — drive Hapbeat haptic devices over Wi-Fi UDP.
2
+
3
+ Quick start::
4
+
5
+ import hapbeat
6
+
7
+ hb = hapbeat.connect(app_name="MyExperiment")
8
+ hb.play("impact.hit", gain=0.3)
9
+ hb.stop_all()
10
+ hb.close()
11
+
12
+ The fire side (``play``/``stop``) and the tuning side (:class:`EventMap`) are
13
+ kept orthogonal and linked only by event id, matching the Hapbeat Unity SDK.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from . import protocol
19
+ from .client import DEFAULT_BROADCAST, DEFAULT_PORT, UdpClient
20
+ from .clip import ClipStreamer
21
+ from .eventmap import EventDef, EventMap
22
+ from .hapbeat import Device, Hapbeat, connect
23
+ from .wav import WavPcm, read_wav_pcm16
24
+
25
+ try: # populated from installed package metadata
26
+ from importlib.metadata import PackageNotFoundError, version
27
+
28
+ try:
29
+ __version__ = version("hapbeat-python-sdk")
30
+ except PackageNotFoundError: # running from a source checkout
31
+ __version__ = "0.1.0+local"
32
+ except ImportError: # pragma: no cover
33
+ __version__ = "0.1.0+local"
34
+
35
+ __all__ = [
36
+ "connect",
37
+ "Hapbeat",
38
+ "Device",
39
+ "EventMap",
40
+ "EventDef",
41
+ "ClipStreamer",
42
+ "WavPcm",
43
+ "read_wav_pcm16",
44
+ "UdpClient",
45
+ "protocol",
46
+ "DEFAULT_PORT",
47
+ "DEFAULT_BROADCAST",
48
+ "__version__",
49
+ ]
@@ -0,0 +1,8 @@
1
+ """Enable ``python -m hapbeat``."""
2
+
3
+ import sys
4
+
5
+ from .cli import main
6
+
7
+ if __name__ == "__main__":
8
+ sys.exit(main())
@@ -0,0 +1,124 @@
1
+ """``hapbeat`` command-line interface.
2
+
3
+ Examples::
4
+
5
+ hapbeat scan
6
+ hapbeat play impact.hit --gain 0.3
7
+ hapbeat stop impact.hit
8
+ hapbeat stop-all
9
+ hapbeat osc-bridge --listen 7702
10
+ hapbeat launchpad
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import sys
17
+
18
+ from . import __version__
19
+ from .eventmap import EventMap
20
+ from .hapbeat import connect
21
+
22
+
23
+ def _add_common(p: argparse.ArgumentParser) -> None:
24
+ p.add_argument("--port", type=int, default=7700, help="UDP port (default 7700)")
25
+ p.add_argument("--target", default="", help="device address; empty = all (broadcast)")
26
+
27
+
28
+ def build_parser() -> argparse.ArgumentParser:
29
+ parser = argparse.ArgumentParser(prog="hapbeat", description="Drive Hapbeat devices.")
30
+ parser.add_argument("--version", action="version", version=f"hapbeat {__version__}")
31
+ sub = parser.add_subparsers(dest="command", required=True)
32
+
33
+ p_play = sub.add_parser("play", help="play an event by id")
34
+ p_play.add_argument("event_id")
35
+ p_play.add_argument("--gain", type=float, default=None, help="0..1 (default: kit baseline)")
36
+ _add_common(p_play)
37
+
38
+ p_stop = sub.add_parser("stop", help="stop one event id")
39
+ p_stop.add_argument("event_id")
40
+ _add_common(p_stop)
41
+
42
+ p_stop_all = sub.add_parser("stop-all", help="stop everything")
43
+ _add_common(p_stop_all)
44
+
45
+ p_ping = sub.add_parser("ping", help="broadcast a keep-alive ping")
46
+ _add_common(p_ping)
47
+
48
+ p_scan = sub.add_parser("scan", help="discover devices on the LAN")
49
+ p_scan.add_argument("--timeout", type=float, default=1.5)
50
+ _add_common(p_scan)
51
+
52
+ p_osc = sub.add_parser("osc-bridge", help="relay /hapbeat/* OSC to devices")
53
+ p_osc.add_argument("--listen", type=int, default=7702, help="OSC listen port (default 7702)")
54
+ p_osc.add_argument("--haptics", default=None,
55
+ help="haptic file (overlay) so OSC events route command/clip + use per-event targets")
56
+ p_osc.add_argument("--kit", default=None,
57
+ help="kit folder (alternative to --haptics; intensity/clip only, no targeting)")
58
+ _add_common(p_osc)
59
+
60
+ p_lp = sub.add_parser(
61
+ "launchpad",
62
+ help="serve a local web page to try features from the browser",
63
+ )
64
+ p_lp.add_argument("--host", default="127.0.0.1", help="HTTP bind host (default 127.0.0.1)")
65
+ p_lp.add_argument("--http-port", type=int, default=7100, help="HTTP port (default 7100)")
66
+ p_lp.add_argument("--no-open", action="store_true", help="do not open a browser")
67
+ _add_common(p_lp)
68
+
69
+ return parser
70
+
71
+
72
+ def main(argv: list[str] | None = None) -> int:
73
+ args = build_parser().parse_args(argv)
74
+
75
+ if args.command == "osc-bridge":
76
+ from .osc import OscBridge
77
+
78
+ event_map = None
79
+ if args.haptics:
80
+ event_map = EventMap.from_file(args.haptics)
81
+ elif args.kit:
82
+ event_map = EventMap.from_kit(args.kit)
83
+ hb = connect(port=args.port, app_name="hapbeat-osc",
84
+ default_target=args.target, event_map=event_map)
85
+ try:
86
+ mode = "command+clip" if event_map else "command only"
87
+ print(f"OSC bridge: listening on :{args.listen}, relaying to UDP {args.port} ({mode})")
88
+ OscBridge(hb, listen_port=args.listen).serve_forever()
89
+ except KeyboardInterrupt:
90
+ pass
91
+ finally:
92
+ hb.close()
93
+ return 0
94
+
95
+ if args.command == "launchpad":
96
+ from .launchpad import serve
97
+
98
+ return serve(host=args.host, port=args.http_port, udp_port=args.port,
99
+ target=args.target, open_browser=not args.no_open)
100
+
101
+ hb = connect(port=args.port, default_target=getattr(args, "target", ""), keepalive=False)
102
+ try:
103
+ if args.command == "play":
104
+ ok = hb.play(args.event_id, args.gain)
105
+ print(f"play {args.event_id} gain={args.gain if args.gain is not None else 'baseline'} -> {'sent' if ok else 'FAILED'}")
106
+ elif args.command == "stop":
107
+ print("sent" if hb.stop(args.event_id) else "FAILED")
108
+ elif args.command == "stop-all":
109
+ print("sent" if hb.stop_all() else "FAILED")
110
+ elif args.command == "ping":
111
+ print("sent" if hb.ping() else "FAILED")
112
+ elif args.command == "scan":
113
+ devices = hb.discover(timeout=args.timeout)
114
+ if not devices:
115
+ print("no devices replied")
116
+ for d in devices:
117
+ print(f"{d.ip:<16} {d.address or '?':<20} {d.name or '?':<16} fw={d.firmware_version or '?'}")
118
+ finally:
119
+ hb.close()
120
+ return 0
121
+
122
+
123
+ if __name__ == "__main__":
124
+ sys.exit(main())