DavidVivancos commited on
Commit
d5c7911
Β·
verified Β·
1 Parent(s): 02fe11c

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +854 -19
index.html CHANGED
@@ -1,19 +1,854 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.0">
6
+ <title>Neuraxon 2.0 β€” Sphero Brain + Word Writer</title>
7
+ <style>
8
+ @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Syne:wght@400;500;600;700;800&display=swap');
9
+ :root {
10
+ --bg:#06080e;--panel:#0b1019;--panel2:#0f1623;--border:rgba(120,200,255,0.08);
11
+ --border2:rgba(120,200,255,0.18);--txt:#c8ddf0;--txt2:#6a8ca8;--txt3:#384f66;
12
+ --exc:#ff3d5a;--inh:#3d7aff;--neu:#2a3548;--cyan:#00e5ff;--green:#00e676;
13
+ --amber:#ffab00;--da:#ff6d00;--sht:#aa00ff;--ach:#00e676;--na:#ff1744;
14
+ }
15
+ *{box-sizing:border-box;margin:0;padding:0}
16
+ html,body{height:100%;background:var(--bg);color:var(--txt);font-family:'JetBrains Mono',monospace;overflow-x:hidden}
17
+
18
+ .header{display:flex;align-items:center;justify-content:space-between;padding:10px 16px;border-bottom:1px solid var(--border);background:linear-gradient(180deg,rgba(11,16,25,.95),rgba(6,8,14,.9));backdrop-filter:blur(12px);position:sticky;top:0;z-index:100}
19
+ .header h1{font-family:'Syne',sans-serif;font-size:15px;font-weight:700}
20
+ .header h1 span{color:var(--cyan)} .header h1 em{font-style:normal;color:var(--txt2);font-weight:400;font-size:10px;margin-left:8px}
21
+ .conn-badge{font-size:9px;font-weight:600;padding:4px 12px;border-radius:20px;border:1px solid var(--border2);text-transform:uppercase;letter-spacing:.1em}
22
+ .conn-badge.off{color:var(--exc);border-color:rgba(255,61,90,.3)} .conn-badge.on{color:var(--green);border-color:rgba(0,230,118,.3);box-shadow:0 0 12px rgba(0,230,118,.15)}
23
+
24
+ .tabs{display:flex;border-bottom:1px solid var(--border);background:var(--panel)}
25
+ .tab{flex:1;padding:10px;text-align:center;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.1em;color:var(--txt3);cursor:pointer;border-bottom:2px solid transparent;font-family:'Syne',sans-serif}
26
+ .tab:hover{color:var(--txt2)} .tab.active{color:var(--cyan);border-bottom-color:var(--cyan);background:rgba(0,229,255,.03)}
27
+
28
+ .app{display:grid;grid-template-columns:1fr 290px;height:calc(100vh - 83px)}
29
+ @media(max-width:900px){.app{grid-template-columns:1fr}}
30
+
31
+ .view-panel{position:relative;overflow:hidden;border-right:1px solid var(--border)}
32
+ .view-panel canvas{width:100%;height:100%;display:block}
33
+ .net-overlay{position:absolute;top:10px;left:10px;pointer-events:none;font-size:9px;color:var(--txt3);line-height:1.6}
34
+ .net-overlay b{color:var(--txt2)}
35
+ .view-panel[data-hidden]{display:none}
36
+
37
+ .sidebar{overflow-y:auto;display:flex;flex-direction:column;gap:0;background:var(--panel);scrollbar-width:thin;scrollbar-color:var(--border) transparent}
38
+ .sb-section{padding:12px 14px;border-bottom:1px solid var(--border)}
39
+ .sb-section h3{font-family:'Syne',sans-serif;font-size:9px;text-transform:uppercase;letter-spacing:.12em;color:var(--txt3);margin-bottom:8px;font-weight:600}
40
+
41
+ .btn-row{display:flex;gap:6px;flex-wrap:wrap}
42
+ .btn{padding:7px 14px;border:1px solid var(--border2);border-radius:6px;background:var(--panel2);color:var(--txt);font-family:inherit;font-size:10px;font-weight:500;cursor:pointer;transition:all .2s;flex:1;text-align:center}
43
+ .btn:hover{background:rgba(0,229,255,.06);border-color:var(--cyan);color:var(--cyan)}
44
+ .btn:disabled{opacity:.3;cursor:not-allowed}
45
+ .btn.primary{background:linear-gradient(135deg,rgba(0,229,255,.15),rgba(61,122,255,.15));border-color:rgba(0,229,255,.3);color:var(--cyan)}
46
+ .btn.danger{border-color:rgba(255,61,90,.3);color:var(--exc)}
47
+ .btn.write-btn{background:linear-gradient(135deg,rgba(255,171,0,.2),rgba(255,109,0,.2));border-color:rgba(255,171,0,.4);color:var(--amber);font-size:11px;font-weight:700}
48
+ .btn.write-btn:hover{box-shadow:0 0 16px rgba(255,171,0,.2)}
49
+
50
+ .slider-group{margin-bottom:6px}
51
+ .slider-label{display:flex;justify-content:space-between;font-size:9px;color:var(--txt2);margin-bottom:2px}
52
+ .slider-label .val{color:var(--cyan);font-weight:600}
53
+ input[type=range]{-webkit-appearance:none;width:100%;height:3px;border-radius:2px;background:var(--border);outline:none}
54
+ input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px;border-radius:50%;background:var(--cyan);cursor:pointer;border:2px solid var(--bg)}
55
+
56
+ .sensor-grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:5px}
57
+ .sensor-btn{padding:8px 4px;border:1px solid var(--border);border-radius:5px;background:var(--panel2);color:var(--txt2);font-family:inherit;font-size:8px;font-weight:500;cursor:pointer;text-align:center;user-select:none}
58
+ .sensor-btn.active-exc{background:rgba(255,61,90,.12);border-color:var(--exc);color:var(--exc)}
59
+ .sensor-btn.active-inh{background:rgba(61,122,255,.12);border-color:var(--inh);color:var(--inh)}
60
+
61
+ .nm-slider{margin-bottom:5px}
62
+ .nm-label{display:flex;align-items:center;gap:5px;font-size:9px;margin-bottom:1px}
63
+ .nm-dot{width:7px;height:7px;border-radius:50%;display:inline-block}
64
+
65
+ .output-row{display:flex;gap:6px;align-items:center;margin-bottom:5px}
66
+ .output-bar-wrap{flex:1;height:8px;background:var(--border);border-radius:4px;overflow:hidden}
67
+ .output-bar{height:100%;border-radius:4px;transition:width .1s}
68
+ .output-label{font-size:9px;color:var(--txt2);width:50px}
69
+ .output-val{font-size:9px;color:var(--cyan);width:40px;text-align:right}
70
+
71
+ .log{font-size:8px;line-height:1.5;color:var(--txt3);padding:8px 14px;max-height:100px;overflow-y:auto;border-top:1px solid var(--border);background:rgba(0,0,0,.3);flex-shrink:0}
72
+
73
+ .stats-row{display:flex;gap:6px} .stat{text-align:center;flex:1}
74
+ .stat .num{font-size:14px;font-weight:700;color:var(--cyan);font-family:'Syne',sans-serif}
75
+ .stat .lbl{font-size:7px;color:var(--txt3);text-transform:uppercase;letter-spacing:.1em}
76
+
77
+ .mode-row{display:flex;gap:3px;margin-bottom:6px}
78
+ .mode-btn{flex:1;padding:5px;font-family:inherit;font-size:8px;font-weight:600;text-align:center;border:1px solid var(--border);border-radius:4px;background:transparent;color:var(--txt3);cursor:pointer}
79
+ .mode-btn.active{background:rgba(0,229,255,.08);border-color:var(--cyan);color:var(--cyan)}
80
+
81
+ .word-input-wrap{display:flex;gap:6px;margin-bottom:8px}
82
+ .word-input{flex:1;padding:8px 10px;border:1px solid var(--border2);border-radius:6px;background:var(--bg);color:var(--amber);font-family:'Syne',sans-serif;font-size:18px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;outline:none;text-align:center}
83
+ .word-input::placeholder{color:var(--txt3);font-size:12px;font-weight:400}
84
+ .word-input:focus{border-color:var(--amber);box-shadow:0 0 12px rgba(255,171,0,.1)}
85
+
86
+ .progress-wrap{margin-top:8px}
87
+ .progress-bar-outer{width:100%;height:6px;background:var(--border);border-radius:3px;overflow:hidden}
88
+ .progress-bar-inner{height:100%;border-radius:3px;background:linear-gradient(90deg,var(--cyan),var(--amber));transition:width .2s;width:0%}
89
+ .progress-label{display:flex;justify-content:space-between;font-size:8px;color:var(--txt3);margin-top:3px}
90
+
91
+ .letter-preview{display:flex;gap:4px;margin-top:8px;flex-wrap:wrap}
92
+ .letter-chip{padding:4px 8px;border:1px solid var(--border);border-radius:4px;font-family:'Syne',sans-serif;font-size:11px;font-weight:700;color:var(--txt3);transition:all .3s}
93
+ .letter-chip.done{color:var(--green);border-color:rgba(0,230,118,.3);background:rgba(0,230,118,.05)}
94
+ .letter-chip.active{color:var(--amber);border-color:rgba(255,171,0,.5);background:rgba(255,171,0,.08);box-shadow:0 0 8px rgba(255,171,0,.15)}
95
+
96
+ .led-preview{width:100%;height:20px;border-radius:6px;margin-top:6px;border:1px solid var(--border);transition:background .3s}
97
+ .write-status-live{font-size:9px;color:var(--amber);text-align:center;padding:6px;font-weight:600;font-family:'Syne',sans-serif}
98
+ </style>
99
+ </head>
100
+ <body>
101
+
102
+ <div class="header">
103
+ <h1><span><a href="https://github.com/DavidVivancos/Neuraxon"> Neuraxon 2.0</a> Mini Writer By <a href="https://www.vivancos.com/">David Vivancos</a> & <a href="https://josesanchezgarcia.com/">Jose Sanchez</a> for <a href="https://qubic.org/">Qubic</a> Open Science </span> <em>Using a <a href="https://sphero.com/collections/mini"> Sphero Mini<a> with a Neuraxon 2.0 Brain</em></h1>
104
+ <div class="conn-badge off" id="badge">OFFLINE</div>
105
+ </div>
106
+
107
+ <div class="tabs">
108
+ <div class="tab active" data-tab="brain" id="tabBrain">🧠 Brain View</div>
109
+ <div class="tab" data-tab="writer" id="tabWriter">✍️ Word Writer</div>
110
+ </div>
111
+
112
+ <div class="app">
113
+ <div class="view-panel" id="panelBrain">
114
+ <canvas id="netCanvas"></canvas>
115
+ <div class="net-overlay" id="overlay">
116
+ <b>t</b>=<span id="oTime">0.000</span>s <b>step</b>=<span id="oStep">0</span>
117
+ <b>energy</b>=<span id="oEnergy">0.00</span> <b>active</b>=<span id="oActive">0</span>/<span id="oTotal">0</span>
118
+ </div>
119
+ </div>
120
+ <div class="view-panel" id="panelWriter" data-hidden>
121
+ <canvas id="pathCanvas"></canvas>
122
+ <div class="net-overlay">
123
+ <b>Word Path</b> β€” <span style="color:var(--green)">━</span> done
124
+ <span style="color:var(--txt3)">β”ˆ</span> planned
125
+ <span style="color:var(--amber)">●</span> current
126
+ </div>
127
+ </div>
128
+
129
+ <div class="sidebar">
130
+ <div class="sb-section">
131
+ <h3>Connection</h3>
132
+ <div class="btn-row">
133
+ <button class="btn primary" id="btnConnect">Connect BLE</button>
134
+ <button class="btn danger" id="btnDisconnect" disabled>Disconnect</button>
135
+ </div>
136
+ <div class="btn-row" style="margin-top:6px">
137
+ <button class="btn" id="btnTestMotor" disabled>πŸ”§ Test Motor</button>
138
+ <button class="btn" id="btnTestLED" disabled>πŸ’‘ Test LED</button>
139
+ </div>
140
+ </div>
141
+
142
+ <div class="sb-section">
143
+ <h3>✍️ Write a Word on the Ground</h3>
144
+ <div class="word-input-wrap">
145
+ <input class="word-input" id="wordInput" placeholder="QUBIC" maxlength="12" value="QUBIC">
146
+ </div>
147
+ <div class="slider-group">
148
+ <div class="slider-label"><span>Letter Size (cm)</span><span class="val" id="vSize">40</span></div>
149
+ <input type="range" min="15" max="100" value="40" id="sSize">
150
+ </div>
151
+ <div class="slider-group">
152
+ <div class="slider-label"><span>Speed (0-255)</span><span class="val" id="vWSpeed">60</span></div>
153
+ <input type="range" min="20" max="180" value="60" id="sWSpeed">
154
+ </div>
155
+ <div class="slider-group">
156
+ <div class="slider-label"><span>Segment time (ms)</span><span class="val" id="vSegTime">400</span></div>
157
+ <input type="range" min="100" max="1500" value="400" id="sSegTime">
158
+ </div>
159
+ <div class="btn-row" style="margin-top:6px">
160
+ <button class="btn write-btn" id="btnWrite">✍️ WRITE</button>
161
+ <button class="btn danger" id="btnAbort" disabled>Stop</button>
162
+ </div>
163
+ <div class="letter-preview" id="letterPreview"></div>
164
+ <div class="progress-wrap">
165
+ <div class="progress-bar-outer"><div class="progress-bar-inner" id="writeProgress"></div></div>
166
+ <div class="progress-label"><span id="writeStatusText">Ready</span><span id="writePercent">0%</span></div>
167
+ </div>
168
+ <div class="write-status-live" id="writeLive"></div>
169
+ <div class="led-preview" id="ledPreview" style="background:#111"></div>
170
+ </div>
171
+
172
+ <div class="sb-section">
173
+ <h3>Simulation</h3>
174
+ <div class="btn-row" style="margin-bottom:6px">
175
+ <button class="btn" id="btnRun">β–Ά Run</button>
176
+ <button class="btn" id="btnStep">Step</button>
177
+ <button class="btn" id="btnReset">Reset</button>
178
+ </div>
179
+ </div>
180
+
181
+ <div class="sb-section">
182
+ <h3>Network</h3>
183
+ <div class="stats-row">
184
+ <div class="stat"><div class="num" id="sExc">0</div><div class="lbl">Exc +1</div></div>
185
+ <div class="stat"><div class="num" id="sNeu">0</div><div class="lbl">Neu 0</div></div>
186
+ <div class="stat"><div class="num" id="sInh">0</div><div class="lbl">Inh βˆ’1</div></div>
187
+ </div>
188
+ </div>
189
+
190
+ <div class="sb-section">
191
+ <h3>Sensory Input</h3>
192
+ <div class="mode-row">
193
+ <button class="mode-btn active" data-mode="manual" id="modeManual">Manual</button>
194
+ <button class="mode-btn" data-mode="auto" id="modeAuto">Auto</button>
195
+ </div>
196
+ <div class="sensor-grid" id="sensorGrid">
197
+ <div class="sensor-btn" data-sensor="0" data-val="0">S0<br><b>0</b></div>
198
+ <div class="sensor-btn" data-sensor="1" data-val="0">S1<br><b>0</b></div>
199
+ <div class="sensor-btn" data-sensor="2" data-val="0">S2<br><b>0</b></div>
200
+ <div class="sensor-btn" data-sensor="3" data-val="0">S3<br><b>0</b></div>
201
+ <div class="sensor-btn" data-sensor="4" data-val="0">S4<br><b>0</b></div>
202
+ <div class="sensor-btn" data-sensor="5" data-val="0">S5<br><b>0</b></div>
203
+ </div>
204
+ </div>
205
+
206
+ <div class="sb-section">
207
+ <h3>Neuromodulation</h3>
208
+ <div class="nm-slider"><div class="nm-label"><div class="nm-dot" style="background:var(--da)"></div>DA<span class="val" id="vDA" style="margin-left:auto;color:var(--da)">0.50</span></div><input type="range" min="0" max="100" value="50" id="sDA"></div>
209
+ <div class="nm-slider"><div class="nm-label"><div class="nm-dot" style="background:var(--sht)"></div>5-HT<span class="val" id="v5HT" style="margin-left:auto;color:var(--sht)">0.50</span></div><input type="range" min="0" max="100" value="50" id="s5HT"></div>
210
+ <div class="nm-slider"><div class="nm-label"><div class="nm-dot" style="background:var(--ach)"></div>ACh<span class="val" id="vACh" style="margin-left:auto;color:var(--ach)">0.50</span></div><input type="range" min="0" max="100" value="50" id="sACh"></div>
211
+ <div class="nm-slider"><div class="nm-label"><div class="nm-dot" style="background:var(--na)"></div>NA<span class="val" id="vNA" style="margin-left:auto;color:var(--na)">0.50</span></div><input type="range" min="0" max="100" value="50" id="sNA"></div>
212
+ </div>
213
+
214
+ <div class="sb-section">
215
+ <h3>Motor β†’ Sphero</h3>
216
+ <div class="output-row"><div class="output-label">Speed</div><div class="output-bar-wrap"><div class="output-bar" id="barSpeed" style="width:0%;background:var(--green)"></div></div><div class="output-val" id="valSpeed">0</div></div>
217
+ <div class="output-row"><div class="output-label">Heading</div><div class="output-bar-wrap"><div class="output-bar" id="barHead" style="width:50%;background:var(--amber)"></div></div><div class="output-val" id="valHead">0Β°</div></div>
218
+ <div class="output-row"><div class="output-label">NX Mod</div><div class="output-bar-wrap"><div class="output-bar" id="barMod" style="width:50%;background:var(--sht)"></div></div><div class="output-val" id="valMod">0</div></div>
219
+ </div>
220
+
221
+ <div class="log" id="logPanel">Neuraxon 2.0 β€” Sphero Writer\n</div>
222
+ </div>
223
+ </div>
224
+
225
+ <script>
226
+ // ═══════════════════════════════════════════════
227
+ // UTILITIES
228
+ // ═══════════════════════════════════════════════
229
+ const $=id=>document.getElementById(id);
230
+ const clamp=(v,lo,hi)=>Math.max(lo,Math.min(hi,v));
231
+ const rand=(lo=0,hi=1)=>lo+Math.random()*(hi-lo);
232
+ const randInt=(lo,hi)=>Math.floor(rand(lo,hi+1));
233
+ const pick=a=>a[randInt(0,a.length-1)];
234
+ const logEl=$('logPanel');
235
+ function log(m){const t=new Date().toLocaleTimeString('en-US',{hour12:false});logEl.textContent+=`[${t}] ${m}\n`;logEl.scrollTop=logEl.scrollHeight}
236
+
237
+ // ═══════════════════════════════════════════════
238
+ // VECTOR FONT β€” stroke paths in 0..1 coord space
239
+ // Each char = array of strokes. Each stroke = [x,y] points.
240
+ // ═══════════════════════════════════════════════
241
+ const FONT={
242
+ Q:[[[.5,0],[.15,.1],[0,.4],[0,.6],[.15,.9],[.5,1],[.85,.9],[1,.6],[1,.4],[.85,.1],[.5,0]],[[.65,.75],[1,1.05]]],
243
+ U:[[[0,0],[0,.7],[.1,.9],[.3,1],[.7,1],[.9,.9],[1,.7],[1,0]]],
244
+ B:[[[0,1],[0,0],[.65,0],[.85,.1],[.85,.35],[.65,.48],[0,.48]],[[.65,.48],[.9,.6],[.9,.85],[.65,1],[0,1]]],
245
+ I:[[[.25,0],[.75,0]],[[.5,0],[.5,1]],[[.25,1],[.75,1]]],
246
+ C:[[[.95,.15],[.7,0],[.3,0],[.05,.25],[0,.5],[.05,.75],[.3,1],[.7,1],[.95,.85]]],
247
+ A:[[[0,1],[.5,0],[1,1]],[[.2,.6],[.8,.6]]],
248
+ D:[[[0,1],[0,0],[.55,0],[.85,.2],[1,.5],[.85,.8],[.55,1],[0,1]]],
249
+ E:[[[.9,0],[0,0],[0,.5],[.65,.5]],[[0,.5],[0,1],[.9,1]]],
250
+ F:[[[.9,0],[0,0],[0,.5],[.65,.5]],[[0,.5],[0,1]]],
251
+ G:[[[.9,.15],[.6,0],[.3,0],[.05,.25],[0,.5],[.05,.75],[.3,1],[.7,1],[.95,.85],[.95,.55],[.5,.55]]],
252
+ H:[[[0,0],[0,1]],[[0,.5],[1,.5]],[[1,0],[1,1]]],
253
+ J:[[[.3,0],[.8,0]],[[.6,0],[.6,.8],[.45,1],[.2,1],[.05,.85]]],
254
+ K:[[[0,0],[0,1]],[[.9,0],[0,.5],[.9,1]]],
255
+ L:[[[0,0],[0,1],[.9,1]]],
256
+ M:[[[0,1],[0,0],[.5,.45],[1,0],[1,1]]],
257
+ N:[[[0,1],[0,0],[1,1],[1,0]]],
258
+ O:[[[.5,0],[.15,.1],[0,.4],[0,.6],[.15,.9],[.5,1],[.85,.9],[1,.6],[1,.4],[.85,.1],[.5,0]]],
259
+ P:[[[0,1],[0,0],[.7,0],[.95,.15],[.95,.35],[.7,.5],[0,.5]]],
260
+ R:[[[0,1],[0,0],[.7,0],[.95,.15],[.95,.35],[.7,.5],[0,.5]],[[.5,.5],[.95,1]]],
261
+ S:[[[.9,.1],[.65,0],[.35,0],[.1,.1],[0,.25],[.1,.42],[.35,.5],[.65,.55],[.9,.65],[1,.8],[.9,.95],[.65,1],[.35,1],[.1,.9]]],
262
+ T:[[[0,0],[1,0]],[[.5,0],[.5,1]]],
263
+ V:[[[0,0],[.5,1],[1,0]]],
264
+ W:[[[0,0],[.25,1],[.5,.5],[.75,1],[1,0]]],
265
+ X:[[[0,0],[1,1]],[[1,0],[0,1]]],
266
+ Y:[[[0,0],[.5,.5],[1,0]],[[.5,.5],[.5,1]]],
267
+ Z:[[[0,0],[1,0],[0,1],[1,1]]],
268
+ ' ':[]
269
+ };
270
+
271
+ // ═══════════════════════════════════════════════
272
+ // WORD β†’ SEGMENT PLAN (heading + duration based)
273
+ // Each segment: {heading, penDown, duration_ms, hue, letter, letterIdx}
274
+ // ═══════════════════════════════════════════════
275
+ function wordToSegments(word, sizeCm, baseSpeed, segTimeMs) {
276
+ const segments = [];
277
+ const scale = sizeCm;
278
+ const spacing = scale * 1.3;
279
+ let curX = 0, curY = 0;
280
+ const upper = word.toUpperCase();
281
+
282
+ for (let ci = 0; ci < upper.length; ci++) {
283
+ const ch = upper[ci];
284
+ const strokes = FONT[ch];
285
+ const hue = (ci / Math.max(1, upper.length)) * 360;
286
+ if (!strokes || strokes.length === 0) { curX += spacing * 0.5; continue; }
287
+
288
+ const ox = curX; // letter origin X
289
+
290
+ for (let si = 0; si < strokes.length; si++) {
291
+ const stroke = strokes[si];
292
+ // First point of each stroke: pen-up move to get there
293
+ const startX = ox + stroke[0][0] * scale;
294
+ const startY = stroke[0][1] * scale;
295
+ const dx0 = startX - curX, dy0 = startY - curY;
296
+ const dist0 = Math.sqrt(dx0*dx0 + dy0*dy0);
297
+ if (dist0 > 0.5) {
298
+ const heading = ((Math.atan2(dx0, -dy0) * 180 / Math.PI) % 360 + 360) % 360;
299
+ // Time proportional to distance
300
+ const t = Math.max(150, (dist0 / scale) * segTimeMs * 0.7);
301
+ segments.push({ heading: Math.round(heading), penDown: false, duration: Math.round(t), hue, letter: ch, letterIdx: ci, x: startX, y: startY });
302
+ curX = startX; curY = startY;
303
+ }
304
+ // Pen-down segments along the stroke
305
+ for (let pi = 1; pi < stroke.length; pi++) {
306
+ const px = ox + stroke[pi][0] * scale;
307
+ const py = stroke[pi][1] * scale;
308
+ const ddx = px - curX, ddy = py - curY;
309
+ const dd = Math.sqrt(ddx*ddx + ddy*ddy);
310
+ if (dd < 0.3) continue;
311
+ const heading = ((Math.atan2(ddx, -ddy) * 180 / Math.PI) % 360 + 360) % 360;
312
+ const t = Math.max(100, (dd / scale) * segTimeMs);
313
+ segments.push({ heading: Math.round(heading), penDown: true, duration: Math.round(t), hue, letter: ch, letterIdx: ci, x: px, y: py });
314
+ curX = px; curY = py;
315
+ }
316
+ // Brief pause between strokes
317
+ segments.push({ heading: 0, penDown: false, duration: 80, hue, letter: ch, letterIdx: ci, x: curX, y: curY, pause: true });
318
+ }
319
+ curX = ox + spacing; // advance cursor for next letter
320
+ // Pause between letters
321
+ segments.push({ heading: 0, penDown: false, duration: 150, hue, letter: ch, letterIdx: ci, x: curX, y: curY, pause: true });
322
+ }
323
+ return segments;
324
+ }
325
+
326
+ // Also build the full planned path for preview
327
+ function wordToPlannedPath(word, sizeCm) {
328
+ const pts = [];
329
+ const scale = sizeCm, spacing = scale * 1.3;
330
+ let curX = 0;
331
+ const upper = word.toUpperCase();
332
+ for (let ci = 0; ci < upper.length; ci++) {
333
+ const ch = upper[ci];
334
+ const strokes = FONT[ch];
335
+ const hue = (ci / Math.max(1, upper.length)) * 360;
336
+ if (!strokes || strokes.length === 0) { curX += spacing * 0.5; continue; }
337
+ for (const stroke of strokes) {
338
+ for (let pi = 0; pi < stroke.length; pi++) {
339
+ pts.push({ x: curX + stroke[pi][0] * scale, y: stroke[pi][1] * scale, penDown: pi > 0, hue, letter: ch, letterIdx: ci });
340
+ }
341
+ }
342
+ curX += spacing;
343
+ }
344
+ return pts;
345
+ }
346
+
347
+
348
+ // ═══════════════════════════════════════════════
349
+ // NEURAXON 2.0 ENGINE (compact, faithful)
350
+ // ═══════════════════════════════════════════════
351
+ class ReceptorSubtype{constructor(n,t,g,i){this.threshold=t;this.gain=g;this.isTonic=i;this.activation=0}computeActivation(c){const k=this.isTonic?20:10;this.activation=this.gain/(1+Math.exp(-k*(c-this.threshold)));return this.activation}}
352
+ class OscillatorBank{constructor(){this.bands={infraslow:{freq:.05,phase:rand(0,6.28),amp:.1},slow:{freq:.5,phase:rand(0,6.28),amp:.15},theta:{freq:6,phase:rand(0,6.28),amp:.25},gamma:{freq:40,phase:rand(0,6.28),amp:.3}};this.coupling=.15}update(dt){for(const b of Object.values(this.bands)){b.phase+=2*Math.PI*b.freq*dt/1000;b.phase%=2*Math.PI}}getDrive(n,N){const p=2*Math.PI*n/N;const g=Math.max(0,Math.cos(this.bands.theta.phase+p));return this.coupling*(this.bands.gamma.amp*g*Math.sin(this.bands.gamma.phase+2*p)+.5*this.bands.slow.amp*Math.sin(this.bands.slow.phase+.3*p)+.3*this.bands.infraslow.amp*Math.sin(this.bands.infraslow.phase))}}
353
+ class NeuromodulatorSystem{constructor(){this.modulators={DA:{tonic:.5,phasic:0,tauP:200,rel:.3},SHT:{tonic:.5,phasic:0,tauP:500,rel:.2},ACh:{tonic:.5,phasic:0,tauP:150,rel:.25},NA:{tonic:.5,phasic:0,tauP:300,rel:.35}};this.receptors={D1:new ReceptorSubtype('D1',.3,1,false),D2:new ReceptorSubtype('D2',.5,.8,true),SHT1A:new ReceptorSubtype('5HT1A',.2,1,true),SHT2A:new ReceptorSubtype('5HT2A',.6,.9,false),M1:new ReceptorSubtype('M1',.3,1,false),M2:new ReceptorSubtype('M2',.5,.8,true),B1:new ReceptorSubtype('B1',.3,1,false),A2:new ReceptorSubtype('A2',.4,.9,true)}}setBaselines(d,s,a,n){this.modulators.DA.tonic=d;this.modulators.SHT.tonic=s;this.modulators.ACh.tonic=a;this.modulators.NA.tonic=n}update(act,dt){const d=dt/1000;for(const m of Object.values(this.modulators))m.phasic*=Math.exp(-d/(m.tauP/1000));this.modulators.DA.phasic+=this.modulators.DA.rel*act.stateChangeRate*d;this.modulators.ACh.phasic+=this.modulators.ACh.rel*act.excFrac*d;this.modulators.NA.phasic+=this.modulators.NA.rel*act.stateChangeRate*d;for(const m of Object.values(this.modulators)){m.tonic=clamp(m.tonic,0,1);m.phasic=clamp(m.phasic,0,1)}}computeReceptorActivations(){const R={},map={D1:'DA',D2:'DA',SHT1A:'SHT',SHT2A:'SHT',M1:'ACh',M2:'ACh',B1:'NA',A2:'NA'};for(const[rn,rec]of Object.entries(this.receptors)){const m=this.modulators[map[rn]];R[rn]=rec.computeActivation(rec.isTonic?m.tonic:m.tonic+m.phasic)}return R}}
354
+
355
+ class Synapse{constructor(p,q,b){this.preId=p;this.postId=q;this.branch=b;this.wf=rand(-.5,.5);this.ws=rand(-.3,.3);this.wm=rand(-.1,.1);this.tauF=50;this.tauS=500;this.tauM=5000;this.silent=Math.random()<.1;this.modulatory=Math.random()<.15;this.preTrace=0;this.postTrace=0;this.recentDw=0;this.integrity=1}computeInput(s){return this.silent?0:(this.wf+this.ws)*s}getModulatoryEffect(){return this.modulatory?this.wm:0}update(pre,post,R,dt){const d=dt/1000;this.preTrace+=(-(this.preTrace)/20+(pre===1?1:0))*d;this.postTrace+=(-(this.postTrace)/20+(post===1?1:0))*d;const eta=.05;let dw=eta*this.preTrace*(post===1?1:0)*(R.D1||.5)-eta*this.postTrace*(pre===1?1:0)*(R.D2||.5);this.recentDw=dw;this.wf+=(d/(this.tauF/1000))*(-.001*this.wf+.3*dw);this.ws+=(d/(this.tauS/1000))*(-.001*this.ws+.1*dw);const sf=.5*(R.SHT2A||.5)+.1*(1-(R.SHT1A||.5));this.wm+=(d/(this.tauM/1000))*(-.001*this.wm+.05*dw*sf);this.wf=clamp(this.wf,-1,1);this.ws=clamp(this.ws,-1,1);this.wm=clamp(this.wm,-.5,.5);this.integrity-=.0001*d;this.integrity=clamp(this.integrity,0,1);if(this.silent&&Math.abs(pre)===1&&Math.abs(post)===1&&Math.random()<.05)this.silent=false}}
356
+
357
+ class Neuraxon{constructor(id,type){this.id=id;this.type=type;this.s=0;this.state=0;this.theta1=.5;this.theta2=-.5;this.adapt=0;this.rBar=0;this.auto=0;this.health=1;this.tau=rand(15,40);this.numBranches=3;this.h=0;this.x=0;this.y=0;this.radius=0;this.label=''}
358
+ update(inputs,modIn,Iext,osc,R,dt){const d=dt/1000;this.rBar+=.01*(Math.abs(this.state)-this.rBar)*d;const gNA=1+.5*(R.B1||.5)+.2*(R.A2||.5);let D=0;for(const v of inputs)D+=v;const sp=(.02+.3*(R.A2||.3))*(Math.random()<.1*d?pick([-1,1]):0);this.s+=(d/(this.tau/1000))*(-this.s+gNA*D+Iext+osc-this.adapt+sp);this.h=.95*this.h+.05*.1*Iext;const sT=this.s+this.h;let rawMod=0;for(const m of modIn)rawMod+=m;rawMod+=.3*(R.M1||.5)-.2*(R.M2||.5);const t1=this.theta1-.3*Math.tanh(rawMod)+.01*(this.rBar-.3)-.1*this.auto;const t2=this.theta2-.3*Math.tanh(rawMod)+.01*(this.rBar-.3)+.1*this.auto;if(sT>t1)this.state=1;else if(sT<t2)this.state=-1;else this.state=0;this.adapt+=(d/(.1))*(-this.adapt+.1*Math.abs(this.state));this.auto+=(d/(.3))*(-this.auto+.2*this.state)}}
359
+
360
+ class NeuraxonNetwork{
361
+ constructor(nI,nH,nO){
362
+ this.neurons=[];this.synapses=[];this.oscillators=new OscillatorBank();this.neuromod=new NeuromodulatorSystem();
363
+ this.time=0;this.step=0;this.energy=0;this.prevStates=[];
364
+ for(let i=0;i<nI;i++)this.neurons.push(new Neuraxon(i,'input'));
365
+ for(let i=0;i<nH;i++)this.neurons.push(new Neuraxon(nI+i,'hidden'));
366
+ for(let i=0;i<nO;i++)this.neurons.push(new Neuraxon(nI+nH+i,'output'));
367
+ this.nI=nI;this.nH=nH;this.nO=nO;this.N=this.neurons.length;
368
+ this._buildSW(4,.3);this.prevStates=this.neurons.map(n=>n.state);
369
+ }
370
+ _buildSW(k,beta){
371
+ const N=this.N,hk=k>>1,edges=new Set();
372
+ for(let i=0;i<N;i++)for(let j=1;j<=hk;j++)edges.add(`${i}-${(i+j)%N}`);
373
+ for(const e of[...edges])if(Math.random()<beta){const[s]=e.split('-').map(Number);edges.delete(e);let t;do{t=randInt(0,N-1)}while(t===s||edges.has(`${s}-${t}`));edges.add(`${s}-${t}`)}
374
+ for(let i=0;i<this.nI;i++)for(let h=0;h<Math.min(4,this.nH);h++)edges.add(`${i}-${this.nI+((i*3+h)%this.nH)}`);
375
+ for(let h=0;h<this.nH;h++)for(let o=0;o<this.nO;o++)if(Math.random()<.6)edges.add(`${this.nI+h}-${this.nI+this.nH+o}`);
376
+ for(const e of edges){const[p,q]=e.split('-').map(Number);this.synapses.push(new Synapse(p,q,randInt(0,2)))}
377
+ }
378
+ computeActivity(){let e=0,a=0,c=0;for(let i=0;i<this.N;i++){if(this.neurons[i].state===1)e++;if(this.neurons[i].state!==0)a++;if(this.neurons[i].state!==this.prevStates[i])c++}return{excFrac:e/this.N,meanActivity:a/this.N,stateChangeRate:c/this.N}}
379
+ simulateStep(ext,dt){
380
+ this.prevStates=this.neurons.map(n=>n.state);
381
+ const act=this.computeActivity();this.neuromod.update(act,dt);const R=this.neuromod.computeReceptorActivations();this.oscillators.update(dt);
382
+ for(let i=0;i<this.nI;i++)if(ext[i]!==undefined){this.neurons[i].state=ext[i];this.neurons[i].s=ext[i]*.8}
383
+ const inp=this.neurons.map(()=>[]),mod=this.neurons.map(()=>[]);
384
+ for(const s of this.synapses){inp[s.postId].push(s.computeInput(this.neurons[s.preId].state));mod[s.postId].push(s.getModulatoryEffect())}
385
+ for(let i=this.nI;i<this.N;i++){const n=this.neurons[i];n.update(inp[i],mod[i],0,this.oscillators.getDrive(n.id,this.N),R,dt)}
386
+ for(const s of this.synapses)s.update(this.neurons[s.preId].state,this.neurons[s.postId].state,R,dt);
387
+ this.synapses=this.synapses.filter(s=>s.integrity>.05);
388
+ this.energy+=.01*this.neurons.filter(n=>n.state!==0).length*(dt/1000);this.time+=dt/1000;this.step++;
389
+ }
390
+ getOutputContinuous(){const o=[];for(let i=this.nI+this.nH;i<this.N;i++)o.push(this.neurons[i].s);return o}
391
+ // Average excitation level β€” used as "neural modulation signal" for the writer
392
+ getExcitationLevel(){let e=0;for(const n of this.neurons)e+=n.state;return e/this.N}
393
+ }
394
+
395
+
396
+ // ═══════════════════════════════════════════════
397
+ // SPHERO BLE β€” EXACT COPY from working spherocontrol.html
398
+ // DO NOT MODIFY β€” flag bytes must be exact
399
+ // ═══════════════════════════════════════════════
400
+ const UUID_SPHERO_SERVICE = '00010001-574f-4f20-5370-6865726f2121';
401
+ const UUID_SPHERO_SERVICE_INIT = '00020001-574f-4f20-5370-6865726f2121';
402
+ const UUID_CHAR_HANDLE = '00010002-574f-4f20-5370-6865726f2121';
403
+ const UUID_CHAR_USETHEFORCE = '00020005-574f-4f20-5370-6865726f2121';
404
+ const UseTheForceBytes = new Uint8Array([0x75,0x73,0x65,0x74,0x68,0x65,0x66,0x6f,0x72,0x63,0x65,0x2e,0x2e,0x2e,0x62,0x61,0x6e,0x64]);
405
+ const API = { ESC: 0xAB, SOP: 0x8D, EOP: 0xD8, ESC_MASK: 0x88 };
406
+ const DeviceId = { powerInfo: 0x13, driving: 0x16, userIO: 0x1A };
407
+ const PowerCmd = { wake: 0x0D, sleep: 0x01 };
408
+ const DrivingCmd = { resetYaw: 0x06, driveWithHeading: 0x07 };
409
+ const UserIOCmd = { allLEDs: 0x0E };
410
+ const Flags = { requestsResponse: 2, requestsOnlyErrorResponse: 4, resetsInactivityTimeout: 8 };
411
+ const sleep = ms => new Promise(r => setTimeout(r, ms));
412
+
413
+ class SpheroMiniBLE {
414
+ constructor() {
415
+ this.device = null; this.server = null; this.ch = new Map(); this.seq = 0;
416
+ this._chain = Promise.resolve(); this._qDepth = 0; this._closed = false;
417
+ this.onDisconnect = null;
418
+ }
419
+ _pushEscaped(out, b) {
420
+ if (b === API.SOP || b === API.EOP || b === API.ESC) { out.push(API.ESC); out.push(b & (~API.ESC_MASK)); }
421
+ else out.push(b);
422
+ }
423
+ _buildPacket(did, cid, dataBytes, cmdFlags) {
424
+ this.seq = (this.seq + 1) & 0xFF; let sum = 0; const out = [];
425
+ out.push(API.SOP); out.push(cmdFlags); sum += cmdFlags;
426
+ this._pushEscaped(out, did); sum += did;
427
+ this._pushEscaped(out, cid); sum += cid;
428
+ this._pushEscaped(out, this.seq); sum += this.seq;
429
+ for (const b of dataBytes) { this._pushEscaped(out, b); sum += b; }
430
+ const chk = (~sum) & 0xFF; this._pushEscaped(out, chk); out.push(API.EOP);
431
+ return new Uint8Array(out);
432
+ }
433
+ _enqueueWrite(fn) {
434
+ if (this._closed) return Promise.reject(new Error('BLE closed'));
435
+ this._qDepth++;
436
+ const run = async () => { try { return await fn(); } finally { this._qDepth = Math.max(0, this._qDepth - 1); } };
437
+ this._chain = this._chain.then(run, run); return this._chain;
438
+ }
439
+ async connect() {
440
+ log('Requesting Bluetooth device...');
441
+ this.device = await navigator.bluetooth.requestDevice({
442
+ filters: [{ services: [UUID_SPHERO_SERVICE] }],
443
+ optionalServices: [UUID_SPHERO_SERVICE_INIT]
444
+ });
445
+ this.device.addEventListener('gattserverdisconnected', () => {
446
+ log('BLE disconnected');
447
+ if (this.onDisconnect) this.onDisconnect();
448
+ });
449
+ log(`Connecting to: ${this.device.name || 'Sphero Mini'}`);
450
+ this.server = await this.device.gatt.connect();
451
+ const svcCmd = await this.server.getPrimaryService(UUID_SPHERO_SERVICE);
452
+ const svcInit = await this.server.getPrimaryService(UUID_SPHERO_SERVICE_INIT);
453
+ const chHandle = await svcCmd.getCharacteristic(UUID_CHAR_HANDLE);
454
+ const chForce = await svcInit.getCharacteristic(UUID_CHAR_USETHEFORCE);
455
+ this.ch.set('handle', chHandle);
456
+ log('Waking up Sphero...');
457
+ await this._enqueueWrite(() => chForce.writeValue(UseTheForceBytes));
458
+ await this.send(DeviceId.powerInfo, PowerCmd.wake, [], { response: 'full' });
459
+ await sleep(200);
460
+ await this.send(DeviceId.powerInfo, PowerCmd.wake, [], { response: 'full' });
461
+ }
462
+ async disconnect() {
463
+ this._closed = true;
464
+ if (this.device && this.device.gatt.connected) this.device.gatt.disconnect();
465
+ }
466
+ // CRITICAL: response:'none' β†’ flags=0x08 only. response:'full' β†’ 0x0A. 'errorOnly' β†’ 0x0C.
467
+ // roll() and setMainLED() MUST use 'none' or Sphero ignores the command!
468
+ async send(did, cid, data, opts = { response: 'errorOnly' }) {
469
+ const chHandle = this.ch.get('handle'); if (!chHandle) throw new Error('Handle missing');
470
+ let cmdFlags = Flags.resetsInactivityTimeout;
471
+ if (opts.response === 'full') cmdFlags |= Flags.requestsResponse;
472
+ else if (opts.response === 'errorOnly') cmdFlags |= Flags.requestsOnlyErrorResponse;
473
+ // 'none' β†’ cmdFlags stays at just 0x08
474
+ const pkt = this._buildPacket(did, cid, data, cmdFlags);
475
+ return this._enqueueWrite(async () => {
476
+ if (chHandle.writeValueWithoutResponse && opts.response !== 'full') await chHandle.writeValueWithoutResponse(pkt);
477
+ else await chHandle.writeValue(pkt);
478
+ return this.seq;
479
+ });
480
+ }
481
+ async setMainLED(r, g, b) { return this.send(DeviceId.userIO, UserIOCmd.allLEDs, [0x00, 0x70, r & 255, g & 255, b & 255], { response: 'none' }); }
482
+ async roll(speed, headingDeg) {
483
+ const head = ((headingDeg % 360) + 360) % 360;
484
+ return this.send(DeviceId.driving, DrivingCmd.driveWithHeading, [speed & 255, (head >> 8) & 0xFF, head & 0xFF, 0x01], { response: 'none' });
485
+ }
486
+ async resetYaw() { return this.send(DeviceId.driving, DrivingCmd.resetYaw, [], { response: 'full' }); }
487
+ async stop() { return this.roll(0, 0); }
488
+ }
489
+
490
+
491
+ // ═══════════════════════════════════════════════
492
+ // APP STATE
493
+ // ═══════════════════════════════════════════════
494
+ const net = new NeuraxonNetwork(6, 12, 3);
495
+ let sphero=null, bleConnected=false, running=false, simInterval=null, mode='manual';
496
+ let sensorValues=[0,0,0,0,0,0], activeTab='brain';
497
+
498
+ // Writer
499
+ let writerActive=false, writerSegments=[], writerIdx=0, writerDone=false;
500
+ let writerPlanned=[], trailHistory=[];
501
+ let segStartTime=0, currentLedR=0, currentLedG=0, currentLedB=0;
502
+ let writerTimerId=null;
503
+
504
+ // ═══════════════════════════════════════════════
505
+ // WRITER ENGINE β€” timed segment execution
506
+ // No position feedback needed: each segment = drive at heading for N ms
507
+ // ═══════════════════════════════════════════════
508
+
509
+ async function startWriting(){
510
+ const word=$('wordInput').value.trim();
511
+ if(!word){log('Enter a word');return}
512
+ const size=parseInt($('sSize').value);
513
+ const speed=parseInt($('sWSpeed').value);
514
+ const segTime=parseInt($('sSegTime').value);
515
+
516
+ writerSegments=wordToSegments(word,size,speed,segTime);
517
+ writerPlanned=wordToPlannedPath(word,size);
518
+ writerIdx=0;writerDone=false;writerActive=true;
519
+ trailHistory=[];
520
+
521
+ updateLetterChips(word);
522
+ $('writeStatusText').textContent=`Writing "${word}"…`;
523
+ $('btnWrite').disabled=true;$('btnAbort').disabled=false;
524
+ $('writeLive').textContent='';
525
+
526
+ // Boost DA + ACh on the Neuraxon network during writing
527
+ $('sDA').value=75;$('vDA').textContent='0.75';
528
+ $('sACh').value=70;$('vACh').textContent='0.70';
529
+ $('sNA').value=65;$('vNA').textContent='0.65';
530
+
531
+ log(`Writing "${word}" β€” ${writerSegments.length} segments, size=${size}cm, speed=${speed}`);
532
+
533
+ if(sphero&&bleConnected){await sphero.resetYaw();await new Promise(r=>setTimeout(r,300))}
534
+
535
+ // Start the Neuraxon simulation if not running
536
+ if(!running)$('btnRun').click();
537
+
538
+ // Switch to writer tab
539
+ $('tabWriter').click();
540
+
541
+ // Execute segments sequentially
542
+ executeNextSegment();
543
+ }
544
+
545
+ async function executeNextSegment(){
546
+ if(!writerActive||writerIdx>=writerSegments.length){
547
+ finishWriting();return;
548
+ }
549
+
550
+ const seg=writerSegments[writerIdx];
551
+ const baseSpeed=parseInt($('sWSpeed').value);
552
+
553
+ // ─── Neuraxon modulation ───
554
+ // Feed segment info into the network as sensor inputs
555
+ const headNorm=seg.heading/360; // S0: heading normalized
556
+ const penSignal=seg.penDown?1:-1; // S1: pen state
557
+ const progressSignal=(writerIdx/writerSegments.length)*2-1; // S2: progress -1..+1
558
+ const letterSignal=(seg.letterIdx||0)/12; // S3: letter index
559
+ const excLevel=net.getExcitationLevel(); // S4: network feedback
560
+ const durationNorm=clamp(seg.duration/1000,-1,1); // S5: duration hint
561
+ sensorValues=[
562
+ headNorm>0.5?1:(headNorm<0.25?-1:0),
563
+ penSignal>0?1:-1,
564
+ progressSignal>0.5?1:(progressSignal<-0.5?-1:0),
565
+ Math.round(clamp(letterSignal*2-1,-1,1)),
566
+ excLevel>0.1?1:(excLevel<-0.1?-1:0),
567
+ durationNorm>0.3?1:(durationNorm<-0.3?-1:0)
568
+ ];
569
+ updateSensorUI();
570
+
571
+ // Get Neuraxon output to modulate speed
572
+ const nxOut=net.getOutputContinuous();
573
+ const speedMod=1.0+clamp(nxOut[0]||0,-0.3,0.3); // Β±30% modulation
574
+ const actualSpeed=seg.pause?0:Math.round(clamp(baseSpeed*speedMod*(seg.penDown?1:1.4),0,200));
575
+
576
+ // LED color from letter hue
577
+ const[lr,lg,lb]=hslToRgb((seg.hue||0)/360,.85,.5);
578
+ currentLedR=lr;currentLedG=lg;currentLedB=lb;
579
+ $('ledPreview').style.background=`rgb(${lr},${lg},${lb})`;
580
+
581
+ // Update UI
582
+ updateMotorUI(actualSpeed,seg.heading,nxOut[0]||0);
583
+ const pct=Math.round((writerIdx/writerSegments.length)*100);
584
+ $('writeProgress').style.width=pct+'%';$('writePercent').textContent=pct+'%';
585
+ $('writeLive').textContent=`[${seg.letter}] seg ${writerIdx+1}/${writerSegments.length} h=${seg.heading}° ${seg.penDown?'✏️PEN':'✈️MOVE'} ${seg.duration}ms spd=${actualSpeed}`;
586
+
587
+ // Update letter chip
588
+ updateChipState(seg.letterIdx);
589
+
590
+ // Record trail point
591
+ trailHistory.push({segIdx:writerIdx,penDown:seg.penDown,hue:seg.hue,heading:seg.heading,duration:seg.duration,speed:actualSpeed,x:seg.x,y:seg.y});
592
+
593
+ // ─── Send to Sphero ───
594
+ if(sphero&&bleConnected){
595
+ try{
596
+ await sphero.setMainLED(seg.penDown?lr:Math.round(lr*.3),seg.penDown?lg:Math.round(lg*.3),seg.penDown?lb:Math.round(lb*.3));
597
+ await sleep(30); // small gap to avoid BLE command collision
598
+ if(seg.pause){
599
+ await sphero.stop();
600
+ }else{
601
+ await sphero.roll(actualSpeed,seg.heading);
602
+ log(` β†’ roll(${actualSpeed}, ${seg.heading}Β°) for ${seg.duration}ms`);
603
+ }
604
+ }catch(e){log('BLE err: '+e.message)}
605
+ }
606
+
607
+ // Wait for segment duration, then advance
608
+ writerTimerId=setTimeout(()=>{
609
+ writerIdx++;
610
+ executeNextSegment();
611
+ },seg.duration);
612
+ }
613
+
614
+ function finishWriting(){
615
+ writerActive=false;writerDone=true;
616
+ $('writeStatusText').textContent='βœ… Complete!';
617
+ $('writeProgress').style.width='100%';$('writePercent').textContent='100%';
618
+ $('btnWrite').disabled=false;$('btnAbort').disabled=true;
619
+ $('writeLive').textContent='Done!';
620
+ if(sphero&&bleConnected){sphero.stop().catch(()=>{});sphero.setMainLED(0,255,80).catch(()=>{});}
621
+ log('Writing complete!');
622
+ }
623
+
624
+ function abortWriting(){
625
+ writerActive=false;writerDone=true;
626
+ if(writerTimerId){clearTimeout(writerTimerId);writerTimerId=null}
627
+ $('writeStatusText').textContent='Aborted';
628
+ $('btnWrite').disabled=false;$('btnAbort').disabled=true;
629
+ $('writeLive').textContent='';
630
+ if(sphero&&bleConnected)sphero.stop().catch(()=>{});
631
+ log('Aborted');
632
+ }
633
+
634
+ function updateLetterChips(word){
635
+ const c=$('letterPreview');c.innerHTML='';
636
+ for(let i=0;i<word.length;i++){const d=document.createElement('div');d.className='letter-chip pending';d.textContent=word[i].toUpperCase();d.id=`chip${i}`;c.appendChild(d)}
637
+ }
638
+ function updateChipState(letterIdx){
639
+ document.querySelectorAll('.letter-chip').forEach((ch,i)=>{
640
+ if(i<letterIdx)ch.className='letter-chip done';
641
+ else if(i===letterIdx)ch.className='letter-chip active';
642
+ else ch.className='letter-chip pending';
643
+ });
644
+ }
645
+ function updateMotorUI(speed,heading,nxMod){
646
+ $('barSpeed').style.width=(speed/200*100)+'%';$('valSpeed').textContent=speed;
647
+ $('barHead').style.width=(heading/360*100)+'%';$('valHead').textContent=heading+'Β°';
648
+ const modNorm=clamp((nxMod+1)/2*100,0,100);
649
+ $('barMod').style.width=modNorm+'%';$('valMod').textContent=nxMod.toFixed(2);
650
+ }
651
+
652
+ function hslToRgb(h,s,l){let r,g,b;if(s===0){r=g=b=l}else{const q=l<.5?l*(1+s):l+s-l*s;const p=2*l-q;const f=(p,q,t)=>{if(t<0)t+=1;if(t>1)t-=1;if(t<1/6)return p+(q-p)*6*t;if(t<.5)return q;if(t<2/3)return p+(q-p)*(2/3-t)*6;return p};r=f(p,q,h+1/3);g=f(p,q,h);b=f(p,q,h-1/3)}return[Math.round(r*255),Math.round(g*255),Math.round(b*255)]}
653
+
654
+
655
+ // ═══════════════════════════════════════════════
656
+ // VISUALIZATION
657
+ // ═══════════════════════════════════════════════
658
+ function layoutNeurons(){
659
+ const c=$('netCanvas'),W=c.width,H=c.height,cx=W/2,cy=H/2;
660
+ const rI=Math.min(W,H)*.37,rH=Math.min(W,H)*.21,rO=Math.min(W,H)*.07;
661
+ const IL=['πŸ’‘L','πŸ’‘R','🚧','πŸ”Š','β†’','↻'],OL=['Spd','TnL','TnR'];
662
+ for(let i=0;i<net.nI;i++){const a=(i/net.nI)*Math.PI*2-Math.PI/2;const n=net.neurons[i];n.x=cx+rI*Math.cos(a);n.y=cy+rI*Math.sin(a);n.radius=13;n.label=IL[i]}
663
+ for(let i=0;i<net.nH;i++){const a=(i/net.nH)*Math.PI*2-Math.PI/2;const n=net.neurons[net.nI+i];n.x=cx+rH*Math.cos(a);n.y=cy+rH*Math.sin(a);n.radius=9;n.label='H'+i}
664
+ for(let i=0;i<net.nO;i++){const a=(i/net.nO)*Math.PI*2-Math.PI/2;const n=net.neurons[net.nI+net.nH+i];n.x=cx+rO*Math.cos(a);n.y=cy+rO*Math.sin(a);n.radius=15;n.label=OL[i]}
665
+ }
666
+
667
+ function drawNetwork(){
668
+ const c=$('netCanvas'),ctx=c.getContext('2d'),W=c.width,H=c.height;
669
+ ctx.clearRect(0,0,W,H);
670
+ const bg=ctx.createRadialGradient(W/2,H/2,0,W/2,H/2,Math.max(W,H)*.6);bg.addColorStop(0,'#0c1220');bg.addColorStop(1,'#06080e');ctx.fillStyle=bg;ctx.fillRect(0,0,W,H);
671
+ ctx.strokeStyle='#0d1a2a';ctx.lineWidth=1;
672
+ [.37,.21].forEach(r=>{ctx.beginPath();ctx.arc(W/2,H/2,Math.min(W,H)*r,0,Math.PI*2);ctx.stroke()});
673
+ ctx.font='8px JetBrains Mono';ctx.fillStyle='#1a2940';ctx.textAlign='center';
674
+ ctx.fillText('SENSORY',W/2,H/2-Math.min(W,H)*.37-5);ctx.fillText('HIDDEN',W/2,H/2-Math.min(W,H)*.21-5);ctx.fillText('MOTOR',W/2,H/2+Math.min(W,H)*.07+20);
675
+ for(const s of net.synapses){if(s.silent)continue;const pr=net.neurons[s.preId],po=net.neurons[s.postId];const st=Math.abs(s.wf+s.ws),al=clamp(st*.4+.02,.02,.25);const w=s.wf+s.ws;ctx.beginPath();ctx.moveTo(pr.x,pr.y);const mx=(pr.x+po.x)/2+(pr.y-po.y)*.08,my=(pr.y+po.y)/2+(po.x-pr.x)*.08;ctx.quadraticCurveTo(mx,my,po.x,po.y);ctx.strokeStyle=w>0?`rgba(255,61,90,${al})`:w<0?`rgba(61,122,255,${al})`:`rgba(42,53,72,${al})`;ctx.lineWidth=clamp(st*3,.3,2.5);ctx.stroke()}
676
+ for(const n of net.neurons){ctx.save();let fc;if(n.state===1){fc='#ff3d5a';ctx.shadowColor='rgba(255,61,90,.4)';ctx.shadowBlur=16}else if(n.state===-1){fc='#3d7aff';ctx.shadowColor='rgba(61,122,255,.4)';ctx.shadowBlur=16}else{fc='#1e2d40'}ctx.beginPath();ctx.arc(n.x,n.y,n.radius,0,Math.PI*2);ctx.fillStyle=fc;ctx.fill();ctx.shadowBlur=0;ctx.lineWidth=n.type==='output'?2.5:n.type==='input'?2:1;ctx.strokeStyle=n.type==='output'?'rgba(255,171,0,.6)':n.type==='input'?'rgba(0,229,255,.4)':'rgba(120,200,255,.12)';ctx.stroke();ctx.fillStyle=n.state!==0?'#fff':'#5a7a98';ctx.font=`${n.type==='output'?700:500} ${n.radius<11?7:9}px JetBrains Mono`;ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText(n.label,n.x,n.y);ctx.restore()}
677
+ }
678
+
679
+ function drawPathCanvas(){
680
+ const c=$('pathCanvas'),ctx=c.getContext('2d'),W=c.width,H=c.height;
681
+ ctx.clearRect(0,0,W,H);
682
+ const bg=ctx.createRadialGradient(W/2,H/2,0,W/2,H/2,Math.max(W,H)*.6);bg.addColorStop(0,'#0a0e18');bg.addColorStop(1,'#06080e');ctx.fillStyle=bg;ctx.fillRect(0,0,W,H);
683
+
684
+ if(writerPlanned.length===0){ctx.fillStyle='#1e2d40';ctx.font='14px Syne,sans-serif';ctx.textAlign='center';ctx.fillText('Enter a word and press WRITE',W/2,H/2);return}
685
+
686
+ // Bounds
687
+ let mnX=Infinity,mxX=-Infinity,mnY=Infinity,mxY=-Infinity;
688
+ for(const p of writerPlanned){mnX=Math.min(mnX,p.x);mxX=Math.max(mxX,p.x);mnY=Math.min(mnY,p.y);mxY=Math.max(mxY,p.y)}
689
+ const pw=mxX-mnX||1,ph=mxY-mnY||1;
690
+ const margin=60;
691
+ const scale=Math.min((W-2*margin)/pw,(H-2*margin)/ph);
692
+ const offX=(W-pw*scale)/2-mnX*scale,offY=(H-ph*scale)/2-mnY*scale;
693
+ const tx=x=>x*scale+offX,ty=y=>y*scale+offY;
694
+
695
+ // Grid
696
+ ctx.strokeStyle='#0d1a2a';ctx.lineWidth=.5;
697
+ for(let x=Math.floor(mnX/10)*10;x<=mxX+10;x+=10){ctx.beginPath();ctx.moveTo(tx(x),margin-20);ctx.lineTo(tx(x),H-margin+20);ctx.stroke()}
698
+
699
+ // Planned strokes (dashed, dim)
700
+ ctx.setLineDash([4,4]);ctx.lineWidth=1.5;
701
+ for(let i=1;i<writerPlanned.length;i++){
702
+ if(!writerPlanned[i].penDown)continue;
703
+ ctx.beginPath();ctx.moveTo(tx(writerPlanned[i-1].x),ty(writerPlanned[i-1].y));
704
+ ctx.lineTo(tx(writerPlanned[i].x),ty(writerPlanned[i].y));
705
+ ctx.strokeStyle=`hsla(${writerPlanned[i].hue},70%,55%,.15)`;ctx.stroke();
706
+ }
707
+ ctx.setLineDash([]);
708
+
709
+ // Completed trail β€” bright, per-segment
710
+ if(trailHistory.length>1){
711
+ ctx.lineCap='round';ctx.lineJoin='round';
712
+ for(let i=1;i<trailHistory.length;i++){
713
+ const p=trailHistory[i-1],c2=trailHistory[i];
714
+ if(!c2.penDown)continue;
715
+ ctx.lineWidth=5;
716
+ ctx.beginPath();ctx.moveTo(tx(p.x),ty(p.y));ctx.lineTo(tx(c2.x),ty(c2.y));
717
+ ctx.strokeStyle=`hsla(${c2.hue},85%,60%,.9)`;
718
+ ctx.shadowColor=`hsla(${c2.hue},85%,60%,.5)`;ctx.shadowBlur=10;ctx.stroke();ctx.shadowBlur=0;
719
+ }
720
+ }
721
+
722
+ // Waypoint dots
723
+ for(let i=0;i<writerPlanned.length;i++){
724
+ const p=writerPlanned[i];
725
+ ctx.beginPath();ctx.arc(tx(p.x),ty(p.y),2,0,Math.PI*2);ctx.fillStyle='#2a3548';ctx.fill();
726
+ }
727
+
728
+ // Current segment marker
729
+ if(writerActive&&writerIdx<writerSegments.length){
730
+ const seg=writerSegments[writerIdx];
731
+ ctx.beginPath();ctx.arc(tx(seg.x),ty(seg.y),8,0,Math.PI*2);ctx.fillStyle='rgba(255,171,0,.3)';ctx.fill();
732
+ ctx.beginPath();ctx.arc(tx(seg.x),ty(seg.y),4,0,Math.PI*2);ctx.fillStyle='#ffab00';ctx.fill();
733
+ }
734
+
735
+ // Completed markers (green)
736
+ for(let i=0;i<trailHistory.length;i++){
737
+ const t=trailHistory[i];
738
+ if(!t.penDown)continue;
739
+ ctx.beginPath();ctx.arc(tx(t.x),ty(t.y),3,0,Math.PI*2);ctx.fillStyle='#00e676';ctx.fill();
740
+ }
741
+
742
+ // Letter labels
743
+ const word=$('wordInput').value.toUpperCase();
744
+ ctx.font='600 12px Syne,sans-serif';ctx.textAlign='center';
745
+ for(let i=0;i<word.length;i++){
746
+ const x=margin+(i+.5)*(W-2*margin)/Math.max(1,word.length);
747
+ const curLetter=writerActive&&writerIdx<writerSegments.length?writerSegments[writerIdx].letterIdx:-1;
748
+ ctx.fillStyle=i<(curLetter>=0?curLetter:writerDone?word.length:0)?'#00e676':i===curLetter?'#ffab00':'#2a3548';
749
+ ctx.fillText(word[i],x,30);
750
+ }
751
+ }
752
+
753
+
754
+ // ═══════════════════════════════════════════════
755
+ // MAIN TICK + UI
756
+ // ═══════════════════════════════════════════════
757
+ function autoSensors(){const t=net.time;sensorValues=[Math.sin(t*.5)>.3?1:(Math.sin(t*.5)<-.3?-1:0),Math.cos(t*.7)>.3?1:(Math.cos(t*.7)<-.3?-1:0),Math.random()<.03?1:0,Math.sin(t*2.1)>.7?1:0,Math.sin(t*.8)>.2?1:-1,Math.cos(t*1.3)>.5?1:-1];updateSensorUI()}
758
+
759
+ function updateSensorUI(){document.querySelectorAll('.sensor-btn').forEach(b=>{const i=parseInt(b.dataset.sensor),v=sensorValues[i];b.dataset.val=v;b.querySelector('b').textContent=v;b.classList.remove('active-exc','active-inh');if(v===1)b.classList.add('active-exc');else if(v===-1)b.classList.add('active-inh')})}
760
+
761
+ function tick(){
762
+ const dt=20;
763
+ if(mode==='auto')autoSensors();
764
+ // In writer mode, sensorValues are set by executeNextSegment
765
+ net.neuromod.setBaselines(parseInt($('sDA').value)/100,parseInt($('s5HT').value)/100,parseInt($('sACh').value)/100,parseInt($('sNA').value)/100);
766
+ net.simulateStep(sensorValues,dt);
767
+ const e=net.neurons.filter(n=>n.state===1).length,i=net.neurons.filter(n=>n.state===-1).length;
768
+ $('sExc').textContent=e;$('sNeu').textContent=net.N-e-i;$('sInh').textContent=i;
769
+ $('oTime').textContent=net.time.toFixed(3);$('oStep').textContent=net.step;$('oEnergy').textContent=net.energy.toFixed(2);$('oActive').textContent=e+i;$('oTotal').textContent=net.N;
770
+ if(activeTab==='brain')drawNetwork();else drawPathCanvas();
771
+ }
772
+
773
+ function resizeCanvas(){
774
+ ['netCanvas','pathCanvas'].forEach(id=>{const c=$(id),p=c.parentElement;if(p.hasAttribute('data-hidden'))return;c.width=p.clientWidth;c.height=p.clientHeight});
775
+ layoutNeurons();if(activeTab==='brain')drawNetwork();else drawPathCanvas();
776
+ }
777
+ window.addEventListener('resize',resizeCanvas);
778
+
779
+ // Sensor clicks (manual)
780
+ document.querySelectorAll('.sensor-btn').forEach(b=>b.addEventListener('click',()=>{if(mode!=='manual'||writerActive)return;const i=parseInt(b.dataset.sensor);let v=parseInt(b.dataset.val);v=v===0?1:v===1?-1:0;sensorValues[i]=v;updateSensorUI()}));
781
+
782
+ $('modeManual').addEventListener('click',()=>{mode='manual';$('modeManual').classList.add('active');$('modeAuto').classList.remove('active')});
783
+ $('modeAuto').addEventListener('click',()=>{mode='auto';$('modeAuto').classList.add('active');$('modeManual').classList.remove('active')});
784
+
785
+ $('btnRun').addEventListener('click',()=>{if(running){clearInterval(simInterval);running=false;$('btnRun').textContent='β–Ά Run'}else{simInterval=setInterval(tick,20);running=true;$('btnRun').textContent='⏸ Pause';log('Simulation running')}});
786
+ $('btnStep').addEventListener('click',tick);
787
+ $('btnReset').addEventListener('click',()=>{if(running){clearInterval(simInterval);running=false;$('btnRun').textContent='β–Ά Run'}abortWriting();Object.assign(net,new NeuraxonNetwork(6,12,3));sensorValues=[0,0,0,0,0,0];trailHistory=[];writerPlanned=[];writerSegments=[];updateSensorUI();layoutNeurons();drawNetwork();log('Reset')});
788
+
789
+ // Sliders
790
+ $('sSize').addEventListener('input',()=>{$('vSize').textContent=$('sSize').value;refreshPreview()});
791
+ $('sWSpeed').addEventListener('input',()=>$('vWSpeed').textContent=$('sWSpeed').value);
792
+ $('sSegTime').addEventListener('input',()=>$('vSegTime').textContent=$('sSegTime').value);
793
+ $('sDA').addEventListener('input',()=>$('vDA').textContent=(parseInt($('sDA').value)/100).toFixed(2));
794
+ $('s5HT').addEventListener('input',()=>$('v5HT').textContent=(parseInt($('s5HT').value)/100).toFixed(2));
795
+ $('sACh').addEventListener('input',()=>$('vACh').textContent=(parseInt($('sACh').value)/100).toFixed(2));
796
+ $('sNA').addEventListener('input',()=>$('vNA').textContent=(parseInt($('sNA').value)/100).toFixed(2));
797
+
798
+ function refreshPreview(){const w=$('wordInput').value.trim();if(w){writerPlanned=wordToPlannedPath(w,parseInt($('sSize').value));trailHistory=[];if(activeTab==='writer')drawPathCanvas()}}
799
+ $('wordInput').addEventListener('input',refreshPreview);
800
+
801
+ $('btnWrite').addEventListener('click',startWriting);
802
+ $('btnAbort').addEventListener('click',abortWriting);
803
+
804
+ // Tabs
805
+ document.querySelectorAll('.tab').forEach(t=>t.addEventListener('click',()=>{document.querySelectorAll('.tab').forEach(x=>x.classList.remove('active'));t.classList.add('active');activeTab=t.dataset.tab;if(activeTab==='brain'){$('panelBrain').removeAttribute('data-hidden');$('panelWriter').setAttribute('data-hidden','')}else{$('panelWriter').removeAttribute('data-hidden');$('panelBrain').setAttribute('data-hidden','')}requestAnimationFrame(resizeCanvas)}));
806
+
807
+ // BLE
808
+ $('btnConnect').addEventListener('click',async()=>{
809
+ try{$('btnConnect').disabled=true;sphero=new SpheroMiniBLE();
810
+ sphero.onDisconnect=()=>{bleConnected=false;$('badge').textContent='OFFLINE';$('badge').className='conn-badge off';$('btnConnect').disabled=false;$('btnDisconnect').disabled=true;log('Disconnected')};
811
+ await sphero.connect();await sphero.setMainLED(0,255,80);await sphero.resetYaw();
812
+ bleConnected=true;$('badge').textContent='CONNECTED';$('badge').className='conn-badge on';$('btnConnect').disabled=true;$('btnDisconnect').disabled=false;
813
+ $('btnTestMotor').disabled=false;$('btnTestLED').disabled=false;
814
+ log('Sphero connected & ready!')}catch(e){log('BLE: '+e.message);$('btnConnect').disabled=false}
815
+ });
816
+ $('btnDisconnect').addEventListener('click',async()=>{if(sphero){try{await sphero.stop();await sphero.setMainLED(0,0,0)}catch(e){}await sphero.disconnect()}bleConnected=false;$('badge').textContent='OFFLINE';$('badge').className='conn-badge off';$('btnConnect').disabled=false;$('btnDisconnect').disabled=true;$('btnTestMotor').disabled=true;$('btnTestLED').disabled=true});
817
+
818
+ // Test buttons β€” verify BLE is really working
819
+ $('btnTestMotor').addEventListener('click',async()=>{
820
+ if(!sphero||!bleConnected)return;
821
+ log('TEST: roll forward heading=0 speed=80 for 1s…');
822
+ try{
823
+ await sphero.setMainLED(255,100,0);
824
+ await sphero.roll(80,0);
825
+ await sleep(1000);
826
+ await sphero.stop();
827
+ await sphero.setMainLED(0,255,80);
828
+ log('TEST: motor OK β€” Sphero should have moved forward');
829
+ }catch(e){log('TEST FAIL: '+e.message)}
830
+ });
831
+ $('btnTestLED').addEventListener('click',async()=>{
832
+ if(!sphero||!bleConnected)return;
833
+ log('TEST: cycling LED colors…');
834
+ try{
835
+ await sphero.setMainLED(255,0,0);await sleep(400);
836
+ await sphero.setMainLED(0,255,0);await sleep(400);
837
+ await sphero.setMainLED(0,0,255);await sleep(400);
838
+ await sphero.setMainLED(255,255,255);await sleep(400);
839
+ await sphero.setMainLED(0,255,80);
840
+ log('TEST: LED OK');
841
+ }catch(e){log('TEST FAIL: '+e.message)}
842
+ });
843
+
844
+ // Init
845
+ requestAnimationFrame(()=>{
846
+ resizeCanvas();refreshPreview();
847
+ log(`Network: 6in β†’ 12 hidden β†’ 3out | ${net.synapses.length} synapses`);
848
+ log('Watts-Strogatz small-world (k=4, Ξ²=0.3)');
849
+ log('Writer: timed segments, Neuraxon modulates speed Β±30%');
850
+ log('Type a word β†’ WRITE β†’ Sphero traces it on the ground');
851
+ });
852
+ </script>
853
+ </body>
854
+ </html>