mudra-skills 1.0.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.
Files changed (149) hide show
  1. package/.claude-plugin/plugin.json +19 -0
  2. package/CLAUDE.md +35 -0
  3. package/bin/install.js +25 -0
  4. package/package.json +28 -0
  5. package/skills/mudra-master/SKILL.md +202 -0
  6. package/skills/mudra-master/mudra-preview/SKILL.md +151 -0
  7. package/skills/mudra-master/mudra-preview/assets/ar-menu.html +382 -0
  8. package/skills/mudra-master/mudra-preview/assets/document-scroller.html +468 -0
  9. package/skills/mudra-master/mudra-preview/assets/drum-machine.html +788 -0
  10. package/skills/mudra-master/mudra-preview/assets/emg-visualizer.html +390 -0
  11. package/skills/mudra-master/mudra-preview/assets/generative-art.html +408 -0
  12. package/skills/mudra-master/mudra-preview/assets/gesture-assistant.html +342 -0
  13. package/skills/mudra-master/mudra-preview/assets/gesture-speech.html +444 -0
  14. package/skills/mudra-master/mudra-preview/assets/hands-free-desktop.html +376 -0
  15. package/skills/mudra-master/mudra-preview/assets/model-rotator.html +365 -0
  16. package/skills/mudra-master/mudra-preview/assets/mudra-duel.html +3200 -0
  17. package/skills/mudra-master/mudra-preview/assets/mudra-monitor.html +1451 -0
  18. package/skills/mudra-master/mudra-preview/assets/mudra-ultimate-template.html +891 -0
  19. package/skills/mudra-master/mudra-preview/assets/music-sequencer.html +455 -0
  20. package/skills/mudra-master/mudra-preview/assets/neural-pong.html +380 -0
  21. package/skills/mudra-master/mudra-preview/assets/neural-snake.html +364 -0
  22. package/skills/mudra-master/mudra-preview/assets/presentation-controller.html +345 -0
  23. package/skills/mudra-master/mudra-preview/assets/pressure-painter.html +285 -0
  24. package/skills/mudra-master/mudra-preview/assets/runner.html +1812 -0
  25. package/skills/mudra-master/mudra-preview/assets/smart-home.html +461 -0
  26. package/skills/mudra-master/mudra-preview/assets/space-invaders.html +1154 -0
  27. package/skills/mudra-master/mudra-preview/assets/waterful-ring-toss.html +1624 -0
  28. package/skills/mudra-master/mudra-preview/references/agent_protocol.json +646 -0
  29. package/skills/mudra-master/mudra-preview/references/promt.md +16829 -0
  30. package/skills/mudra-master/mudra-xr/SKILL.md +245 -0
  31. package/skills/mudra-master/mudra-xr/assets/demos/3dgs-walkthrough.html +242 -0
  32. package/skills/mudra-master/mudra-xr/assets/demos/aisimulator.html +792 -0
  33. package/skills/mudra-master/mudra-xr/assets/demos/balloonpop.html +422 -0
  34. package/skills/mudra-master/mudra-xr/assets/demos/ballpit.html +284 -0
  35. package/skills/mudra-master/mudra-xr/assets/demos/drone.html +41 -0
  36. package/skills/mudra-master/mudra-xr/assets/demos/gemini-icebreakers.html +338 -0
  37. package/skills/mudra-master/mudra-xr/assets/demos/gemini-xrobject.html +302 -0
  38. package/skills/mudra-master/mudra-xr/assets/demos/math3d.html +188 -0
  39. package/skills/mudra-master/mudra-xr/assets/demos/measure.html +247 -0
  40. package/skills/mudra-master/mudra-xr/assets/demos/occlusion.html +251 -0
  41. package/skills/mudra-master/mudra-xr/assets/demos/rain.html +498 -0
  42. package/skills/mudra-master/mudra-xr/assets/demos/screenwiper.html +432 -0
  43. package/skills/mudra-master/mudra-xr/assets/demos/splash.html +519 -0
  44. package/skills/mudra-master/mudra-xr/assets/demos/webcam_gestures.html +253 -0
  45. package/skills/mudra-master/mudra-xr/assets/demos/xremoji.html +833 -0
  46. package/skills/mudra-master/mudra-xr/assets/demos/xrpoet.html +151 -0
  47. package/skills/mudra-master/mudra-xr/assets/samples/depthmap.html +266 -0
  48. package/skills/mudra-master/mudra-xr/assets/samples/depthmesh.html +100 -0
  49. package/skills/mudra-master/mudra-xr/assets/samples/game_rps.html +1099 -0
  50. package/skills/mudra-master/mudra-xr/assets/samples/gestures_custom.html +467 -0
  51. package/skills/mudra-master/mudra-xr/assets/samples/gestures_heuristic.html +256 -0
  52. package/skills/mudra-master/mudra-xr/assets/samples/lighting.html +163 -0
  53. package/skills/mudra-master/mudra-xr/assets/samples/mesh_detection.html +46 -0
  54. package/skills/mudra-master/mudra-xr/assets/samples/modelviewer.html +183 -0
  55. package/skills/mudra-master/mudra-xr/assets/samples/paint.html +133 -0
  56. package/skills/mudra-master/mudra-xr/assets/samples/planar-vst.html +45 -0
  57. package/skills/mudra-master/mudra-xr/assets/samples/reticle.html +95 -0
  58. package/skills/mudra-master/mudra-xr/assets/samples/skybox_agent.html +384 -0
  59. package/skills/mudra-master/mudra-xr/assets/samples/sound.html +390 -0
  60. package/skills/mudra-master/mudra-xr/assets/samples/ui.html +496 -0
  61. package/skills/mudra-master/mudra-xr/assets/samples/virtual-screens.html +808 -0
  62. package/skills/mudra-master/mudra-xr/assets/templates/0_basic.html +64 -0
  63. package/skills/mudra-master/mudra-xr/assets/templates/1_ui.html +107 -0
  64. package/skills/mudra-master/mudra-xr/assets/templates/2_hands.html +183 -0
  65. package/skills/mudra-master/mudra-xr/assets/templates/3_depth.html +89 -0
  66. package/skills/mudra-master/mudra-xr/assets/templates/4_stereo.html +84 -0
  67. package/skills/mudra-master/mudra-xr/assets/templates/5_camera.html +117 -0
  68. package/skills/mudra-master/mudra-xr/assets/templates/6_ai.html +154 -0
  69. package/skills/mudra-master/mudra-xr/assets/templates/7_ai_live.html +253 -0
  70. package/skills/mudra-master/mudra-xr/assets/templates/8_objects.html +95 -0
  71. package/skills/mudra-master/mudra-xr/assets/templates/9_xr-toggle.html +90 -0
  72. package/skills/mudra-master/mudra-xr/assets/templates/heuristic_hand_gestures.html +102 -0
  73. package/skills/mudra-master/mudra-xr/assets/templates/meshes.html +47 -0
  74. package/skills/mudra-master/mudra-xr/assets/templates/planes.html +47 -0
  75. package/skills/mudra-master/mudra-xr/assets/templates/uikit.html +187 -0
  76. package/skills/mudra-master/mudra-xr/references/agent_protocol.json +878 -0
  77. package/skills/mudra-master/mudra-xr/references/promt.md +1516 -0
  78. package/skills/mudra-preview/SKILL.md +151 -0
  79. package/skills/mudra-preview/assets/ar-menu.html +382 -0
  80. package/skills/mudra-preview/assets/document-scroller.html +468 -0
  81. package/skills/mudra-preview/assets/drum-machine.html +788 -0
  82. package/skills/mudra-preview/assets/emg-visualizer.html +390 -0
  83. package/skills/mudra-preview/assets/generative-art.html +408 -0
  84. package/skills/mudra-preview/assets/gesture-assistant.html +342 -0
  85. package/skills/mudra-preview/assets/gesture-speech.html +444 -0
  86. package/skills/mudra-preview/assets/hands-free-desktop.html +376 -0
  87. package/skills/mudra-preview/assets/model-rotator.html +365 -0
  88. package/skills/mudra-preview/assets/mudra-duel.html +3200 -0
  89. package/skills/mudra-preview/assets/mudra-monitor.html +1451 -0
  90. package/skills/mudra-preview/assets/mudra-ultimate-template.html +891 -0
  91. package/skills/mudra-preview/assets/music-sequencer.html +455 -0
  92. package/skills/mudra-preview/assets/neural-pong.html +380 -0
  93. package/skills/mudra-preview/assets/neural-snake.html +364 -0
  94. package/skills/mudra-preview/assets/presentation-controller.html +345 -0
  95. package/skills/mudra-preview/assets/pressure-painter.html +285 -0
  96. package/skills/mudra-preview/assets/runner.html +1812 -0
  97. package/skills/mudra-preview/assets/smart-home.html +461 -0
  98. package/skills/mudra-preview/assets/space-invaders.html +1154 -0
  99. package/skills/mudra-preview/assets/waterful-ring-toss.html +1624 -0
  100. package/skills/mudra-preview/references/agent_protocol.json +646 -0
  101. package/skills/mudra-preview/references/promt.md +16829 -0
  102. package/skills/mudra-xr/SKILL.md +245 -0
  103. package/skills/mudra-xr/assets/demos/3dgs-walkthrough.html +242 -0
  104. package/skills/mudra-xr/assets/demos/aisimulator.html +792 -0
  105. package/skills/mudra-xr/assets/demos/balloonpop.html +422 -0
  106. package/skills/mudra-xr/assets/demos/ballpit.html +284 -0
  107. package/skills/mudra-xr/assets/demos/drone.html +41 -0
  108. package/skills/mudra-xr/assets/demos/gemini-icebreakers.html +338 -0
  109. package/skills/mudra-xr/assets/demos/gemini-xrobject.html +302 -0
  110. package/skills/mudra-xr/assets/demos/math3d.html +188 -0
  111. package/skills/mudra-xr/assets/demos/measure.html +247 -0
  112. package/skills/mudra-xr/assets/demos/occlusion.html +251 -0
  113. package/skills/mudra-xr/assets/demos/rain.html +498 -0
  114. package/skills/mudra-xr/assets/demos/screenwiper.html +432 -0
  115. package/skills/mudra-xr/assets/demos/splash.html +519 -0
  116. package/skills/mudra-xr/assets/demos/webcam_gestures.html +253 -0
  117. package/skills/mudra-xr/assets/demos/xremoji.html +833 -0
  118. package/skills/mudra-xr/assets/demos/xrpoet.html +151 -0
  119. package/skills/mudra-xr/assets/samples/depthmap.html +266 -0
  120. package/skills/mudra-xr/assets/samples/depthmesh.html +100 -0
  121. package/skills/mudra-xr/assets/samples/game_rps.html +1099 -0
  122. package/skills/mudra-xr/assets/samples/gestures_custom.html +467 -0
  123. package/skills/mudra-xr/assets/samples/gestures_heuristic.html +256 -0
  124. package/skills/mudra-xr/assets/samples/lighting.html +163 -0
  125. package/skills/mudra-xr/assets/samples/mesh_detection.html +46 -0
  126. package/skills/mudra-xr/assets/samples/modelviewer.html +183 -0
  127. package/skills/mudra-xr/assets/samples/paint.html +133 -0
  128. package/skills/mudra-xr/assets/samples/planar-vst.html +45 -0
  129. package/skills/mudra-xr/assets/samples/reticle.html +95 -0
  130. package/skills/mudra-xr/assets/samples/skybox_agent.html +384 -0
  131. package/skills/mudra-xr/assets/samples/sound.html +390 -0
  132. package/skills/mudra-xr/assets/samples/ui.html +496 -0
  133. package/skills/mudra-xr/assets/samples/virtual-screens.html +808 -0
  134. package/skills/mudra-xr/assets/templates/0_basic.html +64 -0
  135. package/skills/mudra-xr/assets/templates/1_ui.html +107 -0
  136. package/skills/mudra-xr/assets/templates/2_hands.html +183 -0
  137. package/skills/mudra-xr/assets/templates/3_depth.html +89 -0
  138. package/skills/mudra-xr/assets/templates/4_stereo.html +84 -0
  139. package/skills/mudra-xr/assets/templates/5_camera.html +117 -0
  140. package/skills/mudra-xr/assets/templates/6_ai.html +154 -0
  141. package/skills/mudra-xr/assets/templates/7_ai_live.html +253 -0
  142. package/skills/mudra-xr/assets/templates/8_objects.html +95 -0
  143. package/skills/mudra-xr/assets/templates/9_xr-toggle.html +90 -0
  144. package/skills/mudra-xr/assets/templates/heuristic_hand_gestures.html +102 -0
  145. package/skills/mudra-xr/assets/templates/meshes.html +47 -0
  146. package/skills/mudra-xr/assets/templates/planes.html +47 -0
  147. package/skills/mudra-xr/assets/templates/uikit.html +187 -0
  148. package/skills/mudra-xr/references/agent_protocol.json +878 -0
  149. package/skills/mudra-xr/references/promt.md +1516 -0
@@ -0,0 +1,1451 @@
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>