''' GCode pour piloter la L2544 Laser Engraving Machine GRBLController: Commande uniquement les mouvements (X, Y) Le mode absolue est retenu Created on 25 mars 2026 @author: denis@miraceti.net ''' import logging import serial import time import threading logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) class GRBLController: ''' Contrôleur pour machine de gravure laser L2544 (GRBL 1.1f) Fonctions de base pour la calibration : déplacement manuel et gestion de la position. ''' X_MAX = 350 Y_MAX = 250 X_MIN = 0 Y_MIN = 0 def __init__(self, port='/dev/ttyUSB0', baudrate=115200, timeout=1, send_callback=None, x_max=None, y_max=None): logger.info(f"GRBLController::init begin {port} device port") #self.lock = threading.Lock() self.port = port self.baudrate = baudrate self.timeout = timeout if x_max is not None: self.X_MAX = x_max if y_max is not None: self.Y_MAX = y_max self._state = send_callback if self._state is None: self._state = self._send_msg self.x, self.y = 0, 0 #self.start_connection() def wait_for(self, delay=1.0): threading.Event().wait(delay*1.0) def _send_msg(self, **msg): print(msg) def clear_buffer(self): while self.ser.in_waiting >0: msg = self.ser.readline().decode().strip() logger.info(f"Buffer: {msg}") self._state(state='serial', msg=msg) def start_connection(self): n = 0 while True: try: self.ser = serial.Serial(self.port, self.baudrate, timeout=self.timeout, exclusive=True) # CRITIQUE : self.ser.setDTR(False) self.ser.setRTS(False) self.clear_buffer() self._wake_up() self._init_machine() logger.info(f"GRBLController::start_connexion started {self.port}") break except Exception as e: logger.error(f"GRBLController::start_connexion (essai {n}): {e}") self._state(state='error', msg=f"{e}") n += 1 self.wait_for(1.0) def _init_machine(self): self.send("G21") # Unités en mm self.send("G90") # Mode absolu def _clamp(self, x, y): self.clear_buffer() x = max(self.X_MIN, min(self.X_MAX, x)) y = max(self.Y_MIN, min(self.Y_MAX, y)) return x, y def _wake_up(self): #with self.lock: self.ser.write(b"\r\n\r\n") self.wait_for(1) self.clear_buffer() def send(self, cmd, wait_ok=True, timeout=5): try: return self._send(cmd, wait_ok, timeout) except Exception as e: #print("Send error:", e) self._state(state='error', msg=f"Error send {cmd} command: {e}") self.close() self.start_connection() def recover(self): #print("Récupération de GRBL...") self._state(state='recover', msg=f"Erreur, récupération de GRBL...") self.wait_for(1) self._wake_up() def _send(self, cmd, wait_ok=True, timeout=5): #print(f">>> {cmd}") self._state(state='send', msg=f">>> {cmd}") self.ser.write((cmd + "\n").encode()) if not wait_ok: return None start = time.time() while True: if time.time() - start > timeout: raise TimeoutError(f"Timeout sur la commande: {cmd}") raw = self.ser.readline() if not raw: continue line = raw.decode(errors="ignore").strip() if not line: continue if line.startswith("<"): continue # Ignorer les messages de status asynchrones if "ok" in line.lower(): return line if "error" in line.lower(): raise Exception(f"Erreur GRBL: {line}") def get_status(self): #with self.lock: self.ser.write(b"?\n") while True: line = self.ser.readline() if not line: continue line = line.decode().strip() if line.startswith("<"): return line def reset_grbl(self): self.send("$X") # Réinitialise les alarmes self.wait_idle() self.send("$H") # Homing self.wait_idle() def _mpos(self, status): if "MPos" in status: mpos = status.split("MPos:")[1].split("|")[0] x, y, *_ = mpos.split(",") self._state(state='Mpos', msg=f"pos >>> ({x}, {y})") return float(x), float(y) return None, None def get_mpos(self): return self._mpos(self.get_status()) def wait_idle(self, timeout=20): start = time.time() while True: if time.time() - start > timeout: raise TimeoutError("Délai d'attente pour Idle dépassé") status = self.get_status() self.x, self.y = self._mpos(status) self._state(xy=True, x=self.x, y=self.y) if status and "Idle" in status: break self.wait_for(0.1) def send_command(self, cmd): self.send(cmd) self.wait_idle() def move_to(self, x, y, feed=1000): x, y = self._clamp(x, y) #cmd = f"G0 X{x:.2f} Y{y:.2f} F{feed}" # feed is not updated in G0 mode cmd = f"G53 G1 X{x:.2f} Y{y:.2f} F{feed}" self.send_command(cmd) def move_relative(self, dx=0, dy=0, feed=1000): x, y = self.get_mpos() # Position actuelle self.move_to(x + dx, y + dy, feed=feed) def move_relative__(self, dx=0, dy=0, feed=1000): self.send("G91") # Mode relatif cmd = f"G0 X{dx} Y{dy} F{feed}" self.send(cmd) self.send("G90") # Retour en mode absolu self.wait_idle() def go_origin(self, feed=1000): self.move_to(0, 0, feed=feed) self.wait_for(2.0) def set_position(self, x, y): x, y = self._clamp(x, y) cmd = f"G92 X{x:.2f} Y{y:.2f}" self.send(cmd) self.wait_for(2.0) def move_up(self, step=10, feed=1000): self.move_relative(dy=step, feed=feed) def move_down(self, step=10, feed=1000): self.move_relative(dy=-step, feed=feed) def move_left(self, step=10, feed=1000): self.move_relative(dx=-step, feed=feed) def move_right(self, step=10, feed=1000): self.move_relative(dx=step, feed=feed) def close(self): self.ser.close()