mudra-skills 3.1.2 → 3.2.0

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.
package/README.md CHANGED
@@ -78,7 +78,8 @@ Every generated app includes:
78
78
  |---|---|---|
79
79
  | `gesture` | discrete | finger pinches (index, middle, ring, little, thumb, grab) |
80
80
  | `button` | discrete | hardware button press/release |
81
- | `pressure` | analog | continuous squeeze force (0–1) |
81
+ | `direct_pressure` | analog | Finger pressure 0–100, normalized 0–1 (continuous ungated stream) |
82
+ | `pinch_pressure` | analog | Finger pressure 0–100, normalized 0–1 (tap-to-release filtered stream) |
82
83
  | `navigation` | pointer | 2D cursor delta (x, y) |
83
84
  | `nav_direction` | discrete | swipe direction (up/down/left/right) |
84
85
  | `imu_acc` | analog | accelerometer (x, y, z) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mudra-skills",
3
- "version": "3.1.2",
3
+ "version": "3.2.0",
4
4
  "description": "Mudra Band skills for Claude Code, Codex, and Antigravity — 2D, 3D/XR, or auto-classified",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: mudra-master
3
- version: 3.1.2
3
+ version: 3.2.0
4
4
  description: Master router for Mudra Band app builds. Use when the user describes an app, experience, prototype, timer, counter, or tool to build with the Mudra Band but has not made clear whether they want a 2D (flat/screen) or 3D (spatial/XR/VR/AR) version. This skill classifies the prompt deterministically and hands off the original prompt verbatim to either mudra-preview (2D) or mudra-xr (3D), asking exactly one 2D-or-3D disambiguation question only when the dimension cannot be inferred. Do not use for pure code edits, repo questions, or any request that is not a Mudra-app build.
5
5
  ---
6
6
  # Mudra Master Skill
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: mudra-preview
3
- version: 3.1.2
3
+ version: 3.2.0
4
4
  description: Generate a working Mudra Band interactive app preview as a single-file HTML. Use when the user describes a Mudra-controlled experience (gesture, pressure, navigation, IMU, EMG), wants to prototype a Mudra Link app, or asks to build/preview a Mudra app.
5
5
  ---
6
6
 
@@ -44,7 +44,7 @@ The canonical protocol is in `references/agent_protocol.json`.
44
44
 
45
45
  1. **Read the full instructions** from `references/prompt.md` inside the skill base directory. That file contains the complete protocol contract, signal compatibility rules, the Mode Toggle architecture (mandatory), build defaults, and sample catalog — follow all of it.
46
46
 
47
- 2. **Infer intent** from the user's description (or the args passed to this skill). Fill gaps with smart defaults. Ask only if there is genuine ambiguity (e.g., `gesture` vs `pressure`, `navigation` vs `nav_direction`, or directional motion vs IMU+Biometric bundle).
47
+ 2. **Infer intent** from the user's description (or the args passed to this skill). Fill gaps with smart defaults. Ask only if there is genuine ambiguity (e.g., `gesture` vs pressure, `navigation` vs `nav_direction`, or directional motion vs IMU+Biometric bundle). Choosing between `direct_pressure` and `pinch_pressure` is **not** an ambiguity — default to `direct_pressure` and state the choice in one clause.
48
48
 
49
49
  3. **Select the best-matching template** from `assets/` inside the skill base directory. Use the selection rule from `references/prompt.md` (motion mode → interaction pattern → signal overlap).
50
50
 
@@ -71,10 +71,12 @@ Every generated app MUST include a **compact, always-visible simulator panel** w
71
71
  | `gesture` | `Tap`, `2Tap`, `Twist`, `2Twist` |
72
72
  | `nav_direction` | `↑`, `↓`, `←`, `→`, `Roll L`, `Roll R` |
73
73
  | `navigation` | `↑`, `↓`, `←`, `→` (each click emits one delta event of ±8) |
74
- | `pressure` | Slider `0–100%` (or `−` / `+` buttons if horizontal space is tight) |
74
+ | `direct_pressure` | Slider `0–100%` (or `−` / `+` buttons if horizontal space is tight) |
75
+ | `pinch_pressure` | Slider `0–100%` plus a `Release` button that returns it to 0 — mirrors the pinch-hold-release arc |
75
76
  | `button` | `Press`, `Release` |
76
77
  | `imu_acc` | `Tilt X`, `Tilt Y`, `Tilt Z` (each fires a 5-frame burst at ±2 m/s²) |
77
78
  | `imu_gyro` | `Rot X`, `Rot Y`, `Rot Z` (each fires a 5-frame burst at ±10 deg/s) |
79
+ | `imu_quaternion` | `Yaw ±`, `Pitch ±`, `Roll ±`, `Reset` — each click rotates a held Euler state by 15°, converts to a unit quaternion, and emits it as a one-sample frame `{ values: [[w,x,y,z]] }` |
78
80
  | `emg` | `Spike` (injects a burst of elevated samples on all 3 channels) |
79
81
 
80
82
  **How each button must fire**
@@ -99,8 +101,22 @@ Unless the user explicitly asks for a different signal, every generated
99
101
  app MUST restrict itself to **at most these signals** (subject to the
100
102
  exclusivity rules below):
101
103
 
102
- 1. **One** of `pressure` **or** `gesture` — never both. `gesture` and
103
- `pressure` are mutually exclusive.
104
+ 1. **One** pressure signal **or** `gesture` — never both. `gesture` and
105
+ pressure are mutually exclusive.
106
+ - **Pressure mode rule**: there is no signal named `pressure`. Pick
107
+ **one** of `direct_pressure` or `pinch_pressure` — never both.
108
+ - `direct_pressure` → Finger pressure 0–100, normalized 0–1.
109
+ **New** continuous ungated stream — always on, no tap/release
110
+ gating. **Default.** Requires firmware 6.0.12.11 and above. Use
111
+ it whenever the user just says "pressure" or names an analog
112
+ synonym (volume, brush, throttle, zoom, intensity).
113
+ - `pinch_pressure` → Finger pressure 0–100, normalized 0–1. The
114
+ **original** tap-to-release filtered stream (formerly named
115
+ `pressure`). Values stream only between tap and release: streaming
116
+ starts on tap, the value falls off on release, and streaming
117
+ stops until the next tap. Works on older firmware. Use it only
118
+ when the interaction is explicitly commit-then-modulate:
119
+ grab-and-scale, pinch-to-zoom, pinch-and-hold-to-charge.
104
120
  - **Tap exclusivity rule** (within `gesture`): use `tap` OR `double_tap`
105
121
  — **never both together** unless the user explicitly names both (e.g.,
106
122
  "use single tap for X and double tap for Y").
@@ -120,9 +136,19 @@ Rules:
120
136
  - Drop any of the above if the concept does not need it (e.g., a pure
121
137
  tap-counter subscribes to `gesture` only and skips the directional
122
138
  signal).
123
- - **`gesture` and `pressure` are mutually exclusive** — pick one
139
+ - **`gesture` and pressure are mutually exclusive** — pick one
124
140
  interaction model per app. Tap/twist concepts → `gesture`. Analog
125
- concepts (volume, brush, throttle) → `pressure`.
141
+ concepts (volume, brush, throttle) → `direct_pressure`.
142
+ - **`imu_quaternion` (Hand Orientation) is standalone and free-combining.**
143
+ It streams absolute, drift-free orientation as unit quaternions and
144
+ belongs to no bundle — it combines with `navigation`, `nav_direction`,
145
+ `gesture`, `button`, and the pressure modes alike. Reach for it whenever
146
+ the concept needs aiming, heading, pose gating, or 1:1 rotation, instead
147
+ of subscribing to the IMU+Biometric bundle just to derive an angle.
148
+ Requires firmware 6.0.12.11 and above. **Its payload shape differs
149
+ from the other IMU signals** — `data.values` is a *list of samples*,
150
+ each `[w, x, y, z]`, not three per-axis arrays. Read the latest with
151
+ `values.at(-1)`.
126
152
  - `nav_direction` and `navigation` are **mutually exclusive per app** —
127
153
  pick the one that fits the interaction (discrete swipes →
128
154
  `nav_direction`; continuous cursor/scroll → `navigation`). Never wire
@@ -133,8 +159,8 @@ Rules:
133
159
  `nav_direction` — pick directional motion OR the IMU+Biometric bundle,
134
160
  never both.
135
161
  - Other gesture subtypes (`twist`, `double_twist`, etc.) and other
136
- signals (`button`, `imu_acc`, `imu_gyro`, `emg`) are **off by
137
- default**. Only include them when the user's prompt names them,
162
+ signals (`button`, `imu_acc`, `imu_gyro`, `emg`, `imu_quaternion`) are
163
+ **off by default**. Only include them when the user's prompt names them,
138
164
  names a synonym from the Signal Inference table in
139
165
  `references/prompt.md` § "Signal Inference Reference", or describes
140
166
  an interaction that genuinely cannot be expressed with the defaults
@@ -153,7 +179,9 @@ Rules:
153
179
  - Subscribe one signal per command: `{ "command": "subscribe", "signal": "<name>" }` — singular `signal`, never `signals`, never an array
154
180
  - Motion modes are mutually exclusive: Pointer (`navigation`+`button`) / Direction (`nav_direction`) / IMU+Biometric (`imu_acc`+`imu_gyro`+`emg`, always all three together)
155
181
  - IMU+Biometric bundle: `imu_acc`, `imu_gyro`, `emg` always subscribed together — never partially. The bundle is mutually exclusive with `navigation` and `nav_direction`.
156
- - `gesture` and `pressure` are mutually exclusive never combine them
157
- - `button` combines freely with `gesture`, `pressure`, `emg`, `imu_acc`, `imu_gyro` (subject to the Pointer/Direction/IMU motion-mode XOR `button` belongs to Pointer mode and never combines with `nav_direction`).
182
+ - Hand Orientation (`imu_quaternion`) is **outside** every motion-mode XOR — it is standalone and combines with any other signal, including `navigation` and `nav_direction`. Requires firmware 6.0.12.11 and above. Payload is `data.values` = list of `[w, x, y, z]` samples; read the latest with `values.at(-1)`.
183
+ - Pressure: pick exactly one. Both are Finger pressure 0–100, normalized 0–1. `direct_pressure` is the **new** continuous ungated stream (default). Requires firmware 6.0.12.11 and above. `pinch_pressure` is the **original** tap-to-release filtered stream and works on older firmware. There is no bare `pressure` signal.
184
+ - `gesture` and pressure are mutually exclusive — never combine them
185
+ - `button` combines freely with `gesture`, either pressure mode, `emg`, `imu_acc`, `imu_gyro`, `imu_quaternion` (subject to the Pointer/Direction/IMU motion-mode XOR — `button` belongs to Pointer mode and never combines with `nav_direction`).
158
186
  - **Navigation sensitivity is gentle by default**: keyboard `step = 3`, sim button `±3`, cursor multiplier `0.002`. Raise only when the prompt explicitly asks for fast/snappy movement. See `references/prompt.md` § "Navigation sensitivity defaults".
159
187
  - Canonical protocol JSON: `references/agent_protocol.json`
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "title": "Mudra Link - AI Agent Protocol",
3
- "version": "3.0",
3
+ "version": "3.2",
4
4
  "role": {
5
5
  "description": "You are a creative collaborator for gesture-controlled apps. You help users build applications that respond to hand movements, finger pressure, and muscle activity using the Mudra Band.",
6
6
  "approach": "Read what the user wants, fill gaps with smart defaults, and propose a brief concept before building. Be opinionated - pick sensible defaults, suggest creative additions, and only ask questions when there's genuine ambiguity."
@@ -70,7 +70,7 @@
70
70
  "sprint",
71
71
  "charge"
72
72
  ],
73
- "pressure": [
73
+ "direct_pressure": [
74
74
  "slide",
75
75
  "volume",
76
76
  "size",
@@ -79,7 +79,31 @@
79
79
  "opacity",
80
80
  "brush",
81
81
  "zoom",
82
- "analog"
82
+ "analog",
83
+ "pressure",
84
+ "press harder",
85
+ "force"
86
+ ],
87
+ "pinch_pressure": [
88
+ "pinch",
89
+ "pinch and squeeze",
90
+ "hold after tap",
91
+ "squeeze while holding",
92
+ "press after tap",
93
+ "tap then squeeze",
94
+ "sustained pinch"
95
+ ],
96
+ "imu_quaternion": [
97
+ "hand orientation",
98
+ "wrist orientation",
99
+ "absolute orientation",
100
+ "which way the hand is pointing",
101
+ "aim",
102
+ "point at",
103
+ "heading",
104
+ "roll pitch yaw",
105
+ "quaternion",
106
+ "stabilised rotation"
83
107
  ],
84
108
  "navigation": [
85
109
  "move",
@@ -107,7 +131,8 @@
107
131
  "nerve"
108
132
  ]
109
133
  },
110
- "bundling_rule": "imu_acc, imu_gyro, and emg always come together as a single group. If the user wants any one of them, subscribe to all three.",
134
+ "bundling_rule": "imu_acc, imu_gyro, and emg always come together as a single group. If the user wants any one of them, subscribe to all three. imu_quaternion is NOT part of that bundle — it is independently subscribable and combines freely. imu_quaternion: Requires firmware 6.0.12.11 and above. direct_pressure: Requires firmware 6.0.12.11 and above. pinch_pressure works on older firmware.",
135
+ "pressure_mode_rule": "There is no signal called 'pressure' — that was the old name for pinch_pressure. Pick exactly one of direct_pressure or pinch_pressure. direct_pressure is the NEW continuous ungated stream (always on, no gating; default). Requires firmware 6.0.12.11 and above. pinch_pressure is the ORIGINAL tap-to-release filtered stream (values stream only between tap and release) and works on older firmware. Choose pinch_pressure only when the concept is explicitly about squeezing while a pinch/tap is held.",
111
136
  "when_ambiguous": "If the user's concept could use either navigation or the IMU/EMG bundle, ask which fits better. This is the main case where a question is warranted."
112
137
  },
113
138
  "creative_proposals": {
@@ -118,9 +143,13 @@
118
143
  "proposal": "Button hold could sustain a note."
119
144
  },
120
145
  {
121
- "concept": "Drawing app (pressure)",
146
+ "concept": "Drawing app (direct_pressure)",
122
147
  "proposal": "Button could toggle between draw and erase mode."
123
148
  },
149
+ {
150
+ "concept": "3D model viewer (imu_quaternion)",
151
+ "proposal": "Tap could reset the orientation to a front-on view."
152
+ },
124
153
  {
125
154
  "concept": "Racing game (navigation)",
126
155
  "proposal": "Button for boost, tap on nav_direction for turbo."
@@ -135,8 +164,10 @@
135
164
  }
136
165
  ],
137
166
  "forbidden_proposals": [
138
- "Never propose pressure to complement gesture — they are mutually exclusive",
139
- "Never propose gesture to complement pressure — they are mutually exclusive",
167
+ "Never propose a pressure signal to complement gesture — they are mutually exclusive",
168
+ "Never propose gesture to complement direct_pressure or pinch_pressure — they are mutually exclusive",
169
+ "Never propose direct_pressure and pinch_pressure together — they are mutually exclusive",
170
+ "Never send 'pressure' as a signal name — it is not a valid signal and returns invalid_signal",
140
171
  "Never propose emg alone — it must always come with imu_acc and imu_gyro",
141
172
  "Never propose battery as a subscribable signal — battery is read-only via get_status"
142
173
  ]
@@ -167,9 +198,9 @@
167
198
  },
168
199
  "note": "No confidence field — treat every gesture frame as a valid trigger."
169
200
  },
170
- "pressure": {
171
- "description": "Finger pressure (0-100%)",
172
- "use_for": "Analog control",
201
+ "direct_pressure": {
202
+ "description": "Finger pressure 0100, normalized 0–1. NEW continuous ungated stream — always on, no tap/release gating. The default pressure signal.",
203
+ "use_for": "Analog control that is live whenever the finger is pressing, with no gesture gate",
173
204
  "examples": [
174
205
  "Volume",
175
206
  "Brush size",
@@ -178,7 +209,7 @@
178
209
  "Opacity"
179
210
  ],
180
211
  "data_format": {
181
- "type": "pressure",
212
+ "type": "direct_pressure",
182
213
  "data": {
183
214
  "value": 50,
184
215
  "normalized": 0.5,
@@ -187,8 +218,39 @@
187
218
  "timestamp": 1234567890
188
219
  },
189
220
  "notes": [
221
+ "Finger pressure 0–100, normalized 0–1",
190
222
  "value: 0-100 integer",
191
- "normalized: 0.0-1.0 float (convenient for scaling)"
223
+ "normalized: 0.0-1.0 float (convenient for scaling)",
224
+ "Requires firmware 6.0.12.11 and above.",
225
+ "Mutually exclusive with pinch_pressure — subscribe to exactly one",
226
+ "'pressure' is not a valid signal name. That was the old name for pinch_pressure. direct_pressure is a new signal, not a rename."
227
+ ]
228
+ },
229
+ "pinch_pressure": {
230
+ "description": "Finger pressure 0–100, normalized 0–1. ORIGINAL tap-to-release filtered stream. Formerly named 'pressure'. Values stream only between tap and release: streaming starts on tap, the value falls off on release, and streaming stops until the next tap.",
231
+ "use_for": "Analog control that only streams while a pinch/tap is held",
232
+ "examples": [
233
+ "Tap to grab, then squeeze to scale",
234
+ "Pinch and hold to charge a shot",
235
+ "Tap a target, then press harder to zoom in",
236
+ "Pinch-drag with force-sensitive resistance"
237
+ ],
238
+ "data_format": {
239
+ "type": "pinch_pressure",
240
+ "data": {
241
+ "value": 50,
242
+ "normalized": 0.5,
243
+ "timestamp": 1234567890
244
+ },
245
+ "timestamp": 1234567890
246
+ },
247
+ "notes": [
248
+ "Finger pressure 0–100, normalized 0–1",
249
+ "Identical payload shape to direct_pressure — only the acquisition window differs",
250
+ "Values stream only between tap and release; expect a run of frames bracketed by rest",
251
+ "Mutually exclusive with direct_pressure — subscribe to exactly one",
252
+ "Works on older firmware — not gated on 6.0.12.11",
253
+ "Pick this only when the concept is explicitly 'squeeze while pinching/holding'. Otherwise use direct_pressure."
192
254
  ]
193
255
  },
194
256
  "imu_acc": {
@@ -239,6 +301,37 @@
239
301
  "Integrate over time for absolute rotation"
240
302
  ]
241
303
  },
304
+ "imu_quaternion": {
305
+ "display_name": "Hand Orientation",
306
+ "description": "Absolute hand orientation as a stream of unit quaternions [w, x, y, z]",
307
+ "use_for": "Absolute orientation, aiming, stabilised 3D rotation — no drift integration required",
308
+ "examples": [
309
+ "Point-to-aim reticle",
310
+ "Rotate a 3D model 1:1 with the hand",
311
+ "Compass / heading readout",
312
+ "Wrist-orientation gate (only fire when palm is down)",
313
+ "Orientation-driven camera"
314
+ ],
315
+ "data_format": {
316
+ "type": "imu_quaternion",
317
+ "data": {
318
+ "values": [[0.7071, 0.0, 0.7071, 0.0], [0.706, 0.01, 0.708, 0.0]],
319
+ "frequency": 50,
320
+ "frequency_std": 0.4,
321
+ "timestamp": 1234567890
322
+ },
323
+ "timestamp": 1234567890
324
+ },
325
+ "notes": [
326
+ "Requires firmware 6.0.12.11 and above.",
327
+ "CRITICAL SHAPE DIFFERENCE: values is a LIST OF SAMPLES, each sample a 4-element array [w, x, y, z]. This is one nesting level deeper than imu_acc / imu_gyro, whose values is three flat per-axis arrays. Never assume the two shapes match.",
328
+ "Each sample is a unit quaternion: w² + x² + y² + z² ≈ 1.0. If your norms scatter away from 1, you are chunking the array wrong.",
329
+ "Use the LAST sample in the packet for live display; use all samples if you need the full motion trace.",
330
+ "Prefer this over integrating imu_gyro when you need absolute orientation — it does not drift.",
331
+ "Independently subscribable. NOT part of the imu_acc + imu_gyro + emg bundle, and it combines freely with navigation and nav_direction.",
332
+ "Rendering tip: feed [w, x, y, z] straight into a quaternion type (e.g. THREE.Quaternion(x, y, z, w) — note the argument order differs)."
333
+ ]
334
+ },
242
335
  "navigation": {
243
336
  "description": "Directional deltas for up/down/left/right movement",
244
337
  "use_for": "Directional movement, cursor control, steering",
@@ -344,33 +437,52 @@
344
437
  "compatibility": {
345
438
  "signal_groups": {
346
439
  "discrete": ["gesture", "button"],
347
- "analog": ["pressure"],
440
+ "analog": ["direct_pressure", "pinch_pressure"],
348
441
  "pointer_motion": ["navigation", "button"],
349
442
  "direction_motion": ["nav_direction"],
350
- "imu_biometric": ["imu_acc", "imu_gyro", "emg"]
443
+ "imu_biometric": ["imu_acc", "imu_gyro", "emg"],
444
+ "orientation": ["imu_quaternion"]
351
445
  },
352
446
  "bundling_rules": [
353
447
  "imu_acc, imu_gyro, and emg are always subscribed together — using any one requires all three",
354
- "gesture and pressure are mutually exclusive — never combine them",
448
+ "direct_pressure and pinch_pressure are mutually exclusive — exactly one pressure signal per app. direct_pressure: Requires firmware 6.0.12.11 and above. pinch_pressure works on older firmware.",
449
+ "gesture and either pressure signal are mutually exclusive — never combine them",
450
+ "imu_quaternion is standalone — it belongs to no bundle and combines freely with every other signal, including navigation and nav_direction. Requires firmware 6.0.12.11 and above.",
355
451
  "navigation and nav_direction are mutually exclusive — never combine them",
356
452
  "The imu_biometric bundle (imu_acc + imu_gyro + emg) and navigation are mutually exclusive",
357
453
  "The imu_biometric bundle (imu_acc + imu_gyro + emg) and nav_direction are mutually exclusive"
358
454
  ],
359
455
  "valid_combinations": [
360
456
  "gesture + button",
361
- "pressure + button",
457
+ "direct_pressure + button",
458
+ "pinch_pressure + button",
362
459
  "navigation + button",
363
460
  "nav_direction",
364
461
  "imu_acc + imu_gyro + emg",
365
462
  "imu_acc + imu_gyro + emg + gesture",
366
463
  "imu_acc + imu_gyro + emg + button",
367
- "navigation + button + gesture"
464
+ "navigation + button + gesture",
465
+ "imu_quaternion",
466
+ "imu_quaternion + gesture",
467
+ "imu_quaternion + button",
468
+ "imu_quaternion + navigation + button",
469
+ "imu_quaternion + nav_direction",
470
+ "imu_quaternion + direct_pressure",
471
+ "imu_quaternion + imu_acc + imu_gyro + emg"
368
472
  ],
369
473
  "cannot_combine": [
370
474
  {
371
- "signals": ["gesture", "pressure"],
475
+ "signals": ["gesture", "direct_pressure"],
476
+ "reason": "Mutually exclusive analog vs discrete control — pick one interaction model"
477
+ },
478
+ {
479
+ "signals": ["gesture", "pinch_pressure"],
372
480
  "reason": "Mutually exclusive analog vs discrete control — pick one interaction model"
373
481
  },
482
+ {
483
+ "signals": ["direct_pressure", "pinch_pressure"],
484
+ "reason": "Mutually exclusive — pick direct_pressure (new continuous stream) or pinch_pressure (original tap-to-release stream)"
485
+ },
374
486
  {
375
487
  "signals": ["navigation", "nav_direction"],
376
488
  "reason": "Mutually exclusive motion modes — navigation is continuous pointer, nav_direction is discrete swipes"
@@ -400,7 +512,7 @@
400
512
  "reason": "The imu_biometric bundle (imu_acc + imu_gyro + emg) is incompatible with nav_direction"
401
513
  }
402
514
  ],
403
- "guidance": "If a user's concept needs both directional movement and orientation/biometrics, explain the conflict and recommend which fits better. If they want any of imu_acc, imu_gyro, or emg, subscribe to all three."
515
+ "guidance": "If a user's concept needs both directional movement and biometrics, explain the conflict and recommend which fits better. If they want any of imu_acc, imu_gyro, or emg, subscribe to all three. Note that pure orientation is NOT a conflict case any more — if the user only needs to know which way the hand is pointing, use imu_quaternion, which combines with navigation and nav_direction and does not pull in the biometric bundle. imu_quaternion: Requires firmware 6.0.12.11 and above. direct_pressure: Requires firmware 6.0.12.11 and above. pinch_pressure works on older firmware."
404
516
  },
405
517
  "websocket_api": {
406
518
  "url": "ws://127.0.0.1:8766",
@@ -420,10 +532,10 @@
420
532
  "unsubscribe": {
421
533
  "description": "Stop receiving a signal",
422
534
  "format": { "command": "unsubscribe", "signal": "<signal_name>" },
423
- "example": { "command": "unsubscribe", "signal": "pressure" }
535
+ "example": { "command": "unsubscribe", "signal": "direct_pressure" }
424
536
  },
425
537
  "get_subscriptions": {
426
- "description": "Get current subscriptions (8-key boolean map — no battery key)",
538
+ "description": "Get current subscriptions (10-key boolean map — no battery key)",
427
539
  "format": { "command": "get_subscriptions" }
428
540
  },
429
541
  "get_status": {
@@ -447,7 +559,9 @@
447
559
  "emg": "boolean",
448
560
  "imu_acc": "boolean",
449
561
  "imu_gyro": "boolean",
450
- "pressure": "boolean",
562
+ "imu_quaternion": "boolean",
563
+ "direct_pressure": "boolean",
564
+ "pinch_pressure": "boolean",
451
565
  "gesture": "boolean",
452
566
  "navigation": "boolean",
453
567
  "nav_direction": "boolean",
@@ -486,7 +600,7 @@
486
600
  "missing_command": "Client JSON has no 'command' field. Never expected.",
487
601
  "unknown_command": "Command not in the supported list. Never expected after this update.",
488
602
  "missing_signal": "subscribe/unsubscribe without a 'signal' field. Never expected.",
489
- "invalid_signal": "subscribe/unsubscribe with an unrecognised signal name. Never expected after this update — do NOT send 'battery'.",
603
+ "invalid_signal": "subscribe/unsubscribe with an unrecognised signal name. Never expected after this update — do NOT send 'battery', and do NOT send 'pressure' (use 'direct_pressure' or 'pinch_pressure').",
490
604
  "client_already_connected": "CRITICAL: Server already has another connected client. Show a terminal 'close the other tab' message. Do NOT retry the connection."
491
605
  }
492
606
  },
@@ -527,6 +641,26 @@
527
641
  "suppressed_when": "client_already_connected error frame received (FR-017 path)"
528
642
  },
529
643
  "common_mistakes": [
644
+ {
645
+ "wrong": "{command: 'subscribe', signal: 'pressure'}",
646
+ "correct": "Subscribe to 'direct_pressure' (default) or 'pinch_pressure' — never both.",
647
+ "reason": "'pressure' is the old name for pinch_pressure and is no longer a valid signal. The original tap-to-release stream is pinch_pressure; direct_pressure is a new continuous ungated stream. The server returns error: invalid_signal, and the app silently receives nothing."
648
+ },
649
+ {
650
+ "wrong": "Listening for msg.type === 'pressure'",
651
+ "correct": "Match the mode you subscribed to: msg.type === 'direct_pressure' or msg.type === 'pinch_pressure'.",
652
+ "reason": "The frame type mirrors the signal name. A handler keyed on 'pressure' never fires."
653
+ },
654
+ {
655
+ "wrong": "Reading imu_quaternion as msg.data.values[0], [1], [2], [3] — i.e. treating values as one flat quaternion",
656
+ "correct": "values is a list of samples. Take a sample first: const [w, x, y, z] = msg.data.values.at(-1);",
657
+ "reason": "imu_quaternion nests one level deeper than imu_acc / imu_gyro. Reading it flat gives you the first component of the first four samples — the norm will not be 1 and the rotation will be nonsense."
658
+ },
659
+ {
660
+ "wrong": "Subscribing to imu_acc + imu_gyro + emg just to get hand orientation",
661
+ "correct": "Subscribe to imu_quaternion alone.",
662
+ "reason": "imu_quaternion gives absolute, drift-free orientation without pulling in the biometric bundle — and unlike that bundle, it can coexist with navigation and nav_direction."
663
+ },
530
664
  {
531
665
  "wrong": "Subscribing to 'battery': {command: 'subscribe', signal: 'battery'}",
532
666
  "correct": "Read device.battery and device.charging from get_status response. Battery is NOT a subscribable signal.",
@@ -583,13 +717,24 @@
583
717
  "feedback": "scale pulse (1.0 -> 1.05 -> 1.0) over 150ms"
584
718
  },
585
719
  "pressure_slider": {
586
- "description": "Volume, brush size, zoom",
720
+ "description": "NEW continuous ungated pressure stream — volume, brush size, zoom. Requires firmware 6.0.12.11 and above.",
721
+ "signal": "direct_pressure",
587
722
  "implementation": [
588
723
  "Map normalized (0.0-1.0) to your range",
589
724
  "Smooth with rolling average (3-5 samples)",
590
725
  "Feedback: continuous bar/arc tracking value"
591
726
  ]
592
727
  },
728
+ "pinch_and_squeeze": {
729
+ "description": "ORIGINAL tap-to-release filtered stream — commit with a pinch, then modulate with force. Works on older firmware.",
730
+ "signal": "pinch_pressure",
731
+ "implementation": [
732
+ "Treat the first non-zero frame as the start of the interaction and latch the target being acted on",
733
+ "Map normalized (0.0-1.0) to the modulated range while the pinch is held",
734
+ "Treat the return to ~0 as commit/release — fire the final action there",
735
+ "Feedback: show the latched target plus a force meter, so the user can see what they grabbed and how hard"
736
+ ]
737
+ },
593
738
  "directional_control": {
594
739
  "description": "Steering, movement, menu navigation",
595
740
  "signal": "navigation",
@@ -610,13 +755,26 @@
610
755
  ]
611
756
  },
612
757
  "orientation_control": {
613
- "description": "Tilt sensing, hand angle, 3D rotation",
758
+ "description": "Tilt sensing, hand angle, 3D rotation from raw inertial data",
614
759
  "signal": "imu_acc + imu_gyro",
615
760
  "implementation": [
616
761
  "Use accelerometer for absolute tilt angle",
617
762
  "Use gyroscope for rotation speed",
618
763
  "Combine both for smooth orientation tracking",
619
764
  "Apply low-pass filter to reduce noise"
765
+ ],
766
+ "prefer_instead": "If you only need to know where the hand is pointing, use imu_quaternion — it is already fused, drift-free, and does not drag in the biometric bundle."
767
+ },
768
+ "hand_orientation_control": {
769
+ "description": "Absolute hand orientation — aiming, 1:1 model rotation, heading, pose gating. Requires firmware 6.0.12.11 and above.",
770
+ "signal": "imu_quaternion",
771
+ "implementation": [
772
+ "Read the latest sample per frame: const [w, x, y, z] = msg.data.values.at(-1)",
773
+ "Apply it directly to a 3D object's quaternion — no integration, no drift correction",
774
+ "Capture a reference quaternion on a tap and show rotation relative to it, so the user can zero the pose wherever is comfortable",
775
+ "Slerp toward the incoming value (factor ~0.2) rather than snapping, to smooth packet jitter",
776
+ "For a 2D readout, convert to Euler angles once at the render step — do not store Euler state",
777
+ "Sanity check during development: w² + x² + y² + z² should be ≈ 1.0"
620
778
  ]
621
779
  }
622
780
  },
@@ -649,7 +807,7 @@
649
807
  "code_examples": {
650
808
  "javascript": {
651
809
  "description": "Basic WebSocket connection and signal handling (new Dart server protocol)",
652
- "code": "const ws = new WebSocket('ws://127.0.0.1:8766');\nconst subscriptionRecord = new Set();\nconst backoffSchedule = [1000, 2000, 5000, 10000];\nlet backoffIndex = 0;\nlet suppressReconnect = false;\nlet reconnectTimer = null;\nlet lastBandConnected = false;\n\nfunction subscribe(signal) {\n subscriptionRecord.add(signal);\n ws.send(JSON.stringify({ command: 'subscribe', signal }));\n}\n\nfunction connect() {\n const socket = new WebSocket('ws://127.0.0.1:8766');\n\n socket.onopen = () => {\n backoffIndex = 0;\n socket.send(JSON.stringify({ command: 'get_status' }));\n subscriptionRecord.forEach(signal => socket.send(JSON.stringify({ command: 'subscribe', signal })));\n };\n\n socket.onmessage = (event) => {\n const msg = JSON.parse(event.data);\n if (msg.type === 'status') {\n const bandConnected = Boolean(msg.data.device.firmware && msg.data.device.serial_number);\n const hand = bandConnected ? msg.data.device.hand : 'None';\n updateConnectionLabel(bandConnected, hand);\n if (bandConnected && !lastBandConnected) {\n // Band just reconnected — replay subscriptions (FR-018a)\n subscriptionRecord.forEach(signal => socket.send(JSON.stringify({ command: 'subscribe', signal })));\n }\n lastBandConnected = bandConnected;\n }\n if (msg.type === 'error' && msg.data.error === 'client_already_connected') {\n suppressReconnect = true;\n showAlreadyInUseMessage();\n }\n if (msg.type === 'gesture') handleGesture(msg.data);\n if (msg.type === 'pressure') handlePressure(msg.data);\n if (msg.type === 'navigation') handleNavigation(msg.data);\n };\n\n socket.onclose = () => {\n if (suppressReconnect) return;\n showReconnecting();\n const delay = backoffSchedule[Math.min(backoffIndex, backoffSchedule.length - 1)];\n backoffIndex = Math.min(backoffIndex + 1, backoffSchedule.length - 1);\n reconnectTimer = setTimeout(connect, delay);\n };\n}"
810
+ "code": "const ws = new WebSocket('ws://127.0.0.1:8766');\nconst subscriptionRecord = new Set();\nconst backoffSchedule = [1000, 2000, 5000, 10000];\nlet backoffIndex = 0;\nlet suppressReconnect = false;\nlet reconnectTimer = null;\nlet lastBandConnected = false;\n\nfunction subscribe(signal) {\n subscriptionRecord.add(signal);\n ws.send(JSON.stringify({ command: 'subscribe', signal }));\n}\n\nfunction connect() {\n const socket = new WebSocket('ws://127.0.0.1:8766');\n\n socket.onopen = () => {\n backoffIndex = 0;\n socket.send(JSON.stringify({ command: 'get_status' }));\n subscriptionRecord.forEach(signal => socket.send(JSON.stringify({ command: 'subscribe', signal })));\n };\n\n socket.onmessage = (event) => {\n const msg = JSON.parse(event.data);\n if (msg.type === 'status') {\n const bandConnected = Boolean(msg.data.device.firmware && msg.data.device.serial_number);\n const hand = bandConnected ? msg.data.device.hand : 'None';\n updateConnectionLabel(bandConnected, hand);\n if (bandConnected && !lastBandConnected) {\n // Band just reconnected — replay subscriptions (FR-018a)\n subscriptionRecord.forEach(signal => socket.send(JSON.stringify({ command: 'subscribe', signal })));\n }\n lastBandConnected = bandConnected;\n }\n if (msg.type === 'error' && msg.data.error === 'client_already_connected') {\n suppressReconnect = true;\n showAlreadyInUseMessage();\n }\n if (msg.type === 'gesture') handleGesture(msg.data);\n if (msg.type === 'direct_pressure') handlePressure(msg.data); // or 'pinch_pressure' \u2014 whichever mode you subscribed to\n if (msg.type === 'navigation') handleNavigation(msg.data);\n if (msg.type === 'imu_quaternion') {\n const [w, x, y, z] = msg.data.values.at(-1); // values is a LIST of [w,x,y,z] samples\n handleOrientation(w, x, y, z);\n }\n };\n\n socket.onclose = () => {\n if (suppressReconnect) return;\n showReconnecting();\n const delay = backoffSchedule[Math.min(backoffIndex, backoffSchedule.length - 1)];\n backoffIndex = Math.min(backoffIndex + 1, backoffSchedule.length - 1);\n reconnectTimer = setTimeout(connect, delay);\n };\n}"
653
811
  },
654
812
  "test_gesture": {
655
813
  "description": "Simulate a gesture for testing without device",
@@ -666,9 +824,17 @@
666
824
  "guidance": "Explain the conflict: navigation and the IMU/EMG bundle (imu_acc + imu_gyro + emg) are mutually exclusive. Recommend the better fit: navigation for directional movement/cursor, IMU+EMG for orientation/biometrics."
667
825
  },
668
826
  "gesture_pressure_conflict": {
669
- "scenario": "User wants both gesture and pressure",
827
+ "scenario": "User wants both gesture and a pressure signal",
670
828
  "guidance": "Explain that gesture and pressure are mutually exclusive. Ask which interaction model fits better: gesture for discrete actions (tap/twist) or pressure for analog control (volume/zoom)."
671
829
  },
830
+ "pressure_mode_choice": {
831
+ "scenario": "User says 'pressure' without specifying a mode",
832
+ "guidance": "Default to direct_pressure (new continuous ungated stream) and say so in one clause — do not ask. Requires firmware 6.0.12.11 and above. Switch to pinch_pressure only when the described interaction is explicitly 'pinch/tap first, then squeeze' (grab-and-scale, charge-a-shot, pinch-to-zoom). pinch_pressure is the original tap-to-release stream and works on older firmware. Never subscribe to both."
833
+ },
834
+ "orientation_request": {
835
+ "scenario": "User wants tilt, rotation, aiming, or 'which way my hand is pointing'",
836
+ "guidance": "Use imu_quaternion. It is absolute, drift-free, needs no sensor fusion on your side, and — unlike the imu_acc + imu_gyro + emg bundle — combines freely with navigation and nav_direction. Requires firmware 6.0.12.11 and above. Reach for the raw IMU bundle only when the user actually wants acceleration, shake detection, or biometrics."
837
+ },
672
838
  "partial_imu_request": {
673
839
  "scenario": "User asks for only imu_acc OR only imu_gyro OR only emg",
674
840
  "guidance": "Subscribe to all three (imu_acc + imu_gyro + emg) regardless — they form an inseparable bundle. Mention this to the user so they understand the full signal set available."