hapbeat-python-sdk 0.1.0__tar.gz → 0.3.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.
- {hapbeat_python_sdk-0.1.0 → hapbeat_python_sdk-0.3.0}/PKG-INFO +49 -20
- {hapbeat_python_sdk-0.1.0 → hapbeat_python_sdk-0.3.0}/README.md +48 -19
- {hapbeat_python_sdk-0.1.0 → hapbeat_python_sdk-0.3.0}/pyproject.toml +1 -1
- {hapbeat_python_sdk-0.1.0 → hapbeat_python_sdk-0.3.0}/src/hapbeat/__init__.py +15 -5
- {hapbeat_python_sdk-0.1.0 → hapbeat_python_sdk-0.3.0}/src/hapbeat/cli.py +22 -4
- hapbeat_python_sdk-0.3.0/src/hapbeat/client.py +411 -0
- {hapbeat_python_sdk-0.1.0 → hapbeat_python_sdk-0.3.0}/src/hapbeat/eventmap.py +1 -1
- hapbeat_python_sdk-0.3.0/src/hapbeat/hapbeat.py +786 -0
- {hapbeat_python_sdk-0.1.0 → hapbeat_python_sdk-0.3.0}/src/hapbeat/launchpad.py +3 -2
- hapbeat_python_sdk-0.3.0/src/hapbeat/netif.py +270 -0
- {hapbeat_python_sdk-0.1.0 → hapbeat_python_sdk-0.3.0}/src/hapbeat/osc.py +4 -2
- hapbeat_python_sdk-0.3.0/src/hapbeat/protocol.py +440 -0
- hapbeat_python_sdk-0.3.0/src/hapbeat/stream.py +766 -0
- hapbeat_python_sdk-0.3.0/src/hapbeat/stream_lease.py +182 -0
- hapbeat_python_sdk-0.3.0/src/hapbeat/update_check.py +213 -0
- {hapbeat_python_sdk-0.1.0 → hapbeat_python_sdk-0.3.0}/src/hapbeat_python_sdk.egg-info/PKG-INFO +49 -20
- {hapbeat_python_sdk-0.1.0 → hapbeat_python_sdk-0.3.0}/src/hapbeat_python_sdk.egg-info/SOURCES.txt +10 -2
- hapbeat_python_sdk-0.3.0/tests/test_addressing.py +192 -0
- hapbeat_python_sdk-0.3.0/tests/test_broadcast_routes.py +285 -0
- hapbeat_python_sdk-0.3.0/tests/test_clip.py +390 -0
- {hapbeat_python_sdk-0.1.0 → hapbeat_python_sdk-0.3.0}/tests/test_examples.py +2 -2
- {hapbeat_python_sdk-0.1.0 → hapbeat_python_sdk-0.3.0}/tests/test_hapbeat.py +35 -1
- {hapbeat_python_sdk-0.1.0 → hapbeat_python_sdk-0.3.0}/tests/test_launchpad.py +4 -4
- {hapbeat_python_sdk-0.1.0 → hapbeat_python_sdk-0.3.0}/tests/test_overlay_osc.py +8 -5
- {hapbeat_python_sdk-0.1.0 → hapbeat_python_sdk-0.3.0}/tests/test_protocol.py +20 -1
- hapbeat_python_sdk-0.3.0/tests/test_stream_mix.py +168 -0
- hapbeat_python_sdk-0.3.0/tests/test_stream_session_v2.py +673 -0
- hapbeat_python_sdk-0.3.0/tests/test_update_check.py +162 -0
- hapbeat_python_sdk-0.1.0/src/hapbeat/client.py +0 -152
- hapbeat_python_sdk-0.1.0/src/hapbeat/clip.py +0 -121
- hapbeat_python_sdk-0.1.0/src/hapbeat/hapbeat.py +0 -446
- hapbeat_python_sdk-0.1.0/src/hapbeat/protocol.py +0 -253
- hapbeat_python_sdk-0.1.0/tests/test_clip.py +0 -204
- {hapbeat_python_sdk-0.1.0 → hapbeat_python_sdk-0.3.0}/LICENSE +0 -0
- {hapbeat_python_sdk-0.1.0 → hapbeat_python_sdk-0.3.0}/setup.cfg +0 -0
- {hapbeat_python_sdk-0.1.0 → hapbeat_python_sdk-0.3.0}/src/hapbeat/__main__.py +0 -0
- {hapbeat_python_sdk-0.1.0 → hapbeat_python_sdk-0.3.0}/src/hapbeat/wav.py +0 -0
- {hapbeat_python_sdk-0.1.0 → hapbeat_python_sdk-0.3.0}/src/hapbeat_python_sdk.egg-info/dependency_links.txt +0 -0
- {hapbeat_python_sdk-0.1.0 → hapbeat_python_sdk-0.3.0}/src/hapbeat_python_sdk.egg-info/entry_points.txt +0 -0
- {hapbeat_python_sdk-0.1.0 → hapbeat_python_sdk-0.3.0}/src/hapbeat_python_sdk.egg-info/requires.txt +0 -0
- {hapbeat_python_sdk-0.1.0 → hapbeat_python_sdk-0.3.0}/src/hapbeat_python_sdk.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hapbeat-python-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Python SDK for driving Hapbeat haptic devices over Wi-Fi UDP
|
|
5
5
|
Author-email: Hapbeat <yus988@hapbeat.com>
|
|
6
6
|
License: MIT
|
|
@@ -71,10 +71,11 @@ a tiny stdlib HTTP server that relays button presses to the device over UDP
|
|
|
71
71
|
```python
|
|
72
72
|
import hapbeat
|
|
73
73
|
|
|
74
|
-
hb = hapbeat.connect(app_name="MyExperiment") # opens UDP
|
|
75
|
-
hb.play("
|
|
76
|
-
hb.play("
|
|
77
|
-
hb.
|
|
74
|
+
hb = hapbeat.connect(app_name="MyExperiment") # opens the UDP socket + keep-alive
|
|
75
|
+
hb.play("sample-kit.sine_100hz", gain=0.3) # fire event "sample-kit.sine_100hz" at gain 0.3
|
|
76
|
+
hb.play("sample-kit.sine_100hz") # gain omitted -> kit baseline intensity
|
|
77
|
+
hb.play("sample-kit.sine_100hz", pan=-1) # pan: -1 left / 0 centre / +1 right
|
|
78
|
+
hb.stop("sample-kit.sine_100hz")
|
|
78
79
|
hb.stop_all()
|
|
79
80
|
hb.close()
|
|
80
81
|
```
|
|
@@ -83,10 +84,10 @@ or as a context manager:
|
|
|
83
84
|
|
|
84
85
|
```python
|
|
85
86
|
with hapbeat.connect(app_name="MyExperiment") as hb:
|
|
86
|
-
hb.play("
|
|
87
|
+
hb.play("sample-kit.sine_100hz")
|
|
87
88
|
```
|
|
88
89
|
|
|
89
|
-
`"
|
|
90
|
+
`"sample-kit.sine_100hz"` must be an event id present in the **kit deployed to the
|
|
90
91
|
device** (via [Hapbeat Studio](https://devtools.hapbeat.com)). The SDK sends
|
|
91
92
|
the *instruction*; the waveform lives in the kit on the device.
|
|
92
93
|
|
|
@@ -97,6 +98,21 @@ for dev in hb.discover(timeout=1.5):
|
|
|
97
98
|
print(dev.ip, dev.address, dev.firmware_version)
|
|
98
99
|
```
|
|
99
100
|
|
|
101
|
+
### How packets are sent
|
|
102
|
+
|
|
103
|
+
Commands are unicast to devices that answered a PING and use broadcast only
|
|
104
|
+
while no device is known. Clip streams are stricter: they start only on exact
|
|
105
|
+
PONG-reported endpoints and remain `Deferred` while none matches; stream packets
|
|
106
|
+
are never broadcast. Wi-Fi APs hold broadcast frames
|
|
107
|
+
until their next DTIM beacon (100–300 ms) whenever any client on the AP is
|
|
108
|
+
power-saving, which is heard as late haptics and stuttering streams; unicast is
|
|
109
|
+
not batched that way. The 5-second keep-alive PING keeps that table current, so
|
|
110
|
+
leave `keepalive=True`.
|
|
111
|
+
|
|
112
|
+
Pass `connect(unicast=False)` to always broadcast — worth it when many devices
|
|
113
|
+
must fire in lockstep, since one broadcast reaches them all at once while N
|
|
114
|
+
unicasts go out one after another.
|
|
115
|
+
|
|
100
116
|
## EventMap — the tuning side (optional)
|
|
101
117
|
|
|
102
118
|
Keep per-event default gains in one place and let `play("id")` resolve them,
|
|
@@ -105,11 +121,11 @@ so firing code never hard-codes intensities:
|
|
|
105
121
|
```python
|
|
106
122
|
em = hapbeat.EventMap.from_manifest("my-kit/my-kit-manifest.json")
|
|
107
123
|
hb = hapbeat.connect(event_map=em)
|
|
108
|
-
hb.play("
|
|
124
|
+
hb.play("sample-kit.sine_100hz") # uses the kit manifest's intensity for this event
|
|
109
125
|
```
|
|
110
126
|
|
|
111
127
|
`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({"
|
|
128
|
+
gain. You can also build one by hand: `EventMap.from_dict({"sample-kit.sine_100hz": 0.5})`.
|
|
113
129
|
|
|
114
130
|
### Haptic file — add targeting on top of the kit
|
|
115
131
|
|
|
@@ -123,15 +139,23 @@ resolves the target without the caller passing it:
|
|
|
123
139
|
{
|
|
124
140
|
"kit": "kits/my-kit",
|
|
125
141
|
"events": {
|
|
126
|
-
"
|
|
127
|
-
"rain.loop": { "target": "*/
|
|
142
|
+
"sample-kit.sine_100hz": { "target": "player_1/pos_neck", "gain": 0.8 },
|
|
143
|
+
"rain.loop": { "target": "*/pos_back" }
|
|
128
144
|
}
|
|
129
145
|
}
|
|
130
146
|
```
|
|
131
147
|
|
|
148
|
+
Targets are matched **positionally**: segment *i* of the target is compared
|
|
149
|
+
with segment *i* of the device address, `*` stands for one **whole** segment
|
|
150
|
+
(`pos_*` is compared literally — use `player_1/*`), and a short target
|
|
151
|
+
front-matches. Device addresses are always `player_<N>/<position>/group_<M>`,
|
|
152
|
+
defaulting to `player_1` / `group_1`, so a group has to be written
|
|
153
|
+
`*/*/group_2`: plain `group_2` is compared against the *player* slot and never
|
|
154
|
+
matches.
|
|
155
|
+
|
|
132
156
|
```python
|
|
133
157
|
hb = hapbeat.connect(app_name="MyApp", haptics="haptics.json")
|
|
134
|
-
hb.play("
|
|
158
|
+
hb.play("sample-kit.sine_100hz") # goes to player_1/pos_neck at gain 0.8 — from the file
|
|
135
159
|
```
|
|
136
160
|
|
|
137
161
|
## Two playback modes: command and clip
|
|
@@ -158,26 +182,31 @@ my-app/
|
|
|
158
182
|
```python
|
|
159
183
|
import hapbeat
|
|
160
184
|
hb = hapbeat.connect(app_name="MyApp", kit="kits/my-kit")
|
|
161
|
-
hb.play("
|
|
162
|
-
hb.play("rain.loop")
|
|
163
|
-
|
|
185
|
+
hb.play("sample-kit.sine_100hz") # command -> device plays its installed clip
|
|
186
|
+
rain = hb.play("rain.loop") # clip -> returns a StreamPlayback
|
|
187
|
+
rain.pan = -0.5
|
|
188
|
+
rain.stop() # stops only this logical source
|
|
164
189
|
```
|
|
165
190
|
|
|
166
191
|
You can also stream an ad-hoc PCM16 buffer (e.g. a synthesized stereo cue where
|
|
167
192
|
L/R amplitude conveys direction):
|
|
168
193
|
|
|
169
194
|
```python
|
|
170
|
-
hb.stream_pcm(pcm_bytes, sample_rate=16000, channels=2
|
|
195
|
+
left = hb.stream_pcm(pcm_bytes, sample_rate=16000, channels=2,
|
|
196
|
+
target="player_1/left", loop=True)
|
|
197
|
+
right = hb.stream_pcm(other_pcm, target="player_1/right", loop=True)
|
|
198
|
+
left.gain = 0.5
|
|
171
199
|
```
|
|
172
200
|
|
|
173
|
-
|
|
174
|
-
|
|
201
|
+
PCM16 input is normalized to **16 kHz stereo** inside the shared endpoint mixer.
|
|
202
|
+
Sources aimed at one endpoint are mixed into one wire session; sources aimed at
|
|
203
|
+
different endpoints run independently. A full runnable example is in
|
|
175
204
|
[examples/clip_project/](examples/clip_project/).
|
|
176
205
|
|
|
177
206
|
## Generic OSC bridge
|
|
178
207
|
|
|
179
208
|
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]`:
|
|
209
|
+
without code. Run the bridge and send `/hapbeat/play <event_id> [target] [time] [gain] [pan]`:
|
|
181
210
|
|
|
182
211
|
```bash
|
|
183
212
|
hapbeat osc-bridge --listen 7702
|
|
@@ -189,7 +218,7 @@ See [docs/osc.md](docs/osc.md) for the address spec.
|
|
|
189
218
|
|
|
190
219
|
```bash
|
|
191
220
|
hapbeat scan # list devices on the LAN
|
|
192
|
-
hapbeat play
|
|
221
|
+
hapbeat play sample-kit.sine_100hz --gain 0.3
|
|
193
222
|
hapbeat stop-all
|
|
194
223
|
hapbeat launchpad # browser UI for all of the above
|
|
195
224
|
```
|
|
@@ -38,10 +38,11 @@ a tiny stdlib HTTP server that relays button presses to the device over UDP
|
|
|
38
38
|
```python
|
|
39
39
|
import hapbeat
|
|
40
40
|
|
|
41
|
-
hb = hapbeat.connect(app_name="MyExperiment") # opens UDP
|
|
42
|
-
hb.play("
|
|
43
|
-
hb.play("
|
|
44
|
-
hb.
|
|
41
|
+
hb = hapbeat.connect(app_name="MyExperiment") # opens the UDP socket + keep-alive
|
|
42
|
+
hb.play("sample-kit.sine_100hz", gain=0.3) # fire event "sample-kit.sine_100hz" at gain 0.3
|
|
43
|
+
hb.play("sample-kit.sine_100hz") # gain omitted -> kit baseline intensity
|
|
44
|
+
hb.play("sample-kit.sine_100hz", pan=-1) # pan: -1 left / 0 centre / +1 right
|
|
45
|
+
hb.stop("sample-kit.sine_100hz")
|
|
45
46
|
hb.stop_all()
|
|
46
47
|
hb.close()
|
|
47
48
|
```
|
|
@@ -50,10 +51,10 @@ or as a context manager:
|
|
|
50
51
|
|
|
51
52
|
```python
|
|
52
53
|
with hapbeat.connect(app_name="MyExperiment") as hb:
|
|
53
|
-
hb.play("
|
|
54
|
+
hb.play("sample-kit.sine_100hz")
|
|
54
55
|
```
|
|
55
56
|
|
|
56
|
-
`"
|
|
57
|
+
`"sample-kit.sine_100hz"` must be an event id present in the **kit deployed to the
|
|
57
58
|
device** (via [Hapbeat Studio](https://devtools.hapbeat.com)). The SDK sends
|
|
58
59
|
the *instruction*; the waveform lives in the kit on the device.
|
|
59
60
|
|
|
@@ -64,6 +65,21 @@ for dev in hb.discover(timeout=1.5):
|
|
|
64
65
|
print(dev.ip, dev.address, dev.firmware_version)
|
|
65
66
|
```
|
|
66
67
|
|
|
68
|
+
### How packets are sent
|
|
69
|
+
|
|
70
|
+
Commands are unicast to devices that answered a PING and use broadcast only
|
|
71
|
+
while no device is known. Clip streams are stricter: they start only on exact
|
|
72
|
+
PONG-reported endpoints and remain `Deferred` while none matches; stream packets
|
|
73
|
+
are never broadcast. Wi-Fi APs hold broadcast frames
|
|
74
|
+
until their next DTIM beacon (100–300 ms) whenever any client on the AP is
|
|
75
|
+
power-saving, which is heard as late haptics and stuttering streams; unicast is
|
|
76
|
+
not batched that way. The 5-second keep-alive PING keeps that table current, so
|
|
77
|
+
leave `keepalive=True`.
|
|
78
|
+
|
|
79
|
+
Pass `connect(unicast=False)` to always broadcast — worth it when many devices
|
|
80
|
+
must fire in lockstep, since one broadcast reaches them all at once while N
|
|
81
|
+
unicasts go out one after another.
|
|
82
|
+
|
|
67
83
|
## EventMap — the tuning side (optional)
|
|
68
84
|
|
|
69
85
|
Keep per-event default gains in one place and let `play("id")` resolve them,
|
|
@@ -72,11 +88,11 @@ so firing code never hard-codes intensities:
|
|
|
72
88
|
```python
|
|
73
89
|
em = hapbeat.EventMap.from_manifest("my-kit/my-kit-manifest.json")
|
|
74
90
|
hb = hapbeat.connect(event_map=em)
|
|
75
|
-
hb.play("
|
|
91
|
+
hb.play("sample-kit.sine_100hz") # uses the kit manifest's intensity for this event
|
|
76
92
|
```
|
|
77
93
|
|
|
78
94
|
`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({"
|
|
95
|
+
gain. You can also build one by hand: `EventMap.from_dict({"sample-kit.sine_100hz": 0.5})`.
|
|
80
96
|
|
|
81
97
|
### Haptic file — add targeting on top of the kit
|
|
82
98
|
|
|
@@ -90,15 +106,23 @@ resolves the target without the caller passing it:
|
|
|
90
106
|
{
|
|
91
107
|
"kit": "kits/my-kit",
|
|
92
108
|
"events": {
|
|
93
|
-
"
|
|
94
|
-
"rain.loop": { "target": "*/
|
|
109
|
+
"sample-kit.sine_100hz": { "target": "player_1/pos_neck", "gain": 0.8 },
|
|
110
|
+
"rain.loop": { "target": "*/pos_back" }
|
|
95
111
|
}
|
|
96
112
|
}
|
|
97
113
|
```
|
|
98
114
|
|
|
115
|
+
Targets are matched **positionally**: segment *i* of the target is compared
|
|
116
|
+
with segment *i* of the device address, `*` stands for one **whole** segment
|
|
117
|
+
(`pos_*` is compared literally — use `player_1/*`), and a short target
|
|
118
|
+
front-matches. Device addresses are always `player_<N>/<position>/group_<M>`,
|
|
119
|
+
defaulting to `player_1` / `group_1`, so a group has to be written
|
|
120
|
+
`*/*/group_2`: plain `group_2` is compared against the *player* slot and never
|
|
121
|
+
matches.
|
|
122
|
+
|
|
99
123
|
```python
|
|
100
124
|
hb = hapbeat.connect(app_name="MyApp", haptics="haptics.json")
|
|
101
|
-
hb.play("
|
|
125
|
+
hb.play("sample-kit.sine_100hz") # goes to player_1/pos_neck at gain 0.8 — from the file
|
|
102
126
|
```
|
|
103
127
|
|
|
104
128
|
## Two playback modes: command and clip
|
|
@@ -125,26 +149,31 @@ my-app/
|
|
|
125
149
|
```python
|
|
126
150
|
import hapbeat
|
|
127
151
|
hb = hapbeat.connect(app_name="MyApp", kit="kits/my-kit")
|
|
128
|
-
hb.play("
|
|
129
|
-
hb.play("rain.loop")
|
|
130
|
-
|
|
152
|
+
hb.play("sample-kit.sine_100hz") # command -> device plays its installed clip
|
|
153
|
+
rain = hb.play("rain.loop") # clip -> returns a StreamPlayback
|
|
154
|
+
rain.pan = -0.5
|
|
155
|
+
rain.stop() # stops only this logical source
|
|
131
156
|
```
|
|
132
157
|
|
|
133
158
|
You can also stream an ad-hoc PCM16 buffer (e.g. a synthesized stereo cue where
|
|
134
159
|
L/R amplitude conveys direction):
|
|
135
160
|
|
|
136
161
|
```python
|
|
137
|
-
hb.stream_pcm(pcm_bytes, sample_rate=16000, channels=2
|
|
162
|
+
left = hb.stream_pcm(pcm_bytes, sample_rate=16000, channels=2,
|
|
163
|
+
target="player_1/left", loop=True)
|
|
164
|
+
right = hb.stream_pcm(other_pcm, target="player_1/right", loop=True)
|
|
165
|
+
left.gain = 0.5
|
|
138
166
|
```
|
|
139
167
|
|
|
140
|
-
|
|
141
|
-
|
|
168
|
+
PCM16 input is normalized to **16 kHz stereo** inside the shared endpoint mixer.
|
|
169
|
+
Sources aimed at one endpoint are mixed into one wire session; sources aimed at
|
|
170
|
+
different endpoints run independently. A full runnable example is in
|
|
142
171
|
[examples/clip_project/](examples/clip_project/).
|
|
143
172
|
|
|
144
173
|
## Generic OSC bridge
|
|
145
174
|
|
|
146
175
|
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]`:
|
|
176
|
+
without code. Run the bridge and send `/hapbeat/play <event_id> [target] [time] [gain] [pan]`:
|
|
148
177
|
|
|
149
178
|
```bash
|
|
150
179
|
hapbeat osc-bridge --listen 7702
|
|
@@ -156,7 +185,7 @@ See [docs/osc.md](docs/osc.md) for the address spec.
|
|
|
156
185
|
|
|
157
186
|
```bash
|
|
158
187
|
hapbeat scan # list devices on the LAN
|
|
159
|
-
hapbeat play
|
|
188
|
+
hapbeat play sample-kit.sine_100hz --gain 0.3
|
|
160
189
|
hapbeat stop-all
|
|
161
190
|
hapbeat launchpad # browser UI for all of the above
|
|
162
191
|
```
|
|
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
[project]
|
|
6
6
|
# Distribution name (pip/pipx). The import package stays `import hapbeat`.
|
|
7
7
|
name = "hapbeat-python-sdk"
|
|
8
|
-
version = "0.
|
|
8
|
+
version = "0.3.0"
|
|
9
9
|
description = "Python SDK for driving Hapbeat haptic devices over Wi-Fi UDP"
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
requires-python = ">=3.10"
|
|
@@ -5,7 +5,7 @@ Quick start::
|
|
|
5
5
|
import hapbeat
|
|
6
6
|
|
|
7
7
|
hb = hapbeat.connect(app_name="MyExperiment")
|
|
8
|
-
hb.play("
|
|
8
|
+
hb.play("sample-kit.sine_100hz", gain=0.3)
|
|
9
9
|
hb.stop_all()
|
|
10
10
|
hb.close()
|
|
11
11
|
|
|
@@ -17,9 +17,15 @@ from __future__ import annotations
|
|
|
17
17
|
|
|
18
18
|
from . import protocol
|
|
19
19
|
from .client import DEFAULT_BROADCAST, DEFAULT_PORT, UdpClient
|
|
20
|
-
from .clip import ClipStreamer
|
|
21
20
|
from .eventmap import EventDef, EventMap
|
|
22
21
|
from .hapbeat import Device, Hapbeat, connect
|
|
22
|
+
from .stream import (
|
|
23
|
+
DeferredReason,
|
|
24
|
+
PlaybackStatus,
|
|
25
|
+
StreamEndpoint,
|
|
26
|
+
StreamHub,
|
|
27
|
+
StreamPlayback,
|
|
28
|
+
)
|
|
23
29
|
from .wav import WavPcm, read_wav_pcm16
|
|
24
30
|
|
|
25
31
|
try: # populated from installed package metadata
|
|
@@ -28,9 +34,9 @@ try: # populated from installed package metadata
|
|
|
28
34
|
try:
|
|
29
35
|
__version__ = version("hapbeat-python-sdk")
|
|
30
36
|
except PackageNotFoundError: # running from a source checkout
|
|
31
|
-
__version__ = "0.
|
|
37
|
+
__version__ = "0.3.0+local"
|
|
32
38
|
except ImportError: # pragma: no cover
|
|
33
|
-
__version__ = "0.
|
|
39
|
+
__version__ = "0.3.0+local"
|
|
34
40
|
|
|
35
41
|
__all__ = [
|
|
36
42
|
"connect",
|
|
@@ -38,7 +44,11 @@ __all__ = [
|
|
|
38
44
|
"Device",
|
|
39
45
|
"EventMap",
|
|
40
46
|
"EventDef",
|
|
41
|
-
"
|
|
47
|
+
"StreamHub",
|
|
48
|
+
"StreamPlayback",
|
|
49
|
+
"StreamEndpoint",
|
|
50
|
+
"PlaybackStatus",
|
|
51
|
+
"DeferredReason",
|
|
42
52
|
"WavPcm",
|
|
43
53
|
"read_wav_pcm16",
|
|
44
54
|
"UdpClient",
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
Examples::
|
|
4
4
|
|
|
5
5
|
hapbeat scan
|
|
6
|
-
hapbeat play
|
|
7
|
-
hapbeat stop
|
|
6
|
+
hapbeat play sample-kit.sine_100hz --gain 0.3
|
|
7
|
+
hapbeat stop sample-kit.sine_100hz
|
|
8
8
|
hapbeat stop-all
|
|
9
9
|
hapbeat osc-bridge --listen 7702
|
|
10
10
|
hapbeat launchpad
|
|
@@ -33,6 +33,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
33
33
|
p_play = sub.add_parser("play", help="play an event by id")
|
|
34
34
|
p_play.add_argument("event_id")
|
|
35
35
|
p_play.add_argument("--gain", type=float, default=None, help="0..1 (default: kit baseline)")
|
|
36
|
+
p_play.add_argument("--pan", type=float, default=0.0,
|
|
37
|
+
help="-1 left .. 0 centre .. +1 right (default 0)")
|
|
36
38
|
_add_common(p_play)
|
|
37
39
|
|
|
38
40
|
p_stop = sub.add_parser("stop", help="stop one event id")
|
|
@@ -72,6 +74,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
72
74
|
def main(argv: list[str] | None = None) -> int:
|
|
73
75
|
args = build_parser().parse_args(argv)
|
|
74
76
|
|
|
77
|
+
# 新しい版が出ていれば stderr に 1 行だけ (同じ版では 2 回目を出さない)。
|
|
78
|
+
# キャッシュが無ければ裏で取りに行くだけでコマンドは待たせない。
|
|
79
|
+
# ライブラリ import 側では絶対に呼ばない — CLI 実行時のみ (DEC-053 §5.4)。
|
|
80
|
+
from . import update_check
|
|
81
|
+
update_check.notify_cli(__version__)
|
|
82
|
+
|
|
75
83
|
if args.command == "osc-bridge":
|
|
76
84
|
from .osc import OscBridge
|
|
77
85
|
|
|
@@ -100,9 +108,19 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
100
108
|
|
|
101
109
|
hb = connect(port=args.port, default_target=getattr(args, "target", ""), keepalive=False)
|
|
102
110
|
try:
|
|
111
|
+
if args.command in ("play", "stop", "stop-all"):
|
|
112
|
+
# Discover before sending. Without a PONG the SDK knows no devices,
|
|
113
|
+
# so the command would go out as a broadcast — which the AP holds
|
|
114
|
+
# back until the next DTIM beacon, and which on a multi-homed host
|
|
115
|
+
# may leave through an interface with no Hapbeat behind it. One
|
|
116
|
+
# reply is enough to pick the subnet and unicast, and it normally
|
|
117
|
+
# arrives in a few milliseconds; the wait only costs the full
|
|
118
|
+
# window when nothing is there to answer.
|
|
119
|
+
hb.discover(timeout=0.4, stop_early=True)
|
|
120
|
+
|
|
103
121
|
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'}")
|
|
122
|
+
ok = hb.play(args.event_id, args.gain, pan=args.pan)
|
|
123
|
+
print(f"play {args.event_id} gain={args.gain if args.gain is not None else 'baseline'} pan={args.pan} -> {'sent' if ok else 'FAILED'}")
|
|
106
124
|
elif args.command == "stop":
|
|
107
125
|
print("sent" if hb.stop(args.event_id) else "FAILED")
|
|
108
126
|
elif args.command == "stop-all":
|