diff --git a/esp32-firmware/artNC.ino b/esp32-firmware/artNC.ino index 4222204..fe06857 100644 --- a/esp32-firmware/artNC.ino +++ b/esp32-firmware/artNC.ino @@ -1,5 +1,7 @@ #include #include // library: "ESP32Servo" +#include +#include // ---------------------------- // Pins @@ -35,7 +37,7 @@ static const int32_t MAX_STEPS_PER_CMD = 250000; // ---------------------------- // Acceleration (new) // ---------------------------- -static const float START_STEPS_PER_SEC = 120.0f; // start speed +static const float START_STEPS_PER_SEC = 90.0f; // start speed (lower to reduce jerk) static const float ACCEL_STEPS_PER_SEC2 = 4000.0f; // accel/decel (steps/s^2) // ---------------------------- @@ -87,6 +89,72 @@ static bool qPop(Cmd& out) { return true; } +static inline void qClear() { + qHead = 0; + qTail = 0; +} + +// ---------------------------- +// Machine state + status +// ---------------------------- +enum class MachineState : uint8_t { READY, BUSY, ERROR }; + +static MachineState machineState = MachineState::READY; +static char machineDetail[96] = ""; + +static void sendLineAll(const char* line) { + Serial.print(line); + Serial.print("\n"); + if (tcpClient && tcpClient.connected()) { + tcpClient.print(line); + tcpClient.print("\n"); + } +} + +static void publishState(MachineState newState, const char* detail = nullptr) { + bool sameState = (newState == machineState); + bool sameDetail = false; + if (detail == nullptr || detail[0] == 0) { + sameDetail = (machineDetail[0] == 0); + } else { + sameDetail = (strcmp(machineDetail, detail) == 0); + } + + if (sameState && sameDetail) return; + + machineState = newState; + if (detail && detail[0]) { + strncpy(machineDetail, detail, sizeof(machineDetail) - 1); + machineDetail[sizeof(machineDetail) - 1] = 0; + } else { + machineDetail[0] = 0; + } + + const char* label = (newState == MachineState::READY) + ? "READY" + : (newState == MachineState::BUSY ? "BUSY" : "ERROR"); + + char buf[160]; + if (machineDetail[0]) { + snprintf(buf, sizeof(buf), "STATE %s %s", label, machineDetail); + } else { + snprintf(buf, sizeof(buf), "STATE %s", label); + } + sendLineAll(buf); +} + +static inline void setReady(const char* detail = nullptr) { + publishState(MachineState::READY, detail); +} + +static inline void setBusy(const char* detail) { + publishState(MachineState::BUSY, detail); +} + +static inline void setErrorState(const char* detail) { + publishState(MachineState::ERROR, detail); +} + // ---------------------------- // Helpers // ---------------------------- @@ -148,6 +216,11 @@ static void replyERR(Src src, const char* msg) { } } +static void reportError(Src src, const char* msg) { + setErrorState(msg); + replyERR(src, msg); +} + // Parse: "W L-123 R456 F1200" static bool parseW(const char* s, int32_t& l, int32_t& r, int32_t& f) { const char* pL = strchr(s, 'L'); @@ -269,53 +342,65 @@ static void handleLine(Src src, char* line) { } if (n == 0) { + setReady(); replyOK(src); return; } // END if (strcmp(line, "END") == 0) { + setBusy("END"); setEnable(false); + setReady(); replyOK(src); return; } // H if (strcmp(line, "H") == 0) { + setBusy("HOME"); posL = 0; posR = 0; + setReady(); replyOK(src); return; } // E 0 / E 1 if (line[0] == 'E') { + setBusy("ENABLE"); if (strstr(line, "0")) { setEnable(false); + setReady(); replyOK(src); return; } if (strstr(line, "1")) { setEnable(true); + setReady(); replyOK(src); return; } - replyERR(src, "BAD_E"); + reportError(src, "BAD_E"); return; } // P U / P D if (line[0] == 'P') { + setBusy("PEN"); if (strstr(line, "U")) { penUp(); + setReady(); replyOK(src); return; } if (strstr(line, "D")) { penDown(); + setReady(); replyOK(src); return; } + setReady(); replyOK(src); return; } @@ -324,15 +409,17 @@ static void handleLine(Src src, char* line) { if (line[0] == 'W') { int32_t l, r, f; if (!parseW(line, l, r, f)) { - replyERR(src, "BAD_W"); + reportError(src, "BAD_W"); return; } + setBusy("MOVE"); moveWheels(l, r, f); + setReady(); replyOK(src); return; } - replyERR(src, "UNKNOWN"); + reportError(src, "UNKNOWN"); } // ---------------------------- @@ -344,6 +431,13 @@ static int usbLen = 0; static char netBuf[160]; static int netLen = 0; +static inline void resetInputBuffers() { + usbLen = 0; + usbBuf[0] = 0; + netLen = 0; + netBuf[0] = 0; +} + static void pumpUSB() { while (Serial.available()) { char c = (char)Serial.read(); @@ -366,12 +460,26 @@ static void pumpUSB() { } static void pumpNET() { + if (tcpClient && !tcpClient.connected()) { + tcpClient.stop(); + resetInputBuffers(); + qClear(); + setEnable(false); + setReady("TCP_LOST"); + } + if (!tcpClient || !tcpClient.connected()) { WiFiClient nc = server.available(); if (nc) { + if (tcpClient) { + tcpClient.stop(); + } tcpClient = nc; tcpClient.setNoDelay(true); - netLen = 0; + resetInputBuffers(); + qClear(); + setEnable(false); + setReady("TCP_CONNECTED"); } return; } @@ -425,6 +533,7 @@ void setup() { server.begin(); server.setNoDelay(true); + publishState(MachineState::READY, "BOOT"); Serial.print("OK\n"); } @@ -438,4 +547,4 @@ void loop() { } delay(1); -} \ No newline at end of file +} diff --git a/print.py b/print.py index 4852ab7..85c7929 100644 --- a/print.py +++ b/print.py @@ -28,6 +28,7 @@ def read_acode(path: str) -> List[str]: def connect_tcp(host: str, port: int, connect_timeout: float) -> socket.socket: s = socket.create_connection((host, port), timeout=connect_timeout) s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + recv_line_poll._buf = bytearray() return s def recv_line_poll(sock: socket.socket, slice_timeout: float) -> str | None: @@ -54,6 +55,11 @@ def recv_line_poll(sock: socket.socket, slice_timeout: float) -> str | None: recv_line_poll._buf = buf return None + +def print_status_line(label: str, value: str) -> None: + sys.stdout.write(f"[{label}] {value}\n") + sys.stdout.flush() + def estimate_motion_seconds( line: str, feed_to_sps: float, @@ -122,12 +128,30 @@ def estimate_motion_seconds( return (2.0 * t_ramp) + t_cruise + 0.30 + +def handle_sideband(resp: str) -> bool: + if resp.startswith("STATE"): + print_status_line("STATE", resp[6:].strip()) + return True + return False + + +def drain_sideband(sock: socket.socket, max_wait_s: float, poll_slice_s: float) -> None: + deadline = time.time() + max_wait_s + while time.time() < deadline: + resp = recv_line_poll(sock, poll_slice_s) + if resp is None: + break + if not handle_sideband(resp): + break + def wait_ok( sock: socket.socket, line: str, expected_s: float, hard_cap_s: float, poll_slice_s: float, + sideband_cb=None, ) -> None: # Deadline with safe margin wait_s = max(4.0, expected_s * 3.0 + 2.0) @@ -142,6 +166,9 @@ def wait_ok( if resp is None: continue + if sideband_cb and sideband_cb(resp): + continue + if resp == "OK": return if resp.startswith("ERR"): @@ -167,8 +194,8 @@ def main() -> int: ap.add_argument("--servo-settle-ms", type=int, default=180) # accel model (match ESP32 defaults) - ap.add_argument("--start-sps", type=float, default=120.0) - ap.add_argument("--accel-sps2", type=float, default=8000.0) + ap.add_argument("--start-sps", type=float, default=90.0) + ap.add_argument("--accel-sps2", type=float, default=4000.0) ap.add_argument("--start", type=int, default=1) ap.add_argument("--delay-ms", type=int, default=0) @@ -189,7 +216,8 @@ def main() -> int: sock = connect_tcp(args.host, args.port, args.connect_timeout) try: - print(f"Connected to {args.host}:{args.port}") + print_status_line("WIFI", f"Connected to {args.host}:{args.port}") + drain_sideband(sock, max_wait_s=1.0, poll_slice_s=args.poll_slice) for i in range(idx0, n): line = lines[i] @@ -212,6 +240,7 @@ def main() -> int: expected_s=expected, hard_cap_s=args.hard_cap, poll_slice_s=args.poll_slice, + sideband_cb=handle_sideband, ) print_line_status(i + 1, n, line, "OK") @@ -238,6 +267,10 @@ def main() -> int: print("DONE") return 0 + except (ConnectionError, TimeoutError, RuntimeError) as exc: + print_status_line("ERROR", str(exc)) + return 3 + finally: try: sock.close() @@ -245,4 +278,4 @@ def main() -> int: pass if __name__ == "__main__": - raise SystemExit(main()) \ No newline at end of file + raise SystemExit(main()) diff --git a/templates/index.html b/templates/index.html index 1371ffa..a620922 100644 --- a/templates/index.html +++ b/templates/index.html @@ -171,6 +171,11 @@

artNC

not loaded +
+ WiFi: - + State: - +
+
@@ -880,6 +885,15 @@

artNC

return `${s}s`; } + function setLinkStatus(txt) { + qs("linkStatus").textContent = txt ? `WiFi: ${txt}` : "WiFi: -"; + } + + function setMachineState(state, detail = "") { + const suffix = detail ? ` (${detail})` : ""; + qs("machineState").textContent = state ? `State: ${state}${suffix}` : "State: -"; + } + // Sender async function loadAcode() { const fd = new FormData(); @@ -914,6 +928,8 @@

artNC

}; // SSE + setLinkStatus("disconnected"); + setMachineState(""); const ev = new EventSource("/events"); ev.onmessage = (e) => { const data = JSON.parse(e.data); @@ -944,6 +960,15 @@

artNC

if (payload.line) qs("lastokline").textContent = payload.line; } if (kind === "error") qs("error").textContent = payload.msg; + if (kind === "status") { + qs("runstate").textContent = payload.msg || "idle"; + if (payload.msg === "connected") setLinkStatus(`${qs("host").value}:${qs("port").value}`); + else if (payload.msg === "connecting") setLinkStatus("connecting..."); + else if (payload.msg === "stopped" || payload.msg === "disconnected") setLinkStatus("disconnected"); + } + if (kind === "machine_state") { + setMachineState(payload.state, payload.detail || ""); + } return; } diff --git a/ui_sender.py b/ui_sender.py index f5e9f60..45fb0bf 100644 --- a/ui_sender.py +++ b/ui_sender.py @@ -27,7 +27,7 @@ app = Flask(__name__, static_folder="static", static_url_path="/static") -DEFAULT_PORT = 23 +DEFAULT_PORT = 3333 RECV_TIMEOUT_S = 0.25 LINE_ACK_TIMEOUT_S = 60.0 @@ -67,8 +67,8 @@ def push_event(kind: str, payload: dict): "min_sps": 50.0, "max_sps": 2500.0, "servo_settle_s": 0.18, - "start_sps": 120.0, - "accel_sps2": 8000.0, + "start_sps": 90.0, + "accel_sps2": 4000.0, } def estimate_motion_seconds( @@ -206,6 +206,21 @@ def send_line(sock: socket.socket, line: str): data = (line.strip() + "\n").encode("utf-8") sock.sendall(data) +def _parse_state_line(line: str) -> Optional[Dict[str, str]]: + if not line.startswith("STATE"): + return None + parts = line.split(maxsplit=2) + status = parts[1] if len(parts) > 1 else "" + detail = parts[2] if len(parts) > 2 else "" + return {"state": status or "?", "detail": detail} + +def _handle_sideband(line: str) -> bool: + st = _parse_state_line(line) + if st: + push_event("machine_state", st) + return True + return False + def wait_ok(sock: socket.socket, timeout_s: float) -> bool: deadline = time.time() + timeout_s while time.time() < deadline: @@ -213,6 +228,8 @@ def wait_ok(sock: socket.socket, timeout_s: float) -> bool: if not resp: continue s = resp.strip() + if _handle_sideband(s): + continue if s.startswith("OK"): return True if s.startswith("ERR"): @@ -226,6 +243,7 @@ def send_one_command(host: str, port: int, line: str, timeout_s: float = LINE_AC sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: sock.connect((host, port)) + push_event("status", {"msg": "connected"}) send_line(sock, line) ok = wait_ok(sock, timeout_s) return {"ok": ok, "line": line, "error": None if ok else "timeout or ERR"} @@ -236,6 +254,7 @@ def send_one_command(host: str, port: int, line: str, timeout_s: float = LINE_AC sock.close() except Exception: pass + push_event("status", {"msg": "disconnected"}) def sender_worker(): with state_lock: @@ -320,6 +339,7 @@ def sender_worker(): with state_lock: state.error = f"connection failed: {e}" push_event("error", {"msg": state.error}) + push_event("status", {"msg": "disconnected"}) finally: try: sock.close() @@ -329,6 +349,7 @@ def sender_worker(): state.running = False state.paused = False state.stopping = False + push_event("status", {"msg": "stopped"}) def _safe_float(name: str, default: float) -> float: try: