mudra-skills 3.0.2 → 3.0.4

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.
Files changed (146) hide show
  1. package/package.json +1 -1
  2. package/skills/mudra-master/SKILL.md +0 -196
  3. package/skills/mudra-master/mudra-preview/SKILL.md +0 -150
  4. package/skills/mudra-master/mudra-preview/assets/ar-menu.html +0 -382
  5. package/skills/mudra-master/mudra-preview/assets/document-scroller.html +0 -468
  6. package/skills/mudra-master/mudra-preview/assets/drum-machine.html +0 -788
  7. package/skills/mudra-master/mudra-preview/assets/emg-visualizer.html +0 -390
  8. package/skills/mudra-master/mudra-preview/assets/generative-art.html +0 -408
  9. package/skills/mudra-master/mudra-preview/assets/gesture-assistant.html +0 -342
  10. package/skills/mudra-master/mudra-preview/assets/gesture-speech.html +0 -444
  11. package/skills/mudra-master/mudra-preview/assets/hands-free-desktop.html +0 -376
  12. package/skills/mudra-master/mudra-preview/assets/model-rotator.html +0 -365
  13. package/skills/mudra-master/mudra-preview/assets/mudra-duel.html +0 -3200
  14. package/skills/mudra-master/mudra-preview/assets/mudra-monitor.html +0 -1451
  15. package/skills/mudra-master/mudra-preview/assets/mudra-ultimate-template.html +0 -891
  16. package/skills/mudra-master/mudra-preview/assets/music-sequencer.html +0 -455
  17. package/skills/mudra-master/mudra-preview/assets/neural-pong.html +0 -380
  18. package/skills/mudra-master/mudra-preview/assets/neural-snake.html +0 -364
  19. package/skills/mudra-master/mudra-preview/assets/presentation-controller.html +0 -345
  20. package/skills/mudra-master/mudra-preview/assets/pressure-painter.html +0 -285
  21. package/skills/mudra-master/mudra-preview/assets/runner.html +0 -1812
  22. package/skills/mudra-master/mudra-preview/assets/smart-home.html +0 -461
  23. package/skills/mudra-master/mudra-preview/assets/space-invaders.html +0 -1154
  24. package/skills/mudra-master/mudra-preview/assets/waterful-ring-toss.html +0 -1624
  25. package/skills/mudra-master/mudra-preview/references/agent_protocol.json +0 -634
  26. package/skills/mudra-master/mudra-preview/references/prompt.md +0 -16746
  27. package/skills/mudra-master/mudra-xr/SKILL.md +0 -245
  28. package/skills/mudra-master/mudra-xr/assets/demos/3dgs-walkthrough.html +0 -242
  29. package/skills/mudra-master/mudra-xr/assets/demos/aisimulator.html +0 -792
  30. package/skills/mudra-master/mudra-xr/assets/demos/balloonpop.html +0 -422
  31. package/skills/mudra-master/mudra-xr/assets/demos/ballpit.html +0 -284
  32. package/skills/mudra-master/mudra-xr/assets/demos/drone.html +0 -41
  33. package/skills/mudra-master/mudra-xr/assets/demos/gemini-icebreakers.html +0 -338
  34. package/skills/mudra-master/mudra-xr/assets/demos/gemini-xrobject.html +0 -302
  35. package/skills/mudra-master/mudra-xr/assets/demos/math3d.html +0 -188
  36. package/skills/mudra-master/mudra-xr/assets/demos/measure.html +0 -247
  37. package/skills/mudra-master/mudra-xr/assets/demos/occlusion.html +0 -251
  38. package/skills/mudra-master/mudra-xr/assets/demos/rain.html +0 -498
  39. package/skills/mudra-master/mudra-xr/assets/demos/screenwiper.html +0 -432
  40. package/skills/mudra-master/mudra-xr/assets/demos/splash.html +0 -519
  41. package/skills/mudra-master/mudra-xr/assets/demos/webcam_gestures.html +0 -253
  42. package/skills/mudra-master/mudra-xr/assets/demos/xremoji.html +0 -833
  43. package/skills/mudra-master/mudra-xr/assets/demos/xrpoet.html +0 -151
  44. package/skills/mudra-master/mudra-xr/assets/samples/depthmap.html +0 -266
  45. package/skills/mudra-master/mudra-xr/assets/samples/depthmesh.html +0 -100
  46. package/skills/mudra-master/mudra-xr/assets/samples/game_rps.html +0 -1099
  47. package/skills/mudra-master/mudra-xr/assets/samples/gestures_custom.html +0 -467
  48. package/skills/mudra-master/mudra-xr/assets/samples/gestures_heuristic.html +0 -256
  49. package/skills/mudra-master/mudra-xr/assets/samples/lighting.html +0 -163
  50. package/skills/mudra-master/mudra-xr/assets/samples/mesh_detection.html +0 -46
  51. package/skills/mudra-master/mudra-xr/assets/samples/modelviewer.html +0 -183
  52. package/skills/mudra-master/mudra-xr/assets/samples/paint.html +0 -133
  53. package/skills/mudra-master/mudra-xr/assets/samples/planar-vst.html +0 -45
  54. package/skills/mudra-master/mudra-xr/assets/samples/reticle.html +0 -95
  55. package/skills/mudra-master/mudra-xr/assets/samples/skybox_agent.html +0 -384
  56. package/skills/mudra-master/mudra-xr/assets/samples/sound.html +0 -390
  57. package/skills/mudra-master/mudra-xr/assets/samples/ui.html +0 -496
  58. package/skills/mudra-master/mudra-xr/assets/samples/virtual-screens.html +0 -808
  59. package/skills/mudra-master/mudra-xr/assets/templates/0_basic.html +0 -64
  60. package/skills/mudra-master/mudra-xr/assets/templates/1_ui.html +0 -107
  61. package/skills/mudra-master/mudra-xr/assets/templates/2_hands.html +0 -183
  62. package/skills/mudra-master/mudra-xr/assets/templates/3_depth.html +0 -89
  63. package/skills/mudra-master/mudra-xr/assets/templates/4_stereo.html +0 -84
  64. package/skills/mudra-master/mudra-xr/assets/templates/5_camera.html +0 -117
  65. package/skills/mudra-master/mudra-xr/assets/templates/6_ai.html +0 -154
  66. package/skills/mudra-master/mudra-xr/assets/templates/7_ai_live.html +0 -253
  67. package/skills/mudra-master/mudra-xr/assets/templates/8_objects.html +0 -95
  68. package/skills/mudra-master/mudra-xr/assets/templates/9_xr-toggle.html +0 -90
  69. package/skills/mudra-master/mudra-xr/assets/templates/heuristic_hand_gestures.html +0 -102
  70. package/skills/mudra-master/mudra-xr/assets/templates/meshes.html +0 -47
  71. package/skills/mudra-master/mudra-xr/assets/templates/planes.html +0 -47
  72. package/skills/mudra-master/mudra-xr/assets/templates/uikit.html +0 -187
  73. package/skills/mudra-master/mudra-xr/references/agent_protocol.json +0 -865
  74. package/skills/mudra-master/mudra-xr/references/prompt.md +0 -1605
  75. package/skills/mudra-preview/SKILL.md +0 -159
  76. package/skills/mudra-preview/assets/ar-menu.html +0 -382
  77. package/skills/mudra-preview/assets/document-scroller.html +0 -468
  78. package/skills/mudra-preview/assets/drum-machine.html +0 -788
  79. package/skills/mudra-preview/assets/emg-visualizer.html +0 -390
  80. package/skills/mudra-preview/assets/generative-art.html +0 -408
  81. package/skills/mudra-preview/assets/gesture-assistant.html +0 -342
  82. package/skills/mudra-preview/assets/gesture-speech.html +0 -444
  83. package/skills/mudra-preview/assets/hands-free-desktop.html +0 -376
  84. package/skills/mudra-preview/assets/model-rotator.html +0 -365
  85. package/skills/mudra-preview/assets/mudra-duel.html +0 -3200
  86. package/skills/mudra-preview/assets/mudra-monitor.html +0 -1451
  87. package/skills/mudra-preview/assets/mudra-ultimate-template.html +0 -997
  88. package/skills/mudra-preview/assets/music-sequencer.html +0 -455
  89. package/skills/mudra-preview/assets/neural-pong.html +0 -380
  90. package/skills/mudra-preview/assets/neural-snake.html +0 -364
  91. package/skills/mudra-preview/assets/presentation-controller.html +0 -345
  92. package/skills/mudra-preview/assets/pressure-painter.html +0 -285
  93. package/skills/mudra-preview/assets/runner.html +0 -1812
  94. package/skills/mudra-preview/assets/smart-home.html +0 -461
  95. package/skills/mudra-preview/assets/space-invaders.html +0 -1154
  96. package/skills/mudra-preview/assets/waterful-ring-toss.html +0 -1624
  97. package/skills/mudra-preview/references/agent_protocol.json +0 -701
  98. package/skills/mudra-preview/references/prompt.md +0 -16731
  99. package/skills/mudra-xr/SKILL.md +0 -245
  100. package/skills/mudra-xr/assets/demos/3dgs-walkthrough.html +0 -242
  101. package/skills/mudra-xr/assets/demos/aisimulator.html +0 -792
  102. package/skills/mudra-xr/assets/demos/balloonpop.html +0 -422
  103. package/skills/mudra-xr/assets/demos/ballpit.html +0 -284
  104. package/skills/mudra-xr/assets/demos/drone.html +0 -41
  105. package/skills/mudra-xr/assets/demos/gemini-icebreakers.html +0 -338
  106. package/skills/mudra-xr/assets/demos/gemini-xrobject.html +0 -302
  107. package/skills/mudra-xr/assets/demos/math3d.html +0 -188
  108. package/skills/mudra-xr/assets/demos/measure.html +0 -247
  109. package/skills/mudra-xr/assets/demos/occlusion.html +0 -251
  110. package/skills/mudra-xr/assets/demos/rain.html +0 -498
  111. package/skills/mudra-xr/assets/demos/screenwiper.html +0 -432
  112. package/skills/mudra-xr/assets/demos/splash.html +0 -519
  113. package/skills/mudra-xr/assets/demos/webcam_gestures.html +0 -253
  114. package/skills/mudra-xr/assets/demos/xremoji.html +0 -833
  115. package/skills/mudra-xr/assets/demos/xrpoet.html +0 -151
  116. package/skills/mudra-xr/assets/samples/depthmap.html +0 -266
  117. package/skills/mudra-xr/assets/samples/depthmesh.html +0 -100
  118. package/skills/mudra-xr/assets/samples/game_rps.html +0 -1099
  119. package/skills/mudra-xr/assets/samples/gestures_custom.html +0 -467
  120. package/skills/mudra-xr/assets/samples/gestures_heuristic.html +0 -256
  121. package/skills/mudra-xr/assets/samples/lighting.html +0 -163
  122. package/skills/mudra-xr/assets/samples/mesh_detection.html +0 -46
  123. package/skills/mudra-xr/assets/samples/modelviewer.html +0 -183
  124. package/skills/mudra-xr/assets/samples/paint.html +0 -133
  125. package/skills/mudra-xr/assets/samples/planar-vst.html +0 -45
  126. package/skills/mudra-xr/assets/samples/reticle.html +0 -95
  127. package/skills/mudra-xr/assets/samples/skybox_agent.html +0 -384
  128. package/skills/mudra-xr/assets/samples/sound.html +0 -390
  129. package/skills/mudra-xr/assets/samples/ui.html +0 -496
  130. package/skills/mudra-xr/assets/samples/virtual-screens.html +0 -808
  131. package/skills/mudra-xr/assets/templates/0_basic.html +0 -64
  132. package/skills/mudra-xr/assets/templates/1_ui.html +0 -107
  133. package/skills/mudra-xr/assets/templates/2_hands.html +0 -183
  134. package/skills/mudra-xr/assets/templates/3_depth.html +0 -89
  135. package/skills/mudra-xr/assets/templates/4_stereo.html +0 -84
  136. package/skills/mudra-xr/assets/templates/5_camera.html +0 -117
  137. package/skills/mudra-xr/assets/templates/6_ai.html +0 -154
  138. package/skills/mudra-xr/assets/templates/7_ai_live.html +0 -253
  139. package/skills/mudra-xr/assets/templates/8_objects.html +0 -95
  140. package/skills/mudra-xr/assets/templates/9_xr-toggle.html +0 -90
  141. package/skills/mudra-xr/assets/templates/heuristic_hand_gestures.html +0 -102
  142. package/skills/mudra-xr/assets/templates/meshes.html +0 -47
  143. package/skills/mudra-xr/assets/templates/planes.html +0 -47
  144. package/skills/mudra-xr/assets/templates/uikit.html +0 -187
  145. package/skills/mudra-xr/references/agent_protocol.json +0 -865
  146. package/skills/mudra-xr/references/prompt.md +0 -1605
@@ -1,1451 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>Mudra Device Monitor</title>
7
- <style>
8
- @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
9
-
10
- :root {
11
- --bg: #f8fafc;
12
- --card: #ffffff;
13
- --card-inner: #f1f5f9;
14
- --primary: #0d9488;
15
- --accent: #14b8a6;
16
- --text: #0f172a;
17
- --text-secondary: #475569;
18
- --success: #16a34a;
19
- --warning: #ca8a04;
20
- --error: #dc2626;
21
- --border: rgba(13, 148, 136, 0.2);
22
- --border-dim: rgba(15, 23, 42, 0.1);
23
- --glow: rgba(13, 148, 136, 0.06);
24
- }
25
-
26
- * { box-sizing: border-box; margin: 0; padding: 0; }
27
-
28
- body {
29
- min-height: 100vh;
30
- background: var(--bg);
31
- color: var(--text);
32
- font-family: 'Poppins', system-ui, sans-serif;
33
- overflow-x: hidden;
34
- }
35
-
36
- /* ── Top Bar ── */
37
- .topbar {
38
- position: sticky; top: 0; z-index: 100;
39
- display: flex; align-items: center; justify-content: space-between;
40
- padding: 10px 20px;
41
- background: rgba(248,250,252,0.92);
42
- backdrop-filter: blur(16px);
43
- border-bottom: 1px solid var(--border-dim);
44
- }
45
- .topbar-left { display: flex; align-items: center; gap: 14px; }
46
- .topbar-title { font-size: 1.1rem; font-weight: 700; letter-spacing: -0.01em; }
47
- .topbar-title span { color: var(--primary); }
48
- .topbar-right { display: flex; align-items: center; gap: 10px; }
49
-
50
- .conn-pill {
51
- display: inline-flex; align-items: center; gap: 8px;
52
- padding: 6px 14px; border-radius: 999px;
53
- border: 1px solid var(--border-dim);
54
- background: rgba(255,255,255,0.02);
55
- font-size: 0.82rem; font-weight: 500;
56
- }
57
- .conn-dot {
58
- width: 8px; height: 8px; border-radius: 50%;
59
- background: var(--warning);
60
- box-shadow: 0 0 0 3px rgba(234,179,8,0.15);
61
- transition: all 200ms;
62
- }
63
- .conn-dot.on { background: var(--success); box-shadow: 0 0 0 3px rgba(34,197,94,0.18); }
64
- .conn-dot.err { background: var(--error); box-shadow: 0 0 0 3px rgba(239,68,68,0.18); }
65
-
66
- .battery-pill {
67
- display: inline-flex; align-items: center; gap: 6px;
68
- padding: 6px 12px; border-radius: 999px;
69
- border: 1px solid var(--border-dim);
70
- background: rgba(255,255,255,0.02);
71
- font-size: 0.82rem; font-weight: 500;
72
- }
73
- .battery-icon {
74
- width: 22px; height: 12px; border: 1.5px solid var(--text-secondary);
75
- border-radius: 2px; position: relative;
76
- }
77
- .battery-icon::after {
78
- content: ''; position: absolute; right: -4px; top: 2px;
79
- width: 2px; height: 6px; background: var(--text-secondary); border-radius: 0 1px 1px 0;
80
- }
81
- .battery-fill {
82
- height: 100%; border-radius: 1px;
83
- background: var(--success); transition: width 300ms;
84
- }
85
- .battery-fill.low { background: var(--error); }
86
- .battery-fill.mid { background: var(--warning); }
87
-
88
- /* ── Dashboard Grid ── */
89
- .dashboard {
90
- display: grid;
91
- grid-template-columns: 1fr 1fr 1fr;
92
- grid-template-rows: auto auto auto auto;
93
- gap: 12px;
94
- padding: 14px 20px 80px;
95
- max-width: 1600px;
96
- margin: 0 auto;
97
- }
98
-
99
- .card {
100
- background: var(--card);
101
- border: 1px solid var(--border);
102
- border-radius: 14px;
103
- padding: 14px;
104
- box-shadow: 0 2px 8px rgba(0,0,0,0.06);
105
- overflow: hidden;
106
- }
107
- .card-title {
108
- font-size: 0.78rem; font-weight: 600; text-transform: uppercase;
109
- letter-spacing: 0.08em; color: var(--text-secondary); margin-bottom: 10px;
110
- display: flex; align-items: center; gap: 8px;
111
- }
112
- .card-title .icon { color: var(--primary); font-size: 0.9rem; }
113
-
114
- /* ── Gesture Panel ── */
115
- .gesture-panel { grid-column: 1; grid-row: 1; }
116
- .gesture-current {
117
- text-align: center; padding: 16px 0 12px;
118
- }
119
- .gesture-type {
120
- font-size: 2rem; font-weight: 700; color: var(--primary);
121
- transition: transform 120ms; text-transform: uppercase;
122
- }
123
- .gesture-type.flash { animation: gestureFlash 300ms ease; }
124
- @keyframes gestureFlash {
125
- 0% { transform: scale(1); filter: brightness(1); }
126
- 30% { transform: scale(1.12); filter: brightness(1.4); }
127
- 100% { transform: scale(1); filter: brightness(1); }
128
- }
129
- .gesture-conf { font-size: 0.82rem; color: var(--text-secondary); margin-top: 4px; }
130
- .gesture-grid {
131
- display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; margin-top: 10px;
132
- }
133
- .gesture-btn {
134
- padding: 8px 4px; border-radius: 8px; font-size: 0.72rem; font-weight: 600;
135
- text-align: center; border: 1px solid var(--border-dim);
136
- background: var(--card-inner); color: var(--text-secondary);
137
- cursor: pointer; transition: all 150ms;
138
- }
139
- .gesture-btn:hover { border-color: var(--primary); color: var(--primary); }
140
- .gesture-btn.active {
141
- background: rgba(119,234,233,0.12); border-color: var(--primary); color: var(--primary);
142
- }
143
- .gesture-history {
144
- margin-top: 10px; max-height: 110px; overflow-y: auto;
145
- font-family: 'JetBrains Mono', monospace; font-size: 0.72rem;
146
- color: var(--text-secondary); line-height: 1.6;
147
- }
148
- .gesture-history-item { display: flex; justify-content: space-between; padding: 2px 0; }
149
- .gesture-history-item .type { color: var(--primary); font-weight: 500; }
150
-
151
- /* ── Pressure Panel ── */
152
- .pressure-panel { grid-column: 2; grid-row: 1; }
153
- .gauge-container { display: flex; align-items: center; justify-content: center; padding: 10px 0; }
154
- .gauge-svg { width: 180px; height: 110px; }
155
- .gauge-bg { fill: none; stroke: rgba(148,163,184,0.15); stroke-width: 14; stroke-linecap: round; }
156
- .gauge-fill { fill: none; stroke: url(#gaugeGrad); stroke-width: 14; stroke-linecap: round; transition: stroke-dashoffset 80ms linear; }
157
- .gauge-text { font-size: 2rem; font-weight: 700; fill: var(--primary); text-anchor: middle; font-family: 'Poppins', sans-serif; }
158
- .gauge-label { font-size: 0.7rem; fill: var(--text-secondary); text-anchor: middle; font-family: 'Poppins', sans-serif; }
159
- .pressure-raw {
160
- display: flex; justify-content: space-between; padding: 6px 10px;
161
- background: var(--card-inner); border-radius: 8px; margin-top: 8px;
162
- font-family: 'JetBrains Mono', monospace; font-size: 0.75rem;
163
- }
164
- .pressure-raw span:first-child { color: var(--text-secondary); }
165
- .pressure-raw span:last-child { color: var(--primary); font-weight: 500; }
166
-
167
- /* ── Button Panel ── */
168
- .button-panel { grid-column: 3; grid-row: 1; }
169
- .button-vis {
170
- display: flex; align-items: center; justify-content: center;
171
- padding: 20px 0;
172
- }
173
- .button-circle {
174
- width: 100px; height: 100px; border-radius: 50%;
175
- border: 3px solid var(--border-dim);
176
- display: flex; align-items: center; justify-content: center;
177
- font-size: 0.9rem; font-weight: 700; color: var(--text-secondary);
178
- transition: all 200ms; background: var(--card-inner);
179
- }
180
- .button-circle.pressed {
181
- border-color: var(--primary); color: var(--primary);
182
- background: rgba(119,234,233,0.08);
183
- box-shadow: 0 0 30px rgba(119,234,233,0.2);
184
- transform: scale(0.95);
185
- }
186
- .button-stats {
187
- display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; margin-top: 12px;
188
- }
189
- .button-stat {
190
- text-align: center; padding: 8px; border-radius: 8px;
191
- background: var(--card-inner); border: 1px solid var(--border-dim);
192
- }
193
- .button-stat-val { font-size: 1.1rem; font-weight: 700; color: var(--primary); }
194
- .button-stat-label { font-size: 0.68rem; color: var(--text-secondary); margin-top: 2px; }
195
-
196
- /* ── SNC Panel (wide) ── */
197
- .snc-panel { grid-column: 1 / 3; grid-row: 2; }
198
- .snc-graph-wrap { position: relative; height: 150px; margin-top: 4px; }
199
- .snc-canvas { width: 100%; height: 100%; display: block; border-radius: 8px; background: var(--card-inner); }
200
- .snc-legend {
201
- display: flex; gap: 16px; margin-top: 8px; font-size: 0.75rem;
202
- }
203
- .snc-legend-item { display: flex; align-items: center; gap: 5px; }
204
- .snc-legend-dot { width: 8px; height: 8px; border-radius: 50%; }
205
- .snc-stats {
206
- display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 8px;
207
- }
208
- .snc-stat {
209
- padding: 6px 8px; border-radius: 8px; background: var(--card-inner);
210
- border: 1px solid var(--border-dim); text-align: center;
211
- }
212
- .snc-stat-val { font-size: 1rem; font-weight: 700; font-family: 'JetBrains Mono', monospace; }
213
- .snc-stat-label { font-size: 0.68rem; color: var(--text-secondary); margin-top: 2px; }
214
-
215
- /* ── Navigation / Motion Panel ── */
216
- .motion-panel { grid-column: 3; grid-row: 2; }
217
- .mode-tabs {
218
- display: flex; gap: 4px; margin-bottom: 10px;
219
- }
220
- .mode-tab {
221
- flex: 1; padding: 6px 8px; border-radius: 8px;
222
- font-size: 0.72rem; font-weight: 600; text-align: center;
223
- border: 1px solid var(--border-dim); background: transparent;
224
- color: var(--text-secondary); cursor: pointer; transition: all 150ms;
225
- }
226
- .mode-tab.active {
227
- background: rgba(119,234,233,0.1); border-color: var(--primary); color: var(--primary);
228
- }
229
- .motion-stage {
230
- position: relative; height: 160px; border-radius: 10px;
231
- border: 1px solid var(--border-dim); background: var(--card-inner);
232
- overflow: hidden;
233
- }
234
- .motion-grid {
235
- position: absolute; inset: 0;
236
- background-image:
237
- linear-gradient(rgba(148,163,184,0.06) 1px, transparent 1px),
238
- linear-gradient(90deg, rgba(148,163,184,0.06) 1px, transparent 1px);
239
- background-size: 20px 20px; pointer-events: none;
240
- }
241
- .motion-crosshair-h, .motion-crosshair-v {
242
- position: absolute; background: rgba(119,234,233,0.1);
243
- }
244
- .motion-crosshair-h { left: 0; right: 0; top: 50%; height: 1px; }
245
- .motion-crosshair-v { top: 0; bottom: 0; left: 50%; width: 1px; }
246
- .motion-dot {
247
- position: absolute; left: 50%; top: 50%;
248
- width: 16px; height: 16px; border-radius: 50%;
249
- background: var(--primary); transform: translate(-50%, -50%);
250
- box-shadow: 0 0 12px rgba(119,234,233,0.4);
251
- transition: left 60ms linear, top 60ms linear;
252
- }
253
- .motion-coords {
254
- display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-top: 8px;
255
- font-family: 'JetBrains Mono', monospace; font-size: 0.75rem;
256
- }
257
- .coord-box {
258
- padding: 5px 8px; border-radius: 6px; background: var(--card-inner);
259
- border: 1px solid var(--border-dim); display: flex; justify-content: space-between;
260
- }
261
- .coord-box .label { color: var(--text-secondary); }
262
- .coord-box .val { color: var(--primary); font-weight: 500; }
263
-
264
- /* ── Nav Direction Panel ── */
265
- .navdir-display { display: none; text-align: center; padding: 10px 0; }
266
- .navdir-display.visible { display: block; }
267
- .navdir-arrows {
268
- display: grid; grid-template: 1fr 1fr 1fr / 1fr 1fr 1fr;
269
- width: 130px; height: 130px; margin: 0 auto; gap: 3px;
270
- }
271
- .navdir-cell {
272
- display: flex; align-items: center; justify-content: center;
273
- border-radius: 6px; font-size: 1.2rem; color: var(--text-secondary);
274
- border: 1px solid var(--border-dim); background: var(--card-inner);
275
- transition: all 150ms;
276
- }
277
- .navdir-cell.center { border: none; background: transparent; font-size: 0.6rem; font-weight: 600; }
278
- .navdir-cell.active { background: rgba(119,234,233,0.15); border-color: var(--primary); color: var(--primary); }
279
- .navdir-label { margin-top: 8px; font-size: 0.85rem; font-weight: 600; color: var(--primary); }
280
-
281
- /* ── IMU Panel (wide) ── */
282
- .imu-panel { grid-column: 1 / 3; grid-row: 3; }
283
- .imu-graphs { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
284
- .imu-graph-wrap { position: relative; }
285
- .imu-graph-label {
286
- font-size: 0.72rem; font-weight: 600; color: var(--text-secondary);
287
- margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.06em;
288
- }
289
- .imu-canvas { width: 100%; height: 120px; display: block; border-radius: 8px; background: var(--card-inner); }
290
- .imu-values {
291
- display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; margin-top: 6px;
292
- font-family: 'JetBrains Mono', monospace; font-size: 0.72rem;
293
- }
294
- .imu-val-box {
295
- padding: 4px 6px; border-radius: 6px; background: var(--card-inner);
296
- border: 1px solid var(--border-dim); text-align: center;
297
- }
298
- .imu-val-box .axis { font-weight: 600; }
299
- .imu-legend {
300
- display: flex; gap: 14px; margin-top: 8px; font-size: 0.72rem;
301
- }
302
-
303
- /* ── Controls Panel ── */
304
- .controls-panel { grid-column: 3; grid-row: 3; }
305
- .ctrl-section { margin-bottom: 12px; }
306
- .ctrl-section-title {
307
- font-size: 0.72rem; font-weight: 600; color: var(--text-secondary);
308
- text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 6px;
309
- }
310
- .ctrl-btns { display: flex; flex-wrap: wrap; gap: 6px; }
311
- .ctrl-btn {
312
- padding: 7px 12px; border-radius: 8px; font-size: 0.75rem; font-weight: 600;
313
- border: 1px solid var(--border-dim); background: var(--card-inner);
314
- color: var(--text-secondary); cursor: pointer; transition: all 150ms;
315
- font-family: 'Poppins', sans-serif;
316
- }
317
- .ctrl-btn:hover { border-color: var(--primary); color: var(--primary); }
318
- .ctrl-btn.active { background: rgba(119,234,233,0.1); border-color: var(--primary); color: var(--primary); }
319
- .ctrl-btn.primary-btn {
320
- background: var(--primary); color: #072f33; border-color: var(--primary);
321
- }
322
- .ctrl-btn.primary-btn:hover { filter: brightness(1.1); }
323
-
324
- .toggle-row {
325
- display: flex; align-items: center; justify-content: space-between;
326
- padding: 5px 0; font-size: 0.8rem;
327
- }
328
- .toggle-row .sig-name { flex: 1; }
329
- .toggle-row .sig-pkts {
330
- font-family: 'JetBrains Mono', monospace; font-size: 0.68rem;
331
- color: var(--text-secondary); margin-right: 10px; min-width: 50px; text-align: right;
332
- }
333
- .toggle-switch {
334
- width: 36px; height: 20px; border-radius: 10px; flex-shrink: 0;
335
- background: rgba(148,163,184,0.25); cursor: pointer;
336
- position: relative; transition: background 200ms;
337
- }
338
- .toggle-switch.on { background: var(--primary); }
339
- .toggle-switch::after {
340
- content: ''; position: absolute; top: 2px; left: 2px;
341
- width: 16px; height: 16px; border-radius: 50%;
342
- background: white; transition: transform 200ms;
343
- }
344
- .toggle-switch.on::after { transform: translateX(16px); }
345
- .toggle-switch.conflict { background: var(--warning); }
346
- .toggle-switch.conflict::after { background: #fff; }
347
-
348
- .sub-group-label {
349
- font-size: 0.68rem; font-weight: 600; text-transform: uppercase;
350
- letter-spacing: 0.06em; color: var(--text-secondary); margin-top: 8px;
351
- padding-bottom: 3px; border-bottom: 1px solid var(--border-dim);
352
- }
353
- .sub-group-label:first-child { margin-top: 0; }
354
- .sub-conflict-warn {
355
- font-size: 0.7rem; color: var(--warning); margin-top: 6px;
356
- padding: 5px 8px; border-radius: 6px;
357
- background: rgba(234,179,8,0.08); border: 1px solid rgba(234,179,8,0.2);
358
- display: none;
359
- }
360
- .sub-conflict-warn.visible { display: block; }
361
-
362
- .kbd {
363
- display: inline-block; padding: 2px 6px; border-radius: 4px;
364
- background: rgba(148,163,184,0.12); border: 1px solid var(--border-dim);
365
- font-family: 'JetBrains Mono', monospace; font-size: 0.68rem;
366
- color: var(--text-secondary);
367
- }
368
- .shortcut-list { font-size: 0.75rem; color: var(--text-secondary); line-height: 1.8; }
369
-
370
- /* ── Raw Log ── */
371
- .log-panel { grid-column: 1 / 4; grid-row: 4; }
372
- .log-header { display: flex; justify-content: space-between; align-items: center; }
373
- .log-filters { display: flex; gap: 4px; }
374
- .log-filter {
375
- padding: 4px 10px; border-radius: 6px; font-size: 0.7rem; font-weight: 600;
376
- border: 1px solid var(--border-dim); background: transparent;
377
- color: var(--text-secondary); cursor: pointer; transition: all 150ms;
378
- font-family: 'Poppins', sans-serif;
379
- }
380
- .log-filter.active { background: rgba(119,234,233,0.1); border-color: var(--primary); color: var(--primary); }
381
- .log-pre {
382
- margin-top: 8px; min-height: 120px; max-height: 220px;
383
- overflow: auto; padding: 10px; border-radius: 10px;
384
- border: 1px solid var(--border-dim); background: var(--card-inner);
385
- font-family: 'JetBrains Mono', monospace; font-size: 0.72rem;
386
- line-height: 1.4; color: var(--text-secondary);
387
- }
388
- .log-line { white-space: pre; }
389
- .log-line .ts { color: #94a3b8; }
390
- .log-line .sig { color: var(--primary); font-weight: 500; }
391
- .log-line .err { color: var(--error); }
392
- .log-line .cmd { color: var(--warning); }
393
-
394
- .pkt-counter {
395
- font-family: 'JetBrains Mono', monospace; font-size: 0.75rem;
396
- color: var(--text-secondary);
397
- }
398
-
399
- /* ── Badge ── */
400
- .mudra-badge {
401
- position: fixed; bottom: 12px; right: 16px;
402
- font-size: 11px; color: var(--text-secondary);
403
- opacity: 0.55; letter-spacing: 0.02em; pointer-events: none;
404
- font-family: inherit;
405
- }
406
-
407
- /* ── Responsive ── */
408
- @media (max-width: 1100px) {
409
- .dashboard {
410
- grid-template-columns: 1fr 1fr;
411
- }
412
- .snc-panel { grid-column: 1 / 3; }
413
- .motion-panel { grid-column: 1; grid-row: 3; }
414
- .imu-panel { grid-column: 1 / 3; grid-row: 4; }
415
- .controls-panel { grid-column: 2; grid-row: 3; }
416
- .log-panel { grid-column: 1 / 3; grid-row: 5; }
417
- }
418
- @media (max-width: 700px) {
419
- .dashboard { grid-template-columns: 1fr; }
420
- .snc-panel, .imu-panel, .log-panel { grid-column: 1; }
421
- .motion-panel, .controls-panel { grid-column: 1; }
422
- .imu-graphs { grid-template-columns: 1fr; }
423
- }
424
-
425
- /* scrollbar */
426
- ::-webkit-scrollbar { width: 5px; height: 5px; }
427
- ::-webkit-scrollbar-track { background: transparent; }
428
- ::-webkit-scrollbar-thumb { background: rgba(119,234,233,0.2); border-radius: 3px; }
429
- </style>
430
- </head>
431
- <body>
432
- <!-- ── Top Bar ── -->
433
- <header class="topbar">
434
- <div class="topbar-left">
435
- <a href="/gallery" style="padding:4px 12px;background:rgba(119,234,233,0.1);color:#77eae9;text-decoration:none;font-size:12px;font-family:system-ui,sans-serif;border-radius:6px;border:1px solid rgba(119,234,233,0.2);transition:background 0.2s;white-space:nowrap;"onmouseenter="this.style.background='rgba(119,234,233,0.2)'"onmouseleave="this.style.background='rgba(119,234,233,0.1)'">&larr; Gallery</a>
436
- <div class="topbar-title"><span>Mudra</span> Device Monitor</div>
437
- <div class="conn-pill">
438
- <span class="conn-dot" id="connDot"></span>
439
- <span id="connText">Connecting...</span>
440
- </div>
441
- </div>
442
- <div class="topbar-right">
443
- <div class="battery-pill" id="batteryPill" style="display:none">
444
- <div class="battery-icon"><div class="battery-fill" id="batteryFill" style="width:0%"></div></div>
445
- <span id="batteryText">--%</span>
446
- </div>
447
- <div class="pkt-counter"><span id="pktCount">0</span> packets</div>
448
- </div>
449
- </header>
450
-
451
- <!-- ── Dashboard ── -->
452
- <main class="dashboard">
453
-
454
- <!-- Gesture Panel -->
455
- <section class="card gesture-panel">
456
- <div class="card-title"><span class="icon">&#9995;</span> Gestures</div>
457
- <div class="gesture-current">
458
- <div class="gesture-type" id="gestureType">NONE</div>
459
- <div class="gesture-conf" id="gestureConf">confidence: --</div>
460
- </div>
461
- <div class="gesture-grid">
462
- <div class="gesture-btn" data-gesture="tap">TAP</div>
463
- <div class="gesture-btn" data-gesture="double_tap">DBL TAP</div>
464
- <div class="gesture-btn" data-gesture="twist">TWIST</div>
465
- <div class="gesture-btn" data-gesture="double_twist">DBL TWIST</div>
466
- </div>
467
- <div class="gesture-history" id="gestureHistory"></div>
468
- </section>
469
-
470
- <!-- Pressure Panel -->
471
- <section class="card pressure-panel">
472
- <div class="card-title"><span class="icon">&#9899;</span> Pressure</div>
473
- <div class="gauge-container">
474
- <svg class="gauge-svg" viewBox="0 0 200 120">
475
- <defs>
476
- <linearGradient id="gaugeGrad" x1="0" y1="0" x2="1" y2="0">
477
- <stop offset="0%" stop-color="#2dd4bf"/>
478
- <stop offset="100%" stop-color="#77eae9"/>
479
- </linearGradient>
480
- </defs>
481
- <path class="gauge-bg" d="M 20 100 A 80 80 0 0 1 180 100"/>
482
- <path class="gauge-fill" id="gaugeFill" d="M 20 100 A 80 80 0 0 1 180 100"
483
- stroke-dasharray="251.33" stroke-dashoffset="251.33"/>
484
- <text class="gauge-text" x="100" y="92" id="gaugeText">0%</text>
485
- <text class="gauge-label" x="100" y="110">PRESSURE</text>
486
- </svg>
487
- </div>
488
- <div class="pressure-raw">
489
- <span>Raw Value</span><span id="pressureRaw">0</span>
490
- </div>
491
- <div class="pressure-raw" style="margin-top:4px">
492
- <span>Normalized</span><span id="pressureNorm">0.000</span>
493
- </div>
494
- <div class="pressure-raw" style="margin-top:4px">
495
- <span>Smoothed</span><span id="pressureSmooth">0.000</span>
496
- </div>
497
- </section>
498
-
499
- <!-- Button Panel -->
500
- <section class="card button-panel">
501
- <div class="card-title"><span class="icon">&#9673;</span> Air Touch Button</div>
502
- <div class="button-vis">
503
- <div class="button-circle" id="buttonCircle">RELEASED</div>
504
- </div>
505
- <div class="button-stats">
506
- <div class="button-stat">
507
- <div class="button-stat-val" id="btnPressCount">0</div>
508
- <div class="button-stat-label">Presses</div>
509
- </div>
510
- <div class="button-stat">
511
- <div class="button-stat-val" id="btnHoldTime">0.0s</div>
512
- <div class="button-stat-label">Last Hold</div>
513
- </div>
514
- <div class="button-stat">
515
- <div class="button-stat-val" id="btnState">IDLE</div>
516
- <div class="button-stat-label">State</div>
517
- </div>
518
- </div>
519
- </section>
520
-
521
- <!-- SNC Panel -->
522
- <section class="card snc-panel">
523
- <div class="card-title"><span class="icon">&#9876;</span> SNC — Surface Nerve Conductance</div>
524
- <div class="snc-graph-wrap">
525
- <canvas class="snc-canvas" id="sncCanvas"></canvas>
526
- </div>
527
- <div class="snc-legend">
528
- <div class="snc-legend-item"><div class="snc-legend-dot" style="background:#77eae9"></div>Ulnar</div>
529
- <div class="snc-legend-item"><div class="snc-legend-dot" style="background:#a78bfa"></div>Median</div>
530
- <div class="snc-legend-item"><div class="snc-legend-dot" style="background:#f472b6"></div>Radial</div>
531
- </div>
532
- <div class="snc-stats">
533
- <div class="snc-stat">
534
- <div class="snc-stat-val" id="sncUlnar" style="color:#77eae9">0.000</div>
535
- <div class="snc-stat-label">Ulnar</div>
536
- </div>
537
- <div class="snc-stat">
538
- <div class="snc-stat-val" id="sncMedian" style="color:#a78bfa">0.000</div>
539
- <div class="snc-stat-label">Median</div>
540
- </div>
541
- <div class="snc-stat">
542
- <div class="snc-stat-val" id="sncRadial" style="color:#f472b6">0.000</div>
543
- <div class="snc-stat-label">Radial</div>
544
- </div>
545
- </div>
546
- </section>
547
-
548
- <!-- Motion / Navigation Panel -->
549
- <section class="card motion-panel">
550
- <div class="card-title"><span class="icon">&#10146;</span> Motion & Navigation</div>
551
- <div class="mode-tabs">
552
- <div class="mode-tab active" data-mode="pointer">Pointer</div>
553
- <div class="mode-tab" data-mode="direction">Direction</div>
554
- <div class="mode-tab" data-mode="imu">IMU</div>
555
- </div>
556
-
557
- <!-- Pointer / IMU stage -->
558
- <div class="motion-stage" id="motionStage">
559
- <div class="motion-grid"></div>
560
- <div class="motion-crosshair-h"></div>
561
- <div class="motion-crosshair-v"></div>
562
- <div class="motion-dot" id="motionDot"></div>
563
- </div>
564
- <div class="motion-coords" id="motionCoords">
565
- <div class="coord-box"><span class="label">X</span><span class="val" id="coordX">0.0</span></div>
566
- <div class="coord-box"><span class="label">Y</span><span class="val" id="coordY">0.0</span></div>
567
- </div>
568
-
569
- <!-- Nav Direction display -->
570
- <div class="navdir-display" id="navdirDisplay">
571
- <div class="navdir-arrows">
572
- <div class="navdir-cell"></div>
573
- <div class="navdir-cell" data-dir="Up">&#9650;</div>
574
- <div class="navdir-cell"></div>
575
- <div class="navdir-cell" data-dir="Left">&#9664;</div>
576
- <div class="navdir-cell center" id="navdirCenter">NONE</div>
577
- <div class="navdir-cell" data-dir="Right">&#9654;</div>
578
- <div class="navdir-cell" data-dir="Roll Left">&#8630;</div>
579
- <div class="navdir-cell" data-dir="Down">&#9660;</div>
580
- <div class="navdir-cell" data-dir="Roll Right">&#8631;</div>
581
- </div>
582
- <div class="navdir-label" id="navdirLabel">None</div>
583
- </div>
584
- </section>
585
-
586
- <!-- IMU Panel -->
587
- <section class="card imu-panel">
588
- <div class="card-title"><span class="icon">&#9850;</span> IMU — Inertial Measurement Unit</div>
589
- <div class="imu-graphs">
590
- <div class="imu-graph-wrap">
591
- <div class="imu-graph-label">Accelerometer (m/s&sup2;)</div>
592
- <canvas class="imu-canvas" id="accCanvas"></canvas>
593
- <div class="imu-values" id="accValues">
594
- <div class="imu-val-box"><span class="axis" style="color:#77eae9">X</span> <span id="accX">0.00</span></div>
595
- <div class="imu-val-box"><span class="axis" style="color:#a78bfa">Y</span> <span id="accY">0.00</span></div>
596
- <div class="imu-val-box"><span class="axis" style="color:#f472b6">Z</span> <span id="accZ">9.81</span></div>
597
- </div>
598
- </div>
599
- <div class="imu-graph-wrap">
600
- <div class="imu-graph-label">Gyroscope (deg/s)</div>
601
- <canvas class="imu-canvas" id="gyroCanvas"></canvas>
602
- <div class="imu-values" id="gyroValues">
603
- <div class="imu-val-box"><span class="axis" style="color:#77eae9">X</span> <span id="gyroX">0.00</span></div>
604
- <div class="imu-val-box"><span class="axis" style="color:#a78bfa">Y</span> <span id="gyroY">0.00</span></div>
605
- <div class="imu-val-box"><span class="axis" style="color:#f472b6">Z</span> <span id="gyroZ">0.00</span></div>
606
- </div>
607
- </div>
608
- </div>
609
- <div class="imu-legend">
610
- <div class="snc-legend-item"><div class="snc-legend-dot" style="background:#77eae9"></div>X Axis</div>
611
- <div class="snc-legend-item"><div class="snc-legend-dot" style="background:#a78bfa"></div>Y Axis</div>
612
- <div class="snc-legend-item"><div class="snc-legend-dot" style="background:#f472b6"></div>Z Axis</div>
613
- </div>
614
- </section>
615
-
616
- <!-- Controls Panel -->
617
- <section class="card controls-panel">
618
- <div class="card-title"><span class="icon">&#9881;</span> Controls & Simulation</div>
619
-
620
- <div class="ctrl-section">
621
- <div class="ctrl-section-title">Subscriptions</div>
622
-
623
- <div class="sub-group-label">Universal</div>
624
- <div class="toggle-row" data-signal="gesture">
625
- <span class="sig-name">gesture</span>
626
- <span class="sig-pkts" id="pkts-gesture">0</span>
627
- <div class="toggle-switch on" data-sub="gesture"></div>
628
- </div>
629
- <div class="toggle-row" data-signal="pressure">
630
- <span class="sig-name">pressure</span>
631
- <span class="sig-pkts" id="pkts-pressure">0</span>
632
- <div class="toggle-switch on" data-sub="pressure"></div>
633
- </div>
634
- <div class="toggle-row" data-signal="snc">
635
- <span class="sig-name">snc</span>
636
- <span class="sig-pkts" id="pkts-snc">0</span>
637
- <div class="toggle-switch on" data-sub="snc"></div>
638
- </div>
639
- <div class="toggle-row" data-signal="battery">
640
- <span class="sig-name">battery</span>
641
- <span class="sig-pkts" id="pkts-battery">0</span>
642
- <div class="toggle-switch on" data-sub="battery"></div>
643
- </div>
644
-
645
- <div class="sub-group-label">Pointer Mode</div>
646
- <div class="toggle-row" data-signal="navigation">
647
- <span class="sig-name">navigation</span>
648
- <span class="sig-pkts" id="pkts-navigation">0</span>
649
- <div class="toggle-switch on" data-sub="navigation"></div>
650
- </div>
651
- <div class="toggle-row" data-signal="button">
652
- <span class="sig-name">button</span>
653
- <span class="sig-pkts" id="pkts-button">0</span>
654
- <div class="toggle-switch on" data-sub="button"></div>
655
- </div>
656
-
657
- <div class="sub-group-label">Direction Mode</div>
658
- <div class="toggle-row" data-signal="nav_direction">
659
- <span class="sig-name">nav_direction</span>
660
- <span class="sig-pkts" id="pkts-nav_direction">0</span>
661
- <div class="toggle-switch" data-sub="nav_direction"></div>
662
- </div>
663
-
664
- <div class="sub-group-label">IMU Mode</div>
665
- <div class="toggle-row" data-signal="imu_acc">
666
- <span class="sig-name">imu_acc</span>
667
- <span class="sig-pkts" id="pkts-imu_acc">0</span>
668
- <div class="toggle-switch" data-sub="imu_acc"></div>
669
- </div>
670
- <div class="toggle-row" data-signal="imu_gyro">
671
- <span class="sig-name">imu_gyro</span>
672
- <span class="sig-pkts" id="pkts-imu_gyro">0</span>
673
- <div class="toggle-switch" data-sub="imu_gyro"></div>
674
- </div>
675
-
676
- <div class="sub-conflict-warn" id="subConflictWarn">
677
- &#9888; Conflicting motion groups active — device behavior undefined.
678
- </div>
679
- </div>
680
-
681
- <div class="ctrl-section">
682
- <div class="ctrl-section-title">Commands</div>
683
- <div class="ctrl-btns">
684
- <button class="ctrl-btn primary-btn" id="cmdResubscribe">Resubscribe</button>
685
- <button class="ctrl-btn" id="cmdGetStatus">Status</button>
686
- <button class="ctrl-btn" id="cmdGetSubs">Subs</button>
687
- <button class="ctrl-btn" id="cmdGetDocs">Docs</button>
688
- </div>
689
- </div>
690
-
691
- <div class="ctrl-section">
692
- <div class="ctrl-section-title">Simulate Gestures</div>
693
- <div class="ctrl-btns">
694
- <button class="ctrl-btn" data-sim="tap">Tap</button>
695
- <button class="ctrl-btn" data-sim="double_tap">Dbl Tap</button>
696
- <button class="ctrl-btn" data-sim="twist">Twist</button>
697
- <button class="ctrl-btn" data-sim="double_twist">Dbl Twist</button>
698
- </div>
699
- </div>
700
-
701
- <div class="ctrl-section">
702
- <div class="ctrl-section-title">Keyboard Fallback</div>
703
- <div class="shortcut-list">
704
- <kbd class="kbd">Space</kbd> Tap gesture<br>
705
- <kbd class="kbd">Shift</kbd> Button hold<br>
706
- <kbd class="kbd">[</kbd> <kbd class="kbd">]</kbd> Pressure -/+<br>
707
- <kbd class="kbd">&#8592;&#8593;&#8595;&#8594;</kbd> Motion / Direction<br>
708
- <kbd class="kbd">1-4</kbd> Simulate gestures<br>
709
- <kbd class="kbd">M</kbd> Cycle motion mode
710
- </div>
711
- </div>
712
- </section>
713
-
714
- <!-- Raw Log -->
715
- <section class="card log-panel">
716
- <div class="log-header">
717
- <div class="card-title" style="margin-bottom:0"><span class="icon">&#9637;</span> Raw Data Stream</div>
718
- <div style="display:flex;gap:8px;align-items:center">
719
- <div class="log-filters">
720
- <button class="log-filter active" data-filter="all">All</button>
721
- <button class="log-filter" data-filter="gesture">Gesture</button>
722
- <button class="log-filter" data-filter="pressure">Pressure</button>
723
- <button class="log-filter" data-filter="imu">IMU</button>
724
- <button class="log-filter" data-filter="snc">SNC</button>
725
- <button class="log-filter" data-filter="nav">Nav</button>
726
- <button class="log-filter" data-filter="cmd">Commands</button>
727
- </div>
728
- <button class="ctrl-btn" id="clearLog" style="padding:4px 10px;font-size:0.7rem">Clear</button>
729
- </div>
730
- </div>
731
- <pre class="log-pre" id="logPre"></pre>
732
- </section>
733
-
734
- </main>
735
-
736
- <div class="mudra-badge">Created by Mudra</div>
737
-
738
- <script>
739
- // ═══════════════════════════════════════════════════════════════
740
- // Mudra Device Monitor — Full Signal Dashboard
741
- // ═══════════════════════════════════════════════════════════════
742
-
743
- const WS_URL = "ws://127.0.0.1:8766";
744
- const MAX_LOG = 200;
745
- const GRAPH_POINTS = 200;
746
- const PRESSURE_SMOOTH_WINDOW = 5;
747
-
748
- // ── State ──
749
- let controlMode = "pointer";
750
- let ws = null;
751
- let reconnectTimer = null;
752
- let packetCount = 0;
753
- let logFilter = "all";
754
- const pressureSamples = [];
755
-
756
- // Subscription tracking
757
- const activeSubs = new Set(["gesture", "pressure", "snc", "battery", "navigation", "button"]);
758
- const packetCounts = {};
759
- const ALL_SIGNALS = ["gesture", "pressure", "snc", "battery", "navigation", "button", "nav_direction", "imu_acc", "imu_gyro"];
760
- const MOTION_CONFLICTS = {
761
- navigation: ["nav_direction", "imu_acc", "imu_gyro"],
762
- button: ["nav_direction", "imu_acc", "imu_gyro"],
763
- nav_direction: ["navigation", "button", "imu_acc", "imu_gyro"],
764
- imu_acc: ["navigation", "button", "nav_direction"],
765
- imu_gyro: ["navigation", "button", "nav_direction"],
766
- };
767
-
768
- const state = {
769
- gesture: { type: "none", confidence: 0, timestamp: 0 },
770
- pressure: { value: 0, normalized: 0, smoothed: 0 },
771
- button: { state: "released", pressCount: 0, lastPressTime: 0, lastHoldDuration: 0 },
772
- nav: { x: 0, y: 0 },
773
- navDirection: "None",
774
- imuAcc: [0, 0, 9.81],
775
- imuGyro: [0, 0, 0],
776
- snc: [0, 0, 0],
777
- battery: { level: -1, charging: false }
778
- };
779
-
780
- const gestureHistory = [];
781
-
782
- // Graph data ring buffers
783
- const sncHistory = { ch0: [], ch1: [], ch2: [] };
784
- const accHistory = { x: [], y: [], z: [] };
785
- const gyroHistory = { x: [], y: [], z: [] };
786
-
787
- for (let i = 0; i < GRAPH_POINTS; i++) {
788
- sncHistory.ch0.push(0); sncHistory.ch1.push(0); sncHistory.ch2.push(0);
789
- accHistory.x.push(0); accHistory.y.push(0); accHistory.z.push(9.81);
790
- gyroHistory.x.push(0); gyroHistory.y.push(0); gyroHistory.z.push(0);
791
- }
792
-
793
- const logEntries = [];
794
-
795
- // ── DOM ──
796
- const $ = id => document.getElementById(id);
797
- const ui = {
798
- connDot: $("connDot"), connText: $("connText"),
799
- batteryPill: $("batteryPill"), batteryFill: $("batteryFill"), batteryText: $("batteryText"),
800
- pktCount: $("pktCount"),
801
- gestureType: $("gestureType"), gestureConf: $("gestureConf"), gestureHistory: $("gestureHistory"),
802
- gaugeFill: $("gaugeFill"), gaugeText: $("gaugeText"),
803
- pressureRaw: $("pressureRaw"), pressureNorm: $("pressureNorm"), pressureSmooth: $("pressureSmooth"),
804
- buttonCircle: $("buttonCircle"), btnPressCount: $("btnPressCount"),
805
- btnHoldTime: $("btnHoldTime"), btnState: $("btnState"),
806
- sncCanvas: $("sncCanvas"), sncUlnar: $("sncUlnar"), sncMedian: $("sncMedian"), sncRadial: $("sncRadial"),
807
- motionStage: $("motionStage"), motionDot: $("motionDot"),
808
- coordX: $("coordX"), coordY: $("coordY"), motionCoords: $("motionCoords"),
809
- navdirDisplay: $("navdirDisplay"), navdirCenter: $("navdirCenter"), navdirLabel: $("navdirLabel"),
810
- accCanvas: $("accCanvas"), gyroCanvas: $("gyroCanvas"),
811
- accX: $("accX"), accY: $("accY"), accZ: $("accZ"),
812
- gyroX: $("gyroX"), gyroY: $("gyroY"), gyroZ: $("gyroZ"),
813
- logPre: $("logPre"),
814
- };
815
-
816
- // ── Canvas contexts ──
817
- let sncCtx, accCtx, gyroCtx;
818
-
819
- function initCanvases() {
820
- const dpr = window.devicePixelRatio || 1;
821
- [ui.sncCanvas, ui.accCanvas, ui.gyroCanvas].forEach(c => {
822
- const rect = c.getBoundingClientRect();
823
- c.width = rect.width * dpr;
824
- c.height = rect.height * dpr;
825
- c.getContext("2d").scale(dpr, dpr);
826
- c._w = rect.width;
827
- c._h = rect.height;
828
- });
829
- sncCtx = ui.sncCanvas.getContext("2d");
830
- accCtx = ui.accCanvas.getContext("2d");
831
- gyroCtx = ui.gyroCanvas.getContext("2d");
832
- }
833
-
834
- // ── Connection ──
835
- function connect() {
836
- if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return;
837
- try { ws = new WebSocket(WS_URL); } catch { setConn("err", "Invalid URL"); return; }
838
-
839
- ws.onopen = () => {
840
- setConn("on", "Connected to Mudra Companion");
841
- subscribeAll();
842
- };
843
- ws.onclose = () => {
844
- setConn("off", "Disconnected — retrying...");
845
- scheduleReconnect();
846
- };
847
- ws.onerror = () => {
848
- setConn("err", "Connection error");
849
- };
850
- ws.onmessage = (e) => {
851
- const msg = parseMsg(e.data);
852
- if (!msg) return;
853
- packetCount++;
854
- ui.pktCount.textContent = packetCount;
855
- routeMessage(msg);
856
- };
857
- }
858
-
859
- function scheduleReconnect() {
860
- clearTimeout(reconnectTimer);
861
- reconnectTimer = setTimeout(connect, 1500);
862
- }
863
-
864
- function send(obj) {
865
- if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(obj));
866
- }
867
-
868
- function parseMsg(raw) {
869
- try { return JSON.parse(raw); } catch { return null; }
870
- }
871
-
872
- function setConn(status, text) {
873
- ui.connDot.className = "conn-dot" + (status === "on" ? " on" : status === "err" ? " err" : "");
874
- ui.connText.textContent = text;
875
- }
876
-
877
- // ── Subscriptions ──
878
- function subscribeTo(signal) {
879
- send({ command: "subscribe", signal });
880
- activeSubs.add(signal);
881
- syncToggleUI(signal);
882
- checkConflicts();
883
- addLog("cmd", "subscribe", { signal });
884
- }
885
-
886
- function unsubscribeFrom(signal) {
887
- send({ command: "unsubscribe", signal });
888
- activeSubs.delete(signal);
889
- syncToggleUI(signal);
890
- checkConflicts();
891
- addLog("cmd", "unsubscribe", { signal });
892
- }
893
-
894
- function syncToggleUI(signal) {
895
- const toggle = document.querySelector(`.toggle-switch[data-sub="${signal}"]`);
896
- if (toggle) toggle.classList.toggle("on", activeSubs.has(signal));
897
- }
898
-
899
- function syncAllToggles() {
900
- ALL_SIGNALS.forEach(s => syncToggleUI(s));
901
- checkConflicts();
902
- }
903
-
904
- function checkConflicts() {
905
- const motionSigs = ["navigation", "button", "nav_direction", "imu_acc", "imu_gyro"];
906
- const activeMotion = motionSigs.filter(s => activeSubs.has(s));
907
-
908
- const hasPointer = activeMotion.some(s => s === "navigation" || s === "button");
909
- const hasDir = activeMotion.includes("nav_direction");
910
- const hasImu = activeMotion.some(s => s === "imu_acc" || s === "imu_gyro");
911
- const groupCount = [hasPointer, hasDir, hasImu].filter(Boolean).length;
912
-
913
- const warn = document.getElementById("subConflictWarn");
914
- warn.classList.toggle("visible", groupCount > 1);
915
-
916
- // Mark conflicting toggles
917
- motionSigs.forEach(sig => {
918
- const toggle = document.querySelector(`.toggle-switch[data-sub="${sig}"]`);
919
- if (!toggle) return;
920
- const conflicts = MOTION_CONFLICTS[sig] || [];
921
- const hasConflict = activeSubs.has(sig) && conflicts.some(c => activeSubs.has(c));
922
- toggle.classList.toggle("conflict", hasConflict);
923
- });
924
- }
925
-
926
- function getActiveSignals() {
927
- return [...activeSubs];
928
- }
929
-
930
- function subscribeAll() {
931
- activeSubs.forEach(s => send({ command: "subscribe", signal: s }));
932
- addLog("cmd", "subscribe", { signals: [...activeSubs] });
933
- }
934
-
935
- function unsubMotion() {
936
- ["navigation", "button", "nav_direction", "imu_acc", "imu_gyro"].forEach(s => {
937
- send({ command: "unsubscribe", signal: s });
938
- activeSubs.delete(s);
939
- });
940
- syncAllToggles();
941
- }
942
-
943
- function switchMode(mode) {
944
- if (mode === controlMode) return;
945
- unsubMotion();
946
- controlMode = mode;
947
-
948
- // Subscribe to the new mode's motion signals
949
- const modeSignals = {
950
- pointer: ["navigation", "button"],
951
- direction: ["nav_direction"],
952
- imu: ["imu_acc", "imu_gyro"],
953
- };
954
- (modeSignals[mode] || []).forEach(s => {
955
- activeSubs.add(s);
956
- send({ command: "subscribe", signal: s });
957
- });
958
-
959
- // Also re-subscribe universal ones
960
- ["gesture", "pressure", "snc", "battery"].forEach(s => {
961
- if (activeSubs.has(s)) send({ command: "subscribe", signal: s });
962
- });
963
-
964
- syncAllToggles();
965
- updateModeUI();
966
- resetMotion();
967
- }
968
-
969
- function updatePacketCounts() {
970
- ALL_SIGNALS.forEach(s => {
971
- const el = document.getElementById(`pkts-${s}`);
972
- if (el) el.textContent = packetCounts[s] || 0;
973
- });
974
- }
975
-
976
- // ── Message Router ──
977
- function routeMessage(msg) {
978
- const t = msg.type;
979
- const d = msg.data || {};
980
-
981
- if (t === "connection_status") {
982
- const on = d.status === "connected";
983
- setConn(on ? "on" : "off", on ? "Mudra Band connected" : "Connect your Mudra Band...");
984
- addLog("status", t, d);
985
- return;
986
- }
987
-
988
- // Track packet counts per signal
989
- packetCounts[t] = (packetCounts[t] || 0) + 1;
990
-
991
- if (t === "gesture") onGesture(d);
992
- else if (t === "pressure") onPressure(d);
993
- else if (t === "button") onButton(d);
994
- else if (t === "navigation") onNavigation(d);
995
- else if (t === "nav_direction") onNavDirection(d);
996
- else if (t === "imu_acc") onImuAcc(d);
997
- else if (t === "imu_gyro") onImuGyro(d);
998
- else if (t === "snc") onSnc(d);
999
- else if (t === "battery") onBattery(d);
1000
-
1001
- addLog(categorize(t), t, d);
1002
- }
1003
-
1004
- function categorize(t) {
1005
- if (t === "gesture") return "gesture";
1006
- if (t === "pressure") return "pressure";
1007
- if (t === "imu_acc" || t === "imu_gyro") return "imu";
1008
- if (t === "snc") return "snc";
1009
- if (t === "navigation" || t === "nav_direction" || t === "button") return "nav";
1010
- return "other";
1011
- }
1012
-
1013
- // ── Signal Handlers ──
1014
- function onGesture(d) {
1015
- state.gesture.type = d.type || "unknown";
1016
- state.gesture.confidence = d.confidence ?? 0;
1017
- state.gesture.timestamp = d.timestamp || Date.now();
1018
-
1019
- ui.gestureType.textContent = state.gesture.type.toUpperCase();
1020
- ui.gestureConf.textContent = `confidence: ${(state.gesture.confidence * 100).toFixed(0)}%`;
1021
- flashEl(ui.gestureType, "flash");
1022
-
1023
- // highlight grid btn
1024
- document.querySelectorAll(".gesture-btn").forEach(b => {
1025
- b.classList.toggle("active", b.dataset.gesture === state.gesture.type);
1026
- });
1027
-
1028
- // history
1029
- gestureHistory.unshift({
1030
- type: state.gesture.type,
1031
- conf: state.gesture.confidence,
1032
- time: new Date().toLocaleTimeString()
1033
- });
1034
- if (gestureHistory.length > 30) gestureHistory.pop();
1035
- renderGestureHistory();
1036
- }
1037
-
1038
- function onPressure(d) {
1039
- const raw = d.value ?? 0;
1040
- const norm = clamp01(d.normalized ?? 0);
1041
- pressureSamples.push(norm);
1042
- if (pressureSamples.length > PRESSURE_SMOOTH_WINDOW) pressureSamples.shift();
1043
- const smoothed = pressureSamples.reduce((a, b) => a + b, 0) / pressureSamples.length;
1044
-
1045
- state.pressure = { value: raw, normalized: norm, smoothed };
1046
-
1047
- const pct = Math.round(smoothed * 100);
1048
- const arcLen = 251.33;
1049
- ui.gaugeFill.style.strokeDashoffset = arcLen - (arcLen * smoothed);
1050
- ui.gaugeText.textContent = `${pct}%`;
1051
- ui.pressureRaw.textContent = raw;
1052
- ui.pressureNorm.textContent = norm.toFixed(3);
1053
- ui.pressureSmooth.textContent = smoothed.toFixed(3);
1054
- }
1055
-
1056
- function onButton(d) {
1057
- const s = d.state || "released";
1058
- const prev = state.button.state;
1059
- state.button.state = s;
1060
-
1061
- if (s === "pressed" && prev !== "pressed") {
1062
- state.button.pressCount++;
1063
- state.button.lastPressTime = Date.now();
1064
- }
1065
- if (s === "released" && prev === "pressed" && state.button.lastPressTime) {
1066
- state.button.lastHoldDuration = (Date.now() - state.button.lastPressTime) / 1000;
1067
- }
1068
-
1069
- ui.buttonCircle.textContent = s.toUpperCase();
1070
- ui.buttonCircle.classList.toggle("pressed", s === "pressed");
1071
- ui.btnPressCount.textContent = state.button.pressCount;
1072
- ui.btnHoldTime.textContent = state.button.lastHoldDuration.toFixed(1) + "s";
1073
- ui.btnState.textContent = s === "pressed" ? "HELD" : "IDLE";
1074
- }
1075
-
1076
- function onNavigation(d) {
1077
- if (controlMode !== "pointer") return;
1078
- const dx = Number(d.delta_x || 0);
1079
- const dy = Number(d.delta_y || 0);
1080
- state.nav.x = clamp(state.nav.x + dx * 0.9, -120, 120);
1081
- state.nav.y = clamp(state.nav.y + dy * 0.9, -90, 90);
1082
- updateMotionDot();
1083
- }
1084
-
1085
- function onNavDirection(d) {
1086
- state.navDirection = d.direction || "None";
1087
- ui.navdirCenter.textContent = state.navDirection;
1088
- ui.navdirLabel.textContent = state.navDirection;
1089
-
1090
- document.querySelectorAll(".navdir-cell[data-dir]").forEach(c => {
1091
- c.classList.toggle("active", c.dataset.dir === state.navDirection);
1092
- });
1093
- }
1094
-
1095
- function onImuAcc(d) {
1096
- const v = Array.isArray(d.values) ? d.values : [0, 0, 9.81];
1097
- state.imuAcc = [Number(v[0]||0), Number(v[1]||0), Number(v[2]||9.81)];
1098
-
1099
- pushRing(accHistory.x, state.imuAcc[0]);
1100
- pushRing(accHistory.y, state.imuAcc[1]);
1101
- pushRing(accHistory.z, state.imuAcc[2]);
1102
-
1103
- ui.accX.textContent = state.imuAcc[0].toFixed(2);
1104
- ui.accY.textContent = state.imuAcc[1].toFixed(2);
1105
- ui.accZ.textContent = state.imuAcc[2].toFixed(2);
1106
-
1107
- if (controlMode === "imu") projectImu();
1108
- }
1109
-
1110
- function onImuGyro(d) {
1111
- const v = Array.isArray(d.values) ? d.values : [0, 0, 0];
1112
- state.imuGyro = [Number(v[0]||0), Number(v[1]||0), Number(v[2]||0)];
1113
-
1114
- pushRing(gyroHistory.x, state.imuGyro[0]);
1115
- pushRing(gyroHistory.y, state.imuGyro[1]);
1116
- pushRing(gyroHistory.z, state.imuGyro[2]);
1117
-
1118
- ui.gyroX.textContent = state.imuGyro[0].toFixed(2);
1119
- ui.gyroY.textContent = state.imuGyro[1].toFixed(2);
1120
- ui.gyroZ.textContent = state.imuGyro[2].toFixed(2);
1121
-
1122
- if (controlMode === "imu") projectImu();
1123
- }
1124
-
1125
- function onSnc(d) {
1126
- const v = Array.isArray(d.values) ? d.values : [0, 0, 0];
1127
- state.snc = [Number(v[0]||0), Number(v[1]||0), Number(v[2]||0)];
1128
-
1129
- pushRing(sncHistory.ch0, state.snc[0]);
1130
- pushRing(sncHistory.ch1, state.snc[1]);
1131
- pushRing(sncHistory.ch2, state.snc[2]);
1132
-
1133
- ui.sncUlnar.textContent = state.snc[0].toFixed(3);
1134
- ui.sncMedian.textContent = state.snc[1].toFixed(3);
1135
- ui.sncRadial.textContent = state.snc[2].toFixed(3);
1136
- }
1137
-
1138
- function onBattery(d) {
1139
- if (typeof d.level !== "number") return;
1140
- state.battery.level = d.level;
1141
- state.battery.charging = !!d.charging;
1142
-
1143
- ui.batteryPill.style.display = "inline-flex";
1144
- ui.batteryFill.style.width = d.level + "%";
1145
- ui.batteryFill.className = "battery-fill" + (d.level <= 15 ? " low" : d.level <= 40 ? " mid" : "");
1146
- ui.batteryText.textContent = d.level + "%" + (d.charging ? " &#9889;" : "");
1147
- }
1148
-
1149
- // ── Motion helpers ──
1150
- function projectImu() {
1151
- const x = clamp(state.imuAcc[0] * 12 + state.imuGyro[2] * 0.15, -120, 120);
1152
- const y = clamp(-state.imuAcc[1] * 12 + state.imuGyro[0] * 0.15, -90, 90);
1153
- state.nav.x = x;
1154
- state.nav.y = y;
1155
- updateMotionDot();
1156
- }
1157
-
1158
- function updateMotionDot() {
1159
- const stage = ui.motionStage;
1160
- const w = stage.offsetWidth;
1161
- const h = stage.offsetHeight;
1162
- const px = 50 + (state.nav.x / 120) * 45;
1163
- const py = 50 + (state.nav.y / 90) * 45;
1164
- ui.motionDot.style.left = clamp(px, 5, 95) + "%";
1165
- ui.motionDot.style.top = clamp(py, 5, 95) + "%";
1166
- ui.coordX.textContent = state.nav.x.toFixed(1);
1167
- ui.coordY.textContent = state.nav.y.toFixed(1);
1168
- }
1169
-
1170
- function resetMotion() {
1171
- state.nav.x = 0; state.nav.y = 0;
1172
- state.imuAcc = [0, 0, 9.81]; state.imuGyro = [0, 0, 0];
1173
- state.navDirection = "None";
1174
- updateMotionDot();
1175
- ui.navdirCenter.textContent = "NONE";
1176
- ui.navdirLabel.textContent = "None";
1177
- document.querySelectorAll(".navdir-cell[data-dir]").forEach(c => c.classList.remove("active"));
1178
- }
1179
-
1180
- function updateModeUI() {
1181
- document.querySelectorAll(".mode-tab").forEach(t => {
1182
- t.classList.toggle("active", t.dataset.mode === controlMode);
1183
- });
1184
- const showStage = controlMode !== "direction";
1185
- ui.motionStage.style.display = showStage ? "block" : "none";
1186
- ui.motionCoords.style.display = showStage ? "grid" : "none";
1187
- ui.navdirDisplay.classList.toggle("visible", controlMode === "direction");
1188
- }
1189
-
1190
- // ── Graphs ──
1191
- function drawGraph(ctx, canvas, datasets, yRange) {
1192
- const w = canvas._w;
1193
- const h = canvas._h;
1194
- ctx.clearRect(0, 0, w, h);
1195
-
1196
- const [yMin, yMax] = yRange;
1197
- const range = yMax - yMin;
1198
-
1199
- // grid lines
1200
- ctx.strokeStyle = "rgba(148,163,184,0.06)";
1201
- ctx.lineWidth = 0.5;
1202
- for (let i = 0; i <= 4; i++) {
1203
- const gy = (h / 4) * i;
1204
- ctx.beginPath(); ctx.moveTo(0, gy); ctx.lineTo(w, gy); ctx.stroke();
1205
- }
1206
-
1207
- // zero line
1208
- const zeroY = h - ((0 - yMin) / range) * h;
1209
- ctx.strokeStyle = "rgba(148,163,184,0.12)";
1210
- ctx.lineWidth = 0.5;
1211
- ctx.beginPath(); ctx.moveTo(0, zeroY); ctx.lineTo(w, zeroY); ctx.stroke();
1212
-
1213
- datasets.forEach(({ data, color }) => {
1214
- ctx.strokeStyle = color;
1215
- ctx.lineWidth = 1.5;
1216
- ctx.beginPath();
1217
- for (let i = 0; i < data.length; i++) {
1218
- const x = (i / (data.length - 1)) * w;
1219
- const y = h - ((data[i] - yMin) / range) * h;
1220
- i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
1221
- }
1222
- ctx.stroke();
1223
- });
1224
- }
1225
-
1226
- function renderGraphs() {
1227
- if (!sncCtx) return;
1228
-
1229
- drawGraph(sncCtx, ui.sncCanvas, [
1230
- { data: sncHistory.ch0, color: "#77eae9" },
1231
- { data: sncHistory.ch1, color: "#a78bfa" },
1232
- { data: sncHistory.ch2, color: "#f472b6" }
1233
- ], [-1, 1]);
1234
-
1235
- drawGraph(accCtx, ui.accCanvas, [
1236
- { data: accHistory.x, color: "#77eae9" },
1237
- { data: accHistory.y, color: "#a78bfa" },
1238
- { data: accHistory.z, color: "#f472b6" }
1239
- ], [-20, 20]);
1240
-
1241
- drawGraph(gyroCtx, ui.gyroCanvas, [
1242
- { data: gyroHistory.x, color: "#77eae9" },
1243
- { data: gyroHistory.y, color: "#a78bfa" },
1244
- { data: gyroHistory.z, color: "#f472b6" }
1245
- ], [-250, 250]);
1246
- }
1247
-
1248
- // ── Gesture History ──
1249
- function renderGestureHistory() {
1250
- ui.gestureHistory.innerHTML = gestureHistory.slice(0, 15).map(g =>
1251
- `<div class="gesture-history-item"><span class="type">${g.type}</span><span>${(g.conf * 100).toFixed(0)}%</span><span>${g.time}</span></div>`
1252
- ).join("");
1253
- }
1254
-
1255
- // ── Log ──
1256
- function addLog(category, type, data) {
1257
- const ts = new Date().toLocaleTimeString("en-US", { hour12: false, fractionalSecondDigits: 2 });
1258
- logEntries.unshift({ ts, category, type, data });
1259
- if (logEntries.length > MAX_LOG) logEntries.pop();
1260
- renderLog();
1261
- }
1262
-
1263
- function renderLog() {
1264
- const filtered = logFilter === "all"
1265
- ? logEntries
1266
- : logEntries.filter(e => e.category === logFilter);
1267
-
1268
- ui.logPre.innerHTML = filtered.slice(0, 80).map(e => {
1269
- const cls = e.category === "cmd" ? "cmd" : e.category === "status" ? "err" : "sig";
1270
- const d = typeof e.data === "object" ? JSON.stringify(e.data) : String(e.data);
1271
- return `<div class="log-line"><span class="ts">${e.ts}</span> <span class="${cls}">[${e.type}]</span> ${d}</div>`;
1272
- }).join("");
1273
- }
1274
-
1275
- // ── Event Bindings ──
1276
- // Mode tabs
1277
- document.querySelectorAll(".mode-tab").forEach(tab => {
1278
- tab.addEventListener("click", () => switchMode(tab.dataset.mode));
1279
- });
1280
-
1281
- // Simulate gesture buttons (both in gesture panel & controls panel)
1282
- document.querySelectorAll("[data-sim]").forEach(btn => {
1283
- btn.addEventListener("click", () => {
1284
- const type = btn.dataset.sim;
1285
- send({ command: "trigger_gesture", data: { type } });
1286
- onGesture({ type, confidence: 1.0, timestamp: Date.now() });
1287
- });
1288
- });
1289
- document.querySelectorAll(".gesture-btn").forEach(btn => {
1290
- btn.addEventListener("click", () => {
1291
- const type = btn.dataset.gesture;
1292
- send({ command: "trigger_gesture", data: { type } });
1293
- onGesture({ type, confidence: 1.0, timestamp: Date.now() });
1294
- });
1295
- });
1296
-
1297
- // Subscription toggles
1298
- document.querySelectorAll(".toggle-switch[data-sub]").forEach(toggle => {
1299
- toggle.addEventListener("click", () => {
1300
- const signal = toggle.dataset.sub;
1301
- if (activeSubs.has(signal)) {
1302
- unsubscribeFrom(signal);
1303
- } else {
1304
- subscribeTo(signal);
1305
- }
1306
- });
1307
- });
1308
-
1309
- // Command buttons
1310
- $("cmdResubscribe").addEventListener("click", () => {
1311
- // Unsubscribe all, then resubscribe everything currently toggled on
1312
- ALL_SIGNALS.forEach(s => send({ command: "unsubscribe", signal: s }));
1313
- activeSubs.forEach(s => send({ command: "subscribe", signal: s }));
1314
- addLog("cmd", "resubscribe", { signals: [...activeSubs] });
1315
- });
1316
- $("cmdGetStatus").addEventListener("click", () => {
1317
- send({ command: "get_status" });
1318
- addLog("cmd", "get_status", {});
1319
- });
1320
- $("cmdGetSubs").addEventListener("click", () => {
1321
- send({ command: "get_subscriptions" });
1322
- addLog("cmd", "get_subscriptions", {});
1323
- });
1324
- $("cmdGetDocs").addEventListener("click", () => {
1325
- send({ command: "get_docs" });
1326
- addLog("cmd", "get_docs", {});
1327
- });
1328
-
1329
- // Log filters
1330
- document.querySelectorAll(".log-filter").forEach(btn => {
1331
- btn.addEventListener("click", () => {
1332
- document.querySelectorAll(".log-filter").forEach(b => b.classList.remove("active"));
1333
- btn.classList.add("active");
1334
- logFilter = btn.dataset.filter;
1335
- renderLog();
1336
- });
1337
- });
1338
- $("clearLog").addEventListener("click", () => {
1339
- logEntries.length = 0;
1340
- renderLog();
1341
- });
1342
-
1343
- // ── Keyboard Fallback ──
1344
- document.addEventListener("keydown", (e) => {
1345
- if (e.repeat) return;
1346
-
1347
- if (e.code === "Space") {
1348
- e.preventDefault();
1349
- send({ command: "trigger_gesture", data: { type: "tap" } });
1350
- onGesture({ type: "tap", confidence: 1.0 });
1351
- }
1352
- if (e.key === "Shift") {
1353
- onButton({ state: "pressed" });
1354
- }
1355
- if (e.key === "[") {
1356
- onPressure({ value: Math.max(0, state.pressure.value - 8), normalized: clamp01(state.pressure.smoothed - 0.08) });
1357
- }
1358
- if (e.key === "]") {
1359
- onPressure({ value: Math.min(100, state.pressure.value + 8), normalized: clamp01(state.pressure.smoothed + 0.08) });
1360
- }
1361
- if (e.key === "1") { send({ command: "trigger_gesture", data: { type: "tap" } }); onGesture({ type: "tap", confidence: 1.0 }); }
1362
- if (e.key === "2") { send({ command: "trigger_gesture", data: { type: "double_tap" } }); onGesture({ type: "double_tap", confidence: 1.0 }); }
1363
- if (e.key === "3") { send({ command: "trigger_gesture", data: { type: "twist" } }); onGesture({ type: "twist", confidence: 1.0 }); }
1364
- if (e.key === "4") { send({ command: "trigger_gesture", data: { type: "double_twist" } }); onGesture({ type: "double_twist", confidence: 1.0 }); }
1365
- if (e.key === "m" || e.key === "M") {
1366
- const modes = ["pointer", "direction", "imu"];
1367
- switchMode(modes[(modes.indexOf(controlMode) + 1) % modes.length]);
1368
- }
1369
-
1370
- if (e.key.startsWith("Arrow")) {
1371
- e.preventDefault();
1372
- const step = 9;
1373
- if (controlMode === "pointer") {
1374
- if (e.key === "ArrowLeft") onNavigation({ delta_x: -step, delta_y: 0 });
1375
- if (e.key === "ArrowRight") onNavigation({ delta_x: step, delta_y: 0 });
1376
- if (e.key === "ArrowUp") onNavigation({ delta_x: 0, delta_y: -step });
1377
- if (e.key === "ArrowDown") onNavigation({ delta_x: 0, delta_y: step });
1378
- } else if (controlMode === "direction") {
1379
- const map = { ArrowLeft: "Left", ArrowRight: "Right", ArrowUp: "Up", ArrowDown: "Down" };
1380
- onNavDirection({ direction: map[e.key] || "None" });
1381
- } else {
1382
- const ax = state.imuAcc[0] + (e.key === "ArrowRight" ? 0.5 : e.key === "ArrowLeft" ? -0.5 : 0);
1383
- const ay = state.imuAcc[1] + (e.key === "ArrowDown" ? 0.5 : e.key === "ArrowUp" ? -0.5 : 0);
1384
- onImuAcc({ values: [ax, ay, 9.81] });
1385
- onImuGyro({ values: [ay * 18, ax * 18, ax * 9] });
1386
- }
1387
- }
1388
- });
1389
-
1390
- document.addEventListener("keyup", (e) => {
1391
- if (e.key === "Shift") onButton({ state: "released" });
1392
- });
1393
-
1394
- // ── Stage drag fallback ──
1395
- let stageDrag = false;
1396
- ui.motionStage.addEventListener("pointerdown", (e) => { stageDrag = true; ui.motionStage.setPointerCapture(e.pointerId); });
1397
- ui.motionStage.addEventListener("pointerup", () => { stageDrag = false; });
1398
- ui.motionStage.addEventListener("pointermove", (e) => {
1399
- if (!stageDrag) return;
1400
- const rect = ui.motionStage.getBoundingClientRect();
1401
- const xn = ((e.clientX - rect.left) / rect.width) * 2 - 1;
1402
- const yn = ((e.clientY - rect.top) / rect.height) * 2 - 1;
1403
- if (controlMode === "pointer") {
1404
- state.nav.x = clamp(xn * 120, -120, 120);
1405
- state.nav.y = clamp(yn * 90, -90, 90);
1406
- updateMotionDot();
1407
- } else if (controlMode === "imu") {
1408
- onImuAcc({ values: [xn * 2, -yn * 2, 9.81] });
1409
- onImuGyro({ values: [yn * 30, xn * 30, xn * 15] });
1410
- }
1411
- });
1412
-
1413
- // ── Utilities ──
1414
- function pushRing(arr, val) {
1415
- arr.push(val);
1416
- if (arr.length > GRAPH_POINTS) arr.shift();
1417
- }
1418
-
1419
- function flashEl(el, cls) {
1420
- el.classList.remove(cls);
1421
- void el.offsetWidth;
1422
- el.classList.add(cls);
1423
- }
1424
-
1425
- function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); }
1426
- function clamp01(v) { return Number.isFinite(v) ? clamp(v, 0, 1) : 0; }
1427
-
1428
- // ── Render Loop ──
1429
- let graphFrame = 0;
1430
- function frame() {
1431
- graphFrame++;
1432
- if (graphFrame % 2 === 0) renderGraphs(); // ~30fps graphs
1433
- if (graphFrame % 30 === 0) updatePacketCounts(); // ~2fps packet count update
1434
- requestAnimationFrame(frame);
1435
- }
1436
-
1437
- // ── Init ──
1438
- window.addEventListener("load", () => {
1439
- initCanvases();
1440
- updateModeUI();
1441
- syncAllToggles();
1442
- connect();
1443
- frame();
1444
- });
1445
-
1446
- window.addEventListener("resize", () => {
1447
- initCanvases();
1448
- });
1449
- </script>
1450
- </body>
1451
- </html>