bhaptics-http 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.
- bhaptics_http-0.1.0/.gitignore +13 -0
- bhaptics_http-0.1.0/LICENSE +21 -0
- bhaptics_http-0.1.0/PKG-INFO +294 -0
- bhaptics_http-0.1.0/README.md +242 -0
- bhaptics_http-0.1.0/examples/client.js +123 -0
- bhaptics_http-0.1.0/examples/client_python.py +58 -0
- bhaptics_http-0.1.0/pyproject.toml +61 -0
- bhaptics_http-0.1.0/src/bhaptics_http/__init__.py +30 -0
- bhaptics_http-0.1.0/src/bhaptics_http/__main__.py +75 -0
- bhaptics_http-0.1.0/src/bhaptics_http/server.py +313 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MissCrispenCakes
|
|
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,294 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bhaptics-http
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: HTTP REST bridge for bHaptics haptic hardware — raw dot patterns, no Studio required
|
|
5
|
+
Project-URL: Homepage, https://github.com/MissCrispenCakes/bhaptics-http
|
|
6
|
+
Project-URL: Documentation, https://github.com/MissCrispenCakes/bhaptics-http#readme
|
|
7
|
+
Project-URL: Issues, https://github.com/MissCrispenCakes/bhaptics-http/issues
|
|
8
|
+
Author: MissCrispenCakes
|
|
9
|
+
License: MIT License
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2026 MissCrispenCakes
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Keywords: TactSuit,VR,WSL2,XR,api,bhaptics,bridge,haptics,rest,tactile,wearables
|
|
32
|
+
Classifier: Development Status :: 4 - Beta
|
|
33
|
+
Classifier: Intended Audience :: Developers
|
|
34
|
+
Classifier: Intended Audience :: Science/Research
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
37
|
+
Classifier: Programming Language :: Python :: 3
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
42
|
+
Classifier: Topic :: Scientific/Engineering :: Human Machine Interfaces
|
|
43
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
44
|
+
Requires-Python: >=3.9
|
|
45
|
+
Requires-Dist: aiohttp>=3.9.0
|
|
46
|
+
Requires-Dist: bhaptics-python>=0.2.0
|
|
47
|
+
Provides-Extra: dev
|
|
48
|
+
Requires-Dist: aiohttp[speedups]; extra == 'dev'
|
|
49
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
50
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
51
|
+
Description-Content-Type: text/markdown
|
|
52
|
+
|
|
53
|
+
# bhaptics-http
|
|
54
|
+
|
|
55
|
+
**HTTP REST bridge for bHaptics haptic hardware.**
|
|
56
|
+
|
|
57
|
+
Wraps the [`bhaptics-python`](https://github.com/bhaptics/bhaptics-python) SDK in a lightweight [aiohttp](https://docs.aiohttp.org/) server, exposing a language-agnostic REST API so that **any language or environment** can drive bHaptics vests, arm bands, gloves, and other devices over plain HTTP.
|
|
58
|
+
|
|
59
|
+
### Why?
|
|
60
|
+
|
|
61
|
+
bHaptics hardware is controlled through **bHaptics Player** (a Windows desktop app). The official SDK options are:
|
|
62
|
+
|
|
63
|
+
| SDK | Works in | Raw dot patterns? |
|
|
64
|
+
|-----|----------|------------------|
|
|
65
|
+
| tact-js | Browser only (WASM) | No (needs Studio) |
|
|
66
|
+
| bhaptics-python | Windows Python | Yes — but no HTTP |
|
|
67
|
+
| bHaptics REST API | Via Player | No raw dot control |
|
|
68
|
+
|
|
69
|
+
**bhaptics-http fills the gap:** run it once on Windows alongside bHaptics Player, then call it from Node.js, WSL2, Docker, another machine — or any `curl` command.
|
|
70
|
+
|
|
71
|
+
The key feature is `/haptic/dot`: raw per-motor intensity control with **no bHaptics Studio pre-registration required**.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Install
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pip install bhaptics-http
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
> **Windows only** — must run on the same machine as bHaptics Player.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Quick start
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# Start bHaptics Player, then:
|
|
89
|
+
python -m bhaptics_http
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
bhaptics-http starting on 0.0.0.0:15883 (appId=BHapticsHTTP)
|
|
94
|
+
Endpoints:
|
|
95
|
+
GET http://0.0.0.0:15883/health
|
|
96
|
+
POST http://0.0.0.0:15883/haptic
|
|
97
|
+
POST http://0.0.0.0:15883/haptic/dot
|
|
98
|
+
POST http://0.0.0.0:15883/haptic/stop
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Test it:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
curl http://localhost:15883/health
|
|
105
|
+
# {"ok": true, "backend": "bhaptics-python", "player": true}
|
|
106
|
+
|
|
107
|
+
curl -X POST http://localhost:15883/haptic/dot \
|
|
108
|
+
-H 'Content-Type: application/json' \
|
|
109
|
+
-d '{"deviceType":0,"duration":300,"motors":[{"index":0,"intensity":100},{"index":1,"intensity":100}]}'
|
|
110
|
+
# {"ok": true}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## REST API
|
|
116
|
+
|
|
117
|
+
### `GET /health`
|
|
118
|
+
|
|
119
|
+
Check connection to bHaptics Player.
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{"ok": true, "backend": "bhaptics-python", "player": true}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
### `POST /haptic`
|
|
128
|
+
|
|
129
|
+
Play a named pattern registered in bHaptics Studio.
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{ "event": "HeartBeat", "deviceIndex": 0 }
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
### `POST /haptic/dot`
|
|
138
|
+
|
|
139
|
+
**Play raw per-motor intensities — no Studio required.**
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
{
|
|
143
|
+
"deviceType": 0,
|
|
144
|
+
"duration": 300,
|
|
145
|
+
"motors": [
|
|
146
|
+
{"index": 0, "intensity": 100},
|
|
147
|
+
{"index": 4, "intensity": 60}
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Device type reference:**
|
|
153
|
+
|
|
154
|
+
| deviceType | Device |
|
|
155
|
+
|-----------|--------|
|
|
156
|
+
| 0 | TactSuit X16 / X40 (chest vest) |
|
|
157
|
+
| 1 | Tactosy2 left arm |
|
|
158
|
+
| 2 | Tactosy2 right arm |
|
|
159
|
+
| 3 | TactVisor head |
|
|
160
|
+
| 6 | Tactosy feet left |
|
|
161
|
+
| 7 | Tactosy feet right |
|
|
162
|
+
| 8 | TactGlove left |
|
|
163
|
+
| 9 | TactGlove right |
|
|
164
|
+
|
|
165
|
+
`intensity` range: `0`–`100`
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
### `POST /haptic/stop`
|
|
170
|
+
|
|
171
|
+
Stop all currently playing haptic patterns.
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{"ok": true}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Configuration
|
|
180
|
+
|
|
181
|
+
| Environment variable | Default | Description |
|
|
182
|
+
|---------------------|---------|-------------|
|
|
183
|
+
| `BHAPTICS_HTTP_PORT` | `15883` | Port to listen on |
|
|
184
|
+
| `BHAPTICS_APP_ID` | `BHapticsHTTP` | App ID for bHaptics Player auth |
|
|
185
|
+
| `BHAPTICS_API_KEY` | `""` | API key (usually empty for local Player) |
|
|
186
|
+
|
|
187
|
+
Or place a `tact-config.json` next to the server:
|
|
188
|
+
|
|
189
|
+
```json
|
|
190
|
+
{ "appId": "MyApp", "apiKey": "" }
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## WSL2 / cross-machine setup
|
|
196
|
+
|
|
197
|
+
Run the server on **Windows**, then call it from WSL2 or another machine.
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
# Find your Windows host IP from WSL2:
|
|
201
|
+
cat /etc/resolv.conf | grep nameserver
|
|
202
|
+
# nameserver 172.22.112.1
|
|
203
|
+
|
|
204
|
+
# Then call:
|
|
205
|
+
curl http://172.22.112.1:15883/health
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Windows Firewall:** you may need to allow inbound TCP on port 15883:
|
|
209
|
+
|
|
210
|
+
```powershell
|
|
211
|
+
# PowerShell (as Administrator):
|
|
212
|
+
New-NetFirewallRule -DisplayName "bhaptics-http" -Direction Inbound `
|
|
213
|
+
-Protocol TCP -LocalPort 15883 -Action Allow
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Or simply run the server and the client on the same Windows machine.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Node.js example
|
|
221
|
+
|
|
222
|
+
```js
|
|
223
|
+
const BASE = "http://localhost:15883";
|
|
224
|
+
|
|
225
|
+
// Raw dot pattern — vest upper row
|
|
226
|
+
await fetch(`${BASE}/haptic/dot`, {
|
|
227
|
+
method: "POST",
|
|
228
|
+
headers: { "Content-Type": "application/json" },
|
|
229
|
+
body: JSON.stringify({
|
|
230
|
+
deviceType: 0,
|
|
231
|
+
duration: 300,
|
|
232
|
+
motors: [
|
|
233
|
+
{ index: 0, intensity: 100 },
|
|
234
|
+
{ index: 1, intensity: 100 },
|
|
235
|
+
{ index: 2, intensity: 100 },
|
|
236
|
+
{ index: 3, intensity: 100 },
|
|
237
|
+
],
|
|
238
|
+
}),
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
See [`examples/client.js`](examples/client.js) for a full demo.
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Python library usage
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
from aiohttp import web
|
|
250
|
+
from bhaptics_http.server import make_app, load_config
|
|
251
|
+
|
|
252
|
+
app_id, api_key = load_config() # reads env / tact-config.json
|
|
253
|
+
app = make_app(app_id, api_key)
|
|
254
|
+
web.run_app(app, port=15883)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## CLI options
|
|
260
|
+
|
|
261
|
+
```
|
|
262
|
+
python -m bhaptics_http --help
|
|
263
|
+
|
|
264
|
+
options:
|
|
265
|
+
--port PORT Port (default: 15883, env: BHAPTICS_HTTP_PORT)
|
|
266
|
+
--host HOST Bind interface (default: 0.0.0.0)
|
|
267
|
+
--config PATH Path to tact-config.json
|
|
268
|
+
--app-id ID bHaptics appId
|
|
269
|
+
--api-key KEY bHaptics apiKey
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## Citation
|
|
275
|
+
|
|
276
|
+
If you use this in academic work, please cite via Zenodo:
|
|
277
|
+
|
|
278
|
+
> Vollmer, S.C. (2026). *bhaptics-http: HTTP REST bridge for bHaptics haptic hardware* [Software]. Zenodo. https://doi.org/XXXX/zenodo.XXXXXXX
|
|
279
|
+
|
|
280
|
+
_(DOI will be updated after Zenodo deposit.)_
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Related
|
|
285
|
+
|
|
286
|
+
- [bhaptics-python](https://github.com/bhaptics/bhaptics-python) — official Python SDK (this package wraps it)
|
|
287
|
+
- [bHaptics Developer Portal](https://developer.bhaptics.com/)
|
|
288
|
+
- [VR Ecology Project](https://github.com/MissCrispenCakes/VR_ECOLOGY_PROJECT) — research context where this was first developed
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## License
|
|
293
|
+
|
|
294
|
+
MIT © MissCrispenCakes
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# bhaptics-http
|
|
2
|
+
|
|
3
|
+
**HTTP REST bridge for bHaptics haptic hardware.**
|
|
4
|
+
|
|
5
|
+
Wraps the [`bhaptics-python`](https://github.com/bhaptics/bhaptics-python) SDK in a lightweight [aiohttp](https://docs.aiohttp.org/) server, exposing a language-agnostic REST API so that **any language or environment** can drive bHaptics vests, arm bands, gloves, and other devices over plain HTTP.
|
|
6
|
+
|
|
7
|
+
### Why?
|
|
8
|
+
|
|
9
|
+
bHaptics hardware is controlled through **bHaptics Player** (a Windows desktop app). The official SDK options are:
|
|
10
|
+
|
|
11
|
+
| SDK | Works in | Raw dot patterns? |
|
|
12
|
+
|-----|----------|------------------|
|
|
13
|
+
| tact-js | Browser only (WASM) | No (needs Studio) |
|
|
14
|
+
| bhaptics-python | Windows Python | Yes — but no HTTP |
|
|
15
|
+
| bHaptics REST API | Via Player | No raw dot control |
|
|
16
|
+
|
|
17
|
+
**bhaptics-http fills the gap:** run it once on Windows alongside bHaptics Player, then call it from Node.js, WSL2, Docker, another machine — or any `curl` command.
|
|
18
|
+
|
|
19
|
+
The key feature is `/haptic/dot`: raw per-motor intensity control with **no bHaptics Studio pre-registration required**.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install bhaptics-http
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
> **Windows only** — must run on the same machine as bHaptics Player.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# Start bHaptics Player, then:
|
|
37
|
+
python -m bhaptics_http
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
bhaptics-http starting on 0.0.0.0:15883 (appId=BHapticsHTTP)
|
|
42
|
+
Endpoints:
|
|
43
|
+
GET http://0.0.0.0:15883/health
|
|
44
|
+
POST http://0.0.0.0:15883/haptic
|
|
45
|
+
POST http://0.0.0.0:15883/haptic/dot
|
|
46
|
+
POST http://0.0.0.0:15883/haptic/stop
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Test it:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
curl http://localhost:15883/health
|
|
53
|
+
# {"ok": true, "backend": "bhaptics-python", "player": true}
|
|
54
|
+
|
|
55
|
+
curl -X POST http://localhost:15883/haptic/dot \
|
|
56
|
+
-H 'Content-Type: application/json' \
|
|
57
|
+
-d '{"deviceType":0,"duration":300,"motors":[{"index":0,"intensity":100},{"index":1,"intensity":100}]}'
|
|
58
|
+
# {"ok": true}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## REST API
|
|
64
|
+
|
|
65
|
+
### `GET /health`
|
|
66
|
+
|
|
67
|
+
Check connection to bHaptics Player.
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{"ok": true, "backend": "bhaptics-python", "player": true}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
### `POST /haptic`
|
|
76
|
+
|
|
77
|
+
Play a named pattern registered in bHaptics Studio.
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{ "event": "HeartBeat", "deviceIndex": 0 }
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
### `POST /haptic/dot`
|
|
86
|
+
|
|
87
|
+
**Play raw per-motor intensities — no Studio required.**
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"deviceType": 0,
|
|
92
|
+
"duration": 300,
|
|
93
|
+
"motors": [
|
|
94
|
+
{"index": 0, "intensity": 100},
|
|
95
|
+
{"index": 4, "intensity": 60}
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Device type reference:**
|
|
101
|
+
|
|
102
|
+
| deviceType | Device |
|
|
103
|
+
|-----------|--------|
|
|
104
|
+
| 0 | TactSuit X16 / X40 (chest vest) |
|
|
105
|
+
| 1 | Tactosy2 left arm |
|
|
106
|
+
| 2 | Tactosy2 right arm |
|
|
107
|
+
| 3 | TactVisor head |
|
|
108
|
+
| 6 | Tactosy feet left |
|
|
109
|
+
| 7 | Tactosy feet right |
|
|
110
|
+
| 8 | TactGlove left |
|
|
111
|
+
| 9 | TactGlove right |
|
|
112
|
+
|
|
113
|
+
`intensity` range: `0`–`100`
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
### `POST /haptic/stop`
|
|
118
|
+
|
|
119
|
+
Stop all currently playing haptic patterns.
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{"ok": true}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Configuration
|
|
128
|
+
|
|
129
|
+
| Environment variable | Default | Description |
|
|
130
|
+
|---------------------|---------|-------------|
|
|
131
|
+
| `BHAPTICS_HTTP_PORT` | `15883` | Port to listen on |
|
|
132
|
+
| `BHAPTICS_APP_ID` | `BHapticsHTTP` | App ID for bHaptics Player auth |
|
|
133
|
+
| `BHAPTICS_API_KEY` | `""` | API key (usually empty for local Player) |
|
|
134
|
+
|
|
135
|
+
Or place a `tact-config.json` next to the server:
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{ "appId": "MyApp", "apiKey": "" }
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## WSL2 / cross-machine setup
|
|
144
|
+
|
|
145
|
+
Run the server on **Windows**, then call it from WSL2 or another machine.
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
# Find your Windows host IP from WSL2:
|
|
149
|
+
cat /etc/resolv.conf | grep nameserver
|
|
150
|
+
# nameserver 172.22.112.1
|
|
151
|
+
|
|
152
|
+
# Then call:
|
|
153
|
+
curl http://172.22.112.1:15883/health
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Windows Firewall:** you may need to allow inbound TCP on port 15883:
|
|
157
|
+
|
|
158
|
+
```powershell
|
|
159
|
+
# PowerShell (as Administrator):
|
|
160
|
+
New-NetFirewallRule -DisplayName "bhaptics-http" -Direction Inbound `
|
|
161
|
+
-Protocol TCP -LocalPort 15883 -Action Allow
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Or simply run the server and the client on the same Windows machine.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Node.js example
|
|
169
|
+
|
|
170
|
+
```js
|
|
171
|
+
const BASE = "http://localhost:15883";
|
|
172
|
+
|
|
173
|
+
// Raw dot pattern — vest upper row
|
|
174
|
+
await fetch(`${BASE}/haptic/dot`, {
|
|
175
|
+
method: "POST",
|
|
176
|
+
headers: { "Content-Type": "application/json" },
|
|
177
|
+
body: JSON.stringify({
|
|
178
|
+
deviceType: 0,
|
|
179
|
+
duration: 300,
|
|
180
|
+
motors: [
|
|
181
|
+
{ index: 0, intensity: 100 },
|
|
182
|
+
{ index: 1, intensity: 100 },
|
|
183
|
+
{ index: 2, intensity: 100 },
|
|
184
|
+
{ index: 3, intensity: 100 },
|
|
185
|
+
],
|
|
186
|
+
}),
|
|
187
|
+
});
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
See [`examples/client.js`](examples/client.js) for a full demo.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Python library usage
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
from aiohttp import web
|
|
198
|
+
from bhaptics_http.server import make_app, load_config
|
|
199
|
+
|
|
200
|
+
app_id, api_key = load_config() # reads env / tact-config.json
|
|
201
|
+
app = make_app(app_id, api_key)
|
|
202
|
+
web.run_app(app, port=15883)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## CLI options
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
python -m bhaptics_http --help
|
|
211
|
+
|
|
212
|
+
options:
|
|
213
|
+
--port PORT Port (default: 15883, env: BHAPTICS_HTTP_PORT)
|
|
214
|
+
--host HOST Bind interface (default: 0.0.0.0)
|
|
215
|
+
--config PATH Path to tact-config.json
|
|
216
|
+
--app-id ID bHaptics appId
|
|
217
|
+
--api-key KEY bHaptics apiKey
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Citation
|
|
223
|
+
|
|
224
|
+
If you use this in academic work, please cite via Zenodo:
|
|
225
|
+
|
|
226
|
+
> Vollmer, S.C. (2026). *bhaptics-http: HTTP REST bridge for bHaptics haptic hardware* [Software]. Zenodo. https://doi.org/XXXX/zenodo.XXXXXXX
|
|
227
|
+
|
|
228
|
+
_(DOI will be updated after Zenodo deposit.)_
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Related
|
|
233
|
+
|
|
234
|
+
- [bhaptics-python](https://github.com/bhaptics/bhaptics-python) — official Python SDK (this package wraps it)
|
|
235
|
+
- [bHaptics Developer Portal](https://developer.bhaptics.com/)
|
|
236
|
+
- [VR Ecology Project](https://github.com/MissCrispenCakes/VR_ECOLOGY_PROJECT) — research context where this was first developed
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## License
|
|
241
|
+
|
|
242
|
+
MIT © MissCrispenCakes
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bhaptics-http — Node.js client example
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates how to drive bHaptics hardware from Node.js (or any
|
|
5
|
+
* environment with fetch) once bhaptics-http is running on Windows.
|
|
6
|
+
*
|
|
7
|
+
* Setup:
|
|
8
|
+
* 1. On Windows: pip install bhaptics-http && python -m bhaptics_http
|
|
9
|
+
* 2. On WSL2/Linux/Mac: set HOST to your Windows IP (see WSL2 note below)
|
|
10
|
+
* 3. node examples/client.js
|
|
11
|
+
*
|
|
12
|
+
* WSL2 note:
|
|
13
|
+
* Your Windows host IP is typically 172.x.x.1 — find it with:
|
|
14
|
+
* cat /etc/resolv.conf | grep nameserver
|
|
15
|
+
* Set HOST below (or export BHAPTICS_HTTP_HOST=172.x.x.1 in your shell).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const HOST = process.env.BHAPTICS_HTTP_HOST ?? "localhost";
|
|
19
|
+
const PORT = process.env.BHAPTICS_HTTP_PORT ?? "15883";
|
|
20
|
+
const BASE = `http://${HOST}:${PORT}`;
|
|
21
|
+
|
|
22
|
+
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
async function health() {
|
|
25
|
+
const res = await fetch(`${BASE}/health`);
|
|
26
|
+
return res.json();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Play a named Studio event (must be registered in bHaptics Studio first).
|
|
31
|
+
* @param {string} event Pattern name as registered in Studio
|
|
32
|
+
* @param {number} deviceIndex 0 = first device of this type
|
|
33
|
+
*/
|
|
34
|
+
async function playEvent(event, deviceIndex = 0) {
|
|
35
|
+
const res = await fetch(`${BASE}/haptic`, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: { "Content-Type": "application/json" },
|
|
38
|
+
body: JSON.stringify({ event, deviceIndex }),
|
|
39
|
+
});
|
|
40
|
+
return res.json();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Play raw motor intensities — no Studio pre-registration required.
|
|
45
|
+
*
|
|
46
|
+
* @param {number} deviceType Device type ID (see device table below)
|
|
47
|
+
* @param {number} duration Duration in milliseconds
|
|
48
|
+
* @param {Array} motors Array of {index, intensity} objects
|
|
49
|
+
*
|
|
50
|
+
* Device types:
|
|
51
|
+
* 0 = TactSuit X16/X40 (chest vest), 16 or 40 motors
|
|
52
|
+
* 1 = Tactosy2 left arm, 6 motors
|
|
53
|
+
* 2 = Tactosy2 right arm, 6 motors
|
|
54
|
+
* 3 = TactVisor head, 4 motors
|
|
55
|
+
* 6 = Tactosy feet left, 3 motors
|
|
56
|
+
* 7 = Tactosy feet right, 3 motors
|
|
57
|
+
* 8 = TactGlove left, 6 motors
|
|
58
|
+
* 9 = TactGlove right, 6 motors
|
|
59
|
+
*/
|
|
60
|
+
async function playDot(deviceType, duration, motors) {
|
|
61
|
+
const res = await fetch(`${BASE}/haptic/dot`, {
|
|
62
|
+
method: "POST",
|
|
63
|
+
headers: { "Content-Type": "application/json" },
|
|
64
|
+
body: JSON.stringify({ deviceType, duration, motors }),
|
|
65
|
+
});
|
|
66
|
+
return res.json();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Stop all playing patterns. */
|
|
70
|
+
async function stopAll() {
|
|
71
|
+
const res = await fetch(`${BASE}/haptic/stop`, { method: "POST" });
|
|
72
|
+
return res.json();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── demo ─────────────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
async function demo() {
|
|
78
|
+
console.log("1. Health check …");
|
|
79
|
+
const h = await health();
|
|
80
|
+
console.log(" ", h);
|
|
81
|
+
|
|
82
|
+
if (!h.ok) {
|
|
83
|
+
console.error("bHaptics Player not connected. Is it running on Windows?");
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// -- Raw dot pattern: vest upper-front row (motors 0-3), full intensity
|
|
88
|
+
console.log("2. Vest upper-front row — 300 ms …");
|
|
89
|
+
await playDot(0, 300, [
|
|
90
|
+
{ index: 0, intensity: 100 },
|
|
91
|
+
{ index: 1, intensity: 100 },
|
|
92
|
+
{ index: 2, intensity: 100 },
|
|
93
|
+
{ index: 3, intensity: 100 },
|
|
94
|
+
]);
|
|
95
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
96
|
+
|
|
97
|
+
// -- Raw dot pattern: left arm full, then right arm full
|
|
98
|
+
console.log("3. Left arm — 200 ms …");
|
|
99
|
+
await playDot(1, 200, [
|
|
100
|
+
{ index: 0, intensity: 80 },
|
|
101
|
+
{ index: 1, intensity: 80 },
|
|
102
|
+
{ index: 2, intensity: 80 },
|
|
103
|
+
]);
|
|
104
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
105
|
+
|
|
106
|
+
console.log("4. Right arm — 200 ms …");
|
|
107
|
+
await playDot(2, 200, [
|
|
108
|
+
{ index: 0, intensity: 80 },
|
|
109
|
+
{ index: 1, intensity: 80 },
|
|
110
|
+
{ index: 2, intensity: 80 },
|
|
111
|
+
]);
|
|
112
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
113
|
+
|
|
114
|
+
// -- Named Studio event (uncomment if you have a pattern registered)
|
|
115
|
+
// console.log("5. Named event …");
|
|
116
|
+
// await playEvent("HeartBeat");
|
|
117
|
+
|
|
118
|
+
console.log("5. Stop all.");
|
|
119
|
+
await stopAll();
|
|
120
|
+
console.log("Done.");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
demo().catch(console.error);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
bhaptics-http — Python client example (requests)
|
|
3
|
+
|
|
4
|
+
Shows how to call the server from a separate Python script
|
|
5
|
+
(e.g. from WSL2 or another machine).
|
|
6
|
+
|
|
7
|
+
Setup:
|
|
8
|
+
pip install requests
|
|
9
|
+
# On Windows: python -m bhaptics_http
|
|
10
|
+
# Then: python examples/client_python.py
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import time
|
|
15
|
+
import requests
|
|
16
|
+
|
|
17
|
+
HOST = os.environ.get("BHAPTICS_HTTP_HOST", "localhost")
|
|
18
|
+
PORT = os.environ.get("BHAPTICS_HTTP_PORT", "15883")
|
|
19
|
+
BASE = f"http://{HOST}:{PORT}"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def health():
|
|
23
|
+
return requests.get(f"{BASE}/health").json()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def play_dot(device_type: int, duration: int, motors: list[dict]) -> dict:
|
|
27
|
+
return requests.post(
|
|
28
|
+
f"{BASE}/haptic/dot",
|
|
29
|
+
json={"deviceType": device_type, "duration": duration, "motors": motors},
|
|
30
|
+
).json()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def play_event(event: str, device_index: int = 0) -> dict:
|
|
34
|
+
return requests.post(
|
|
35
|
+
f"{BASE}/haptic",
|
|
36
|
+
json={"event": event, "deviceIndex": device_index},
|
|
37
|
+
).json()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def stop_all() -> dict:
|
|
41
|
+
return requests.post(f"{BASE}/haptic/stop").json()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
if __name__ == "__main__":
|
|
45
|
+
print("Health:", health())
|
|
46
|
+
|
|
47
|
+
print("Left arm sweep …")
|
|
48
|
+
for i in range(6):
|
|
49
|
+
play_dot(1, 80, [{"index": i, "intensity": 100}])
|
|
50
|
+
time.sleep(0.1)
|
|
51
|
+
|
|
52
|
+
print("Right arm sweep …")
|
|
53
|
+
for i in range(6):
|
|
54
|
+
play_dot(2, 80, [{"index": i, "intensity": 100}])
|
|
55
|
+
time.sleep(0.1)
|
|
56
|
+
|
|
57
|
+
print("Stop all.")
|
|
58
|
+
stop_all()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "bhaptics-http"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "HTTP REST bridge for bHaptics haptic hardware — raw dot patterns, no Studio required"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
authors = [{ name = "MissCrispenCakes" }]
|
|
12
|
+
keywords = [
|
|
13
|
+
"bhaptics", "haptics", "tactile", "XR", "VR", "wearables",
|
|
14
|
+
"TactSuit", "rest", "api", "bridge", "WSL2"
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Intended Audience :: Science/Research",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Operating System :: Microsoft :: Windows",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.9",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Topic :: Scientific/Engineering :: Human Machine Interfaces",
|
|
28
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
29
|
+
]
|
|
30
|
+
requires-python = ">=3.9"
|
|
31
|
+
dependencies = [
|
|
32
|
+
"bhaptics-python>=0.2.0",
|
|
33
|
+
"aiohttp>=3.9.0",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
dev = [
|
|
38
|
+
"pytest>=7",
|
|
39
|
+
"pytest-asyncio>=0.23",
|
|
40
|
+
"aiohttp[speedups]",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Homepage = "https://github.com/MissCrispenCakes/bhaptics-http"
|
|
45
|
+
Documentation = "https://github.com/MissCrispenCakes/bhaptics-http#readme"
|
|
46
|
+
Issues = "https://github.com/MissCrispenCakes/bhaptics-http/issues"
|
|
47
|
+
|
|
48
|
+
[project.scripts]
|
|
49
|
+
bhaptics-http = "bhaptics_http.__main__:main"
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.wheel]
|
|
52
|
+
packages = ["src/bhaptics_http"]
|
|
53
|
+
|
|
54
|
+
[tool.hatch.build.targets.sdist]
|
|
55
|
+
include = [
|
|
56
|
+
"src/",
|
|
57
|
+
"examples/",
|
|
58
|
+
"README.md",
|
|
59
|
+
"LICENSE",
|
|
60
|
+
"pyproject.toml",
|
|
61
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
bhaptics-http
|
|
3
|
+
~~~~~~~~~~~~~
|
|
4
|
+
|
|
5
|
+
HTTP REST bridge for the bHaptics haptic hardware ecosystem.
|
|
6
|
+
|
|
7
|
+
Wraps the bhaptics-python SDK in a lightweight aiohttp server so that
|
|
8
|
+
any language or environment (Node.js, browser fetch, WSL2, Docker, …)
|
|
9
|
+
can drive bHaptics vests, arm bands, gloves, and other devices over
|
|
10
|
+
plain HTTP — including raw per-motor dot patterns that require no
|
|
11
|
+
bHaptics Studio pre-registration.
|
|
12
|
+
|
|
13
|
+
Quick start:
|
|
14
|
+
pip install bhaptics-http
|
|
15
|
+
python -m bhaptics_http # starts on port 15883
|
|
16
|
+
|
|
17
|
+
# then from anywhere:
|
|
18
|
+
curl http://localhost:15883/health
|
|
19
|
+
curl -X POST http://localhost:15883/haptic/dot \\
|
|
20
|
+
-H 'Content-Type: application/json' \\
|
|
21
|
+
-d '{"deviceType":0,"duration":200,"motors":[{"index":0,"intensity":100}]}'
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
__version__ = "0.1.0"
|
|
25
|
+
__author__ = "MissCrispenCakes"
|
|
26
|
+
__license__ = "MIT"
|
|
27
|
+
|
|
28
|
+
from bhaptics_http.server import make_app, load_config, init_bhaptics
|
|
29
|
+
|
|
30
|
+
__all__ = ["make_app", "load_config", "init_bhaptics", "__version__"]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Entry point for: python -m bhaptics_http
|
|
3
|
+
|
|
4
|
+
Starts the bhaptics-http REST server.
|
|
5
|
+
All configuration is via environment variables or tact-config.json.
|
|
6
|
+
See `python -m bhaptics_http --help` for options.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from aiohttp import web
|
|
17
|
+
|
|
18
|
+
from bhaptics_http.server import make_app, load_config
|
|
19
|
+
|
|
20
|
+
logging.basicConfig(level=logging.INFO, format="[%(levelname)s] %(message)s")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main() -> None:
|
|
24
|
+
parser = argparse.ArgumentParser(
|
|
25
|
+
prog="python -m bhaptics_http",
|
|
26
|
+
description="bHaptics HTTP REST bridge — drive haptic hardware over HTTP",
|
|
27
|
+
)
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"--port", "-p",
|
|
30
|
+
type=int,
|
|
31
|
+
default=int(os.environ.get("BHAPTICS_HTTP_PORT", "15883")),
|
|
32
|
+
help="Port to listen on (default: 15883, env: BHAPTICS_HTTP_PORT)",
|
|
33
|
+
)
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"--host",
|
|
36
|
+
default="0.0.0.0",
|
|
37
|
+
help="Host interface to bind (default: 0.0.0.0)",
|
|
38
|
+
)
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"--config",
|
|
41
|
+
type=Path,
|
|
42
|
+
default=None,
|
|
43
|
+
help="Path to tact-config.json (default: auto-detect)",
|
|
44
|
+
)
|
|
45
|
+
parser.add_argument(
|
|
46
|
+
"--app-id",
|
|
47
|
+
default=None,
|
|
48
|
+
help="bHaptics appId (overrides env / config file)",
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"--api-key",
|
|
52
|
+
default=None,
|
|
53
|
+
help="bHaptics apiKey (overrides env / config file)",
|
|
54
|
+
)
|
|
55
|
+
args = parser.parse_args()
|
|
56
|
+
|
|
57
|
+
app_id, api_key = load_config(args.config)
|
|
58
|
+
if args.app_id:
|
|
59
|
+
app_id = args.app_id
|
|
60
|
+
if args.api_key:
|
|
61
|
+
api_key = args.api_key
|
|
62
|
+
|
|
63
|
+
print(f"bhaptics-http starting on {args.host}:{args.port} (appId={app_id})")
|
|
64
|
+
print("Endpoints:")
|
|
65
|
+
print(f" GET http://{args.host}:{args.port}/health")
|
|
66
|
+
print(f" POST http://{args.host}:{args.port}/haptic")
|
|
67
|
+
print(f" POST http://{args.host}:{args.port}/haptic/dot")
|
|
68
|
+
print(f" POST http://{args.host}:{args.port}/haptic/stop")
|
|
69
|
+
|
|
70
|
+
app = make_app(app_id, api_key, config_path=args.config)
|
|
71
|
+
web.run_app(app, host=args.host, port=args.port)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
main()
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""
|
|
2
|
+
bhaptics_http.server
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
|
|
5
|
+
Lightweight aiohttp HTTP server that wraps the bhaptics-python SDK,
|
|
6
|
+
exposing a language-agnostic REST interface for bHaptics hardware.
|
|
7
|
+
|
|
8
|
+
Useful when:
|
|
9
|
+
- You are running in an environment where tact-js (browser WASM) is
|
|
10
|
+
unavailable (Node.js server, Python script, WSL2, Docker, CI).
|
|
11
|
+
- You want to drive raw dot/motor patterns without pre-registering
|
|
12
|
+
.tact files in bHaptics Studio.
|
|
13
|
+
- You need a single Windows-side process that any language can call
|
|
14
|
+
over HTTP.
|
|
15
|
+
|
|
16
|
+
Usage (standalone):
|
|
17
|
+
pip install bhaptics-http
|
|
18
|
+
python -m bhaptics_http # default port 15883
|
|
19
|
+
python -m bhaptics_http --port 8080
|
|
20
|
+
|
|
21
|
+
Usage (library):
|
|
22
|
+
from bhaptics_http.server import make_app
|
|
23
|
+
app = make_app(app_id="MyApp", api_key="")
|
|
24
|
+
# pass to aiohttp.web.run_app(app, port=15883)
|
|
25
|
+
|
|
26
|
+
Environment variables:
|
|
27
|
+
BHAPTICS_APP_ID Override appId (default: from tact-config.json or "BHapticsHTTP")
|
|
28
|
+
BHAPTICS_API_KEY Override apiKey (default: from tact-config.json or "")
|
|
29
|
+
BHAPTICS_HTTP_PORT Port to listen on (default: 15883)
|
|
30
|
+
|
|
31
|
+
REST endpoints:
|
|
32
|
+
GET /health Connection status
|
|
33
|
+
POST /haptic Play a named Studio event
|
|
34
|
+
POST /haptic/dot Play raw motor array (no Studio required)
|
|
35
|
+
POST /haptic/stop Stop all haptics
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import asyncio
|
|
41
|
+
import json
|
|
42
|
+
import logging
|
|
43
|
+
import os
|
|
44
|
+
import sys
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
from typing import Optional
|
|
47
|
+
|
|
48
|
+
from aiohttp import web
|
|
49
|
+
|
|
50
|
+
# ── bhaptics-python import ────────────────────────────────────────────────────
|
|
51
|
+
try:
|
|
52
|
+
import bhaptics_python as bh
|
|
53
|
+
except ImportError:
|
|
54
|
+
print(
|
|
55
|
+
"[ERROR] bhaptics-python not installed.\n"
|
|
56
|
+
" Run: pip install bhaptics-python\n"
|
|
57
|
+
" Docs: https://github.com/bhaptics/bhaptics-python"
|
|
58
|
+
)
|
|
59
|
+
sys.exit(1)
|
|
60
|
+
|
|
61
|
+
log = logging.getLogger("bhaptics_http")
|
|
62
|
+
|
|
63
|
+
# ── Module-level credential cache ────────────────────────────────────────────
|
|
64
|
+
_app_id: Optional[str] = None
|
|
65
|
+
_api_key: Optional[str] = None
|
|
66
|
+
|
|
67
|
+
RECONNECT_INTERVAL = 5 # seconds between liveness checks
|
|
68
|
+
|
|
69
|
+
# ── Config helpers ────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
def load_config(config_path: Optional[Path] = None) -> tuple[str, str]:
|
|
72
|
+
"""
|
|
73
|
+
Resolve appId / apiKey from (in priority order):
|
|
74
|
+
1. Environment variables BHAPTICS_APP_ID / BHAPTICS_API_KEY
|
|
75
|
+
2. tact-config.json at *config_path* (if provided or auto-detected)
|
|
76
|
+
3. Built-in defaults ("BHapticsHTTP", "")
|
|
77
|
+
"""
|
|
78
|
+
app_id = os.environ.get("BHAPTICS_APP_ID")
|
|
79
|
+
api_key = os.environ.get("BHAPTICS_API_KEY")
|
|
80
|
+
|
|
81
|
+
if not app_id or not api_key:
|
|
82
|
+
if config_path is None:
|
|
83
|
+
# Look next to this file, then cwd
|
|
84
|
+
candidates = [
|
|
85
|
+
Path(__file__).parent / "tact-config.json",
|
|
86
|
+
Path.cwd() / "tact-config.json",
|
|
87
|
+
]
|
|
88
|
+
config_path = next((p for p in candidates if p.exists()), None)
|
|
89
|
+
|
|
90
|
+
if config_path and config_path.exists():
|
|
91
|
+
try:
|
|
92
|
+
cfg = json.loads(config_path.read_text())
|
|
93
|
+
app_id = app_id or cfg.get("appId", "BHapticsHTTP")
|
|
94
|
+
api_key = api_key or cfg.get("apiKey", "")
|
|
95
|
+
log.info(f"Loaded config from {config_path}")
|
|
96
|
+
except Exception as exc:
|
|
97
|
+
log.warning(f"Could not parse {config_path}: {exc} — using defaults")
|
|
98
|
+
|
|
99
|
+
return (app_id or "BHapticsHTTP"), (api_key or "")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ── SDK connection helpers ────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
def _is_connected() -> bool:
|
|
105
|
+
try:
|
|
106
|
+
return bool(bh.is_connected())
|
|
107
|
+
except Exception:
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _try_reconnect() -> bool:
|
|
112
|
+
"""Attempt reconnect; returns True on success."""
|
|
113
|
+
global _app_id, _api_key
|
|
114
|
+
try:
|
|
115
|
+
bh.retry_initialize()
|
|
116
|
+
if _is_connected():
|
|
117
|
+
log.info("[OK] Reconnected via retry_initialize()")
|
|
118
|
+
return True
|
|
119
|
+
bh.registry_and_initialize(_app_id, _api_key, "BHapticsHTTP")
|
|
120
|
+
ok = _is_connected()
|
|
121
|
+
if ok:
|
|
122
|
+
log.info("[OK] Reconnected via re-auth")
|
|
123
|
+
return ok
|
|
124
|
+
except Exception as exc:
|
|
125
|
+
log.warning(f"[WARN] Reconnect failed: {exc}")
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def init_bhaptics(app_id: str, api_key: str) -> bool:
|
|
130
|
+
"""Initialise the SDK; returns True if connected."""
|
|
131
|
+
global _app_id, _api_key
|
|
132
|
+
_app_id, _api_key = app_id, api_key
|
|
133
|
+
try:
|
|
134
|
+
bh.registry_and_initialize(app_id, api_key, "BHapticsHTTP")
|
|
135
|
+
if _is_connected():
|
|
136
|
+
log.info(f"[OK] bhaptics-python connected (appId={app_id})")
|
|
137
|
+
return True
|
|
138
|
+
log.warning("[WARN] Initialised but not yet connected — is bHaptics Player running?")
|
|
139
|
+
return False
|
|
140
|
+
except Exception as exc:
|
|
141
|
+
log.error(f"[ERROR] Init failed: {exc}")
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
async def reconnect_loop() -> None:
|
|
146
|
+
"""Background task: reconnect whenever the SDK loses the Player connection."""
|
|
147
|
+
was_connected = False
|
|
148
|
+
while True:
|
|
149
|
+
await asyncio.sleep(RECONNECT_INTERVAL)
|
|
150
|
+
try:
|
|
151
|
+
connected = _is_connected()
|
|
152
|
+
if not connected:
|
|
153
|
+
msg = "Lost connection" if was_connected else "Not connected"
|
|
154
|
+
log.info(f"[...] {msg} — attempting reconnect")
|
|
155
|
+
_try_reconnect()
|
|
156
|
+
was_connected = _is_connected()
|
|
157
|
+
else:
|
|
158
|
+
was_connected = True
|
|
159
|
+
except Exception as exc:
|
|
160
|
+
log.error(f"[ERROR] reconnect_loop: {exc}")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ── Route handlers ────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
async def handle_health(request: web.Request) -> web.Response:
|
|
166
|
+
"""
|
|
167
|
+
GET /health
|
|
168
|
+
|
|
169
|
+
Response:
|
|
170
|
+
{"ok": bool, "backend": "bhaptics-python", "player": bool|null}
|
|
171
|
+
"""
|
|
172
|
+
connected = _is_connected()
|
|
173
|
+
player = None
|
|
174
|
+
if hasattr(bh, "is_bhaptics_player_running"):
|
|
175
|
+
try:
|
|
176
|
+
result = bh.is_bhaptics_player_running()
|
|
177
|
+
player = bool(await result) if asyncio.isfuture(result) else bool(result)
|
|
178
|
+
except Exception:
|
|
179
|
+
player = None
|
|
180
|
+
return web.json_response({"ok": connected, "backend": "bhaptics-python", "player": player})
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
async def handle_haptic(request: web.Request) -> web.Response:
|
|
184
|
+
"""
|
|
185
|
+
POST /haptic
|
|
186
|
+
|
|
187
|
+
Play a named tact event previously registered in bHaptics Studio.
|
|
188
|
+
|
|
189
|
+
Body:
|
|
190
|
+
{"event": "PatternName", "deviceIndex": 0}
|
|
191
|
+
|
|
192
|
+
Response:
|
|
193
|
+
{"ok": true, "event": "PatternName"}
|
|
194
|
+
"""
|
|
195
|
+
try:
|
|
196
|
+
body = await request.json()
|
|
197
|
+
except Exception:
|
|
198
|
+
return web.json_response({"error": "Invalid JSON"}, status=400)
|
|
199
|
+
|
|
200
|
+
event = body.get("event")
|
|
201
|
+
if not event or not isinstance(event, str):
|
|
202
|
+
return web.json_response({"error": "Missing or invalid 'event' key"}, status=400)
|
|
203
|
+
|
|
204
|
+
if not _is_connected():
|
|
205
|
+
log.warning(f"[WARN] Not connected — reconnecting before play_event '{event}'")
|
|
206
|
+
if not _try_reconnect():
|
|
207
|
+
return web.json_response({"error": "Not connected to bHaptics Player"}, status=503)
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
device_index = int(body.get("deviceIndex", 0))
|
|
211
|
+
bh.play_event(event, device_index)
|
|
212
|
+
log.info(f"[>] play_event: {event}")
|
|
213
|
+
return web.json_response({"ok": True, "event": event})
|
|
214
|
+
except Exception as exc:
|
|
215
|
+
log.error(f"[ERROR] play_event '{event}': {exc}")
|
|
216
|
+
return web.json_response({"error": str(exc)}, status=500)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
async def handle_haptic_dot(request: web.Request) -> web.Response:
|
|
220
|
+
"""
|
|
221
|
+
POST /haptic/dot
|
|
222
|
+
|
|
223
|
+
Play raw motor intensities without a Studio-registered pattern.
|
|
224
|
+
This is the key endpoint that bHaptics Player's own REST API does not expose.
|
|
225
|
+
|
|
226
|
+
Body:
|
|
227
|
+
{
|
|
228
|
+
"deviceType": 0, // 0=vest, 1=left arm, 2=right arm, ...
|
|
229
|
+
"duration": 100, // milliseconds
|
|
230
|
+
"motors": [ // one entry per motor slot
|
|
231
|
+
{"index": 0, "intensity": 100},
|
|
232
|
+
{"index": 4, "intensity": 50}
|
|
233
|
+
]
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
Device type reference (bHaptics TactSuit family):
|
|
237
|
+
0 = TactSuit X16/X40 (chest vest)
|
|
238
|
+
1 = Tactosy2 left arm
|
|
239
|
+
2 = Tactosy2 right arm
|
|
240
|
+
3 = TactVisor head
|
|
241
|
+
6 = Tactosy feet left
|
|
242
|
+
7 = Tactosy feet right
|
|
243
|
+
8 = TactGlove left
|
|
244
|
+
9 = TactGlove right
|
|
245
|
+
|
|
246
|
+
Response:
|
|
247
|
+
{"ok": true}
|
|
248
|
+
"""
|
|
249
|
+
try:
|
|
250
|
+
body = await request.json()
|
|
251
|
+
except Exception:
|
|
252
|
+
return web.json_response({"error": "Invalid JSON"}, status=400)
|
|
253
|
+
|
|
254
|
+
if not _is_connected():
|
|
255
|
+
log.warning("[WARN] Not connected — reconnecting before play_dot")
|
|
256
|
+
if not _try_reconnect():
|
|
257
|
+
return web.json_response({"error": "Not connected to bHaptics Player"}, status=503)
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
device_type = int(body.get("deviceType", 0))
|
|
261
|
+
duration = int(body.get("duration", 100))
|
|
262
|
+
motors = body.get("motors", [])
|
|
263
|
+
bh.play_dot(device_type, duration, motors)
|
|
264
|
+
log.info(f"[>] play_dot device={device_type} duration={duration}ms motors={len(motors)}")
|
|
265
|
+
return web.json_response({"ok": True})
|
|
266
|
+
except Exception as exc:
|
|
267
|
+
log.error(f"[ERROR] play_dot: {exc}")
|
|
268
|
+
return web.json_response({"error": str(exc)}, status=500)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
async def handle_haptic_stop(request: web.Request) -> web.Response:
|
|
272
|
+
"""
|
|
273
|
+
POST /haptic/stop
|
|
274
|
+
|
|
275
|
+
Stop all currently playing haptic patterns.
|
|
276
|
+
|
|
277
|
+
Response:
|
|
278
|
+
{"ok": true}
|
|
279
|
+
"""
|
|
280
|
+
try:
|
|
281
|
+
bh.stop_all()
|
|
282
|
+
log.info("[>] stop_all")
|
|
283
|
+
return web.json_response({"ok": True})
|
|
284
|
+
except Exception as exc:
|
|
285
|
+
log.error(f"[ERROR] stop_all: {exc}")
|
|
286
|
+
return web.json_response({"error": str(exc)}, status=500)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# ── App factory ───────────────────────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
def make_app(
|
|
292
|
+
app_id: str,
|
|
293
|
+
api_key: str,
|
|
294
|
+
*,
|
|
295
|
+
config_path: Optional[Path] = None,
|
|
296
|
+
) -> web.Application:
|
|
297
|
+
"""
|
|
298
|
+
Create and return the aiohttp Application.
|
|
299
|
+
|
|
300
|
+
Startup connects to bHaptics Player and launches the background
|
|
301
|
+
reconnect loop. Pass the returned app to ``aiohttp.web.run_app()``.
|
|
302
|
+
"""
|
|
303
|
+
async def on_startup(app: web.Application) -> None:
|
|
304
|
+
init_bhaptics(app_id, api_key)
|
|
305
|
+
asyncio.ensure_future(reconnect_loop())
|
|
306
|
+
|
|
307
|
+
app = web.Application()
|
|
308
|
+
app.on_startup.append(on_startup)
|
|
309
|
+
app.router.add_get( "/health", handle_health)
|
|
310
|
+
app.router.add_post("/haptic", handle_haptic)
|
|
311
|
+
app.router.add_post("/haptic/dot", handle_haptic_dot)
|
|
312
|
+
app.router.add_post("/haptic/stop", handle_haptic_stop)
|
|
313
|
+
return app
|