6 akser, StallGuard og basic web interface med ledningsdiagrammer tilføjet
This commit is contained in:
commit
0ea0080886
14 files changed
+2207
No files matched your search
@@ -0,0 +1,3 @@
|
||||
fastapi
|
||||
uvicorn[standard]
|
||||
pyserial
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
"""Web-interface til stepper-test. Kører på PC'en og taler med ESP32 over USB-serial.
|
||||
|
||||
Start: web/.venv/bin/python web/server.py [--port /dev/ttyUSB0]
|
||||
Åbn: http://127.0.0.1:8000
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
|
||||
import serial
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
STATIC_DIR = Path(__file__).parent / "static"
|
||||
HEARTBEAT_S = 0.5 # firmwarens watchdog stopper efter 2 s uden data
|
||||
|
||||
|
||||
class SerialLink:
|
||||
"""Én seriel forbindelse til ESP32. Genforbinder automatisk hvis kablet ryger."""
|
||||
|
||||
def __init__(self, port: str, baud: int, on_message):
|
||||
self.port = port
|
||||
self.baud = baud
|
||||
self.on_message = on_message # kaldes fra læsetråden med en dict
|
||||
self.ser: serial.Serial | None = None
|
||||
self.lock = threading.Lock()
|
||||
self.connected = False
|
||||
self.error = ""
|
||||
|
||||
def start(self):
|
||||
threading.Thread(target=self._run, daemon=True).start()
|
||||
|
||||
def _set_state(self, connected: bool, error: str = ""):
|
||||
if connected != self.connected or error != self.error:
|
||||
self.connected, self.error = connected, error
|
||||
self.on_message({"type": "conn", "connected": connected, "port": self.port, "error": error})
|
||||
|
||||
def _open(self) -> serial.Serial:
|
||||
ser = serial.Serial()
|
||||
ser.port, ser.baudrate, ser.timeout = self.port, self.baud, 0.2
|
||||
# Undgå at DTR/RTS resetter ESP32 når porten åbnes (virker på de fleste boards).
|
||||
ser.dtr = False
|
||||
ser.rts = False
|
||||
ser.open()
|
||||
return ser
|
||||
|
||||
def _run(self):
|
||||
while True:
|
||||
try:
|
||||
ser = self._open()
|
||||
except (serial.SerialException, OSError) as e:
|
||||
self._set_state(False, str(e))
|
||||
time.sleep(2)
|
||||
continue
|
||||
with self.lock:
|
||||
self.ser = ser
|
||||
self._set_state(True)
|
||||
# Reset ved hver forbindelse, så ESP32 altid starter i kendt tilstand (driver disabled).
|
||||
# Firmwaren sender selv boot-log og en status-linje.
|
||||
self.reset()
|
||||
try:
|
||||
while True:
|
||||
raw = ser.readline()
|
||||
if raw:
|
||||
text = raw.decode("utf-8", errors="replace").rstrip("\r\n")
|
||||
if text:
|
||||
self.on_message({"type": "line", "text": text})
|
||||
except (serial.SerialException, OSError) as e:
|
||||
with self.lock:
|
||||
self.ser = None
|
||||
try:
|
||||
ser.close()
|
||||
except Exception:
|
||||
pass
|
||||
self._set_state(False, str(e))
|
||||
time.sleep(1)
|
||||
|
||||
def reset(self) -> bool:
|
||||
"""Genstart ESP32 via auto-reset-kredsløbet: RTS trækker EN lav, DTR=False holder IO0 høj."""
|
||||
with self.lock:
|
||||
if not self.ser:
|
||||
return False
|
||||
try:
|
||||
self.ser.dtr = False
|
||||
self.ser.rts = True
|
||||
time.sleep(0.15)
|
||||
self.ser.rts = False
|
||||
return True
|
||||
except (serial.SerialException, OSError):
|
||||
return False
|
||||
|
||||
def write(self, line: str) -> bool:
|
||||
with self.lock:
|
||||
if not self.ser:
|
||||
return False
|
||||
try:
|
||||
self.ser.write((line.strip() + "\n").encode())
|
||||
return True
|
||||
except (serial.SerialException, OSError):
|
||||
return False
|
||||
|
||||
|
||||
clients: set[WebSocket] = set()
|
||||
loop: asyncio.AbstractEventLoop | None = None
|
||||
link: SerialLink | None = None
|
||||
|
||||
|
||||
async def broadcast(msg: dict):
|
||||
data = json.dumps(msg)
|
||||
for ws in list(clients):
|
||||
try:
|
||||
await ws.send_text(data)
|
||||
except Exception:
|
||||
clients.discard(ws)
|
||||
|
||||
|
||||
def on_serial_message(msg: dict):
|
||||
# Kaldes fra læsetråden – flyt over i asyncio-loopet.
|
||||
if loop:
|
||||
asyncio.run_coroutine_threadsafe(broadcast(msg), loop)
|
||||
|
||||
|
||||
async def heartbeat():
|
||||
# Kun heartbeat mens en browser er forbundet. Lukkes fanen, stopper firmwaren selv.
|
||||
while True:
|
||||
await asyncio.sleep(HEARTBEAT_S)
|
||||
if clients and link:
|
||||
link.write("hb")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_app):
|
||||
global loop
|
||||
loop = asyncio.get_running_loop()
|
||||
link.start()
|
||||
hb = asyncio.create_task(heartbeat())
|
||||
yield
|
||||
hb.cancel()
|
||||
|
||||
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def index():
|
||||
return FileResponse(STATIC_DIR / "index.html")
|
||||
|
||||
|
||||
@app.websocket("/ws")
|
||||
async def ws_endpoint(ws: WebSocket):
|
||||
await ws.accept()
|
||||
clients.add(ws)
|
||||
await ws.send_text(json.dumps(
|
||||
{"type": "conn", "connected": link.connected, "port": link.port, "error": link.error}))
|
||||
try:
|
||||
while True:
|
||||
cmd = (await ws.receive_text()).strip()
|
||||
if cmd == "!reset":
|
||||
ok = await asyncio.to_thread(link.reset)
|
||||
await ws.send_text(json.dumps(
|
||||
{"type": "line", "text": "# ESP32 resettes" if ok else "err ikke forbundet til ESP32"}))
|
||||
elif cmd and not link.write(cmd):
|
||||
await ws.send_text(json.dumps({"type": "line", "text": "err ikke forbundet til ESP32"}))
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
finally:
|
||||
clients.discard(ws)
|
||||
if not clients:
|
||||
link.write("stop") # sidste fane lukket → blød stop med det samme
|
||||
|
||||
|
||||
def main():
|
||||
global link
|
||||
p = argparse.ArgumentParser(description="Stepper-test web-interface")
|
||||
p.add_argument("--port", default="/dev/ttyUSB0", help="seriel port til ESP32")
|
||||
p.add_argument("--baud", type=int, default=115200)
|
||||
p.add_argument("--host", default="127.0.0.1")
|
||||
p.add_argument("--http-port", type=int, default=8000)
|
||||
args = p.parse_args()
|
||||
|
||||
link = SerialLink(args.port, args.baud, on_serial_message)
|
||||
print(f"Åbn http://{args.host}:{args.http_port} (ESP32 på {args.port})")
|
||||
uvicorn.run(app, host=args.host, port=args.http_port, log_level="warning")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,913 @@
|
||||
<!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"> </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>
|
||||
Reference in new issue
Block a user