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.
@@ -74,7 +74,7 @@
74
74
  "sprint",
75
75
  "charge"
76
76
  ],
77
- "pressure": [
77
+ "direct_pressure": [
78
78
  "slide",
79
79
  "volume",
80
80
  "size",
@@ -83,7 +83,31 @@
83
83
  "opacity",
84
84
  "brush",
85
85
  "zoom",
86
- "analog"
86
+ "analog",
87
+ "pressure",
88
+ "press harder",
89
+ "force"
90
+ ],
91
+ "pinch_pressure": [
92
+ "pinch",
93
+ "squeeze",
94
+ "pinch and hold",
95
+ "tap then squeeze",
96
+ "grab and scale",
97
+ "pinch-to-zoom",
98
+ "hold to charge"
99
+ ],
100
+ "imu_quaternion": [
101
+ "hand orientation",
102
+ "wrist orientation",
103
+ "absolute orientation",
104
+ "aim",
105
+ "point at",
106
+ "heading",
107
+ "which way the hand is pointing",
108
+ "roll pitch yaw",
109
+ "quaternion",
110
+ "1:1 rotation"
87
111
  ],
88
112
  "navigation": [
89
113
  "move",
@@ -98,10 +122,9 @@
98
122
  ],
99
123
  "imu_acc+imu_gyro": [
100
124
  "tilt",
101
- "orientation",
125
+ "shake",
126
+ "acceleration",
102
127
  "angle",
103
- "rotate",
104
- "3D",
105
128
  "balance",
106
129
  "level"
107
130
  ],
@@ -120,7 +143,9 @@
120
143
  "nerve"
121
144
  ]
122
145
  },
123
- "when_ambiguous": "If the user's concept could use either navigation or IMU, ask which fits better. Also ask when two motion modes (Pointer / Direction / IMU) map equally well — do not silently pick. Default to 'direction' when motion language is present but no clear winner."
146
+ "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 for explicit commit-then-modulate interactions (grab-and-scale, pinch-to-zoom, hold-to-charge). Do not ask the user which mode — infer it.",
147
+ "orientation_rule": "For aiming, heading, pose gating, or 1:1 rotation of a mesh, use imu_quaternion. It is absolute and drift-free, needs no fusion on your side, belongs to no bundle, and is exempt from the Pointer/Direction/IMU motion-mode XOR — so it combines with navigation and nav_direction. Requires firmware 6.0.12.11 and above. Reach for imu_acc + imu_gyro only when the concept wants raw acceleration or shake.",
148
+ "when_ambiguous": "If the user's concept could use either navigation or IMU, ask which fits better. Also ask when two motion modes (Pointer / Direction / IMU) map equally well — do not silently pick. Default to 'direction' when motion language is present but no clear winner. Note: pure orientation is not an ambiguity — use imu_quaternion, which conflicts with nothing."
124
149
  },
125
150
  "creative_proposals": {
126
151
  "description": "When the user's concept maps to one signal, propose a complementary one that respects motion-mode exclusivity. Keep it to one sentence.",
@@ -177,9 +202,9 @@
177
202
  "Forward gesture.double_tap → onSelectEnd for release"
178
203
  ]
179
204
  },
180
- "pressure": {
181
- "description": "Finger pressure (0-100%)",
182
- "use_for": "Analog control (scale, zoom, opacity, intensity)",
205
+ "direct_pressure": {
206
+ "description": "Finger pressure 0–100, normalized 0–1. NEW continuous ungated stream — always on, no tap/release gating. The default pressure signal.",
207
+ "use_for": "Analog control (scale, zoom, opacity, intensity) that is live whenever the finger is pressing, with no gesture gate",
183
208
  "examples": [
184
209
  "Brush size",
185
210
  "Zoom level",
@@ -188,7 +213,7 @@
188
213
  "Mesh scale"
189
214
  ],
190
215
  "data_format": {
191
- "type": "pressure",
216
+ "type": "direct_pressure",
192
217
  "data": {
193
218
  "value": 50,
194
219
  "normalized": 0.5,
@@ -197,8 +222,70 @@
197
222
  "timestamp": 1234567890
198
223
  },
199
224
  "notes": [
225
+ "Finger pressure 0–100, normalized 0–1",
200
226
  "value: 0-100 integer",
201
- "normalized: 0.0-1.0 float (convenient for scale, hue, opacity)"
227
+ "normalized: 0.0-1.0 float (convenient for scale, hue, opacity)",
228
+ "Mutually exclusive with pinch_pressure — subscribe to exactly one",
229
+ "Requires firmware 6.0.12.11 and above.",
230
+ "'pressure' is not a valid signal name. That was the old name for pinch_pressure. direct_pressure is a new signal, not a rename."
231
+ ]
232
+ },
233
+ "pinch_pressure": {
234
+ "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.",
235
+ "use_for": "Analog control that only begins once the user has committed with a pinch",
236
+ "examples": [
237
+ "Pinch a mesh, then squeeze to scale it",
238
+ "Pinch and hold to charge a throw",
239
+ "Pinch-to-zoom on a virtual screen",
240
+ "Grab-and-drag with force-sensitive resistance"
241
+ ],
242
+ "data_format": {
243
+ "type": "pinch_pressure",
244
+ "data": {
245
+ "value": 50,
246
+ "normalized": 0.5,
247
+ "timestamp": 1234567890
248
+ },
249
+ "timestamp": 1234567890
250
+ },
251
+ "notes": [
252
+ "Finger pressure 0–100, normalized 0–1",
253
+ "Identical payload to direct_pressure — only the acquisition window differs",
254
+ "Values stream only between tap and release; expect a run of frames bracketed by rest",
255
+ "Mutually exclusive with direct_pressure — subscribe to exactly one",
256
+ "Works on older firmware — not gated on 6.0.12.11",
257
+ "In XR this pairs naturally with a latched target: pinch to grab an object, squeeze to modulate it, release to commit"
258
+ ]
259
+ },
260
+ "imu_quaternion": {
261
+ "display_name": "Hand Orientation",
262
+ "description": "Absolute hand orientation as a stream of unit quaternions [w, x, y, z]",
263
+ "use_for": "Aiming, ray direction, 1:1 mesh rotation, heading, pose gating — no drift, no fusion needed",
264
+ "examples": [
265
+ "Aim a ray/reticle by pointing the hand",
266
+ "Rotate a model 1:1 with the wrist",
267
+ "Orient a virtual screen to face the hand",
268
+ "Palm-down / palm-up pose gate",
269
+ "Compass heading in a spatial HUD"
270
+ ],
271
+ "data_format": {
272
+ "type": "imu_quaternion",
273
+ "data": {
274
+ "values": [[0.7071, 0.0, 0.7071, 0.0], [0.706, 0.01, 0.708, 0.0]],
275
+ "frequency": 50,
276
+ "frequency_std": 0.4,
277
+ "timestamp": 1234567890
278
+ },
279
+ "timestamp": 1234567890
280
+ },
281
+ "notes": [
282
+ "Requires firmware 6.0.12.11 and above.",
283
+ "CRITICAL SHAPE DIFFERENCE: values is a LIST OF SAMPLES, each a 4-element [w, x, y, z] array. One nesting level deeper than imu_acc / imu_gyro, whose values is three flat per-axis arrays. Never assume the shapes match.",
284
+ "Read the latest sample per frame: const [w, x, y, z] = msg.data.values.at(-1)",
285
+ "Each sample is a unit quaternion (w² + x² + y² + z² ≈ 1.0). Scattered norms mean you are reading at the wrong depth.",
286
+ "three.js argument order differs: new THREE.Quaternion(x, y, z, w)",
287
+ "Prefer object.quaternion.slerp(incoming, 0.2) over a hard set, to absorb packet jitter",
288
+ "Standalone signal. NOT part of the imu_acc + imu_gyro + emg bundle and exempt from the Pointer/Direction/IMU motion-mode XOR — it combines with navigation and nav_direction."
202
289
  ]
203
290
  },
204
291
  "imu_acc": {
@@ -404,24 +491,41 @@
404
491
  }
405
492
  },
406
493
  "xor_rules": [
407
- "gesture XOR pressure (never both)",
494
+ "gesture XOR pressure (never both) — applies to direct_pressure and pinch_pressure alike",
495
+ "direct_pressure XOR pinch_pressure (exactly one pressure signal per app — they are mutually exclusive)",
408
496
  "navigation XOR nav_direction (never both)",
409
497
  "(navigation OR nav_direction) XOR imu_biometric_mode (never combine pointer/direction with the IMU+Biometric bundle)",
410
- "imu_acc, imu_gyro, emg are bundled — partial subscriptions are forbidden"
498
+ "imu_acc, imu_gyro, emg are bundled — partial subscriptions are forbidden",
499
+ "imu_quaternion is exempt from every rule above — it is standalone and combines with anything"
411
500
  ],
412
501
  "can_combine": [
413
- "gesture OR pressure",
502
+ "gesture OR one pressure mode (direct_pressure or pinch_pressure)",
414
503
  "button (with pointer/direction/imu_biometric/none)",
415
- "ONE OF: pointer_mode OR direction_mode OR imu_biometric_mode"
504
+ "ONE OF: pointer_mode OR direction_mode OR imu_biometric_mode",
505
+ "imu_quaternion + ANY of the above, including navigation and nav_direction"
416
506
  ],
417
507
  "cannot_combine": [
418
508
  {
419
509
  "signals": [
420
510
  "gesture",
421
- "pressure"
511
+ "direct_pressure"
512
+ ],
513
+ "reason": "XOR rule - pick one"
514
+ },
515
+ {
516
+ "signals": [
517
+ "gesture",
518
+ "pinch_pressure"
422
519
  ],
423
520
  "reason": "XOR rule - pick one"
424
521
  },
522
+ {
523
+ "signals": [
524
+ "direct_pressure",
525
+ "pinch_pressure"
526
+ ],
527
+ "reason": "Mutually exclusive — pick direct_pressure (new continuous stream) or pinch_pressure (original tap-to-release stream)"
528
+ },
425
529
  {
426
530
  "signals": [
427
531
  "navigation",
@@ -479,7 +583,7 @@
479
583
  "reason": "Button is part of pointer mode - cannot mix with direction mode"
480
584
  }
481
585
  ],
482
- "guidance": "Three mutually exclusive motion groups: (1) Pointer mode: navigation+button for continuous cursor/drag/pan, (2) Direction mode: nav_direction for discrete swipe-like gestures, (3) IMU+Biometric mode: imu_acc+imu_gyro+emg as an inseparable bundle for orientation/tilt/rotation/biometric. Pick ONE per app. gesture XOR pressure; button combines freely (subject to mode rules)."
586
+ "guidance": "Three mutually exclusive motion groups: (1) Pointer mode: navigation+button for continuous cursor/drag/pan, (2) Direction mode: nav_direction for discrete swipe-like gestures, (3) IMU+Biometric mode: imu_acc+imu_gyro+emg as an inseparable bundle for shake/acceleration/biometrics. Pick ONE per app. gesture XOR pressure, and direct_pressure XOR pinch_pressure; button combines freely (subject to mode rules). Hand Orientation (imu_quaternion) is a FOURTH, non-exclusive option that sits outside this scheme — if the app needs aiming or 1:1 rotation, add imu_quaternion to whichever motion mode you picked rather than treating it as a conflict. 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."
483
587
  },
484
588
  "websocket_api": {
485
589
  "url": "ws://127.0.0.1:8766",
@@ -524,7 +628,7 @@
524
628
  },
525
629
  "example": {
526
630
  "command": "unsubscribe",
527
- "signal": "pressure"
631
+ "signal": "direct_pressure"
528
632
  }
529
633
  },
530
634
  "get_subscriptions": {
@@ -590,10 +694,25 @@
590
694
  "reason": "Raw WebSocket has no mock fallback; MudraClient handles real + simulated paths transparently"
591
695
  },
592
696
  {
593
- "wrong": "{command: 'subscribe', signals: ['gesture', 'pressure']}",
697
+ "wrong": "{command: 'subscribe', signals: ['gesture', 'navigation']}",
594
698
  "correct": "Send separate subscribe commands — one per signal",
595
699
  "reason": "Parameter is 'signal' (singular), not 'signals'"
596
700
  },
701
+ {
702
+ "wrong": "{command: 'subscribe', signal: 'pressure'}",
703
+ "correct": "Subscribe to 'direct_pressure' (default) or 'pinch_pressure' — never both",
704
+ "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 invalid_signal and the app silently receives nothing. The frame type mirrors the signal name, so handlers keyed on 'pressure' never fire either."
705
+ },
706
+ {
707
+ "wrong": "const [w, x, y, z] = msg.data.values // on an imu_quaternion frame",
708
+ "correct": "const [w, x, y, z] = msg.data.values.at(-1)",
709
+ "reason": "imu_quaternion values is a LIST of [w,x,y,z] samples, one nesting level deeper than imu_acc / imu_gyro. Read flat, you get whole samples where components belong and the rotation is nonsense."
710
+ },
711
+ {
712
+ "wrong": "Subscribing to imu_acc + imu_gyro + emg to derive where the hand is pointing",
713
+ "correct": "Subscribe to imu_quaternion alone",
714
+ "reason": "imu_quaternion is already fused and drift-free, and unlike the bundle it can coexist with navigation / nav_direction — so it does not force a motion-mode choice on the app."
715
+ },
597
716
  {
598
717
  "wrong": "Instantiating MudraClient inside init()",
599
718
  "correct": "Instantiate MudraClient at module scope, outside the xb.Script class",
@@ -814,14 +933,36 @@
814
933
  ]
815
934
  },
816
935
  "pressure_analog": {
817
- "description": "Analog control via finger pressure — brush size, zoom, opacity",
818
- "signal": "pressure",
936
+ "description": "NEW continuous ungated pressure stream — brush size, zoom, opacity. Requires firmware 6.0.12.11 and above.",
937
+ "signal": "direct_pressure",
819
938
  "implementation": [
820
939
  "Map normalized (0..1) to your range",
821
940
  "Smooth with rolling average (3-5 samples)",
822
941
  "Visual feedback: scale, color hue, bar indicator"
823
942
  ]
824
943
  },
944
+ "pinch_and_squeeze": {
945
+ "description": "ORIGINAL tap-to-release filtered stream — commit with a pinch, then modulate with force. Works on older firmware.",
946
+ "signal": "pinch_pressure",
947
+ "implementation": [
948
+ "Latch the target mesh on the first non-zero frame — that is the grab",
949
+ "Map normalized (0..1) to the modulated property while the pinch is held",
950
+ "Treat the return to ~0 as release — commit the final value and clear the latch",
951
+ "Visual feedback: highlight the latched mesh plus a force meter, so the user sees what they grabbed and how hard"
952
+ ]
953
+ },
954
+ "hand_orientation": {
955
+ "description": "Absolute hand orientation — aiming a ray, 1:1 mesh rotation, pose gating, heading. Requires firmware 6.0.12.11 and above.",
956
+ "signal": "imu_quaternion",
957
+ "implementation": [
958
+ "Read the latest sample each frame: const [w, x, y, z] = msg.data.values.at(-1)",
959
+ "Build the three.js quaternion with swapped argument order: new THREE.Quaternion(x, y, z, w)",
960
+ "Slerp toward it (factor ~0.2) rather than setting it hard, to absorb packet jitter",
961
+ "For aiming, apply the quaternion to a forward vector (0, 0, -1) and raycast along the result",
962
+ "Latch a reference quaternion on gesture.tap so the user can zero the pose in a comfortable arm position, then apply incoming rotations relative to it",
963
+ "Sanity check while developing: w² + x² + y² + z² should be ≈ 1.0"
964
+ ]
965
+ },
825
966
  "emg_time_series": {
826
967
  "description": "EMG visualization, muscle-activity overlays",
827
968
  "signal": "emg",
@@ -17,19 +17,36 @@ ws://127.0.0.1:8766
17
17
  Always construct the connection through `MudraClient` (Section 4).
18
18
  Never use raw `new WebSocket(...)`.
19
19
 
20
- ### Eight canonical signals
20
+ ### Ten canonical signals
21
21
 
22
22
  | Signal | Category | Description |
23
23
  |--------|----------|-------------|
24
24
  | `gesture` | Discrete | Hand gesture events (tap, double_tap, twist, double_twist) |
25
25
  | `button` | Discrete | Button hold / release |
26
- | `pressure` | Analog | Finger pressure 0–100, normalized 0–1 |
26
+ | `direct_pressure` | Analog | Finger pressure 0–100, normalized 0–1. **New** continuous ungated stream (always on). **Default.** Requires firmware 6.0.12.11 and above. |
27
+ | `pinch_pressure` | Analog | Finger pressure 0–100, normalized 0–1. **Original** tap-to-release filtered stream. Works on older firmware. |
27
28
  | `navigation` | Motion (Pointer) | Continuous delta_x / delta_y cursor movement |
28
29
  | `nav_direction` | Motion (Direction) | Discrete directional swipes: None, Right, Left, Up, Down, Roll Left, Roll Right |
29
30
  | `imu_acc` | Motion (IMU) | Accelerometer values [x, y, z] m/s², frequency 1125 Hz |
30
31
  | `imu_gyro` | Motion (IMU) | Gyroscope values [x, y, z] deg/s, frequency 1125 Hz |
32
+ | `imu_quaternion` | Orientation (standalone) | **Hand Orientation** — absolute unit quaternions. `values` is a **list** of `[w, x, y, z]` samples. Requires firmware 6.0.12.11 and above. |
31
33
  | `emg` | Biometric | 3 de-interleaved channel arrays [[ch1], [ch2], [ch3]] |
32
34
 
35
+ **`pressure` is not a signal name.** It was the old name for
36
+ `pinch_pressure`. Both signals are Finger pressure 0–100, normalized 0–1.
37
+ `direct_pressure` is a **new** continuous ungated
38
+ stream, not a split of the old signal. Sending `pressure` returns
39
+ `invalid_signal` and the app receives nothing. Subscribe to exactly one
40
+ pressure signal. Default to `direct_pressure`. Requires firmware 6.0.12.11 and above. Choose `pinch_pressure` only for explicit
41
+ commit-then-modulate interactions (grab-and-scale, pinch-to-zoom,
42
+ hold-to-charge). `pinch_pressure` works on older firmware.
43
+
44
+ **`imu_quaternion` is exempt from the motion-mode XOR** in Section 8 — it
45
+ is standalone and combines with any other signal, including `navigation`
46
+ and `nav_direction`. Prefer it over the IMU bundle whenever the app needs
47
+ absolute orientation (aiming, ray direction, 1:1 mesh rotation, pose
48
+ gating) rather than raw acceleration. Requires firmware 6.0.12.11 and above.
49
+
33
50
 
34
51
  ### Subscription handshake
35
52
 
@@ -38,11 +55,15 @@ Send one command per signal — never use plural `signals`, arrays, or batch com
38
55
  ```js
39
56
  // CORRECT
40
57
  ws.send(JSON.stringify({ command: 'subscribe', signal: 'gesture' }));
41
- ws.send(JSON.stringify({ command: 'subscribe', signal: 'pressure' }));
58
+ ws.send(JSON.stringify({ command: 'subscribe', signal: 'direct_pressure' }));
59
+ ws.send(JSON.stringify({ command: 'subscribe', signal: 'imu_quaternion' }));
42
60
 
43
61
  // WRONG — never do this
44
- ws.send(JSON.stringify({ command: 'subscribe', signals: ['gesture', 'pressure'] }));
45
- ws.send(JSON.stringify({ command: 'subscribe', signal: ['gesture', 'pressure'] }));
62
+ ws.send(JSON.stringify({ command: 'subscribe', signals: ['gesture', 'navigation'] }));
63
+ ws.send(JSON.stringify({ command: 'subscribe', signal: ['gesture', 'navigation'] }));
64
+
65
+ // WRONG — 'pressure' is the old name for pinch_pressure, not a signal
66
+ ws.send(JSON.stringify({ command: 'subscribe', signal: 'pressure' }));
46
67
  ```
47
68
 
48
69
  ### Full command surface
@@ -60,8 +81,8 @@ ws.send(JSON.stringify({ command: 'subscribe', signal: ['gesture', 'pressure'] }
60
81
  // button
61
82
  { type: 'button', data: { state: 'pressed'|'released', timestamp }, timestamp }
62
83
 
63
- // pressure
64
- { type: 'pressure', data: { value: 0–100, normalized: 0–1, timestamp }, timestamp }
84
+ // direct_pressure / pinch_pressure — same payload; pick exactly one
85
+ { type: 'direct_pressure'|'pinch_pressure', data: { value: 0–100, normalized: 0–1, timestamp }, timestamp }
65
86
 
66
87
  // navigation
67
88
  { type: 'navigation', data: { delta_x: number, delta_y: number, timestamp }, timestamp }
@@ -79,7 +100,7 @@ ws.send(JSON.stringify({ command: 'subscribe', signal: ['gesture', 'pressure'] }
79
100
  { type: 'emg', data: { values: [[ch1_samples], [ch2_samples], [ch3_samples]], frequency: number, frequency_std: number, timestamp }, timestamp }
80
101
 
81
102
  // status — response to get_status command
82
- { type: 'status', data: { device: { name, address, battery, charging, firmware, serial_number, hand, state, firmware_config: { target, active } }, subscriptions: { emg, imu_acc, imu_gyro, pressure, gesture, navigation, nav_direction, button } }, timestamp }
103
+ { type: 'status', data: { device: { name, address, battery, charging, firmware, serial_number, hand, state, firmware_config: { target, active } }, subscriptions: { emg, imu_acc, imu_gyro, imu_quaternion, direct_pressure, pinch_pressure, gesture, navigation, nav_direction, button } }, timestamp }
83
104
 
84
105
  // subscription_status — response to subscribe/unsubscribe
85
106
  { type: 'subscription_status', data: { signal: string, subscribed: boolean }, timestamp }
@@ -630,6 +651,20 @@ mudra.subscribe('emg'); // missing imu_acc and imu_gyro
630
651
  mudra.subscribe('imu_acc'); // missing imu_gyro and emg
631
652
  ```
632
653
 
654
+ ### Hand Orientation — `imu_quaternion` (standalone)
655
+
656
+ Exempt from every XOR in this section. It belongs to no bundle, requires
657
+ no motion mode, and combines with any other signal — including
658
+ `navigation` and `nav_direction`. Use it for aiming, ray direction, 1:1
659
+ mesh rotation, pose gating, and heading. Requires firmware 6.0.12.11 and above.
660
+
661
+ ### Pressure — `direct_pressure` vs `pinch_pressure`
662
+
663
+ Pick exactly one. There is no bare `pressure` signal.
664
+
665
+ - `direct_pressure` — Finger pressure 0–100, normalized 0–1. **New** continuous ungated stream (always on, no tap/release gating). Default. Requires firmware 6.0.12.11 and above.
666
+ - `pinch_pressure` — Finger pressure 0–100, normalized 0–1. **Original** tap-to-release filtered stream (the former `pressure` signal). Values stream only between tap and release. Works on older firmware.
667
+
633
668
  ### XOR rules (all non-negotiable)
634
669
 
635
670
  1. **Gesture ⊕ Pressure** — an app may use `gesture` OR `pressure`, never both.