This repository has been archived on 2026-10-25. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Robot-Arm/web/static/index.html
T

914 lines
46 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="da">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Stepper-test</title>
<style>
:root {
--bg: #f4f5f7; --panel: #fff; --text: #1d2228; --muted: #6b7380; --border: #d9dde3;
--accent: #2f6fde; --ok: #1f9d55; --warn: #c98a00; --bad: #d23c3c;
--side: #1f2630; --side-text: #c9d1db; --side-active: #2f3a48;
/* ledningsfarver */
--w-power: #d23c3c; --w-gnd: #444b55; --w-logic: #2f6fde; --w-uart: #8e44ad; --w-motor: #1f9d55; --w-diag: #d98a00;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #15181c; --panel: #1e2227; --text: #e4e7eb; --muted: #8b939e; --border: #333940;
--accent: #5b8ff0; --ok: #3cc478; --warn: #e0a72a; --bad: #f05a5a;
--side: #101317; --side-text: #b4bcc6; --side-active: #232a33;
--w-power: #f05a5a; --w-gnd: #9aa3ad; --w-logic: #5b8ff0; --w-uart: #b77ad6; --w-motor: #3cc478; --w-diag: #e8a33a;
}
}
* { box-sizing: border-box; }
body { margin: 0; font: 15px/1.4 system-ui, sans-serif; background: var(--bg); color: var(--text); display: flex; min-height: 100vh; }
/* ---------- Sidebar ---------- */
nav { width: 220px; flex-shrink: 0; background: var(--side); color: var(--side-text); display: flex; flex-direction: column; padding: 16px 0; position: sticky; top: 0; height: 100vh; }
nav .brand { font-weight: 700; font-size: 17px; color: #fff; padding: 0 18px 16px; }
nav a { color: var(--side-text); text-decoration: none; padding: 10px 18px; display: block; border-left: 3px solid transparent; }
nav a:hover { background: var(--side-active); }
nav a.active { background: var(--side-active); border-left-color: var(--accent); color: #fff; }
nav .spacer { flex: 1; }
nav .side-stop { padding: 0 14px; display: flex; flex-direction: column; gap: 8px; }
nav .conn { padding: 14px 18px 0; font-size: 13px; }
nav .master { padding: 6px 18px 0; font-size: 13px; }
.content { flex: 1; min-width: 0; }
.page { display: none; }
.page.active { display: block; }
.page-head { padding: 14px 16px; border-bottom: 1px solid var(--border); background: var(--panel); }
.page-head h1 { font-size: 17px; margin: 0; }
/* ---------- Akse-vælger ---------- */
.axisbar { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 10px; }
.axisbtn { display: inline-flex; align-items: center; gap: 7px; }
.axisbtn.sel { background: var(--accent); border-color: var(--accent); color: #fff; }
.adot { width: 10px; height: 10px; border-radius: 50%; background: var(--border); border: 1px solid rgba(255,255,255,.6); display: inline-block; }
.adot.found { background: var(--warn); }
.adot.en { background: var(--ok); }
.adot.run { background: #fff; box-shadow: 0 0 0 2px var(--accent); }
.adot.stall { background: var(--bad); }
.dot { width: 10px; height: 10px; border-radius: 50%; background: var(--bad); display: inline-block; margin-right: 6px; }
.dot.on { background: var(--ok); }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 16px; padding: 16px; max-width: 1200px; }
section { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 14px; min-width: 0; }
h2 { font-size: 13px; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); margin: 0 0 10px; }
.row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-bottom: 8px; }
button { font: inherit; padding: 7px 12px; border-radius: 6px; border: 1px solid var(--border); background: var(--bg); color: var(--text); cursor: pointer; }
button:hover { border-color: var(--accent); }
button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
button.stop { background: var(--warn); border-color: var(--warn); color: #000; flex: 1; }
button.estop { background: var(--bad); border-color: var(--bad); color: #fff; font-weight: 700; flex: 1; padding: 14px; font-size: 18px; }
nav button.estop { font-size: 15px; padding: 10px; }
input, select { font: inherit; padding: 6px 8px; border-radius: 6px; border: 1px solid var(--border); background: var(--bg); color: var(--text); width: 110px; }
label { color: var(--muted); min-width: 70px; }
.big { font-size: 28px; font-variant-numeric: tabular-nums; font-weight: 600; }
.kv { display: grid; grid-template-columns: auto 1fr; gap: 3px 12px; font-variant-numeric: tabular-nums; }
.kv span:nth-child(odd) { color: var(--muted); }
.flag { padding: 1px 7px; border-radius: 10px; font-size: 12px; margin-right: 4px; background: var(--bg); border: 1px solid var(--border); white-space: nowrap; }
.flag.bad { background: var(--bad); color: #fff; border-color: var(--bad); }
.flag.good { background: var(--ok); color: #fff; border-color: var(--ok); }
#log { height: 220px; overflow-y: auto; font: 12px/1.35 ui-monospace, monospace; background: var(--bg); border-radius: 6px; padding: 8px; white-space: pre-wrap; word-break: break-all; }
.err { color: var(--bad); } .sent { color: var(--accent); } .info { color: var(--muted); }
.full { grid-column: 1 / -1; }
small { color: var(--muted); }
.tablewrap { overflow-x: auto; }
/* ---------- Oversigt ---------- */
table { border-collapse: collapse; width: 100%; font-size: 14px; }
th, td { text-align: left; padding: 6px 8px; border-bottom: 1px solid var(--border); vertical-align: top; }
th { color: var(--muted); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: .04em; }
td code { font: 13px ui-monospace, monospace; }
#overview td { font-variant-numeric: tabular-nums; vertical-align: middle; }
#overview tr.axrow { cursor: pointer; }
#overview tr.axrow:hover { background: var(--bg); }
#overview tr.axrow.sel { background: var(--bg); box-shadow: inset 3px 0 0 var(--accent); }
#overview tr.missing td { color: var(--muted); }
/* ---------- StallGuard ---------- */
#sgChart { width: 100%; height: 220px; display: block; margin-top: 10px; background: var(--bg); border-radius: 6px; }
.badge { font-size: 22px; font-weight: 700; padding: 10px 22px; border-radius: 8px; background: var(--bg); border: 1px solid var(--border); color: var(--muted); }
.badge.ok { color: var(--ok); border-color: var(--ok); }
.badge.stall { background: var(--bad); border-color: var(--bad); color: #fff; }
/* ---------- Ledningsdiagram ---------- */
.diagram svg { width: 100%; height: auto; display: block; }
.diagram text { fill: var(--text); font: 13px system-ui, sans-serif; }
.diagram .lbl-muted { fill: var(--muted); font-size: 12px; }
.diagram .title { font-weight: 700; font-size: 15px; }
.diagram .box { fill: var(--bg); stroke: var(--border); stroke-width: 2; }
.diagram .wire { fill: none; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; }
.w-power { stroke: var(--w-power); } .w-gnd { stroke: var(--w-gnd); } .w-logic { stroke: var(--w-logic); }
.w-uart { stroke: var(--w-uart); } .w-motor { stroke: var(--w-motor); } .w-diag { stroke: var(--w-diag); }
.diagram .junction { fill: var(--w-uart); }
.diagram .junction-diag { fill: var(--w-diag); }
.diagram .diode { fill: var(--w-diag); stroke: var(--w-diag); stroke-width: 2; }
.diagram .res { fill: var(--panel); stroke-width: 2.5; }
.legend { display: flex; gap: 16px; flex-wrap: wrap; font-size: 13px; color: var(--muted); margin-top: 8px; }
.legend i { display: inline-block; width: 18px; height: 4px; border-radius: 2px; vertical-align: middle; margin-right: 6px; }
.check { margin: 0; padding-left: 20px; } .check li { margin-bottom: 6px; }
/* DIAG-variant i diagrammerne: vis kun den valgte */
#wiring[data-diag="adc"] .v-diode, #wiring[data-diag="diode"] .v-adc { display: none; }
.seg button.on { background: var(--accent); border-color: var(--accent); color: #fff; }
@media (max-width: 760px) {
body { flex-direction: column; }
nav { width: 100%; height: auto; position: static; flex-direction: row; flex-wrap: wrap; align-items: center; padding: 8px; }
nav .brand { padding: 0 10px; }
nav a { border-left: none; border-bottom: 3px solid transparent; padding: 8px 10px; }
nav a.active { border-bottom-color: var(--accent); }
nav .spacer { display: none; }
nav .side-stop { flex-direction: row; width: 100%; padding: 8px 4px 0; }
nav .conn, nav .master { padding: 8px 6px 0; width: 100%; }
}
</style>
</head>
<body>
<nav>
<div class="brand">Stepper-test</div>
<a href="#styring" data-page="styring">Styring</a>
<a href="#stallguard" data-page="stallguard">StallGuard</a>
<a href="#ledninger" data-page="ledninger">Ledningsdiagram</a>
<div class="spacer"></div>
<div class="side-stop">
<button class="stop" onclick="send('stop')">STOP ALLE <small>[mellemrum]</small></button>
<button class="estop" onclick="send('estop')">NØDSTOP <small>[Esc]</small></button>
</div>
<div class="conn" id="conn"><span class="dot"></span>Ikke forbundet</div>
<div class="master" id="master">EN: –</div>
</nav>
<div class="content">
<!-- ================= STYRING ================= -->
<div class="page" id="page-styring">
<div class="page-head">
<h1>Styring</h1>
<div class="axisbar"></div>
</div>
<div class="grid">
<section class="full">
<div class="row">
<button class="stop" onclick="send('stop')">STOP ALLE (blødt) <small>[mellemrum]</small></button>
<button class="estop" onclick="send('estop')">NØDSTOP <small>[Esc]</small></button>
</div>
<div class="row" style="margin:0">
<button class="primary" onclick="send('en')">Enable alle fundne</button>
<button onclick="send('dis')">Disable alle</button>
<button onclick="send('status')">Opdatér alle</button>
<button onclick="send('!reset')">Reset ESP32</button>
</div>
</section>
<section class="full">
<h2>Oversigt – klik på en akse for at vælge den</h2>
<div class="tablewrap">
<table id="overview">
<thead><tr><th>Akse</th><th>Bus/adr</th><th>Driver</th><th>Position</th><th>Omdr</th><th>Hastighed</th><th>SG / sgt</th><th>Flag</th></tr></thead>
<tbody></tbody>
</table>
</div>
</section>
<section>
<h2>Status – <span class="selName">akse 0</span></h2>
<div class="big" id="pos">–</div>
<div class="info" id="posRev">&nbsp;</div>
<div class="kv" style="margin-top:10px">
<span>Mål</span><span id="target">–</span>
<span>Kører</span><span id="running">–</span>
<span>Driver</span><span id="enabled">–</span>
<span>Hastighed</span><span id="speed">–</span>
<span>Accel</span><span id="accel">–</span>
<span>Strøm</span><span id="ma">–</span>
<span>Mode</span><span id="mode">–</span>
<span>TMC</span><span id="tmc">–</span>
</div>
</section>
<section>
<h2>Driver og jog – <span class="selName">akse 0</span></h2>
<div class="row">
<button class="primary" onclick="sendA('en')">Enable</button>
<button onclick="sendA('dis')">Disable</button>
<button onclick="sendA('stop')">Stop</button>
<button onclick="sendA('zero')">Nulstil position</button>
</div>
<div class="row">
<button onclick="sendA('rev -1')">−1 omdr</button>
<button onclick="sendA('rev -0.25')">−¼</button>
<button onclick="sendA('rev 0.25')">+¼</button>
<button onclick="sendA('rev 1')">+1 omdr</button>
</div>
<div class="row">
<label>Move</label><input id="moveSteps" type="number" value="3200"><small>steps</small>
<button onclick="sendA('move ' + val('moveSteps'))">Kør</button>
</div>
<div class="row">
<label>Goto</label><input id="gotoSteps" type="number" value="0"><small>steps</small>
<button onclick="sendA('goto ' + val('gotoSteps'))">Kør</button>
</div>
<div class="row"><button onclick="sendA('uartdiag')">UART-diagnose (aksens bus)</button></div>
</section>
<section>
<h2>Indstillinger – <span class="selName">akse 0</span></h2>
<div class="row">
<label>Hastighed</label><input id="speedIn" type="number" min="1">
<small id="speedMax"></small><button onclick="sendA('speed ' + val('speedIn'))">Sæt</button>
</div>
<div class="row">
<label>Accel</label><input id="accelIn" type="number" min="1">
<small id="accelMax"></small><button onclick="sendA('accel ' + val('accelIn'))">Sæt</button>
</div>
<div class="row">
<label>Strøm</label><input id="maIn" type="number" min="50">
<small id="maMax"></small><button onclick="sendA('current ' + val('maIn'))">Sæt</button>
</div>
<div class="row">
<label>Mode</label>
<select id="modeIn"><option value="stealth">StealthChop</option><option value="spread">SpreadCycle</option></select>
<button onclick="sendA('mode ' + $('modeIn').value)">Sæt</button>
</div>
<small>Maks-værdier sættes i firmwaren (config.h) og gælder alle akser.</small>
</section>
<section class="full">
<h2>Log</h2>
<div id="log"></div>
<div class="row" style="margin-top:8px">
<input id="raw" style="flex:1" placeholder="Rå kommando, fx help eller 1 rev 1">
<button onclick="sendRaw()">Send</button>
<label style="min-width:0"><input type="checkbox" id="showStatus" style="width:auto"> vis S/G-linjer</label>
</div>
</section>
</div>
</div>
<!-- ================= STALLGUARD ================= -->
<div class="page" id="page-stallguard">
<div class="page-head">
<h1>StallGuard – belastning og stall-detektion</h1>
<div class="axisbar"></div>
</div>
<div class="grid">
<section class="full">
<div class="row" style="justify-content:space-between">
<div>
<span class="info">SG_RESULT – <span class="selName">akse 0</span></span>
<div class="big" id="sgNow">–</div>
<small>høj = let belastning, lav = tung belastning</small>
</div>
<div id="stallBadge" class="badge">–</div>
</div>
<canvas id="sgChart"></canvas>
<div class="legend">
<span><i style="background:var(--accent)"></i>SG_RESULT (live mens aksen kører)</span>
<span><i style="background:var(--bad)"></i>Stall-grænse = 2 × sgt</span>
</div>
</section>
<section>
<h2>Tærskel og stall-stop – <span class="selName">akse 0</span></h2>
<div class="row">
<span class="info">Effektiv sgt:</span> <b id="sgtEff">–</b>
<span id="sgtMode" class="flag">–</span>
</div>
<div class="row"><small id="sgtInfo">Stall når SG ≤ 2 × sgt</small></div>
<div class="row">
<button class="primary" onclick="sendA('sgauto on')">Auto (følger hastighed)</button>
</div>
<div class="row">
<label>Finjustér</label>
<button onclick="sgOffBy(-5)">−5</button>
<button onclick="sgOffBy(-1)">−1</button>
<b id="sgOffNow" style="min-width:36px;text-align:center">0</b>
<button onclick="sgOffBy(1)">+1</button>
<button onclick="sgOffBy(5)">+5</button>
<button onclick="sendA('sgoff 0')">Nulstil</button>
</div>
<div class="row"><small>Auto bruger aksens kalibreringspunkter i config.h (akse 0: 800 → 35, 3200 → 90). Falske stop: −. Intet stop: +.</small></div>
<div class="row" style="margin-top:6px">
<label>Manuel</label>
<input id="sgtRange" type="range" min="0" max="255" value="0" style="width:140px"
oninput="$('sgtIn').value=this.value">
<input id="sgtIn" type="number" min="0" max="255" value="0" style="width:70px"
oninput="$('sgtRange').value=this.value">
<button onclick="sendA('sgt ' + val('sgtIn'))">Sæt</button>
</div>
<div class="row"><small>Manuel slår auto fra. Bruges til at kalibrere nye akser.</small></div>
<div class="row">
<button class="primary" onclick="sendA('sg on')">Stall-stop til</button>
<button onclick="sendA('sg off')">Stall-stop fra</button>
<span id="stallStopState" class="info"></span>
</div>
<h2 style="margin-top:14px">Homing (sensorløs)</h2>
<div class="row">
<button onclick="sendA('home -')">Home −</button>
<button onclick="sendA('home +')">Home +</button>
<small>kører højst 2 omdr. mod et mekanisk stop</small>
</div>
</section>
<section>
<h2>Kør – <span class="selName">akse 0</span></h2>
<div class="row">
<button class="primary" onclick="sendA('en')">Enable</button>
<button onclick="sendA('dis')">Disable</button>
</div>
<div class="row">
<label>Hastighed</label>
<button onclick="sendA('speed 800')">800</button>
<button onclick="sendA('speed 1600')">1600</button>
<button onclick="sendA('speed 3200')">3200</button>
<small>steps/s · nu <span id="sgSpeed">–</span></small>
</div>
<div class="row">
<button onclick="sendA('rev -5')">−5 omdr</button>
<button onclick="sendA('rev -1')">−1 omdr</button>
<button onclick="sendA('rev 1')">+1 omdr</button>
<button onclick="sendA('rev 5')">+5 omdr</button>
</div>
</section>
<section>
<h2>Sådan tuner du</h2>
<ol class="check">
<li><b>Auto</b> er standard: sgt følger hastigheden. Tjek at den røde linje ligger under den frie kurve ved alle tre hastigheder.</li>
<li>Slå <b>stall-stop til</b>. Fri kørsel må ikke stoppe; bremser du, skal den stoppe med det samme.</li>
<li>Falske stop: finjustér −. Intet stop: finjustér +.</li>
<li>Et stall på én akse stopper <b>alle</b> akser (fælles DIAG-linje). Loggen viser, hvilken akse der stallede.</li>
<li><b>Ny akse/motor:</b> brug Manuel. Find sgt ved 800 og ved 3200, og indsæt dem som aksens kalibreringspunkter i <code>config.h</code>.</li>
</ol>
<small>StallGuard virker kun i StealthChop og kun over halvdelen af den indstillede hastighed (ikke under op- og nedbremsning).</small>
</section>
</div>
</div>
<!-- ================= LEDNINGSDIAGRAM ================= -->
<div class="page" id="page-ledninger">
<div class="page-head"><h1>Ledningsdiagram – ESP32 + TMC2209 + steppere</h1></div>
<div class="grid" id="wiring" data-diag="adc">
<section class="full">
<div class="row seg" style="margin:0">
<span class="info">DIAG-linjen:</span>
<button id="diagBtnAdc" onclick="setWiringDiag('adc', true)">Modstande + ADC</button>
<button id="diagBtnDiode" onclick="setWiringDiag('diode', true)">Diode-OR</button>
<small id="diagFw">firmwaren: –</small>
</div>
<small>Modstande + ADC virker uden dioder, men har mindre støjmargin. Diode-OR er mest pålidelig.
Skift i firmwaren med <code>DIAG_MODE</code> i <code>config.h</code> – og kobl om, så det passer.</small>
</section>
<section class="full diagram">
<h2>Én akse i detaljer (akse 0)</h2>
<svg viewBox="0 0 1000 600" role="img" aria-label="Ledningsdiagram for akse 0">
<!-- ===== ESP32 ===== -->
<rect class="box" x="30" y="50" width="200" height="420" rx="10"/>
<text class="title" x="130" y="80" text-anchor="middle">ESP32 DevKit</text>
<text x="222" y="104" text-anchor="end">3V3</text>
<text x="222" y="144" text-anchor="end">GND</text>
<text x="222" y="204" text-anchor="end">GPIO25</text>
<text x="222" y="244" text-anchor="end">GPIO26</text>
<text x="222" y="284" text-anchor="end">GPIO27</text>
<text x="222" y="344" text-anchor="end">GPIO17 (TX A)</text>
<text x="222" y="384" text-anchor="end">GPIO35 (RX A)</text>
<text x="222" y="444" text-anchor="end">GPIO39 (DIAG)</text>
<rect class="box" x="100" y="470" width="60" height="22" rx="4"/>
<text class="lbl-muted" x="130" y="486" text-anchor="middle">USB-C</text>
<text class="lbl-muted" x="130" y="516" text-anchor="middle">→ PC (strøm + seriel)</text>
<!-- ===== TMC2209 ===== -->
<rect class="box" x="490" y="50" width="200" height="420" rx="10"/>
<text class="title" x="590" y="80" text-anchor="middle">TMC2209 (BTT V1.3)</text>
<text class="lbl-muted" x="590" y="98" text-anchor="middle">akse 0 · adresse 0</text>
<text x="500" y="124">VIO</text>
<text x="500" y="164">GND</text>
<text x="500" y="204">STEP</text>
<text x="500" y="244">DIR</text>
<text x="500" y="284">EN</text>
<text x="500" y="364">PDN_UART</text>
<text x="500" y="414">MS1</text>
<text x="500" y="444">MS2</text>
<text x="590" y="462" text-anchor="middle">DIAG</text>
<text x="680" y="124" text-anchor="end">VM</text>
<text x="680" y="164" text-anchor="end">GND</text>
<text x="680" y="244" text-anchor="end">A1</text>
<text x="680" y="284" text-anchor="end">A2</text>
<text x="680" y="344" text-anchor="end">B1</text>
<text x="680" y="384" text-anchor="end">B2</text>
<!-- ===== Ledninger ESP32 → TMC2209 ===== -->
<path class="wire w-power" d="M230 100 H330 V120 H490"/>
<text x="360" y="112">3,3 V</text>
<path class="wire w-gnd" d="M230 140 H300 V160 H490"/>
<text x="360" y="152">GND</text>
<path class="wire w-logic" d="M230 200 H490"/>
<text x="360" y="192">STEP</text>
<path class="wire w-logic" d="M230 240 H490"/>
<text x="360" y="232">DIR</text>
<path class="wire w-logic" d="M230 280 H490"/>
<text x="300" y="272">EN (fælles, aktiv lav)</text>
<!-- UART: TX via 1 kΩ, RX direkte, begge på PDN_UART -->
<path class="wire w-uart" d="M230 340 H300"/>
<rect class="res" x="300" y="331" width="60" height="18" rx="3" stroke="var(--w-uart)"/>
<text x="330" y="326" text-anchor="middle" class="lbl-muted">1 kΩ</text>
<path class="wire w-uart" d="M360 340 H440 V360 H490"/>
<path class="wire w-uart" d="M230 380 H440 V360"/>
<circle class="junction" cx="440" cy="360" r="5"/>
<text class="lbl-muted" x="380" y="400">UART-bus A</text>
<!-- MS1/MS2 til GND (adresse 0) -->
<path class="wire w-gnd" d="M490 410 H460 V460"/>
<path class="wire w-gnd" d="M490 440 H460"/>
<path class="wire w-gnd" d="M446 460 H474 M451 466 H469 M456 472 H464"/>
<text class="lbl-muted" x="440" y="494" text-anchor="end">MS1 + MS2 → GND (adr 0)</text>
<!-- DIAG → (modstand eller diode) → GPIO39 -->
<path class="wire w-diag" d="M590 470 V520 H450"/>
<path class="wire w-diag" d="M400 520 H250 V440 H230"/>
<g class="v-adc">
<rect class="res" x="400" y="511" width="50" height="18" rx="3" stroke="var(--w-diag)"/>
<text class="lbl-muted" x="425" y="550" text-anchor="middle">4,7–10 kΩ (1 kΩ virker) · ingen pull-down</text>
</g>
<g class="v-diode">
<path class="wire w-diag" d="M450 520 H440 M418 520 H400"/>
<polygon class="diode" points="440,510 440,530 420,520"/>
<path class="wire w-diag" d="M418 508 V532"/>
<circle class="junction-diag" cx="300" cy="520" r="5"/>
<path class="wire w-diag" d="M300 520 V540"/>
<rect class="res" x="292" y="540" width="16" height="34" rx="3" stroke="var(--w-diag)"/>
<path class="wire w-gnd" d="M300 574 V582 M288 582 H312 M292 588 H308 M296 594 H304"/>
<text class="lbl-muted" x="316" y="562">10 kΩ</text>
<text class="lbl-muted" x="430" y="550" text-anchor="middle">Schottky (BAT54 / 1N5817)</text>
</g>
<!-- ===== Strømforsyning ===== -->
<rect class="box" x="820" y="60" width="150" height="120" rx="10"/>
<text class="title" x="895" y="90" text-anchor="middle">Strømforsyning</text>
<text class="lbl-muted" x="895" y="108" text-anchor="middle">12–24 V DC</text>
<text x="830" y="124">+</text>
<text x="830" y="164">−</text>
<path class="wire w-power" d="M690 120 H820"/>
<path class="wire w-gnd" d="M690 160 H820"/>
<path class="wire w-power" d="M750 120 V134"/>
<path class="wire w-gnd" d="M750 146 V160"/>
<path d="M738 134 H762" stroke="var(--text)" stroke-width="3"/>
<path d="M738 146 H762" stroke="var(--text)" stroke-width="3"/>
<text class="lbl-muted" x="768" y="145">≥100 µF</text>
<!-- ===== Motor ===== -->
<circle cx="880" cy="320" r="72" fill="var(--bg)" stroke="var(--border)" stroke-width="2"/>
<text class="title" x="880" y="314" text-anchor="middle">Stepper</text>
<text class="lbl-muted" x="880" y="334" text-anchor="middle">42-motor (1,8°)</text>
<path class="wire w-motor" d="M690 240 H760 V285 H812"/>
<path class="wire w-motor" d="M690 280 H745 V300 H808"/>
<text class="lbl-muted" x="760" y="232">spole A</text>
<path class="wire w-motor" d="M690 340 H808"/>
<path class="wire w-motor" d="M690 380 H760 V355 H812"/>
<text class="lbl-muted" x="760" y="402">spole B</text>
</svg>
<div class="legend">
<span><i style="background:var(--w-power)"></i>Forsyning</span>
<span><i style="background:var(--w-gnd)"></i>GND</span>
<span><i style="background:var(--w-logic)"></i>STEP / DIR / EN</span>
<span><i style="background:var(--w-uart)"></i>UART</span>
<span><i style="background:var(--w-motor)"></i>Motor</span>
<span><i style="background:var(--w-diag)"></i>DIAG</span>
</div>
</section>
<section class="full diagram">
<h2>Flere drivere på samme bus (bus A, akse 0–3) – <span class="v-adc">DIAG via modstande</span><span class="v-diode">DIAG via diode-OR</span></h2>
<svg viewBox="0 0 1000 430" role="img" aria-label="UART-bus og DIAG diode-OR for fire drivere">
<rect class="box" x="20" y="40" width="180" height="340" rx="10"/>
<text class="title" x="110" y="68" text-anchor="middle">ESP32</text>
<text x="192" y="94" text-anchor="end">GPIO17 (TX A)</text>
<text x="192" y="134" text-anchor="end">GPIO35 (RX A)</text>
<text x="192" y="334" text-anchor="end">GPIO39 (DIAG)</text>
<!-- UART-bus -->
<path class="wire w-uart" d="M200 90 H230"/>
<rect class="res" x="230" y="81" width="50" height="18" rx="3" stroke="var(--w-uart)"/>
<text class="lbl-muted" x="255" y="76" text-anchor="middle">1 kΩ</text>
<path class="wire w-uart" d="M280 90 H930"/>
<path class="wire w-uart" d="M200 130 H330 V90"/>
<circle class="junction" cx="330" cy="90" r="5"/>
<text class="lbl-muted" x="930" y="80" text-anchor="end">UART-bus A (én ledning til alle PDN_UART)</text>
<!-- DIAG-bus -->
<path class="wire w-diag" d="M200 330 H880"/>
<text class="lbl-muted" x="880" y="352" text-anchor="end">fælles DIAG-linje</text>
<g class="v-diode">
<circle class="junction-diag" cx="260" cy="330" r="5"/>
<path class="wire w-diag" d="M260 330 V345"/>
<rect class="res" x="252" y="345" width="16" height="34" rx="3" stroke="var(--w-diag)"/>
<path class="wire w-gnd" d="M260 379 V388 M248 388 H272 M252 394 H268 M256 400 H264"/>
<text class="lbl-muted" x="276" y="366">10 kΩ pull-down</text>
</g>
<text class="v-adc lbl-muted" x="220" y="366">ingen pull-down – de lave DIAG-udgange holder linjen nede</text>
<!-- Fire drivere -->
<g id="busDrivers"></g>
</svg>
<p><small>Bus B (GPIO4 TX via 1 kΩ, GPIO34 RX) til akse 4–5 kobles på samme måde, og deres DIAG går til den <b>samme</b> DIAG-linje via hver sin
<span class="v-adc">modstand (samme værdi som de andre)</span><span class="v-diode">diode</span>.
EN (GPIO27), VIO (3,3 V) og GND går til alle drivere.</small></p>
</section>
<section class="full">
<h2>Alle akser</h2>
<div class="tablewrap">
<table>
<tr><th>Akse</th><th>STEP</th><th>DIR</th><th>Bus</th><th>Adresse</th><th>MS1</th><th>MS2</th></tr>
<tr><td>0</td><td><code>GPIO25</code></td><td><code>GPIO26</code></td><td>A</td><td>0</td><td>GND</td><td>GND</td></tr>
<tr><td>1</td><td><code>GPIO33</code></td><td><code>GPIO14</code></td><td>A</td><td>1</td><td>3,3 V</td><td>GND</td></tr>
<tr><td>2</td><td><code>GPIO23</code></td><td><code>GPIO18</code></td><td>A</td><td>2</td><td>GND</td><td>3,3 V</td></tr>
<tr><td>3</td><td><code>GPIO19</code></td><td><code>GPIO13</code></td><td>A</td><td>3</td><td>3,3 V</td><td>3,3 V</td></tr>
<tr><td>4</td><td><code>GPIO32</code></td><td><code>GPIO5</code></td><td>B</td><td>0</td><td>GND</td><td>GND</td></tr>
<tr><td>5</td><td><code>GPIO16</code></td><td><code>GPIO15</code></td><td>B</td><td>1</td><td>3,3 V</td><td>GND</td></tr>
</table>
</div>
<h2 style="margin-top:16px">Fælles</h2>
<div class="tablewrap">
<table>
<tr><th>Signal</th><th>ESP32</th><th>Note</th></tr>
<tr><td>EN (alle drivere)</td><td><code>GPIO27</code></td><td>Lav = drivere aktive. Den enkelte akse slås til/fra over UART.</td></tr>
<tr><td>Bus A TX / RX</td><td><code>GPIO17</code> / <code>GPIO35</code></td><td>TX via 1 kΩ, RX direkte på bussen. Akse 0–3.</td></tr>
<tr><td>Bus B TX / RX</td><td><code>GPIO4</code> / <code>GPIO34</code></td><td>TX via 1 kΩ, RX direkte på bussen. Akse 4–5.</td></tr>
<tr><td>DIAG (alle drivere)</td><td><code>GPIO39</code></td><td>
<b>Modstande + ADC</b> (<code>DIAG_MODE = ResistorAdc</code>): én modstand pr. driver fra DIAG til linjen, alle med <b>samme værdi</b> (4,7–10 kΩ anbefales, 1 kΩ virker). Ingen pull-down. Linjen er 0 V uden stall og mindst ~0,55 V ved stall (6 drivere), så den måles med ADC'en.<br>
<b>Diode-OR</b> (<code>DIAG_MODE = DiodeOr</code>): én Schottky-diode pr. driver (BAT54/1N5817/1N5819/BAT85; anode på DIAG, katode på linjen) + 10 kΩ fra GPIO39 til GND. Pull-down'en er <b>påkrævet</b>, fordi GPIO39 ikke har en intern. 1N4148 virker, men giver kun ~2,7 V (lille margin).<br>
Forbind aldrig flere DIAG direkte uden modstand eller diode.</td></tr>
<tr><td>I²C (encodere senere)</td><td><code>GPIO21</code> / <code>GPIO22</code></td><td>Reserveret – må ikke bruges til andet.</td></tr>
<tr><td>VIO</td><td><code>3V3</code></td><td>Til alle drivere. 3,3 V, ikke 5 V.</td></tr>
<tr><td>GND</td><td><code>GND</code></td><td>Fælles GND mellem ESP32, drivere og strømforsyning er påkrævet.</td></tr>
</table>
</div>
<p><small><b>Adresse:</b> MS1 er bit 0 og MS2 er bit 1. "3,3 V" betyder forbundet til VIO (3,3 V) – aldrig til VM.
<b>Modul:</b> brug BigTreeTech TMC2209 V1.3 (eller et andet modul med dokumenteret UART). De lilla AliExpress-moduler har ikke PDN_UART ført ud og virker kun i standalone-mode.
Benavne varierer mellem moduler, og pinplaceringen på ESP32 varierer også: find benene efter teksten på boardet.</small></p>
</section>
<section class="full">
<h2>Før strømmen tændes</h2>
<ul class="check">
<li>Tilslut <b>USB først</b>, tænd derefter for motorstrømmen. Sluk motorstrømmen, <b>før</b> USB tages ud.</li>
<li><b>Tag aldrig motorstikket ud eller i</b>, mens der er strøm på VM. Det kan ødelægge driveren.</li>
<li>Hold motorkabler væk fra USB-kablet. Støj kan få pc'en til at miste forbindelsen til ESP32.</li>
<li><b>TMC2209 svarer kun på UART, når VM er tændt.</b> Firmwaren leder efter alle drivere løbende, så når VM tændes, står der <code># aN UART OK - driver fundet</code> i loggen for hver driver.</li>
<li>Finder den ikke en driver: kør <b>UART-diagnose</b> på aksen. Den viser, hvilke adresser der svarer på bussen. Svarer driveren på en anden adresse end forventet, sidder MS1/MS2 forkert.</li>
<li>Slukkes VM, mens en akse er enabled, disabler firmwaren den. Send <code>en</code> igen, når VM er tilbage.</li>
</ul>
</section>
</div>
</div>
</div>
<script>
let ws, noReplyTimer = null, glob = null, filledFor = -1;
const axes = []; // seneste status pr. akse (fra S-linjer)
const sgHist = {}; // akse -> [[tid_ms, sg], ...] mens aksen kører
const SG_WINDOW_MS = 10000;
let sel = 0;
try { sel = parseInt(localStorage.getItem('selAxis'), 10) || 0; } catch (e) {}
const $ = id => document.getElementById(id);
const val = id => $(id).value.trim();
// ---------- Sider (hash-routing, så WebSocket'en ikke lukkes ved sideskift) ----------
function showPage() {
const name = (location.hash || '#styring').slice(1);
const page = $('page-' + name) ? name : 'styring';
document.querySelectorAll('.page').forEach(p => p.classList.toggle('active', p.id === 'page-' + page));
document.querySelectorAll('nav a').forEach(a => a.classList.toggle('active', a.dataset.page === page));
drawSgChart();
}
window.addEventListener('hashchange', showPage);
// ---------- Log og afsendelse ----------
function log(text, cls) {
const el = $('log');
const line = document.createElement('div');
if (cls) line.className = cls;
line.textContent = text;
el.appendChild(line);
while (el.childNodes.length > 500) el.removeChild(el.firstChild);
el.scrollTop = el.scrollHeight;
}
function send(cmd) {
if (!ws || ws.readyState !== 1) { log('ikke forbundet til serveren', 'err'); return; }
ws.send(cmd);
log('> ' + cmd, 'sent');
}
const sendA = cmd => send(`${sel} ${cmd}`); // kommando til den valgte akse
function sendRaw() { const c = val('raw'); if (c) { send(c); $('raw').value = ''; } }
function setConn(ok, text) {
$('conn').innerHTML = `<span class="dot ${ok ? 'on' : ''}"></span>`;
$('conn').append(text);
}
// Bed ESP32 om fuld status (G + én S pr. akse) uden at fylde loggen. Siden kender ellers kun de
// akser, den tilfældigvis har hørt fra siden indlæsning.
let lastStatusReq = 0;
function requestStatus() {
if (!ws || ws.readyState !== 1 || Date.now() - lastStatusReq < 1000) return;
lastStatusReq = Date.now();
ws.send('status');
}
// ---------- Akse-valg ----------
// Antallet af akser kommer fra firmwaren (G-linjen) – aldrig fra hvor mange akser vi har data for.
function axisCount() { return (glob && glob.axes) || 6; }
function axisDotClass(s) {
if (!s || !s.tmc.uart) return '';
if (s.stalled) return 'stall';
if (s.running) return 'run';
return s.enabled ? 'en' : 'found';
}
function selectAxis(i) {
sel = i;
try { localStorage.setItem('selAxis', String(i)); } catch (e) {}
filledFor = -1;
renderAxisBars(); renderOverview(); showSelected();
}
// Knapper og tabelrækker oprettes kun én gang og opdateres derefter på stedet. Blev de bygget
// forfra ved hver status-linje (10×/s mens en akse kører), blev knappen udskiftet mellem
// "tryk ned" og "slip", og browseren registrerede aldrig klikket.
function renderAxisBars() {
const n = axisCount();
document.querySelectorAll('.axisbar').forEach(bar => {
if (bar.children.length !== n) {
bar.innerHTML = Array.from({ length: n }, (_, i) =>
`<button class="axisbtn" data-axis="${i}"><span class="adot"></span>Akse ${i}</button>`).join('');
}
Array.from(bar.children).forEach((b, i) => {
b.classList.toggle('sel', i === sel);
b.firstElementChild.className = 'adot ' + axisDotClass(axes[i]);
});
});
document.querySelectorAll('.selName').forEach(e => { e.textContent = 'akse ' + sel; });
}
// Valg af akse sker allerede ved "tryk ned" (pointerdown), så det virker selv om indholdet
// opdateres midt i klikket. 'click' er med for tastatur (Enter/mellemrum på en knap).
function onAxisPick(e) {
const t = e.target.closest('[data-axis]');
if (t && +t.dataset.axis !== sel) selectAxis(+t.dataset.axis);
}
document.querySelectorAll('.axisbar, #overview tbody').forEach(el => {
el.addEventListener('pointerdown', onAxisPick);
el.addEventListener('click', onAxisPick);
});
function tmcFlags(t) {
const flag = (name, bad) => `<span class="flag ${bad ? 'bad' : ''}">${name}</span>`;
if (!t.uart) return '<span class="flag">ingen driver</span>';
const bad = [];
if (t.ot) bad.push(flag('overtemp', true));
if (t.otpw) bad.push(flag('varm', true));
if (t.s2g) bad.push(flag('kortslut', true));
if (t.ol) bad.push(flag('åben spole', true));
return bad.length ? bad.join('') : '<span class="flag good">ok</span>';
}
function renderOverview() {
const tb = document.querySelector('#overview tbody');
const n = axisCount();
if (tb.children.length !== n) {
tb.innerHTML = Array.from({ length: n }, (_, i) =>
`<tr class="axrow" data-axis="${i}">${'<td></td>'.repeat(8)}</tr>`).join('');
}
Array.from(tb.children).forEach((tr, i) => {
const s = axes[i];
const found = !!(s && s.tmc.uart);
tr.classList.toggle('sel', i === sel);
tr.classList.toggle('missing', !found);
const drv = !s ? '–' : !found ? 'mangler' : s.enabled ? (s.running ? 'kører' : 'enabled') : 'disabled';
const cells = [
`<span class="adot ${axisDotClass(s)}"></span> ${i}`,
s ? `${'AB'[s.bus]}/${s.adr}` : '–',
drv,
s ? s.pos : '–',
s ? (s.pos / s.spr).toFixed(2) : '–',
s ? s.speed : '–',
(found ? s.sg + ' / ' + s.sgt : '–') + (s && s.stallStop ? ' 🛡' : ''),
s ? tmcFlags(s.tmc) + (s.stalled ? '<span class="flag bad">STALL</span>' : '') + (s.homing ? '<span class="flag">homing</span>' : '') : '',
];
// Skriv kun celler der har ændret sig (mindre DOM-arbejde mens akser kører)
cells.forEach((html, k) => {
const td = tr.children[k];
if (td._html !== html) { td._html = html; td.innerHTML = html; }
});
});
}
// ---------- Valgt akse: Styring ----------
function showSelected() {
const s = axes[sel];
if (!s) {
// Ingen data for denne akse endnu: vis tomt (ikke en anden akses værdier) og hent status.
for (const id of ['pos', 'posRev', 'target', 'running', 'enabled', 'speed', 'accel', 'ma', 'mode', 'sgNow', 'sgtEff', 'sgSpeed'])
$(id).textContent = '–';
$('tmc').innerHTML = '–';
for (const id of ['speedIn', 'accelIn', 'maIn']) $(id).value = '';
filledFor = -1;
requestStatus();
return;
}
$('pos').textContent = s.pos + ' steps';
$('posRev').textContent = (s.pos / s.spr).toFixed(3) + ' omdr';
$('target').textContent = s.target;
$('running').textContent = s.running ? 'ja' : 'nej';
$('enabled').textContent = !s.tmc.uart ? 'ingen driver fundet' : s.enabled ? 'enabled' : 'disabled';
$('speed').textContent = s.speed + ' steps/s';
$('accel').textContent = s.accel + ' steps/s²';
$('ma').textContent = s.ma + ' mA';
$('mode').textContent = s.mode;
$('tmc').innerHTML = (s.tmc.uart ? `bus ${'AB'[s.bus]}, adr ${s.adr}, v0x${s.tmc.ver.toString(16)} ` : '') + tmcFlags(s.tmc);
if (glob) {
$('speedMax').textContent = 'max ' + glob.max.speed;
$('accelMax').textContent = 'max ' + glob.max.accel;
$('maMax').textContent = 'max ' + glob.max.ma;
for (const [id, k] of [['speedIn', 'speed'], ['accelIn', 'accel'], ['maIn', 'ma']]) $(id).max = glob.max[k];
}
if (filledFor !== sel) {
$('speedIn').value = s.speed; $('accelIn').value = s.accel; $('maIn').value = s.ma; $('modeIn').value = s.mode;
$('sgtIn').value = $('sgtRange').value = s.sgt;
filledFor = sel;
}
showStallGuard(s);
}
// ---------- Valgt akse: StallGuard ----------
let lastSgt = 0, lastSgOff = 0;
function sgOffBy(d) { sendA('sgoff ' + (lastSgOff + d)); }
function showStallGuard(s) {
lastSgt = s.sgt;
lastSgOff = s.sgOff;
$('sgNow').textContent = s.tmc.uart ? s.sg : '–';
$('sgSpeed').textContent = s.speed;
$('sgtEff').textContent = s.sgt;
$('sgtMode').textContent = s.sgAuto ? `auto @ ${s.speed} steps/s` : 'manuel';
$('sgOffNow').textContent = (s.sgOff > 0 ? '+' : '') + s.sgOff;
$('sgtInfo').textContent = `Stall når SG ≤ ${2 * s.sgt}` + (s.sgt === 0 ? ' (0 = reelt slået fra)' : '');
$('stallStopState').textContent = s.stallStop ? 'stall-stop er TIL' : 'stall-stop er fra';
const b = $('stallBadge');
if (!s.tmc.uart) { b.className = 'badge'; b.textContent = 'ingen driver'; }
else if (s.stalled) { b.className = 'badge stall'; b.textContent = 'STALL'; }
else if (s.homing) { b.className = 'badge'; b.textContent = 'homing…'; }
else { b.className = 'badge ok'; b.textContent = s.stallStop ? 'overvåger' : 'ok'; }
drawSgChart();
}
function drawSgChart() {
const c = $('sgChart');
const w = c.clientWidth, h = c.clientHeight;
if (!w) return; // siden er skjult
const dpr = window.devicePixelRatio || 1;
c.width = w * dpr; c.height = h * dpr;
const g = c.getContext('2d');
g.scale(dpr, dpr);
const css = getComputedStyle(document.documentElement);
const now = Date.now();
const hist = sgHist[sel] || [];
while (hist.length && hist[0][0] < now - SG_WINDOW_MS) hist.shift();
const yMax = Math.max(100, 2 * lastSgt * 1.2, ...hist.map(p => p[1] * 1.1));
const y = v => h - 20 - (v / yMax) * (h - 30);
const x = t => 40 + ((t - (now - SG_WINDOW_MS)) / SG_WINDOW_MS) * (w - 50);
g.font = '11px system-ui'; g.fillStyle = css.getPropertyValue('--muted');
g.strokeStyle = css.getPropertyValue('--border'); g.lineWidth = 1;
for (let i = 0; i <= 4; i++) {
const v = Math.round(yMax * i / 4);
g.beginPath(); g.moveTo(40, y(v)); g.lineTo(w - 10, y(v)); g.stroke();
g.fillText(v, 4, y(v) + 4);
}
g.fillText('sidste 10 s', w - 70, h - 4);
if (lastSgt > 0) {
g.strokeStyle = css.getPropertyValue('--bad'); g.setLineDash([6, 4]); g.lineWidth = 2;
g.beginPath(); g.moveTo(40, y(2 * lastSgt)); g.lineTo(w - 10, y(2 * lastSgt)); g.stroke();
g.setLineDash([]);
}
g.strokeStyle = css.getPropertyValue('--accent'); g.lineWidth = 2;
g.beginPath();
hist.forEach(([t, v], i) => {
// Ny linje-start hvis der er et hul (aksen stod stille imellem)
if (i === 0 || t - hist[i - 1][0] > 1000) g.moveTo(x(t), y(v)); else g.lineTo(x(t), y(v));
});
g.stroke();
}
setInterval(drawSgChart, 500); // rul grafen videre, også når der ikke kommer data
// ---------- Fælles tilstand ----------
function showGlobal(gj) {
glob = gj;
const mv = gj.diagMode === 'adc' && gj.diagMv >= 0 ? ` (${gj.diagMv} mV)` : '';
$('master').textContent = 'EN: ' + (gj.master ? 'aktiv (mindst én akse enabled)' : 'slukket') +
' · DIAG ' + (gj.diag ? 'HØJ' : 'lav') + mv;
$('diagFw').textContent = 'firmwaren: ' + (gj.diagMode === 'adc' ? 'modstande + ADC' : 'diode-OR');
if (!wiringDiagChosen) setWiringDiag(gj.diagMode, false);
renderAxisBars();
if (axes[sel]) showSelected();
}
// ---------- Indkommende linjer ----------
function handleLine(text) {
if (text.startsWith('S ')) {
clearTimeout(noReplyTimer);
const m = text.match(/^S (\d+) (\{.*\})$/);
if (!m) { log(text, 'err'); return; }
try {
const i = +m[1], s = JSON.parse(m[2]);
axes[i] = s;
if (s.running) (sgHist[i] = sgHist[i] || []).push([Date.now(), s.sg]);
renderAxisBars(); renderOverview();
if (i === sel) showSelected();
} catch (e) { log(text, 'err'); }
if ($('showStatus').checked) log(text, 'info');
} else if (text.startsWith('G ')) {
clearTimeout(noReplyTimer);
try { showGlobal(JSON.parse(text.slice(2))); } catch (e) { log(text, 'err'); }
if ($('showStatus').checked) log(text, 'info');
} else if (text.startsWith('err') || text.startsWith('# STALL') || text.includes('HOME fejl') || text.includes('driver mistet')) {
log(text, 'err');
} else if (text.startsWith('#')) log(text, 'info');
else log(text);
}
function connect() {
ws = new WebSocket(`ws://${location.host}/ws`);
ws.onopen = () => { log('forbundet til server', 'info'); requestStatus(); };
ws.onclose = () => { setConn(false, 'Server ikke tilgængelig'); setTimeout(connect, 1500); };
ws.onmessage = ev => {
const m = JSON.parse(ev.data);
if (m.type === 'conn') {
setConn(m.connected, m.connected ? `ESP32 på ${m.port}` : `Ingen ESP32 (${m.error || m.port})`);
clearTimeout(noReplyTimer);
if (m.connected) {
filledFor = -1;
noReplyTimer = setTimeout(() => log('ESP32 svarer ikke – prøv "Reset ESP32" eller tjek at firmwaren er uploadet', 'err'), 4000);
}
return;
}
handleLine(m.text);
};
}
document.addEventListener('keydown', e => {
if (e.key === 'Escape') { e.preventDefault(); send('estop'); }
else if (e.key === ' ' && !['INPUT', 'SELECT', 'BUTTON'].includes(e.target.tagName)) { e.preventDefault(); send('stop'); }
});
$('raw').addEventListener('keydown', e => { if (e.key === 'Enter') sendRaw(); });
// ---------- Ledningsdiagram: DIAG-variant ----------
let wiringDiagChosen = false; // har brugeren selv valgt? Ellers følg firmwaren
function setWiringDiag(mode, byUser) {
if (byUser) wiringDiagChosen = true;
$('wiring').dataset.diag = mode;
$('diagBtnAdc').classList.toggle('on', mode === 'adc');
$('diagBtnDiode').classList.toggle('on', mode === 'diode');
}
// ---------- Busdiagram: fire drivere på bus A (begge DIAG-varianter, CSS viser én) ----------
(function drawBusDrivers() {
const ms = ['MS1 GND · MS2 GND', 'MS1 3V3 · MS2 GND', 'MS1 GND · MS2 3V3', 'MS1 3V3 · MS2 3V3'];
let svg = '';
for (let k = 0; k < 4; k++) {
const x = 380 + k * 140, cx = x + 60;
svg += `<path class="wire w-uart" d="M${cx} 90 V150"/><circle class="junction" cx="${cx}" cy="90" r="5"/>` +
`<rect class="box" x="${x}" y="150" width="120" height="100" rx="8"/>` +
`<text class="lbl-muted" x="${cx}" y="166" text-anchor="middle">PDN_UART</text>` +
`<text class="title" x="${cx}" y="190" text-anchor="middle">Akse ${k}</text>` +
`<text class="lbl-muted" x="${cx}" y="208" text-anchor="middle">adresse ${k}</text>` +
`<text class="lbl-muted" x="${cx}" y="224" text-anchor="middle">${ms[k]}</text>` +
`<text class="lbl-muted" x="${cx}" y="244" text-anchor="middle">DIAG</text>` +
`<g class="v-diode"><path class="wire w-diag" d="M${cx} 250 V283"/>` +
`<polygon class="diode" points="${cx - 9},283 ${cx + 9},283 ${cx},300"/>` +
`<path class="wire w-diag" d="M${cx - 10} 302 H${cx + 10} M${cx} 302 V330"/></g>` +
`<g class="v-adc"><path class="wire w-diag" d="M${cx} 250 V270 M${cx} 304 V330"/>` +
`<rect class="res" x="${cx - 8}" y="270" width="16" height="34" rx="3" stroke="var(--w-diag)"/></g>` +
`<circle class="junction-diag" cx="${cx}" cy="330" r="5"/>`;
}
svg += `<text class="v-diode lbl-muted" x="${380 + 60 + 14}" y="296">Schottky</text>` +
`<text class="v-adc lbl-muted" x="${380 + 60 + 14}" y="292">ens modstande</text>`;
$('busDrivers').innerHTML = svg;
})();
setWiringDiag('adc', false);
renderAxisBars();
renderOverview();
showPage();
connect();
</script>
</body>
</html>