feat(drkota-motor): add transparent multi-node CLI for V-plotter stepper calibration and jog

This commit is contained in:
Krystof Pesek
2026-11-09 15:50:25 +01:00
parent 0dd456d33a
commit f98514f6d8
Executable
+337
View File
@@ -0,0 +1,337 @@
#!/usr/bin/env python3
"""
drkota-motor: CLI nástroj pro kalibraci a rozhýbání krokových motorů V-plotteru (Drkota).
Komunikuje s Arduino CNC Shieldem přes /dev/ttyUSB0 (lokálně nebo transparentně přes SSH na t520).
"""
import sys
import os
import time
import json
import subprocess
import argparse
BAUD_RATE = 115200
DEFAULT_PORT = "/dev/ttyUSB0"
STATE_FILE = "/tmp/drkota_pos.json"
def get_target_host():
"""Vrátí 'local', pokud ttyUSB0 existuje lokálně, jinak 't520'."""
if os.path.exists(DEFAULT_PORT):
return "local"
# Zkontroluj, zda ttyUSB0 existuje na t520
try:
res = subprocess.run(["ssh", "-q", "t520", "test -e " + DEFAULT_PORT], timeout=3)
if res.returncode == 0:
return "t520"
except Exception:
pass
return "local"
def run_remote_if_needed():
target = get_target_host()
if target == "t520" and os.uname().nodename != "t520":
# Spusť tento skript na t520 se stejnými argumenty
cmd = ["ssh", "-t", "t520", "python3 - " + " ".join([f'"{a}"' for a in sys.argv[1:]])]
# Pošli obsah tohoto skriptu na stdin
with open(__file__, "rb") as f:
code = f.read()
p = subprocess.Popen(cmd, stdin=subprocess.PIPE)
p.communicate(input=code)
sys.exit(p.returncode)
run_remote_if_needed()
# --- Zde již běžíme přímo na stroji s připojeným hardwarem ---
import serial
def load_pos():
if os.path.exists(STATE_FILE):
try:
with open(STATE_FILE, "r") as f:
d = json.load(f)
return d.get("x", 0), d.get("y", 0)
except Exception:
pass
return 0, 0
def save_pos(x, y):
try:
with open(STATE_FILE, "w") as f:
json.dump({"x": x, "y": y}, f)
except Exception:
pass
def open_serial(port=DEFAULT_PORT, timeout=5):
if not os.path.exists(port):
print(f"[CHYBA] Sériový port {port} neexistuje!")
sys.exit(1)
s = serial.Serial(port, BAUD_RATE, timeout=timeout)
time.sleep(1.8) # čekání na bootloader / DTR reset
s.reset_input_buffer()
return s
def send_cmd(s, cmd):
clean = cmd.strip()
s.write((clean + "\n").encode("utf-8"))
resp = s.readline().decode("utf-8", errors="ignore").strip()
return resp
def cmd_status(args):
print(f"=== Drkota Motor Status ===")
print(f"Host: {os.uname().nodename}")
print(f"Port: {args.port} @ {BAUD_RATE} baud")
cur_x, cur_y = load_pos()
print(f"Uložená pozice: X={cur_x}, Y={cur_y} kroků")
try:
s = open_serial(args.port, timeout=2)
resp = send_cmd(s, "P0")
s.close()
if resp == "ok":
print("Odezva Arduina: [OK] (Spojení je aktivní a funkční)")
else:
print(f"Odezva Arduina: [{resp}] (Neočekávaná odezva)")
except Exception as e:
print(f"Chyba spojení: {e}")
def cmd_zero(args):
"""Resetuje Arduino a nastaví software počátek na 0,0."""
print("Resetuji software pozici a Arduino...")
try:
s = serial.Serial(args.port, BAUD_RATE, timeout=1)
s.setDTR(False)
time.sleep(0.1)
s.setDTR(True)
time.sleep(1.5)
s.close()
save_pos(0, 0)
print("Pozice resetována na X=0, Y=0. Motory jsou v nulovém bodě.")
except Exception as e:
print(f"Chyba při resetu: {e}")
def cmd_jog(args):
"""Posune motory o relativní počet kroků."""
dx = args.dx
dy = args.dy
cur_x, cur_y = load_pos()
target_x = cur_x + dx
target_y = cur_y + dy
print(f"Posun o rel: dX={dx:+d}, dY={dy:+d} -> Cíl: X={target_x}, Y={target_y}")
s = open_serial(args.port, timeout=10)
cmd = f"X{target_x}Y{target_y}"
print(f"Odesílám: {cmd}")
resp = send_cmd(s, cmd)
s.close()
if resp == "ok":
save_pos(target_x, target_y)
print("Pohyb dokončen: [OK]")
else:
print(f"Chyba odezvy: {resp}")
def cmd_spin(args):
"""Roztočí zvolený motor o daný počet kroků."""
motor = args.motor.lower()
steps = args.steps
dx, dy = 0, 0
if motor in ("x", "left", "l", "1"):
dx = steps
elif motor in ("y", "right", "r", "2"):
dy = steps
elif motor in ("both", "xy", "all"):
dx = steps
dy = steps
else:
print(f"Neznámý motor: {motor}. Použij 'x', 'y' nebo 'both'.")
return
cur_x, cur_y = load_pos()
target_x = cur_x + dx
target_y = cur_y + dy
print(f"Spin motor {motor.upper()} o {steps:+d} kroků...")
s = open_serial(args.port, timeout=15)
resp = send_cmd(s, f"X{target_x}Y{target_y}")
s.close()
if resp == "ok":
save_pos(target_x, target_y)
print(f"Spin dokončen: [OK]")
else:
print(f"Chyba odezvy: {resp}")
def cmd_test(args):
"""Otestuje oba motory pohybem tam a zpět (800 kroků)."""
steps = args.steps
print(f"=== Spouštím diagnostický test motorů ({steps} kroků) ===")
print("1. Krok: Motor X (levý) dopředu...")
s = open_serial(args.port, timeout=10)
cur_x, cur_y = load_pos()
# 1. X forward
send_cmd(s, f"X{cur_x + steps}Y{cur_y}")
time.sleep(0.3)
# 2. X backward
print("2. Krok: Motor X (levý) zpět...")
send_cmd(s, f"X{cur_x}Y{cur_y}")
time.sleep(0.3)
# 3. Y forward
print("3. Krok: Motor Y (pravý) dopředu...")
send_cmd(s, f"X{cur_x}Y{cur_y + steps}")
time.sleep(0.3)
# 4. Y backward
print("4. Krok: Motor Y (pravý) zpět...")
send_cmd(s, f"X{cur_x}Y{cur_y}")
time.sleep(0.3)
# 5. Both forward
print("5. Krok: Oba motory současně dopředu a zpět...")
send_cmd(s, f"X{cur_x + steps}Y{cur_y + steps}")
time.sleep(0.3)
send_cmd(s, f"X{cur_x}Y{cur_y}")
s.close()
print("=== Diagnostický test motorů úspěšně dokončen! ===")
def cmd_calib(args):
"""Vypočítá kroky na milimetr na základě fyzického měření."""
steps = args.steps
measured_mm = args.measured_mm
if measured_mm <= 0:
print("[CHYBA] Naměřené milimetry musí být větší než 0!")
return
steps_per_mm = steps / measured_mm
print(f"\n=== Výpočet kalibrace StepsPerMm ===")
print(f"Zadaný posun: {steps} kroků")
print(f"Skutečně naměřeno posuvným měřítkem / metrem: {measured_mm:.2f} mm")
print(f"-> Skutečná hodnota: StepsPerMm = {steps_per_mm:.4f} kroků/mm\n")
# Porovnání se standardními řemenicemi GT2 (20 zubů = 40 mm/otáčka)
rev_mm = 40.0
steps_per_rev = steps_per_mm * rev_mm
print(f"Při GT2 řemenici (40 mm/ot) to odpovídá cca {steps_per_rev:.0f} krokům na otáčku:")
if abs(steps_per_rev - 200) < 30:
print(" -> Pravděpodobně ZÁKLADNÍ KROK (Full-step, bez mikrokrokování, cca 5 kroků/mm)")
elif abs(steps_per_rev - 400) < 50:
print(" -> Pravděpodobně 1/2 mikrokrok (cca 10 kroků/mm)")
elif abs(steps_per_rev - 800) < 100:
print(" -> Pravděpodobně 1/4 mikrokrok (cca 20 kroků/mm)")
elif abs(steps_per_rev - 1600) < 200:
print(" -> Pravděpodobně 1/8 mikrokrok (cca 40 kroků/mm)")
elif abs(steps_per_rev - 3200) < 300:
print(" -> Pravděpodobně 1/16 mikrokrok (cca 80 kroků/mm)")
else:
print(f" -> Vlastní převod / cívka.")
def cmd_interactive(args):
"""Interaktivní ovládání z klávesnice."""
import tty, termios
print("=== Interaktivní režim motorů ===")
print("Ovládání:")
print(" [A] / [D] : Motor X (levý) - / +")
print(" [W] / [S] : Motor Y (pravý) + / -")
print(" [J] / [L] : Oba motory - / +")
print(" [1] 10 kroků | [2] 50 kroků | [3] 200 kroků (1 ot) | [4] 800 kroků")
print(" [Z] Vynulovat pozici")
print(" [Q] Konec")
step_size = 200
s = open_serial(args.port, timeout=5)
cur_x, cur_y = load_pos()
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(fd)
while True:
ch = sys.stdin.read(1)
if ch.lower() == 'q' or ch == '\x03': # Ctrl+C or Q
break
elif ch == '1':
step_size = 10
elif ch == '2':
step_size = 50
elif ch == '3':
step_size = 200
elif ch == '4':
step_size = 800
elif ch == 'z' or ch == 'Z':
cur_x, cur_y = 0, 0
save_pos(0, 0)
else:
dx, dy = 0, 0
if ch in ('a', 'A'):
dx = -step_size
elif ch in ('d', 'D'):
dx = step_size
elif ch in ('w', 'W'):
dy = step_size
elif ch in ('s', 'S'):
dy = -step_size
elif ch in ('j', 'J'):
dx = -step_size
dy = -step_size
elif ch in ('l', 'L'):
dx = step_size
dy = step_size
if dx != 0 or dy != 0:
cur_x += dx
cur_y += dy
send_cmd(s, f"X{cur_x}Y{cur_y}")
save_pos(cur_x, cur_y)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
s.close()
print(f"\nUkončeno. Aktuální pozice: X={cur_x}, Y={cur_y}")
def main():
parser = argparse.ArgumentParser(description="Drkota Motor CLI Tool")
parser.add_argument("--port", default=DEFAULT_PORT, help="Sériový port (výchozí /dev/ttyUSB0)")
sub = parser.add_subparsers(dest="subcommand")
sub.add_parser("status", help="Ověřit stav a spojení")
sub.add_parser("zero", help="Vynulovat pozici")
p_jog = sub.add_parser("jog", help="Relativní posun dX dY kroků")
p_jog.add_argument("dx", type=int, help="Kroky X")
p_jog.add_argument("dy", type=int, help="Kroky Y")
p_spin = sub.add_parser("spin", help="Roztočit motor o N kroků")
p_spin.add_argument("motor", choices=["x", "y", "both"], help="Který motor")
p_spin.add_argument("steps", type=int, help="Počet kroků")
p_test = sub.add_parser("test", help="Diagnostický test motorů")
p_test.add_argument("--steps", type=int, default=800, help="Počet kroků pro test")
p_calib = sub.add_parser("calib", help="Výpočet StepsPerMm z měření")
p_calib.add_argument("steps", type=float, help="Zadané kroky")
p_calib.add_argument("measured_mm", type=float, help="Naměřená vzdálenost v mm")
sub.add_parser("interactive", help="Interaktivní řízení z klávesnice")
args = parser.parse_args()
if not args.subcommand:
parser.print_help()
return
cmds = {
"status": cmd_status,
"zero": cmd_zero,
"jog": cmd_jog,
"spin": cmd_spin,
"test": cmd_test,
"calib": cmd_calib,
"interactive": cmd_interactive,
}
cmds[args.subcommand](args)
if __name__ == "__main__":
main()