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,4 @@
|
||||
.pio/
|
||||
.vscode/
|
||||
web/.venv/
|
||||
__pycache__/
|
||||
@@ -0,0 +1,24 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,14 @@
|
||||
; 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
@@ -0,0 +1,307 @@
|
||||
#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
@@ -0,0 +1,111 @@
|
||||
#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;
|
||||
};
|
||||
@@ -0,0 +1,155 @@
|
||||
#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)";
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#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
@@ -0,0 +1,246 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
#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)
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
#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)
|
||||
@@ -0,0 +1,63 @@
|
||||
#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
|
||||
}
|
||||
}
|
||||
@@ -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