|
|
@@ -7,31 +7,41 @@
|
|
|
<style>
|
|
|
:root{ --bg:#11131a; --card:#1b1f2b; --fg:#e7ecf3; --mut:#8a93a6; --acc:#5bd6a0; --acc2:#5b9bd6; --bad:#d65b6e; }
|
|
|
*{box-sizing:border-box}
|
|
|
- body{margin:0;font:14px/1.5 -apple-system,Segoe UI,Roboto,sans-serif;background:var(--bg);color:var(--fg);padding:24px}
|
|
|
+ body{margin:0;font:14px/1.5 -apple-system,Segoe UI,Roboto,sans-serif;background:var(--bg);color:var(--fg)}
|
|
|
h1{font-size:20px;margin:0 0 4px}
|
|
|
- .sub{color:var(--mut);margin:0 0 20px}
|
|
|
- .wrap{max-width:680px;margin:0 auto}
|
|
|
- .card{background:var(--card);border:1px solid #272d3d;border-radius:12px;padding:18px 20px;margin-bottom:16px}
|
|
|
+ .sub{color:var(--mut);margin:0 0 18px;font-size:13px}
|
|
|
+ /* sticky top action bar: connection + device-maintenance actions */
|
|
|
+ .topbar{position:sticky;top:0;z-index:20;display:flex;align-items:center;gap:10px;flex-wrap:wrap;
|
|
|
+ padding:9px 18px;background:#151925;border-bottom:1px solid #272d3d}
|
|
|
+ .topbar .brand{font-weight:700;font-size:15px;letter-spacing:.02em}
|
|
|
+ .topbar .brand small{color:var(--mut);font-weight:400;font-size:11px;margin-left:6px}
|
|
|
+ .topbar .spacer{flex:1 1 16px}
|
|
|
+ .topbar button{padding:7px 12px}
|
|
|
+ .wrap{max-width:980px;margin:0 auto;padding:20px}
|
|
|
+ /* cards stack in one column; width comes from the wider wrap + grids/rows filling it horizontally */
|
|
|
+ .card{background:var(--card);border:1px solid #272d3d;border-radius:12px;padding:16px 18px;margin:0 0 16px}
|
|
|
.card h2{font-size:13px;text-transform:uppercase;letter-spacing:.06em;color:var(--mut);margin:0 0 14px}
|
|
|
- .row{display:flex;align-items:center;gap:14px;margin:12px 0}
|
|
|
+ .row{display:flex;align-items:center;gap:14px;margin:12px 0;flex-wrap:wrap}
|
|
|
.row label{flex:0 0 150px;color:var(--fg)}
|
|
|
.row .val{flex:0 0 64px;text-align:right;color:var(--acc);font-variant-numeric:tabular-nums}
|
|
|
- input[type=range]{flex:1;accent-color:var(--acc)}
|
|
|
- select{background:#0e1017;color:var(--fg);border:1px solid #313a4f;border-radius:8px;padding:6px 8px;flex:1}
|
|
|
+ input[type=range]{flex:1;min-width:120px;accent-color:var(--acc)}
|
|
|
+ select{background:#0e1017;color:var(--fg);border:1px solid #313a4f;border-radius:8px;padding:6px 8px;flex:1;min-width:120px}
|
|
|
button{background:#222a3a;color:var(--fg);border:1px solid #36405a;border-radius:8px;padding:9px 14px;cursor:pointer;font:inherit}
|
|
|
button:hover{border-color:var(--acc2)}
|
|
|
button.primary{background:var(--acc2);border-color:var(--acc2);color:#06121f;font-weight:600}
|
|
|
button.active{background:var(--acc);border-color:var(--acc);color:#062018;font-weight:600}
|
|
|
+ button.danger{background:#3a1420;border-color:#7a2233;color:#ff9bb0;font-weight:600}
|
|
|
+ button.danger:hover{border-color:#ff9bb0}
|
|
|
button:disabled{opacity:.4;cursor:default}
|
|
|
.pill{display:inline-block;padding:2px 9px;border-radius:99px;font-size:12px;font-weight:600}
|
|
|
.pill.up{background:#143626;color:var(--acc)} .pill.dn{background:#3a1822;color:var(--bad)}
|
|
|
- .statgrid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}
|
|
|
+ .statgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(155px,1fr));gap:10px}
|
|
|
.stat{background:#0e1017;border:1px solid #232a3a;border-radius:8px;padding:10px}
|
|
|
.stat .k{color:var(--mut);font-size:11px;text-transform:uppercase;letter-spacing:.05em}
|
|
|
.stat .v{font-size:18px;margin-top:2px}
|
|
|
- .modes{display:flex;gap:10px}
|
|
|
+ .modes{display:flex;gap:10px;flex-wrap:wrap}
|
|
|
.note{color:var(--mut);font-size:12px;margin-top:10px}
|
|
|
- #log{font:12px ui-monospace,Menlo,monospace;color:var(--mut);white-space:pre-wrap;margin-top:8px;max-height:120px;overflow:auto}
|
|
|
+ #log{font:12px ui-monospace,Menlo,monospace;color:var(--mut);white-space:pre-wrap;margin-top:8px;max-height:140px;overflow:auto}
|
|
|
.hide{display:none}
|
|
|
|
|
|
/* ---- controller slot tabs ---- */
|
|
|
@@ -50,19 +60,20 @@
|
|
|
</style>
|
|
|
</head>
|
|
|
<body>
|
|
|
+<div class="topbar">
|
|
|
+ <span class="brand">OpenPuck <small>SC2 puck · WebUSB</small></span>
|
|
|
+ <span id="connState" class="pill dn">disconnected</span>
|
|
|
+ <button id="connectBtn" class="primary">Connect</button>
|
|
|
+ <span class="spacer"></span>
|
|
|
+ <button id="dfuSerial" title="Reboot into serial DFU (adafruit-nrfutil), then replug">Serial DFU</button>
|
|
|
+ <button id="dfuUf2" title="Reboot into the UF2 mass-storage bootloader; drop a .uf2 on the drive">UF2 DFU</button>
|
|
|
+ <button id="factoryErase" class="danger" title="Wipe ALL bonds + settings and reboot to defaults. You must re-pair. Cannot be undone.">⚠ Factory erase</button>
|
|
|
+</div>
|
|
|
<div class="wrap">
|
|
|
- <h1>OpenPuck Config</h1>
|
|
|
- <p class="sub">nRF52840 Steam Controller 2 puck reimplementation — WebUSB. Chrome/Edge only.</p>
|
|
|
-
|
|
|
- <div class="card" id="connectCard">
|
|
|
- <div class="row" style="margin:0">
|
|
|
- <button id="connectBtn" class="primary">Connect to copycat</button>
|
|
|
- <span id="connState" class="pill dn">disconnected</span>
|
|
|
- </div>
|
|
|
- <p class="note">If nothing appears in the device picker, the copycat may be busy (quit any app holding it) or needs a replug.</p>
|
|
|
- </div>
|
|
|
+ <p class="sub">First connect uses the Chrome/Edge device picker; reconnects are automatic. If the picker is empty, quit any app holding the device or replug.</p>
|
|
|
|
|
|
<div id="panel" class="hide">
|
|
|
+ <div class="cards">
|
|
|
<div class="card">
|
|
|
<h2>USB mode</h2>
|
|
|
<div class="modes" style="flex-wrap:wrap">
|
|
|
@@ -89,7 +100,7 @@
|
|
|
<div class="row"><label>Y + back4 →</label><select class="chord" data-i="2"></select></div>
|
|
|
</div>
|
|
|
|
|
|
- <div class="card">
|
|
|
+ <div class="card wide">
|
|
|
<h2>Link status</h2>
|
|
|
|
|
|
<!-- Per-controller tabs: visible only when 2+ controllers are bonded -->
|
|
|
@@ -134,18 +145,6 @@
|
|
|
<span class="v" id="stabStatus" style="flex:0 0 auto;min-width:120px">—</span>
|
|
|
<span class="note" style="flex:1">Buzzes the controllers every 10 s (keeps them awake) and times how long the puck stays up. On a reset it records the uptime and shows it by <em>Last reset</em>; it auto-resumes after the puck reconnects. The count is kept in this tab (a page refresh clears it).</span>
|
|
|
</div>
|
|
|
- <div class="row" style="margin:10px 0 0;border-top:1px solid #272d3d;padding-top:14px">
|
|
|
- <button id="dfuSerial">Reboot to Serial DFU</button>
|
|
|
- <span class="note" style="flex:1">Reboots the puck into serial DFU mode (adafruit-nrfutil). The device drops off USB — flash with <code>adafruit-nrfutil dfu serial …</code> then replug.</span>
|
|
|
- </div>
|
|
|
- <div class="row" style="margin:10px 0 0">
|
|
|
- <button id="dfuUf2">Reboot to UF2 Bootloader</button>
|
|
|
- <span class="note" style="flex:1">Reboots the puck into the UF2 mass-storage bootloader. It mounts as a USB drive — drag the <code>.uf2</code> file onto it to flash, then it reboots automatically.</span>
|
|
|
- </div>
|
|
|
- <div class="row" style="margin:18px 0 0;border-top:1px solid #4a2030;padding-top:14px">
|
|
|
- <button id="factoryErase" style="background:#3a1420;border-color:#7a2233;color:#ff9bb0;font-weight:600">⚠ Factory erase</button>
|
|
|
- <span class="note" style="flex:1"><strong style="color:#ff9bb0">Danger:</strong> wipes <em>all</em> persistent storage on the copycat — the paired controller bond <em>and</em> every saved setting (mode, chords, back-paddle map, sensitivity). The copycat reboots to factory defaults and you must <strong>re-pair the controller</strong>. This cannot be undone.</span>
|
|
|
- </div>
|
|
|
</div>
|
|
|
|
|
|
<div class="card" id="mouseCard">
|
|
|
@@ -169,7 +168,7 @@
|
|
|
<p class="note">Gyro sensitivity multiplier for Switch Pro mode (1.0× = unchanged).</p>
|
|
|
</div>
|
|
|
|
|
|
- <div class="card">
|
|
|
+ <div class="card wide">
|
|
|
<h2>Button mapping</h2>
|
|
|
<p class="note" style="margin-top:0">Separate mappings per emulated controller type — each only offers targets that exist on that controller. The type matching the current mode is highlighted. Steam & Lizard are native puck modes (Steam does its own remapping) and aren't configured here.</p>
|
|
|
<div id="typeCfgs"></div>
|
|
|
@@ -186,7 +185,7 @@
|
|
|
<p class="note">Import <strong>overwrites</strong> all pairings and settings on the connected puck, then reboots it (~2 s — reconnect after). Tip: pair every controller to one puck, export once, then import onto each spare.</p>
|
|
|
</div>
|
|
|
|
|
|
- <div class="card">
|
|
|
+ <div class="card wide">
|
|
|
<h2>Hang log</h2>
|
|
|
<p class="note" style="margin-top:0">One row per reset: uptime before it, reason, the stuck loop stage and captured PC (when the watchdog ISR could run), and usbd stack free. Accumulates across resets in this tab — a page refresh clears it. Pair with <em>Test stability</em> above for unattended runs.</p>
|
|
|
<div class="row" style="margin:0 0 8px;flex-wrap:wrap">
|
|
|
@@ -197,7 +196,7 @@
|
|
|
<pre id="hangOut" style="font:12px ui-monospace,Menlo,monospace;color:var(--fg);background:#0e1017;border:1px solid #232a3a;border-radius:8px;padding:10px;margin:0;max-height:260px;overflow:auto;white-space:pre"></pre>
|
|
|
</div>
|
|
|
|
|
|
- <div class="card logonly">
|
|
|
+ <div class="card wide logonly">
|
|
|
<h2>Capture — host → controller commands</h2>
|
|
|
<p class="note" style="margin-top:0">The firmware logs everything from boot into a big RAM ring: Steam's writes (<code>ifN</code>), the frames we transmit to the controller (<code>TX→ctlr</code>), and RF <code>LINK UP/DOWN</code> edges. For the reconnect buzz: trigger it (it appears moments after the puck boots / the controller reconnects), then connect here and click <b>Dump from boot</b> — the trigger is still in the ring. (Only present in a logging build, <code>-DOPK_LOG=1</code>.)</p>
|
|
|
<div class="row" style="margin:0;flex-wrap:wrap">
|
|
|
@@ -208,6 +207,7 @@
|
|
|
<p class="note">Dump streams the whole ring (oldest→newest) and then keeps live-updating until you press Stop. <code>cmd</code> is the command/report byte, then the raw bytes.</p>
|
|
|
<pre id="capOut" style="font:12px ui-monospace,Menlo,monospace;color:var(--fg);background:#0e1017;border:1px solid #232a3a;border-radius:8px;padding:10px;margin-top:10px;max-height:260px;overflow:auto;white-space:pre-wrap"></pre>
|
|
|
</div>
|
|
|
+ </div><!-- .cards -->
|
|
|
|
|
|
<div id="log"></div>
|
|
|
</div>
|