Update index.html
Browse files- index.html +854 -19
index.html
CHANGED
|
@@ -1,19 +1,854 @@
|
|
| 1 |
-
<!
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>
|