diff --git a/acode.py b/acode.py index 856cb09..48b76a7 100644 --- a/acode.py +++ b/acode.py @@ -46,6 +46,8 @@ # ---------------------------- DEFAULT_FLAT_STEP_MM = 1.0 DEFAULT_EPSILON_MM = 0.25 +DEFAULT_ARC_SEG_MM = 0.0 +DEFAULT_ARC_SEG_DEG = 0.0 # ---------------------------- # Helpers @@ -351,6 +353,30 @@ def emit_arc_one_shot(out: List[str], radius: float, dtheta: float, feed_arc: in dr = (radius * dtheta) + (TURN_GAIN * (WHEELBASE_MM / 2.0) * dtheta) emit_w(out, steps_from_mm(dl), steps_from_mm(dr), feed_arc) +def emit_arc_segmented( + out: List[str], + radius: float, + dtheta: float, + feed_arc: int, + arc_seg_mm: float, + arc_seg_deg: float, +): + arc_len = abs(radius * dtheta) + + n_mm = 1 + if arc_seg_mm > 1e-9 and arc_len > 1e-9: + n_mm = max(1, int(math.ceil(arc_len / arc_seg_mm))) + + n_deg = 1 + if arc_seg_deg > 1e-9: + n_deg = max(1, int(math.ceil(abs(math.degrees(dtheta)) / arc_seg_deg))) + + n = max(1, n_mm, n_deg) + step_theta = dtheta / n + + for _ in range(n): + emit_arc_one_shot(out, radius, step_theta, feed_arc) + def tangent_heading_for_arc(a0: float, dtheta: float) -> float: ccw = dtheta > 0 return wrap_pi(a0 + (math.pi / 2.0 if ccw else -math.pi / 2.0)) @@ -360,6 +386,8 @@ def primitives_to_acode( feed_lin: int, feed_turn: int, feed_arc: int, + arc_seg_mm: float, + arc_seg_deg: float, ) -> List[str]: x = 0.0 y = 0.0 @@ -411,7 +439,14 @@ def go_to_point(nx: float, ny: float): heading = wrap_pi(heading + dtheta_align) if abs(prim.dtheta) > 1e-9: - emit_arc_one_shot(out, prim.radius, prim.dtheta, feed_arc) + emit_arc_segmented( + out, + prim.radius, + prim.dtheta, + feed_arc, + max(0.0, arc_seg_mm), + max(0.0, arc_seg_deg), + ) ex, ey = prim_end(prim) x, y = ex, ey @@ -437,6 +472,8 @@ def main(): ap.add_argument("--flat-step", type=float, default=DEFAULT_FLAT_STEP_MM) ap.add_argument("--epsilon", type=float, default=DEFAULT_EPSILON_MM) + ap.add_argument("--arc-seg-mm", type=float, default=DEFAULT_ARC_SEG_MM, help="maksymalna długość segmentu łuku") + ap.add_argument("--arc-seg-deg", type=float, default=DEFAULT_ARC_SEG_DEG, help="maksymalny kąt segmentu łuku") args = ap.parse_args() out_path = args.out or (os.path.splitext(args.dxf)[0] + ".acode") @@ -455,6 +492,8 @@ def main(): feed_lin=args.feed_lin, feed_turn=args.feed_turn, feed_arc=args.feed_arc, + arc_seg_mm=args.arc_seg_mm, + arc_seg_deg=args.arc_seg_deg, ) with open(out_path, "w", encoding="utf-8") as f: @@ -467,4 +506,4 @@ def main(): print("turn_gain:", f"{TURN_GAIN:.6f}") if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/templates/index.html b/templates/index.html index 9129d9d..649e6a7 100644 --- a/templates/index.html +++ b/templates/index.html @@ -31,6 +31,8 @@ input, select { padding: 8px 10px; border:1px solid #ddd; border-radius: 10px; } .divider { margin: 10px 0; border-top: 1px solid #eee; } + .serial-only { display: none; } + .tcp-only { display: inline-block; } /* Settings list */ .setting-list { display:flex; flex-direction: column; gap: 10px; } @@ -133,17 +135,31 @@

artNC

+
- - + + + + + + not loaded
+
@@ -232,6 +248,138 @@

artNC

+ +
+
+ +
+
+ + + no output + +
+ +
Konwersja DXF → ACODE z segmentacją łuków; sterowanie krokowcami z PID po stronie maszyny.
+ +
+ +
+
+ +
+
feed_linFeed dla jazdy prostej
+
+
+ +
+
feed_turnFeed dla skrętów
+
+
+ +
+
feed_arcFeed dla łuków
+
+
+ +
+
flat_stepPróbkowanie SPLINE/krzywych (mm)
+
+
+ +
+
epsilonUproszczenie linii (mm)
+
+
+ +
+
arc_seg_mmMaks. długość segmentu łuku (mm)
+
+
+ +
+
arc_seg_degMaks. kąt segmentu łuku (deg)
+
+
+ +
+ +
+
viz_dpiPodgląd: DPI
+
+
+ +
+
viz_arc_step_mmPodgląd: krok łuku w mm
+
+
+ +
+
viz_arc_step_degPodgląd: krok łuku w stopniach
+
+
+ +
+
viz_equalPodgląd: zachowaj proporcje
+
+
+ +
+
viz_invert_yPodgląd: invert Y
+
+
+ +
+
viz_home_resets_posePodgląd: H resetuje pozycję
+
+
+ +
+
+ +
+ +
+ + +
+
+ +
+
+
Preview + Symulator (maszyna.png)
+ +
+ preview + +
+ +
+ + + + + Speed + + 2.00x + + Frame + + 0/0 + + Sprite scale + + 0.35 + + wheelbase + - +
+
+
+ +
+
+
@@ -380,12 +528,12 @@

artNC

viz_arc_step_mmPodgląd: krok łuku w mm
-
+
viz_arc_step_degPodgląd: krok łuku w stopniach
-
+
@@ -615,6 +763,7 @@

artNC

requestAnimationFrame(() => { resizeSim("gen"); resizeSim("text"); + resizeSim("dxf"); }); }; }); @@ -642,11 +791,43 @@

artNC

return json; } + function applyTransport(fd) { + fd.set("transport", qs("transport").value); + fd.set("host", qs("host").value.trim()); + fd.set("port", qs("port").value.trim()); + fd.set("serial_port", qs("serial_port").value.trim()); + fd.set("serial_baud", qs("serial_baud").value.trim()); + } + + function updateTransportUI() { + const isSerial = qs("transport").value === "serial"; + document.querySelectorAll(".serial-only").forEach(el => el.style.display = isSerial ? "" : "none"); + document.querySelectorAll(".tcp-only").forEach(el => el.style.display = isSerial ? "none" : ""); + } + + async function refreshSerialPorts(showError = false) { + const errEl = qs("serial_err"); + if (errEl) errEl.textContent = ""; + try { + const out = await getJson("/api/serial_ports"); + const dl = qs("serial_ports"); + if (!dl) return; + dl.innerHTML = ""; + out.ports.forEach(p => { + const opt = document.createElement("option"); + opt.value = p.device; + opt.textContent = p.description || p.device; + dl.appendChild(opt); + }); + } catch (e) { + if (showError && errEl) errEl.textContent = e.message || "błąd wykrywania portów"; + } + } + // Sender async function loadAcode() { const fd = new FormData(); - fd.set("host", qs("host").value.trim()); - fd.set("port", qs("port").value.trim()); + applyTransport(fd); fd.set("acode", qs("acode").value); const out = await post("/api/load", fd); @@ -692,8 +873,7 @@

artNC

async function jog(dir) { qs("jogerr").textContent = ""; const fd = new FormData(); - fd.set("host", qs("host").value.trim()); - fd.set("port", qs("port").value.trim()); + applyTransport(fd); fd.set("dir", dir); fd.set("jog_mm", qs("jog_mm").value.trim()); fd.set("steps_per_mm", qs("steps_per_mm").value.trim()); @@ -710,8 +890,7 @@

artNC

qs("pen_up").onclick = () => { qs("jogerr").textContent = ""; const fd = new FormData(); - fd.set("host", qs("host").value.trim()); - fd.set("port", qs("port").value.trim()); + applyTransport(fd); fd.set("mode", "up"); post("/api/pen", fd).catch(e => qs("jogerr").textContent = e.message); }; @@ -719,8 +898,7 @@

artNC

qs("pen_down").onclick = () => { qs("jogerr").textContent = ""; const fd = new FormData(); - fd.set("host", qs("host").value.trim()); - fd.set("port", qs("port").value.trim()); + applyTransport(fd); fd.set("mode", "down"); post("/api/pen", fd).catch(e => qs("jogerr").textContent = e.message); }; @@ -728,8 +906,7 @@

artNC

qs("step_enable").onclick = () => { qs("jogerr").textContent = ""; const fd = new FormData(); - fd.set("host", qs("host").value.trim()); - fd.set("port", qs("port").value.trim()); + applyTransport(fd); fd.set("cmd", qs("step_en_cmd").value.trim()); post("/api/steppers", fd).catch(e => qs("jogerr").textContent = e.message); }; @@ -737,8 +914,7 @@

artNC

qs("step_disable").onclick = () => { qs("jogerr").textContent = ""; const fd = new FormData(); - fd.set("host", qs("host").value.trim()); - fd.set("port", qs("port").value.trim()); + applyTransport(fd); fd.set("cmd", qs("step_dis_cmd").value.trim()); post("/api/steppers", fd).catch(e => qs("jogerr").textContent = e.message); }; @@ -1066,9 +1242,19 @@

artNC

"simSpriteScaleText", "simSpriteScaleValText" ); + const simDxf = makeSim( + "dxf", + "d_previmg", "simCanvasDxf", + "simWheelbaseDxf", "simPosDxf", "simScrubDxf", + "simPlayDxf", "simPauseDxf", "simResetDxf", + "simSpeedDxf", "simSpeedValDxf", + "simSpriteScaleDxf", "simSpriteScaleValDxf" + ); + function resizeSim(which) { if (which === "gen") simGen.resize(); if (which === "text") simText.resize(); + if (which === "dxf") simDxf.resize(); } // ---------------------------- @@ -1131,8 +1317,7 @@

artNC

qs("generr").textContent = ""; if (!lastGenId) throw new Error("generate first"); const fd = new FormData(); - fd.set("host", qs("host").value.trim()); - fd.set("port", qs("port").value.trim()); + applyTransport(fd); const out = await post("/api/push_to_sender", fd); qs("acode").value = "loaded from generator (" + out.lines + " lines)\n"; @@ -1242,8 +1427,7 @@

artNC

syncTextVizDefaults(); if (!lastTextId) throw new Error("generate first"); const fd = new FormData(); - fd.set("host", qs("host").value.trim()); - fd.set("port", qs("port").value.trim()); + applyTransport(fd); const out = await post("/api/push_to_sender", fd); qs("acode").value = "loaded from text outline (" + out.lines + " lines)\n"; @@ -1262,6 +1446,71 @@

artNC

}; loadFonts(); + + // ---------------------------- + // DXF -> ACODE + // ---------------------------- + let lastDxfId = ""; + + async function convertDxf() { + qs("dxf_err").textContent = ""; + const file = qs("dxf").files[0]; + if (!file) throw new Error("select a DXF first"); + + const fd = new FormData(); + fd.set("dxf", file); + fd.set("feed_lin", formVal("d_feed_lin")); + fd.set("feed_turn", formVal("d_feed_turn")); + fd.set("feed_arc", formVal("d_feed_arc")); + fd.set("flat_step", formVal("d_flat_step")); + fd.set("epsilon", formVal("d_epsilon")); + fd.set("arc_seg_mm", formVal("d_arc_seg_mm")); + fd.set("arc_seg_deg", formVal("d_arc_seg_deg")); + + fd.set("viz_dpi", formVal("d_viz_dpi")); + fd.set("viz_arc_step_mm", formVal("d_viz_arc_step_mm")); + fd.set("viz_arc_step_deg", formVal("d_viz_arc_step_deg")); + if (qs("d_viz_equal").checked) fd.set("viz_equal", "1"); + if (qs("d_viz_invert_y").checked) fd.set("viz_invert_y", "1"); + if (qs("d_viz_home_resets_pose").checked) fd.set("viz_home_resets_pose", "1"); + + const out = await post("/api/dxf_to_acode", fd); + lastDxfId = out.gen_id; + + qs("dxf_info").textContent = "gen_id: " + out.gen_id + " lines: " + out.acode_lines; + qs("d_previmg").src = "/preview/" + out.gen_id + ".png?ts=" + Date.now(); + await simDxf.loadData(out.gen_id); + } + + qs("dxf_gen").onclick = () => convertDxf().catch(e => qs("dxf_err").textContent = e.message); + + qs("dxf_push").onclick = async () => { + qs("dxf_err").textContent = ""; + if (!lastDxfId) throw new Error("generate first"); + const fd = new FormData(); + applyTransport(fd); + const out = await post("/api/push_to_sender", fd); + + qs("acode").value = "loaded from dxf (" + out.lines + " lines)\n"; + qs("loaded").textContent = "loaded lines: " + out.lines; + + document.querySelector('.tab[data-tab="sender"]').click(); + }; + + qs("dxf_download").onclick = () => { + qs("dxf_err").textContent = ""; + if (!lastDxfId) { + qs("dxf_err").textContent = "generate first"; + return; + } + window.location.href = "/download/" + lastDxfId + ".acode"; + }; + + // Transport UI wiring + qs("transport").onchange = () => updateTransportUI(); + qs("serial_refresh").onclick = () => refreshSerialPorts(true); + updateTransportUI(); + refreshSerialPorts(false); diff --git a/ui_sender.py b/ui_sender.py index 29f2756..a18d456 100644 --- a/ui_sender.py +++ b/ui_sender.py @@ -19,10 +19,18 @@ import queue import subprocess import re +import contextlib from dataclasses import dataclass from typing import Optional, List, Dict, Any, Tuple from io import BytesIO +try: # pyserial is optional; serial mode is enabled only if installed + import serial # type: ignore + from serial.tools import list_ports # type: ignore +except ImportError: # pragma: no cover - optional dependency + serial = None # type: ignore + list_ports = None # type: ignore + from flask import Flask, request, render_template, Response, jsonify, send_file, abort app = Flask(__name__, static_folder="static", static_url_path="/static") @@ -66,6 +74,9 @@ class JobState: stopping: bool = False host: str = "192.168.4.1" port: int = DEFAULT_PORT + transport: str = "tcp" # tcp or serial + serial_port: str = "/dev/ttyUSB0" + serial_baud: int = 115200 lines: Optional[List[str]] = None idx: int = 0 last_sent: str = "" @@ -143,10 +154,109 @@ def send_one_command(host: str, port: int, line: str, timeout_s: float = LINE_AC except Exception: pass +@dataclass +class Transport: + kind: str + sock: Optional[socket.socket] = None + ser: Any = None + +def _open_transport(kind: str, host: str, port: int, serial_port: str, serial_baud: int) -> Transport: + if kind == "serial": + if serial is None: + raise RuntimeError("pyserial not installed") + ser = serial.Serial(serial_port, serial_baud, timeout=RECV_TIMEOUT_S, write_timeout=LINE_ACK_TIMEOUT_S) + return Transport(kind="serial", ser=ser) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((host, port)) + return Transport(kind="tcp", sock=sock) + +def _close_transport(t: Transport): + if t.kind == "serial" and t.ser: + with contextlib.suppress(Exception): + t.ser.close() + if t.kind == "tcp" and t.sock: + with contextlib.suppress(Exception): + t.sock.close() + +def _recv_line_transport(t: Transport, timeout_s: float) -> Optional[str]: + if t.kind == "serial": + ser = t.ser + if not ser: + return None + ser.timeout = timeout_s + buf = bytearray() + start = time.time() + while True: + if time.time() - start > timeout_s: + return None + b = ser.read(1) + if not b: + continue + if b == b"\n": + return buf.decode("utf-8", errors="replace").strip() + if b != b"\r": + buf.extend(b) + sock = t.sock + if not sock: + return None + return recv_line(sock, timeout_s) + +def _send_line_transport(t: Transport, line: str): + if t.kind == "serial": + if not t.ser: + raise RuntimeError("serial not opened") + data = (line.strip() + "\n").encode("utf-8") + t.ser.write(data) + t.ser.flush() + return + if not t.sock: + raise RuntimeError("socket not opened") + send_line(t.sock, line) + +def _wait_ok_transport(t: Transport, timeout_s: float) -> bool: + deadline = time.time() + timeout_s + while time.time() < deadline: + resp = _recv_line_transport(t, RECV_TIMEOUT_S) + if not resp: + continue + s = resp.strip() + if s.startswith("OK"): + return True + if s.startswith("ERR"): + with state_lock: + state.error = s + push_event("error", {"msg": s}) + return False + return False + +def send_one_command_transport( + transport: str, + host: str, + port: int, + serial_port: str, + serial_baud: int, + line: str, + timeout_s: float = LINE_ACK_TIMEOUT_S, +) -> Dict[str, Any]: + try: + t = _open_transport(transport, host, port, serial_port, serial_baud) + except Exception as e: + return {"ok": False, "line": line, "error": str(e)} + try: + _send_line_transport(t, line) + ok = _wait_ok_transport(t, timeout_s) + return {"ok": ok, "line": line, "error": None if ok else "timeout or ERR"} + except Exception as e: + return {"ok": False, "line": line, "error": str(e)} + finally: + _close_transport(t) def sender_worker(): with state_lock: host = state.host port = state.port + transport_kind = state.transport + serial_port = state.serial_port + serial_baud = state.serial_baud lines = state.lines or [] state.idx = 0 state.last_sent = "" @@ -156,11 +266,20 @@ def sender_worker(): push_event("status", {"msg": "connecting", "host": host, "port": port}) - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: - sock.connect((host, port)) + t = _open_transport(transport_kind, host, port, serial_port, serial_baud) push_event("status", {"msg": "connected"}) + except Exception as e: + with state_lock: + state.error = f"connection failed: {e}" + push_event("error", {"msg": state.error}) + with state_lock: + state.running = False + state.paused = False + state.stopping = False + return + try: while True: with state_lock: if state.stopping: @@ -197,14 +316,14 @@ def sender_worker(): push_event("line", {"idx": idx + 1, "total": total, "line": line}) try: - send_line(sock, line) - except OSError as e: + _send_line_transport(t, line) + except Exception as e: with state_lock: state.error = f"send failed: {e}" push_event("error", {"msg": state.error}) break - ok = wait_ok(sock, LINE_ACK_TIMEOUT_S) + ok = _wait_ok_transport(t, LINE_ACK_TIMEOUT_S) with state_lock: state.last_ok = ok @@ -220,15 +339,8 @@ def sender_worker(): state.idx += 1 push_event("status", {"msg": "stopped"}) - except OSError as e: - with state_lock: - state.error = f"connection failed: {e}" - push_event("error", {"msg": state.error}) finally: - try: - sock.close() - except Exception: - pass + _close_transport(t) with state_lock: state.running = False state.paused = False @@ -690,6 +802,91 @@ def generate_from_png(png_path: str, params: Dict[str, Any]) -> Dict[str, Any]: "meta": meta, } +def generate_from_dxf(dxf_path: str, params: Dict[str, Any]) -> Dict[str, Any]: + err = _assert_tools_exist() + if err: + return {"ok": False, "error": err} + + gen_id = str(uuid.uuid4())[:8] + out_acode = os.path.join(GEN_DIR, f"{gen_id}.acode") + out_preview = os.path.join(GEN_DIR, f"{gen_id}.preview.png") + + cmd = [ + "python3", + ACODE_PY_PATH, + dxf_path, + "-o", + out_acode, + "--feed-lin", + str(params["feed_lin"]), + "--feed-turn", + str(params["feed_turn"]), + "--feed-arc", + str(params["feed_arc"]), + "--flat-step", + str(params["flat_step"]), + "--epsilon", + str(params["epsilon"]), + "--arc-seg-mm", + str(params["arc_seg_mm"]), + "--arc-seg-deg", + str(params["arc_seg_deg"]), + ] + + p1 = _run_cmd(cmd, cwd=HERE) + if p1.returncode != 0 or not os.path.isfile(out_acode): + msg = (p1.stderr or p1.stdout or "").strip() or "dxf convert failed" + return {"ok": False, "error": msg} + + cmd2 = [ + "python3", + ACODEVIZ_PATH, + out_acode, + "-o", + out_preview, + "--acode-py", + ACODE_PY_PATH, + "--dpi", + str(params["viz_dpi"]), + "--arc-step-mm", + str(params["viz_arc_step_mm"]), + "--arc-step-deg", + str(params["viz_arc_step_deg"]), + ] + if params.get("viz_equal"): + cmd2.append("--equal") + if params.get("viz_invert_y"): + cmd2.append("--invert-y") + if params.get("viz_home_resets_pose"): + cmd2.append("--home-resets-pose") + + p2 = _run_cmd(cmd2, cwd=HERE) + if p2.returncode != 0 or not os.path.isfile(out_preview): + msg = (p2.stderr or p2.stdout or "").strip() or "preview failed" + return {"ok": False, "error": msg} + + with open(out_acode, "r", encoding="utf-8", errors="ignore") as f: + acode_text = f.read() + + meta = { + "gen_id": gen_id, + "params": params, + "acode_lines": len([ln for ln in acode_text.splitlines() if ln.strip()]), + "generator_stdout": (p1.stdout or "").strip(), + "generator_stderr": (p1.stderr or "").strip(), + "viz_stdout": (p2.stdout or "").strip(), + "viz_stderr": (p2.stderr or "").strip(), + } + + return { + "ok": True, + "gen_id": gen_id, + "acode_path": out_acode, + "preview_path": out_preview, + "acode_text": acode_text, + "meta": meta, + } + # ---------------------------- # Routes # ---------------------------- @@ -701,6 +898,9 @@ def index(): def api_load(): host = request.form.get("host", "").strip() or "192.168.4.1" port = _safe_int("port", DEFAULT_PORT) + transport = (request.form.get("transport", "tcp") or "tcp").lower() + serial_port = request.form.get("serial_port", "").strip() or "/dev/ttyUSB0" + serial_baud = _safe_int("serial_baud", 115200) content = request.form.get("acode", "") lines = [ln.rstrip("\r\n") for ln in content.splitlines()] @@ -709,6 +909,9 @@ def api_load(): with state_lock: state.host = host state.port = port + state.transport = transport + state.serial_port = serial_port + state.serial_baud = serial_baud state.lines = lines state.idx = 0 state.error = "" @@ -724,6 +927,27 @@ def api_start(): with state_lock: if not state.lines: return jsonify({"ok": False, "error": "no ACODE loaded"}), 400 + host = state.host + port = state.port + transport = state.transport + serial_port = state.serial_port + serial_baud = state.serial_baud + + # Preflight connection to fail fast instead of waiting inside worker + try: + t = _open_transport(transport, host, port, serial_port, serial_baud) + _close_transport(t) + except Exception as e: + msg = f"connection failed: {e}" + with state_lock: + state.running = False + state.paused = False + state.stopping = False + state.error = msg + push_event("error", {"msg": msg}) + return jsonify({"ok": False, "error": msg}), 502 + + with state_lock: state.running = True state.paused = False state.stopping = False @@ -789,6 +1013,15 @@ def _busy_guard() -> Optional[str]: return "busy: stop or pause printing before jog/pen/steppers" return None +@app.route("/api/serial_ports", methods=["GET"]) +def api_serial_ports(): + if list_ports is None: + return jsonify({"ok": False, "error": "pyserial not installed"}), 500 + ports = [] + for p in list_ports.comports(): + ports.append({"device": p.device, "description": p.description}) + return jsonify({"ok": True, "ports": ports}) + @app.route("/api/jog", methods=["POST"]) def api_jog(): err = _busy_guard() @@ -798,6 +1031,9 @@ def api_jog(): direction = (request.form.get("dir", "") or "").strip().lower() host = request.form.get("host", "").strip() or "192.168.4.1" port = _safe_int("port", DEFAULT_PORT) + transport = (request.form.get("transport", "tcp") or "tcp").lower() + serial_port = request.form.get("serial_port", "").strip() or "/dev/ttyUSB0" + serial_baud = _safe_int("serial_baud", 115200) jog_mm = _safe_float("jog_mm", 10.0) steps_per_mm = _safe_float("steps_per_mm", 9.142857) @@ -819,7 +1055,9 @@ def api_jog(): return jsonify({"ok": False, "error": "bad dir"}), 400 push_event("line", {"idx": 0, "total": 0, "line": line}) - out = send_one_command(host, port, line, timeout_s=LINE_ACK_TIMEOUT_S) + out = send_one_command_transport( + transport, host, port, serial_port, serial_baud, line, timeout_s=LINE_ACK_TIMEOUT_S + ) if out["ok"]: push_event("ok", {"idx": 0}) return jsonify({"ok": True, "line": line}) @@ -834,6 +1072,9 @@ def api_pen(): host = request.form.get("host", "").strip() or "192.168.4.1" port = _safe_int("port", DEFAULT_PORT) + transport = (request.form.get("transport", "tcp") or "tcp").lower() + serial_port = request.form.get("serial_port", "").strip() or "/dev/ttyUSB0" + serial_baud = _safe_int("serial_baud", 115200) mode = (request.form.get("mode", "") or "").strip().lower() if mode == "up": @@ -844,7 +1085,9 @@ def api_pen(): return jsonify({"ok": False, "error": "bad mode"}), 400 push_event("line", {"idx": 0, "total": 0, "line": line}) - out = send_one_command(host, port, line, timeout_s=LINE_ACK_TIMEOUT_S) + out = send_one_command_transport( + transport, host, port, serial_port, serial_baud, line, timeout_s=LINE_ACK_TIMEOUT_S + ) if out["ok"]: push_event("ok", {"idx": 0}) return jsonify({"ok": True, "line": line}) @@ -859,13 +1102,18 @@ def api_steppers(): host = request.form.get("host", "").strip() or "192.168.4.1" port = _safe_int("port", DEFAULT_PORT) + transport = (request.form.get("transport", "tcp") or "tcp").lower() + serial_port = request.form.get("serial_port", "").strip() or "/dev/ttyUSB0" + serial_baud = _safe_int("serial_baud", 115200) cmd = (request.form.get("cmd", "") or "").strip() if not cmd: return jsonify({"ok": False, "error": "missing cmd"}), 400 push_event("line", {"idx": 0, "total": 0, "line": cmd}) - out = send_one_command(host, port, cmd, timeout_s=LINE_ACK_TIMEOUT_S) + out = send_one_command_transport( + transport, host, port, serial_port, serial_baud, cmd, timeout_s=LINE_ACK_TIMEOUT_S + ) if out["ok"]: push_event("ok", {"idx": 0}) return jsonify({"ok": True, "line": cmd}) @@ -911,8 +1159,8 @@ def api_gen(): "feed_turn": _safe_int("feed_turn", 800), "viz_dpi": _safe_int("viz_dpi", 160), - "viz_arc_step_mm": _safe_float("viz_arc_step_mm", 1.0), - "viz_arc_step_deg": _safe_float("viz_arc_step_deg", 1.0), + "viz_arc_step_mm": _safe_float("viz_arc_step_mm", 0.4), + "viz_arc_step_deg": _safe_float("viz_arc_step_deg", 0.5), "viz_equal": _safe_bool("viz_equal"), "viz_invert_y": _safe_bool("viz_invert_y"), "viz_home_resets_pose": _safe_bool("viz_home_resets_pose"), @@ -1013,8 +1261,8 @@ def api_text_outline(): "feed_turn": _safe_int("feed_turn", 800), "viz_dpi": _safe_int("viz_dpi", 160), - "viz_arc_step_mm": _safe_float("viz_arc_step_mm", 1.0), - "viz_arc_step_deg": _safe_float("viz_arc_step_deg", 1.0), + "viz_arc_step_mm": _safe_float("viz_arc_step_mm", 0.4), + "viz_arc_step_deg": _safe_float("viz_arc_step_deg", 0.5), "viz_equal": _safe_bool("viz_equal"), "viz_invert_y": _safe_bool("viz_invert_y"), "viz_home_resets_pose": _safe_bool("viz_home_resets_pose"), @@ -1054,6 +1302,71 @@ def api_text_outline(): "warnings": result["meta"].get("warnings", []), }) +# ---------------------------- +# Generator: DXF -> ACODE +# ---------------------------- +@app.route("/api/dxf_to_acode", methods=["POST"]) +def api_dxf_to_acode(): + if "dxf" not in request.files: + return jsonify({"ok": False, "error": "missing file field: dxf"}), 400 + + f = request.files["dxf"] + if not f.filename.lower().endswith(".dxf"): + return jsonify({"ok": False, "error": "only .dxf accepted"}), 400 + + gen_id_tmp = str(uuid.uuid4())[:8] + dxf_path = os.path.join(UPLOADS_DIR, f"{gen_id_tmp}.dxf") + f.save(dxf_path) + + params = { + "feed_lin": _safe_int("feed_lin", 1200), + "feed_turn": _safe_int("feed_turn", 800), + "feed_arc": _safe_int("feed_arc", 800), + "flat_step": _safe_float("flat_step", 1.0), + "epsilon": _safe_float("epsilon", 0.25), + "arc_seg_mm": _safe_float("arc_seg_mm", 2.0), + "arc_seg_deg": _safe_float("arc_seg_deg", 5.0), + "viz_dpi": _safe_int("viz_dpi", 160), + "viz_arc_step_mm": _safe_float("viz_arc_step_mm", 0.4), + "viz_arc_step_deg": _safe_float("viz_arc_step_deg", 0.5), + "viz_equal": _safe_bool("viz_equal"), + "viz_invert_y": _safe_bool("viz_invert_y"), + "viz_home_resets_pose": _safe_bool("viz_home_resets_pose"), + } + + result = generate_from_dxf(dxf_path, params) + if not result["ok"]: + with gen_lock: + gen_state.error = result["error"] + return jsonify({"ok": False, "error": result["error"]}), 500 + + machine = resolve_machine_settings(ACODE_PY_PATH) + with gen_lock: + gen_state.gen_id = result["gen_id"] + gen_state.png_path = "" + gen_state.acode_path = result["acode_path"] + gen_state.preview_path = result["preview_path"] + gen_state.acode_text = result["acode_text"] + gen_state.meta = result["meta"] + gen_state.error = "" + gen_state.viz_settings = { + "wheelbase_mm": machine["wheelbase_mm"], + "steps_per_mm": machine["steps_per_mm"], + "turn_gain": machine["turn_gain"], + "viz_arc_step_mm": params["viz_arc_step_mm"], + "viz_arc_step_deg": params["viz_arc_step_deg"], + "viz_home_resets_pose": params["viz_home_resets_pose"], + "viz_equal": params["viz_equal"], + "viz_invert_y": params["viz_invert_y"], + } + + push_event("gen", {"msg": "dxf_generated", "gen_id": result["gen_id"], "lines": result["meta"]["acode_lines"]}) + return jsonify({ + "ok": True, + "gen_id": result["gen_id"], + "acode_lines": result["meta"]["acode_lines"], + }) + # ---------------------------- # Preview + download + push # ---------------------------- @@ -1081,6 +1394,9 @@ def download_acode(gen_id: str): def api_push_to_sender(): host = request.form.get("host", "").strip() port_s = request.form.get("port", "").strip() + transport = (request.form.get("transport", "") or "").lower() + serial_port = request.form.get("serial_port", "").strip() + serial_baud_s = request.form.get("serial_baud", "").strip() with gen_lock: if not gen_state.acode_text: @@ -1098,6 +1414,15 @@ def api_push_to_sender(): state.port = int(port_s) except ValueError: pass + if transport: + state.transport = transport + if serial_port: + state.serial_port = serial_port + if serial_baud_s: + try: + state.serial_baud = int(serial_baud_s) + except ValueError: + pass state.lines = lines state.idx = 0 state.error = ""