Compare commits

..
3 Commits
Author SHA1 Message Date
Stani 03141271e5 Upload af dokumenter 2026-10-25 19:15:47 +01:00
Stani 52cd13c0f9 Uploaded README 2026-10-25 19:06:15 +01:00
Stani ee7dc20890 Initial commit 2026-10-25 19:02:39 +01:00
14 changed files with 0 additions and 2207 deletions

No files matched your search

-4
View File
@@ -1,4 +0,0 @@
.pio/
.vscode/
web/.venv/
__pycache__/
-24
View File
@@ -1,24 +0,0 @@
# Stepper-test (ESP32 + TMC2209)
Første del af et DIY 5–6-akset robotarm-projekt (senere ROS2 + MoveIt2).
Målet nu: få steppere til at køre pålideligt fra en ESP32 via TMC2209 (UART).
## Hardware
- Board: ESP32 DevKit (esp32dev), Arduino-framework via PlatformIO
- Driver: TMC2209, UART på Serial2 (RX=GPIO16, TX=GPIO17 via 1 kΩ), adresse 0
- Pins: STEP=25, DIR=26, EN=27 (aktiv lav)
- R_SENSE = 0.11 Ω, 16 mikroskridt, motorstrøm starter på 600 mA
- Ingen encodere endnu (open-loop) – design så de kan tilføjes senere
## Kommandoer
- Byg: `pio run`
- Upload: `pio run -t upload`
- Seriel monitor: `pio device monitor` (115200 baud)
## Regler
- Brug FastAccelStepper til step-generering og TMCStepper til driver-konfiguration.
- IMPORTANT: Hæv aldrig motorstrøm, hastighed eller acceleration uden at spørge mig først.
- Koden skal kunne bygges med `pio run` før du siger, at du er færdig.
- Jeg uploader og tester selv på hardwaren – fortæl mig præcis, hvad jeg skal teste og forvente.
- Forklar kort ændringer i embedded-kode, så jeg forstår dem.
- Kommentarer og svar på dansk.
-14
View File
@@ -1,14 +0,0 @@
; Stepper-test: ESP32 + TMC2209 (UART)
[env:esp32dev]
platform = espressif32@7.1.3
board = esp32dev
framework = arduino
monitor_speed = 115200
upload_port = /dev/ttyUSB0
monitor_port = /dev/ttyUSB0
lib_deps =
gin66/FastAccelStepper@2.0.0
teemuatlut/TMCStepper@0.7.3
-307
View File
@@ -1,307 +0,0 @@
#include "Axis.h"
Axis::Axis(uint8_t index, const AxisConfig& cfg, FastAccelStepperEngine& engine, HardwareSerial& bus)
: _index(index),
_cfg(cfg),
_engine(engine),
_bus(bus),
_driver(&bus, R_SENSE, cfg.address),
_speedHz(SPEED_HZ_DEFAULT), _accel(ACCEL_DEFAULT), _currentMa(CURRENT_MA_DEFAULT),
_sgManual(SG_THRESHOLD_DEFAULT) {}
bool Axis::begin() {
_driver.begin();
initDriver();
applySgThreshold(); // beregn auto-tærsklen med det samme, også hvis driveren ikke er fundet endnu
// ---- Step-generering ----
_stepper = _engine.stepperConnectToPin(_cfg.stepPin);
if (!_stepper) return false;
_stepper->setDirectionPin(_cfg.dirPin);
_stepper->setAutoEnable(false); // EN er fælles og styres af Machine
_stepper->setSpeedInHz(_speedHz);
_stepper->setAcceleration(_accel);
return true;
}
// Skriver hele konfigurationen til TMC2209. Kaldes ved opstart og igen hver gang
// driveren (gen)opdages – fx når VM tændes efter ESP32, for så har driveren mistet alt.
// En manglende driver koster ~14 ms (TMCStepper venter på svar), så vi læser kun ét register.
bool Axis::initDriver() {
_tmcVersion = _driver.version();
_uartOk = (_tmcVersion == 0x21);
if (!_uartOk) return false;
_driver.pdn_disable(true); // PDN_UART-benet bruges til UART, ikke standby
_driver.I_scale_analog(false); // strøm sættes via register, ikke VREF-potmeter
_driver.mstep_reg_select(true); // mikroskridt fra register, ikke MS1/MS2 (de er adresse)
_driver.toff(0); // udgang slukket indtil 'en' (driveren er disabled efter init)
_driver.blank_time(24);
_driver.rms_current(_currentMa);
_driver.microsteps(MICROSTEPS);
_driver.en_spreadCycle(!_stealth);
_driver.pwm_autoscale(true);
_driver.TPWMTHRS(0); // bliv i StealthChop ved alle hastigheder (StallGuard4 kræver det)
_driver.semin(0); // CoolStep slået fra indtil videre
_driver.GSTAT(0b111); // nulstil "reset"-flaget, så et senere strømudfald kan ses
updateSgSpeedWindow();
applySgThreshold();
return true;
}
// TCOOLTHRS bestemmer den laveste hastighed, hvor driveren melder stall på DIAG.
// TSTEP = tid mellem 1/256-mikroskridt i clock-perioder = FCLK / (steps/s * 256/MICROSTEPS).
// StallGuard er aktiv når TSTEP <= TCOOLTHRS, dvs. over SG_MIN_SPEED_FRACTION af sat hastighed.
void Axis::updateSgSpeedWindow() {
if (!_uartOk) return;
float vMin = _speedHz * SG_MIN_SPEED_FRACTION;
uint32_t t = (uint32_t)(TMC_FCLK / (vMin * (256.0f / MICROSTEPS)));
_driver.TCOOLTHRS(min<uint32_t>(t, 0xFFFFF));
}
// Beregner den effektive StallGuard-tærskel og skriver den til driveren.
// Auto: lineær kurve gennem aksens to kalibreringspunkter, plus brugerens finjustering.
// Manuel: den faste værdi fra 'sgt'.
// Driveren får kun tærsklen når stall-stop er slået til – ellers 0, så en akse uden
// stall-stop aldrig trækker den fælles DIAG-linje og stopper de andre akser.
void Axis::applySgThreshold() {
int32_t thr = _sgManual;
if (_sgAuto) {
float slope = float(_cfg.sgSgtHi - _cfg.sgSgtLo) / float(_cfg.sgSpeedHi - _cfg.sgSpeedLo);
thr = lroundf(_cfg.sgSgtLo + slope * (float(_speedHz) - float(_cfg.sgSpeedLo))) + _sgOffset;
}
_sgThreshold = constrain(thr, 0, 255);
if (_uartOk) _driver.SGTHRS(_stallStop ? _sgThreshold : 0);
}
Axis::DriverEvent Axis::checkDriver() {
if (!_uartOk) {
// Prøv igen – driveren svarer først når VM er tændt.
return initDriver() ? DriverEvent::Found : DriverEvent::None;
}
// Mistet forbindelse, eller har driveren været nede (reset-flag i GSTAT)?
bool lost = _driver.version() != 0x21;
bool wasReset = !lost && (_driver.GSTAT() & 0x01);
if (!lost && !wasReset) return DriverEvent::None;
// Konfigurationen i driveren er væk/ukendt -> stop sikkert og kræv nyt 'en'.
disable();
_uartOk = false;
if (wasReset) initDriver();
return DriverEvent::Lost;
}
// CRC8 fra TMC2209-databladet (polynomium x^8 + x^2 + x + 1).
static uint8_t tmcCrc(const uint8_t* data, size_t len) {
uint8_t crc = 0;
for (size_t i = 0; i < len; i++) {
uint8_t b = data[i];
for (int j = 0; j < 8; j++) {
crc = ((crc >> 7) ^ (b & 0x01)) ? (crc << 1) ^ 0x07 : (crc << 1);
b >>= 1;
}
}
return crc;
}
String Axis::uartDiag() {
// Sender en læse-forespørgsel på IOIN (0x06) til hver adresse og tæller modtagne bytes.
// TX og RX deler bussen, så de første 4 bytes er altid vores eget ekko; svaret er 8 bytes mere.
String out = String("ok uartdiag bus ") + char('A' + _cfg.bus);
int echoes = 0, replies = 0;
for (uint8_t addr = 0; addr < 4; addr++) {
uint8_t req[4] = {0x05, addr, 0x06, 0};
req[3] = tmcCrc(req, 3);
while (_bus.available()) _bus.read();
_bus.write(req, 4);
_bus.flush();
delay(15);
uint8_t buf[16];
int n = 0;
while (_bus.available() && n < (int)sizeof(buf)) buf[n++] = _bus.read();
bool echo = n >= 4 && memcmp(buf, req, 4) == 0;
bool reply = n >= 12 && tmcCrc(buf + 4, 7) == buf[11];
echoes += echo;
replies += reply;
out += " | adr" + String(addr) + ": " + String(n) + " bytes";
if (reply) out += " SVAR (ver 0x" + String(buf[7], HEX) + ")";
else if (echo) out += " kun ekko";
else if (n == 0) out += " intet";
}
if (replies) out += " => " + String(replies) + " driver(e) svarer";
else if (echoes) out += " => ekko ok, men ingen TMC svarer: tjek VIO, GND, PDN_UART-benet og at VM er tændt";
else out += " => intet ekko: TX/RX når ikke frem til hinanden (tjek pins, 1 kΩ og samlingspunktet)";
return out;
}
const char* Axis::enable() {
if (!_uartOk) return "ingen driver fundet på denne akse";
_driver.toff(4); // driver-udgang til (0 = slukket)
_enabled = true;
return nullptr;
}
void Axis::disable() {
if (_stepper && _stepper->isRunning()) _stepper->forceStop();
if (_uartOk) _driver.toff(0);
_enabled = false;
if (_homing) endHome();
}
// Fælles for alle bevægelser: nulstil stall-tilstand fra sidste gang.
void Axis::startMove() {
_stalled = false;
}
const char* Axis::move(int32_t steps) {
if (!_enabled) return "akse ikke enabled";
startMove();
return _stepper->move(steps) == MOVE_OK ? nullptr : "move afvist";
}
const char* Axis::moveTo(int32_t target) {
if (!_enabled) return "akse ikke enabled";
startMove();
return _stepper->moveTo(target) == MOVE_OK ? nullptr : "goto afvist";
}
void Axis::stop() {
if (_stepper) _stepper->stopMove();
}
// forceStopAndNewPosition() afbryder step-køen med det samme (forceStop() ville køre de
// allerede planlagte ~20 ms færdig) og sætter positionen i samme ombæring.
void Axis::halt(bool zeroPosition) {
if (!_stepper) return;
_stepper->forceStopAndNewPosition(zeroPosition ? 0 : _stepper->getCurrentPosition());
}
bool Axis::isRunning() const { return _stepper && _stepper->isRunning(); }
int32_t Axis::getPosition() const { return _stepper ? _stepper->getCurrentPosition() : 0; }
int32_t Axis::getTarget() const { return _stepper ? _stepper->targetPos() : 0; }
const char* Axis::setZero() {
if (isRunning()) return "kan ikke nulstille under bevægelse";
_stepper->setCurrentPosition(0);
return nullptr;
}
uint32_t Axis::setSpeed(uint32_t hz) {
_speedHz = constrain(hz, 1u, SPEED_HZ_MAX);
_stepper->setSpeedInHz(_speedHz);
_stepper->applySpeedAcceleration(); // gælder også en igangværende bevægelse
updateSgSpeedWindow();
applySgThreshold(); // auto-tærsklen følger den nye hastighed
return _speedHz;
}
uint32_t Axis::setAccel(uint32_t a) {
_accel = constrain(a, 1u, ACCEL_MAX);
_stepper->setAcceleration(_accel);
_stepper->applySpeedAcceleration();
return _accel;
}
uint16_t Axis::setCurrent(uint16_t mA) {
_currentMa = constrain(mA, (uint16_t)50, CURRENT_MA_MAX);
if (_uartOk) _driver.rms_current(_currentMa);
return _currentMa;
}
void Axis::setStealthChop(bool on) {
_stealth = on;
if (!on) setStallStop(false); // StallGuard4 virker ikke i SpreadCycle
if (_uartOk) _driver.en_spreadCycle(!on);
}
// ---------- StallGuard ----------
void Axis::setSgThreshold(uint8_t thr) {
_sgManual = thr;
_sgAuto = false;
applySgThreshold();
}
void Axis::setSgAuto(bool on) {
_sgAuto = on;
applySgThreshold();
}
int16_t Axis::setSgOffset(int16_t off) {
_sgOffset = constrain(off, (int16_t)-SG_OFFSET_MAX, SG_OFFSET_MAX);
applySgThreshold();
return _sgOffset;
}
const char* Axis::setStallStop(bool on) {
if (on && !_stealth) return "StallGuard kræver StealthChop (mode stealth)";
_stallStop = on;
applySgThreshold();
return nullptr;
}
uint16_t Axis::sgResult() {
return _uartOk ? _driver.SG_RESULT() : 0;
}
bool Axis::isStallCandidate() const {
if (!_stallStop || !isRunning()) return false;
// Under accel/decel er StallGuard upålidelig – samme grænse som TCOOLTHRS i driveren.
int32_t vNow = abs(_stepper->getCurrentSpeedInMilliHz()) / 1000;
return vNow >= _speedHz * SG_MIN_SPEED_FRACTION;
}
const char* Axis::startHome(int dir) {
if (!_enabled) return "akse ikke enabled";
if (!_stealth) return "StallGuard kræver StealthChop (mode stealth)";
if (_sgThreshold == 0) return "StallGuard-tærsklen er 0 (sgauto on eller sgt <n>)";
if (isRunning()) return "aksen kører allerede";
_stallStopBeforeHome = _stallStop;
setStallStop(true);
startMove();
if (_stepper->move((int32_t)dir * HOME_MAX_REVS * STEPS_PER_REV) != MOVE_OK) {
setStallStop(_stallStopBeforeHome);
return "home afvist";
}
_homing = true;
return nullptr;
}
void Axis::endHome() {
_homing = false;
setStallStop(_stallStopBeforeHome);
}
String Axis::statusJson() {
// DRV_STATUS læses i ét UART-kald (~3 ms); step-generering kører i hardware og påvirkes ikke.
bool ot = false, otpw = false, s2g = false, ol = false;
uint16_t csActual = 0, sg = 0;
if (_uartOk) {
uint32_t ds = _driver.DRV_STATUS();
otpw = ds & (1u << 0);
ot = ds & (1u << 1);
s2g = ds & (0b11u << 2);
ol = ds & (0b11u << 6);
csActual = (ds >> 16) & 0x1F;
sg = _driver.SG_RESULT();
}
char buf[512];
snprintf(buf, sizeof(buf),
"{\"i\":%u,\"bus\":%u,\"adr\":%u,\"pos\":%ld,\"target\":%ld,\"running\":%s,\"enabled\":%s,"
"\"speed\":%lu,\"accel\":%lu,\"ma\":%u,\"mode\":\"%s\",\"spr\":%ld,"
"\"sg\":%u,\"sgt\":%u,\"sgAuto\":%s,\"sgOff\":%d,\"stallStop\":%s,\"stalled\":%s,\"homing\":%s,"
"\"tmc\":{\"uart\":%s,\"ver\":%u,\"ot\":%s,\"otpw\":%s,\"s2g\":%s,\"ol\":%s,\"cs\":%u}}",
_index, _cfg.bus, _cfg.address,
(long)getPosition(), (long)getTarget(),
isRunning() ? "true" : "false", _enabled ? "true" : "false",
(unsigned long)_speedHz, (unsigned long)_accel, _currentMa,
_stealth ? "stealth" : "spread", (long)STEPS_PER_REV,
sg, _sgThreshold, _sgAuto ? "true" : "false", _sgOffset,
_stallStop ? "true" : "false", _stalled ? "true" : "false", _homing ? "true" : "false",
_uartOk ? "true" : "false", _tmcVersion,
ot ? "true" : "false", otpw ? "true" : "false",
s2g ? "true" : "false", ol ? "true" : "false", csActual);
return String(buf);
}
-111
View File
@@ -1,111 +0,0 @@
#pragma once
#include <Arduino.h>
#include <FastAccelStepper.h>
#include <TMCStepper.h>
#include "config.h"
// Én akse = én stepper + én TMC2209 på en delt UART-bus.
// EN-pinnen er fælles for alle drivere (styres af Machine); den enkelte akse slås
// til/fra med driverens toff-register over UART.
// Positionen kommer i dag fra step-tælleren (open-loop). Når der kommer encodere,
// kan getPosition() læse dem i stedet uden at resten af koden ændres.
class Axis {
public:
Axis(uint8_t index, const AxisConfig& cfg, FastAccelStepperEngine& engine, HardwareSerial& bus);
// Returnerer false hvis step-generatoren ikke kunne oprettes.
bool begin();
uint8_t index() const { return _index; }
const AxisConfig& config() const { return _cfg; }
enum class DriverEvent { None, Found, Lost };
// Kaldes periodisk: genfinder driveren (fx når VM tændes efter ESP32) og
// opdager hvis den forsvinder eller har været nulstillet (strømudfald på VM).
DriverEvent checkDriver();
// Fejlfinding af UART-bussen: ekko-test + svar fra adresse 0–3.
String uartDiag();
bool uartOk() const { return _uartOk; }
uint8_t tmcVersion() const { return _tmcVersion; }
// Bevægelse – returnerer nullptr ved succes, ellers en fejltekst.
const char* move(int32_t steps);
const char* moveTo(int32_t target);
void stop(); // blød stop med deceleration
void halt(bool zeroPosition); // øjeblikkeligt stop uden flere steps (stall/kollision)
const char* enable(); // driver-udgang til (toff) – Machine sætter fælles EN
void disable();
bool isEnabled() const { return _enabled; }
bool isRunning() const;
int32_t getPosition() const;
int32_t getTarget() const;
const char* setZero();
// Værdierne clampes til maks fra config.h; returnerer den faktisk satte værdi.
uint32_t setSpeed(uint32_t hz);
uint32_t setAccel(uint32_t stepsPerS2);
uint16_t setCurrent(uint16_t mA);
void setStealthChop(bool on);
uint32_t speed() const { return _speedHz; }
uint32_t accel() const { return _accel; }
uint16_t current() const { return _currentMa; }
bool stealthChop() const { return _stealth; }
// ---- StallGuard ----
void setSgThreshold(uint8_t thr); // fast manuel tærskel – slår auto fra
void setSgAuto(bool on); // tærskel følger hastigheden (kalibreringskurve)
int16_t setSgOffset(int16_t off); // finjustering ± oven på kurven (kun i auto)
uint8_t sgThreshold() const { return _sgThreshold; } // den effektive værdi
bool sgAuto() const { return _sgAuto; }
int16_t sgOffset() const { return _sgOffset; }
const char* setStallStop(bool on); // kræver StealthChop
bool stallStop() const { return _stallStop; }
uint16_t sgResult(); // læser SG_RESULT over UART (høj = lav belastning)
// Kan denne akse lige nu have udløst DIAG? (stall-stop til, kører over min.-hastighed)
bool isStallCandidate() const;
void markStalled() { _stalled = true; }
bool stalled() const { return _stalled; }
const char* startHome(int dir); // kør mod mekanisk stop til stall
bool isHoming() const { return _homing; }
void endHome(); // afslut homing (gendanner stall-stop-indstilling)
// Én linje JSON med aksens status (inkl. TMC-fejlflag og StallGuard).
String statusJson();
private:
bool initDriver();
void updateSgSpeedWindow();
void applySgThreshold();
void startMove();
uint8_t _index;
AxisConfig _cfg;
FastAccelStepperEngine& _engine;
HardwareSerial& _bus;
TMC2209Stepper _driver;
FastAccelStepper* _stepper = nullptr;
bool _uartOk = false;
uint8_t _tmcVersion = 0;
bool _enabled = false;
bool _stealth = true;
uint32_t _speedHz;
uint32_t _accel;
uint16_t _currentMa;
uint8_t _sgManual;
uint8_t _sgThreshold = 0; // effektiv værdi (auto-kurve eller manuel)
bool _sgAuto = true;
int16_t _sgOffset = 0;
bool _stallStop = false;
bool _stalled = false; // låst indtil næste bevægelse
bool _homing = false;
bool _stallStopBeforeHome = false;
};
-155
View File
@@ -1,155 +0,0 @@
#include "Commands.h"
#include "config.h"
// Parser et heltal; returnerer false hvis teksten ikke er et gyldigt tal.
static bool parseLong(const String& s, long& out) {
if (s.length() == 0) return false;
char* end = nullptr;
out = strtol(s.c_str(), &end, 10);
return *end == '\0';
}
static bool parseFloat(const String& s, float& out) {
if (s.length() == 0) return false;
char* end = nullptr;
out = strtof(s.c_str(), &end);
return *end == '\0';
}
static bool isDigits(const String& s) {
if (s.length() == 0) return false;
for (size_t i = 0; i < s.length(); i++) {
if (!isdigit((unsigned char)s[i])) return false;
}
return true;
}
String Commands::handle(const String& raw) {
String line = raw;
line.trim();
if (line.length() == 0) return "";
// ---- Valgfrit akse-index først ----
int axisIdx = -1;
int sp = line.indexOf(' ');
String first = sp < 0 ? line : line.substring(0, sp);
if (isDigits(first)) {
axisIdx = first.toInt();
if (axisIdx >= NUM_AXES) return "err ukendt akse " + first + " (0-" + String(NUM_AXES - 1) + ")";
line = sp < 0 ? String() : line.substring(sp + 1);
line.trim();
if (line.length() == 0) return "err mangler kommando efter akse-index";
sp = line.indexOf(' ');
}
String cmd = sp < 0 ? line : line.substring(0, sp);
String arg = sp < 0 ? String() : line.substring(sp + 1);
cmd.toLowerCase();
arg.trim();
// ---- Fælles kommandoer ----
if (cmd == "hb") {
_hbArmed = true;
return "";
}
if (cmd == "help") {
return "ok kommandoer: [akse] <kommando>. Fælles: status estop, og uden akse: en dis stop (alle). "
"Pr. akse: en dis stop move <steps> goto <steps> rev <omdr> speed <steps/s> accel <steps/s2> "
"zero current <mA> mode stealth|spread uartdiag "
"sgt <0-255> sgauto on|off sgoff <+-n> sg on|off sgr home +|-";
}
if (cmd == "status") {
String out = "G " + _m.globalJson();
for (uint8_t i = 0; i < NUM_AXES; i++) out += "\nS " + String(i) + " " + _m.axis(i).statusJson();
return out;
}
if (cmd == "estop") { _m.estop(); return "ok estop (alle drivere disabled)"; }
if (axisIdx < 0) {
if (cmd == "en") {
const char* err = _m.enableAll();
return err ? String("err ") + err : String("ok alle fundne akser enabled");
}
if (cmd == "dis") { _m.disableAll(); return "ok alle akser disabled"; }
if (cmd == "stop") { _m.stopAll(); return "ok stop alle"; }
}
// ---- Akse-kommandoer (uden index: akse 0) ----
uint8_t ai = axisIdx < 0 ? 0 : axisIdx;
Axis& a = _m.axis(ai);
_m.requestStatus(ai); // ny S-linje efter kommandoen, så UI'et altid er opdateret
String pre = "a" + String(ai) + " ";
auto okOrErr = [&](const char* err, const String& okText) {
return err ? "err " + pre + err : "ok " + pre + okText;
};
if (cmd == "en") return okOrErr(_m.enableAxis(ai), "enabled");
if (cmd == "dis") { _m.disableAxis(ai); return "ok " + pre + "disabled"; }
if (cmd == "stop") { a.stop(); return "ok " + pre + "stop"; }
if (cmd == "zero") return okOrErr(a.setZero(), "pos=0");
if (cmd == "uartdiag") return a.uartDiag();
long n;
float f;
if (cmd == "move") {
if (!parseLong(arg, n)) return "err brug: [akse] move <steps>";
return okOrErr(a.move(n), "move " + String(n));
}
if (cmd == "goto") {
if (!parseLong(arg, n)) return "err brug: [akse] goto <steps>";
return okOrErr(a.moveTo(n), "goto " + String(n));
}
if (cmd == "rev") {
if (!parseFloat(arg, f)) return "err brug: [akse] rev <omdrejninger>";
long steps = lroundf(f * STEPS_PER_REV);
return okOrErr(a.move(steps), "move " + String(steps));
}
if (cmd == "speed") {
if (!parseLong(arg, n) || n <= 0) return "err brug: [akse] speed <steps/s>";
a.setSpeed(n);
return "ok " + pre + "speed=" + String(a.speed()) + " sgt=" + String(a.sgThreshold());
}
if (cmd == "accel") {
if (!parseLong(arg, n) || n <= 0) return "err brug: [akse] accel <steps/s2>";
return "ok " + pre + "accel=" + String(a.setAccel(n));
}
if (cmd == "current") {
if (!parseLong(arg, n) || n <= 0) return "err brug: [akse] current <mA>";
return "ok " + pre + "current=" + String(a.setCurrent(n > 65535 ? 65535 : n));
}
if (cmd == "sgt") {
if (!parseLong(arg, n) || n < 0 || n > 255) return "err brug: [akse] sgt <0-255>";
a.setSgThreshold(n);
return "ok " + pre + "sgt=" + String(n) + " manuel (stall når sg <= " + String(2 * n) + ")";
}
if (cmd == "sgauto") {
arg.toLowerCase();
if (arg != "on" && arg != "off") return "err brug: [akse] sgauto on|off";
a.setSgAuto(arg == "on");
return "ok " + pre + "sgauto=" + arg + " sgt=" + String(a.sgThreshold());
}
if (cmd == "sgoff") {
if (!parseLong(arg, n)) return "err brug: [akse] sgoff <-100..100>";
int16_t off = a.setSgOffset(constrain(n, -1000L, 1000L));
return "ok " + pre + "sgoff=" + String(off) + " sgt=" + String(a.sgThreshold());
}
if (cmd == "sg") {
arg.toLowerCase();
if (arg == "on") return okOrErr(a.setStallStop(true), "stall-stop til");
if (arg == "off") { a.setStallStop(false); return "ok " + pre + "stall-stop fra"; }
return "err brug: [akse] sg on|off";
}
if (cmd == "sgr") return "ok " + pre + "sg=" + String(a.sgResult());
if (cmd == "home") {
if (arg == "+") return okOrErr(_m.home(ai, 1), "homing +");
if (arg == "-") return okOrErr(_m.home(ai, -1), "homing -");
return "err brug: [akse] home +|-";
}
if (cmd == "mode") {
arg.toLowerCase();
if (arg == "stealth") { a.setStealthChop(true); return "ok " + pre + "mode=stealth"; }
if (arg == "spread") { a.setStealthChop(false); return "ok " + pre + "mode=spread"; }
return "err brug: [akse] mode stealth|spread";
}
return "err ukendt kommando '" + cmd + "' (prøv help)";
}
-26
View File
@@ -1,26 +0,0 @@
#pragma once
#include <Arduino.h>
#include "Machine.h"
// Tekstprotokol (én kommando pr. linje):
// kommando: "[akse] <kommando> [argument]" – fx "1 move 3200". Uden akse-index:
// akse-kommandoer går til akse 0, mens en/dis/stop gælder alle akser.
// svar: "ok ..." / "err ..." – præcis én linje pr. kommando (undtagen status)
// status: "G {json}" – fælles tilstand (EN, DIAG, grænser)
// "S <akse> {json}" – én akse; løbende mens den kører
// "status" svarer med én G-linje + én S-linje pr. akse
// log: "# ..." – kun til mennesker, ignoreres af PC-programmet
// "hb" er undtagelsen: heartbeat fra PC'en, aktiverer watchdog, giver intet svar.
class Commands {
public:
explicit Commands(Machine& machine) : _m(machine) {}
// Returnerer svarlinjen/-linjerne (tom streng = intet svar).
String handle(const String& line);
bool heartbeatArmed() const { return _hbArmed; }
private:
Machine& _m;
bool _hbArmed = false;
};
-246
View File
@@ -1,246 +0,0 @@
#include "Machine.h"
// DIAG-interrupt (kun DiodeOr): sætter kun et flag – selve stoppet sker i poll(), fordi vi skal
// finde ud af, hvilken akse der stallede, og det kræver UART-læsninger (ikke tilladt i en ISR).
static volatile bool s_diagFlag = false;
static void IRAM_ATTR onDiagRising() { s_diagFlag = true; }
void Machine::begin() {
// Alle drivere holdes frakoblet (EN høj) mens de konfigureres.
pinMode(PIN_EN, OUTPUT);
digitalWrite(PIN_EN, HIGH);
// GPIO39 er kun indgang og har ingen intern pull. DiodeOr: ekstern 10 kΩ pull-down holder den
// lav. ResistorAdc: de lave DIAG-udgange holder selv linjen nede gennem modstandene.
pinMode(PIN_DIAG, INPUT);
if (DIAG_MODE == DiagMode::DiodeOr) {
attachInterrupt(digitalPinToInterrupt(PIN_DIAG), onDiagRising, RISING);
} else {
analogSetPinAttenuation(PIN_DIAG, ADC_11db); // måleområde ~0–3,1 V
}
Serial2.begin(TMC_BAUD, SERIAL_8N1, PIN_BUS_A_RX, PIN_BUS_A_TX);
Serial1.begin(TMC_BAUD, SERIAL_8N1, PIN_BUS_B_RX, PIN_BUS_B_TX);
delay(50);
_engine.init();
for (uint8_t i = 0; i < NUM_AXES; i++) {
HardwareSerial& bus = AXES[i].bus == 0 ? Serial2 : Serial1;
_axes[i] = new Axis(i, AXES[i], _engine, bus);
_axes[i]->begin();
}
}
bool Machine::diagActive() {
if (DIAG_MODE == DiagMode::DiodeOr) return digitalRead(PIN_DIAG);
_diagMv = analogReadMilliVolts(PIN_DIAG); // kalibreret måling, ~20 µs
return _diagMv > (int32_t)DIAG_ADC_THRESHOLD_MV;
}
void Machine::updateMasterEnable() {
bool any = false;
for (uint8_t i = 0; i < NUM_AXES; i++) any |= _axes[i]->isEnabled();
_master = any;
digitalWrite(PIN_EN, any ? LOW : HIGH);
}
const char* Machine::enableAxis(uint8_t i) {
const char* err = _axes[i]->enable();
if (!err) updateMasterEnable();
return err;
}
void Machine::disableAxis(uint8_t i) {
_axes[i]->disable();
updateMasterEnable();
requestStatus(i);
}
const char* Machine::enableAll() {
uint8_t n = 0;
for (uint8_t i = 0; i < NUM_AXES; i++) {
if (_axes[i]->uartOk() && !_axes[i]->enable()) n++;
}
updateMasterEnable();
requestAllStatus();
return n ? nullptr : "ingen drivere fundet";
}
void Machine::disableAll() {
for (uint8_t i = 0; i < NUM_AXES; i++) _axes[i]->disable();
updateMasterEnable();
requestAllStatus();
}
void Machine::stopAll() {
for (uint8_t i = 0; i < NUM_AXES; i++) _axes[i]->stop();
}
void Machine::estop() {
digitalWrite(PIN_EN, HIGH); // hardware-sluk først, så intet afhænger af UART
for (uint8_t i = 0; i < NUM_AXES; i++) _axes[i]->disable();
_master = false;
requestAllStatus();
}
const char* Machine::home(uint8_t i, int dir) {
for (uint8_t j = 0; j < NUM_AXES; j++) {
if (_axes[j]->isHoming()) return "en anden akse er i gang med homing";
}
return _axes[i]->startHome(dir);
}
bool Machine::anyRunning() {
for (uint8_t i = 0; i < NUM_AXES; i++) {
if (_axes[i]->isRunning()) return true;
}
return false;
}
void Machine::requestAllStatus() {
for (uint8_t i = 0; i < NUM_AXES; i++) _statusPending[i] = true;
_globalPending = true;
}
String Machine::globalJson() {
int8_t homing = -1;
for (uint8_t i = 0; i < NUM_AXES; i++) {
if (_axes[i]->isHoming()) homing = i;
}
bool diag = diagActive();
char buf[240];
snprintf(buf, sizeof(buf),
"{\"axes\":%u,\"master\":%s,\"diag\":%s,\"diagMode\":\"%s\",\"diagMv\":%ld,\"homing\":%d,"
"\"max\":{\"speed\":%lu,\"accel\":%lu,\"ma\":%u}}",
NUM_AXES, _master ? "true" : "false", diag ? "true" : "false",
DIAG_MODE == DiagMode::DiodeOr ? "diode" : "adc", (long)_diagMv, homing,
(unsigned long)SPEED_HZ_MAX, (unsigned long)ACCEL_MAX, CURRENT_MA_MAX);
return String(buf);
}
void Machine::poll(Print& out) {
pollDiag(out); // først: stall skal stoppes så hurtigt som muligt
pollHoming(out);
pollDrivers(out);
pollStatus(out);
}
// DIAG er fælles for alle akser (diode-OR). Ved et stall stoppes ALLE kørende akser –
// en kollision et sted i armen skal stoppe hele armen. Derefter findes synderen.
void Machine::pollDiag(Print& out) {
bool flag = s_diagFlag; // kun sat i DiodeOr (fanger korte pulser mellem to loops)
s_diagFlag = false;
if (!flag && !diagActive()) return;
uint8_t cand[NUM_AXES];
uint8_t n = 0;
int8_t homingCand = -1;
for (uint8_t i = 0; i < NUM_AXES; i++) {
if (_axes[i]->isStallCandidate()) {
cand[n++] = i;
if (_axes[i]->isHoming()) homingCand = i;
}
}
// Ingen akse kan have udløst et stall (fx under accel, eller stall-stop er fra) -> ignorér.
if (n == 0) return;
// Stop alle kørende akser med det samme. En homing-akse, der stallede, får position 0.
for (uint8_t i = 0; i < NUM_AXES; i++) {
if (_axes[i]->isRunning()) _axes[i]->halt(i == homingCand);
}
// Find synderen: homing-aksen hvis den var kandidat, ellers den eneste kandidat,
// ellers den med lavest SG i forhold til sin tærskel (aflæst efter stop).
int8_t culprit = homingCand;
if (culprit < 0) {
culprit = cand[0];
if (n > 1) {
float best = 1e9f;
for (uint8_t k = 0; k < n; k++) {
Axis& a = *_axes[cand[k]];
float ratio = float(a.sgResult()) / float(max<uint16_t>(1, 2 * a.sgThreshold()));
if (ratio < best) { best = ratio; culprit = cand[k]; }
}
}
}
Axis& c = *_axes[culprit];
c.markStalled();
if (c.isHoming()) {
c.endHome();
out.printf("# HOME ok a%d - stall fundet, position = 0\n", culprit);
} else {
out.printf("# STALL a%d ved pos %ld (sg=%u) - alle akser stoppet\n",
culprit, (long)c.getPosition(), c.sgResult());
}
requestAllStatus();
}
// En homing, der slutter uden stall (eller stoppes udefra), er fejlet.
void Machine::pollHoming(Print& out) {
for (uint8_t i = 0; i < NUM_AXES; i++) {
Axis& a = *_axes[i];
if (a.isHoming() && !a.isRunning()) {
a.endHome();
out.printf("# HOME fejl a%d - intet stall inden for grænsen (eller stoppet)\n", i);
requestStatus(i);
}
}
}
// Én akse tjekkes ad gangen, spredt over DRIVER_CHECK_INTERVAL_MS. En manglende driver
// koster ~14 ms pr. forsøg, så det må ikke ske for alle akser i samme loop.
void Machine::pollDrivers(Print& out) {
uint32_t now = millis();
if (now - _lastDriverCheckMs < DRIVER_CHECK_INTERVAL_MS / NUM_AXES) return;
_lastDriverCheckMs = now;
uint8_t i = _nextDriverCheck;
_nextDriverCheck = (i + 1) % NUM_AXES;
Axis::DriverEvent ev = _axes[i]->checkDriver();
if (ev == Axis::DriverEvent::None) return;
if (ev == Axis::DriverEvent::Found) {
out.printf("# a%d UART OK - driver fundet og konfigureret\n", i);
} else {
out.printf("# a%d driver mistet eller nulstillet (VM?) - aksen er disabled, send 'en' igen\n", i);
updateMasterEnable();
}
requestStatus(i);
_globalPending = true;
}
// Højst én status-linje pr. loop (hver koster et par UART-læsninger), og lavere rate pr. akse
// når flere kører, så 115200 baud til PC'en ikke bliver flaskehals.
void Machine::pollStatus(Print& out) {
uint32_t now = millis();
uint8_t running = 0;
for (uint8_t i = 0; i < NUM_AXES; i++) {
bool r = _axes[i]->isRunning();
if (_wasRunning[i] && !r) _statusPending[i] = true; // én sidste status når den stopper
_wasRunning[i] = r;
running += r;
}
bool diag = diagActive();
if (_globalPending || diag != _lastDiagLevel || _master != _lastMaster) {
_globalPending = false;
_lastDiagLevel = diag;
_lastMaster = _master;
out.print("G ");
out.println(globalJson());
return;
}
uint32_t interval = max<uint32_t>(STATUS_INTERVAL_MS, running * STATUS_MS_PER_RUNNING_AXIS);
for (uint8_t k = 0; k < NUM_AXES; k++) {
uint8_t i = (_nextStatus + k) % NUM_AXES;
bool due = _statusPending[i] || (_wasRunning[i] && now - _lastStatusMs[i] >= interval);
if (!due) continue;
_statusPending[i] = false;
_lastStatusMs[i] = now;
_nextStatus = (i + 1) % NUM_AXES;
out.printf("S %u ", i);
out.println(_axes[i]->statusJson());
return;
}
}
-59
View File
@@ -1,59 +0,0 @@
#pragma once
#include <Arduino.h>
#include <FastAccelStepper.h>
#include "config.h"
#include "Axis.h"
// Hele maskinen: alle akser + de fælles ressourcer (EN-pin, DIAG-linje, UART-busser)
// og den løbende overvågning (stall, homing, driver-tjek, live-status).
class Machine {
public:
explicit Machine(FastAccelStepperEngine& engine) : _engine(engine) {}
void begin();
uint8_t count() const { return NUM_AXES; }
Axis& axis(uint8_t i) { return *_axes[i]; }
// Fælles EN er aktiv, så længe mindst én akse er enabled.
const char* enableAxis(uint8_t i);
void disableAxis(uint8_t i);
const char* enableAll(); // alle akser med fundet driver
void disableAll();
void stopAll(); // blød stop af alle akser
void estop(); // EN høj med det samme + alle akser disabled
const char* home(uint8_t i, int dir); // kun én akse ad gangen
bool masterEnabled() const { return _master; }
bool anyRunning();
String globalJson();
void requestStatus(uint8_t i) { _statusPending[i] = true; }
void requestAllStatus();
// Kaldes hver loop. Skriver log- og status-linjer til out.
void poll(Print& out);
private:
void pollDiag(Print& out);
void pollHoming(Print& out);
void pollDrivers(Print& out);
void pollStatus(Print& out);
void updateMasterEnable();
bool diagActive(); // er den fælles DIAG-linje aktiv (afhænger af DIAG_MODE)?
FastAccelStepperEngine& _engine;
Axis* _axes[NUM_AXES] = {};
bool _master = false;
uint8_t _nextDriverCheck = 0;
uint32_t _lastDriverCheckMs = 0;
uint8_t _nextStatus = 0;
uint32_t _lastStatusMs[NUM_AXES] = {};
bool _wasRunning[NUM_AXES] = {};
bool _statusPending[NUM_AXES] = {};
bool _globalPending = true;
bool _lastDiagLevel = false;
bool _lastMaster = false;
int32_t _diagMv = -1; // seneste ADC-måling af DIAG-linjen (kun ResistorAdc)
};
-89
View File
@@ -1,89 +0,0 @@
#pragma once
#include <Arduino.h>
// ---------- Akser ----------
// Alle drivere deler EN, DIAG (via diode-OR) og en af to UART-busser.
// Hver akse har sin egen STEP/DIR og en UART-adresse 0–3 (MS1 = bit 0, MS2 = bit 1).
constexpr uint8_t NUM_AXES = 6;
struct AxisConfig {
uint8_t stepPin;
uint8_t dirPin;
uint8_t bus; // 0 = bus A (Serial2), 1 = bus B (Serial1)
uint8_t address; // TMC2209 UART-adresse 0–3
// StallGuard-kalibrering: sgt ved to hastigheder (steps/s), lineært imellem.
uint32_t sgSpeedLo;
uint8_t sgSgtLo;
uint32_t sgSpeedHi;
uint8_t sgSgtHi;
};
// Pins valgt så GPIO21/22 er fri til I²C (encodere senere). Boot-støjende pins (5, 14, 15)
// bruges kun til DIR, aldrig STEP, så der ikke kommer falske steps ved opstart.
constexpr AxisConfig AXES[NUM_AXES] = {
// STEP DIR bus adr StallGuard: lav hast., sgt, høj hast., sgt
{ 25, 26, 0, 0, 800, 35, 3200, 90 }, // målt 2026-10-23 (42-motor, 600 mA)
{ 33, 14, 0, 1, 800, 35, 3200, 90 }, // ikke kalibreret – kopi af akse 0
{ 23, 18, 0, 2, 800, 35, 3200, 90 }, // ikke kalibreret
{ 19, 13, 0, 3, 800, 35, 3200, 90 }, // ikke kalibreret
{ 32, 5, 1, 0, 800, 35, 3200, 90 }, // ikke kalibreret
{ 16, 15, 1, 1, 800, 35, 3200, 90 }, // ikke kalibreret
};
// ---------- Fælles pins ----------
constexpr uint8_t PIN_EN = 27; // fælles enable til alle drivere, aktiv lav
constexpr uint8_t PIN_DIAG = 39; // fælles DIAG-linje fra alle drivere (kun indgang, ingen intern pull)
// Hvordan DIAG-udgangene er samlet på GPIO39:
// - DiodeOr: Schottky-diode pr. driver (anode på DIAG, katode på linjen) + 10 kΩ pull-down
// til GND. Rent digitalt signal (~3 V ved stall) -> interrupt. Mest pålidelig.
// - ResistorAdc: ens modstand pr. driver (4,7–10 kΩ anbefales, 1 kΩ virker) fra DIAG til linjen,
// ingen pull-down. Linjen bliver 3,3 V × (antal høje)/(antal drivere), altså for lav
// til digital læsning – den måles i stedet med ADC'en.
enum class DiagMode { DiodeOr, ResistorAdc };
constexpr DiagMode DIAG_MODE = DiagMode::ResistorAdc; // skift til DiodeOr når dioderne er monteret
constexpr uint32_t DIAG_ADC_THRESHOLD_MV = 250; // én høj DIAG ud af 6 drivere giver ~550 mV
// UART-busser (TX via 1 kΩ til bussen, RX direkte på bussen)
constexpr uint8_t PIN_BUS_A_TX = 17;
constexpr uint8_t PIN_BUS_A_RX = 35; // kun-indgang-pin
constexpr uint8_t PIN_BUS_B_TX = 4;
constexpr uint8_t PIN_BUS_B_RX = 34; // kun-indgang-pin
constexpr uint32_t TMC_BAUD = 115200;
// Reserveret til encodere (I²C) – bruges ikke endnu.
constexpr uint8_t PIN_I2C_SDA = 21;
constexpr uint8_t PIN_I2C_SCL = 22;
// ---------- Driver ----------
constexpr float R_SENSE = 0.11f; // Ohm (BTT TMC2209 V1.3)
constexpr uint16_t MICROSTEPS = 16;
constexpr uint16_t FULL_STEPS_PER_REV = 200; // 1,8° motor
constexpr int32_t STEPS_PER_REV = (int32_t)FULL_STEPS_PER_REV * MICROSTEPS; // 3200
// ---------- Grænser ----------
// VIGTIGT: Maks-værdierne hæves kun efter aftale. Kommandoer kan kun sænke under dem.
constexpr uint16_t CURRENT_MA_DEFAULT = 600;
constexpr uint16_t CURRENT_MA_MAX = 600;
constexpr uint32_t SPEED_HZ_DEFAULT = 800; // steps/s (= 0,25 omdr./s)
constexpr uint32_t SPEED_HZ_MAX = 3200; // = 1 omdr./s – hævet efter aftale til StallGuard-test
constexpr uint32_t ACCEL_DEFAULT = 2000; // steps/s²
constexpr uint32_t ACCEL_MAX = 2000;
// ---------- StallGuard ----------
// Stall når SG_RESULT <= 2 * SGTHRS. Tærsklen følger som standard kalibreringskurven pr. akse (auto).
constexpr int16_t SG_OFFSET_MAX = 100; // finjustering ± oven på kurven
constexpr uint8_t SG_THRESHOLD_DEFAULT = 0; // manuel tærskel (når auto slås fra med 'sgt <n>')
// StallGuard er upålidelig ved lav fart (accel/decel) – kun aktiv over denne andel af sat hastighed.
constexpr float SG_MIN_SPEED_FRACTION = 0.5f;
constexpr uint8_t HOME_MAX_REVS = 2; // homing giver op efter så mange omdrejninger
constexpr uint32_t TMC_FCLK = 12000000; // intern clock i TMC2209
// ---------- Seriel / sikkerhed ----------
constexpr uint32_t PC_BAUD = 115200;
constexpr uint32_t STATUS_INTERVAL_MS = 100; // live-status pr. kørende akse (mindst)
constexpr uint32_t STATUS_MS_PER_RUNNING_AXIS = 75; // sænk raten når flere kører (115200 baud-budget)
constexpr uint32_t WATCHDOG_TIMEOUT_MS = 2000; // blød stop hvis PC'en tier (når "hb" er aktiveret)
constexpr uint32_t DRIVER_CHECK_INTERVAL_MS = 1000; // hver akse tjekkes så ofte (spredt ud)
-63
View File
@@ -1,63 +0,0 @@
#include <Arduino.h>
#include <FastAccelStepper.h>
#include "config.h"
#include "Machine.h"
#include "Commands.h"
FastAccelStepperEngine engine;
Machine machine(engine);
Commands commands(machine);
static String rxLine;
static uint32_t lastRxMs = 0;
void setup() {
Serial.begin(PC_BAUD);
delay(200);
machine.begin();
Serial.println();
Serial.printf("# Stepper-test klar - %u akser\n", NUM_AXES);
uint8_t found = 0;
for (uint8_t i = 0; i < NUM_AXES; i++) {
Axis& a = machine.axis(i);
found += a.uartOk();
Serial.printf("# a%u (bus %c, adr %u, STEP %u, DIR %u): %s\n", i, 'A' + a.config().bus,
a.config().address, a.config().stepPin, a.config().dirPin,
a.uartOk() ? "TMC2209 fundet" : "ingen driver");
}
if (!found) Serial.println("# Ingen drivere fundet - er VM (12/24 V) tændt? Prøver igen løbende.");
Serial.printf("# DIAG: %s på GPIO%u\n",
DIAG_MODE == DiagMode::DiodeOr ? "diode-OR (digital)" : "modstande + ADC", PIN_DIAG);
Serial.println("# Skriv 'help' for kommandoer");
Serial.println(commands.handle("status"));
}
void loop() {
// ---- Læs kommandoer fra PC'en (ikke-blokerende) ----
while (Serial.available()) {
char c = (char)Serial.read();
lastRxMs = millis();
if (c == '\n' || c == '\r') {
if (rxLine.length()) {
String reply = commands.handle(rxLine);
if (reply.length()) Serial.println(reply);
rxLine = "";
}
} else if (rxLine.length() < 80) {
rxLine += c;
}
}
// ---- Stall, homing, driver-tjek og live-status ----
machine.poll(Serial);
// ---- Watchdog: kun aktiv når PC-programmet sender "hb" ----
uint32_t now = millis();
if (commands.heartbeatArmed() && now - lastRxMs > WATCHDOG_TIMEOUT_MS && machine.anyRunning()) {
machine.stopAll();
Serial.println("# watchdog: ingen heartbeat fra PC - blød stop af alle akser");
lastRxMs = now; // undgå at gentage beskeden hver loop
}
}
-3
View File
@@ -1,3 +0,0 @@
fastapi
uvicorn[standard]
pyserial
-193
View File
@@ -1,193 +0,0 @@
"""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()
-913
View File
@@ -1,913 +0,0 @@
<!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>