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.
@@ -0,0 +1,13 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ venv/
8
+ .env
9
+ *.egg
10
+ MANIFEST
11
+ .pytest_cache/
12
+ .mypy_cache/
13
+ tact-config.json
@@ -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