Pārlūkot izejas kodu

add websocket supports

Tuan Nguyen 11 mēneši atpakaļ
vecāks
revīzija
11026e9840

+ 1 - 1
README.md

@@ -93,7 +93,7 @@ Example:
 
 The project exposes RESTful APIs for various actions. Here are some key endpoints:
  • List Serial Ports: /list_serial_ports (GET)
- • Connect to Serial: /connect_serial (POST)
+ • Connect to Serial: /connect (POST)
  • Upload Pattern: /upload_theta_rho (POST)
  • Run Pattern: /run_theta_rho (POST)
  • Stop Execution: /stop_execution (POST)

+ 21 - 20
dune_weaver_flask/app.py

@@ -3,10 +3,10 @@ import atexit
 import os
 import logging
 from datetime import datetime
-from .modules.serial import serial_manager
+from .modules.connection import connection_manager
 from dune_weaver_flask.modules.core import pattern_manager
 from dune_weaver_flask.modules.core import playlist_manager
-from .modules.firmware import firmware_manager
+from .modules.update import update_manager
 from dune_weaver_flask.modules.core.state import state
 from dune_weaver_flask.modules import mqtt
 
@@ -34,27 +34,28 @@ def index():
 @app.route('/list_serial_ports', methods=['GET'])
 def list_ports():
     logger.debug("Listing available serial ports")
-    return jsonify(serial_manager.list_serial_ports())
+    return jsonify(connection_manager.list_serial_ports())
 
-@app.route('/connect_serial', methods=['POST'])
-def connect_serial():
+@app.route('/connect', methods=['POST'])
+def connect():
     port = request.json.get('port')
     if not port:
-        logger.warning('Serial connection attempt without port specified')
-        return jsonify({'error': 'No port provided'}), 400
+        state.conn = WebSocketConnection('ws://fluidnc.local:81')
+        logger.info(f'Successfully connected to websocket ws://fluidnc.local:81')
+        return jsonify({'success': True})
 
     try:
-        serial_manager.connect_to_serial(port)
+        state.conn = SerialConnection(port)
         logger.info(f'Successfully connected to serial port {port}')
         return jsonify({'success': True})
     except Exception as e:
         logger.error(f'Failed to connect to serial port {port}: {str(e)}')
         return jsonify({'error': str(e)}), 500
 
-@app.route('/disconnect_serial', methods=['POST'])
+@app.route('/disconnect', methods=['POST'])
 def disconnect():
     try:
-        serial_manager.disconnect_serial()
+        connection_manager.disconnect()
         logger.info('Successfully disconnected from serial port')
         return jsonify({'success': True})
     except Exception as e:
@@ -70,7 +71,7 @@ def restart():
 
     try:
         logger.info(f"Restarting serial connection on port {port}")
-        serial_manager.restart_serial(port)
+        connection_manager.restart_serial(port)
         return jsonify({'success': True})
     except Exception as e:
         logger.error(f"Failed to restart serial on port {port}: {str(e)}")
@@ -129,7 +130,7 @@ def stop_execution():
 @app.route('/send_home', methods=['POST'])
 def send_home():
     try:
-        serial_manager.home()
+        connection_manager.home()
         return jsonify({'success': True})
     except Exception as e:
         logger.error(f"Failed to send home command: {str(e)}")
@@ -168,7 +169,7 @@ def delete_theta_rho_file():
 def move_to_center():
     global current_theta
     try:
-        if not serial_manager.is_connected():
+        if not state.conn.is_connected():
             logger.warning("Attempted to move to center without serial connection")
             return jsonify({"success": False, "error": "Serial connection not established"}), 400
 
@@ -184,7 +185,7 @@ def move_to_center():
 def move_to_perimeter():
     global current_theta
     try:
-        if not serial_manager.is_connected():
+        if not state.conn.is_connected():
             logger.warning("Attempted to move to perimeter without serial connection")
             return jsonify({"success": False, "error": "Serial connection not established"}), 400
         pattern_manager.reset_theta()
@@ -215,7 +216,7 @@ def preview_thr():
 
 @app.route('/send_coordinate', methods=['POST'])
 def send_coordinate():
-    if not serial_manager.is_connected():
+    if not state.conn.is_connected():
         logger.warning("Attempted to send coordinate without serial connection")
         return jsonify({"success": False, "error": "Serial connection not established"}), 400
 
@@ -241,8 +242,8 @@ def download_file(filename):
 
 @app.route('/serial_status', methods=['GET'])
 def serial_status():
-    connected = serial_manager.is_connected()
-    port = serial_manager.get_port()
+    connected = state.conn.is_connected()
+    port = state.port
     logger.debug(f"Serial status check - connected: {connected}, port: {port}")
     return jsonify({
         'connected': connected,
@@ -397,13 +398,13 @@ def set_speed():
 
 @app.route('/check_software_update', methods=['GET'])
 def check_updates():
-    update_info = firmware_manager.check_git_updates()
+    update_info = update_manager.check_git_updates()
     return jsonify(update_info)
 
 @app.route('/update_software', methods=['POST'])
 def update_software():
     logger.info("Starting software update process")
-    success, error_message, error_log = firmware_manager.update_software()
+    success, error_message, error_log = update_manager.update_software()
     
     if success:
         logger.info("Software update completed successfully")
@@ -430,7 +431,7 @@ def entrypoint():
     atexit.register(on_exit)
     # Auto-connect to serial
     try:
-        serial_manager.connect_to_serial()
+        connection_manager.connect_device()
     except Exception as e:
         logger.warning(f"Failed to auto-connect to serial port: {str(e)}")
         

+ 0 - 0
dune_weaver_flask/modules/firmware/__init__.py → dune_weaver_flask/modules/connection/__init__.py


+ 308 - 0
dune_weaver_flask/modules/connection/connection_manager.py

@@ -0,0 +1,308 @@
+import threading
+import time
+import logging
+import serial
+import serial.tools.list_ports
+import websocket
+
+from dune_weaver_flask.modules.core.state import state
+
+logger = logging.getLogger(__name__)
+
+IGNORE_PORTS = ['/dev/cu.debug-console', '/dev/cu.Bluetooth-Incoming-Port']
+
+###############################################################################
+# Connection Abstraction
+###############################################################################
+
+class BaseConnection:
+    """Abstract base class for a connection."""
+    def send(self, data: str) -> None:
+        raise NotImplementedError
+
+    def flush(self) -> None:
+        raise NotImplementedError
+
+    def readline(self) -> str:
+        raise NotImplementedError
+
+    def in_waiting(self) -> int:
+        raise NotImplementedError
+
+    def is_connected(self) -> bool:
+        raise NotImplementedError
+
+    def close(self) -> None:
+        raise NotImplementedError
+
+###############################################################################
+# Serial Connection Implementation
+###############################################################################
+
+class SerialConnection(BaseConnection):
+    def __init__(self, port: str, baudrate: int = 115200, timeout: int = 2):
+        self.port = port
+        self.baudrate = baudrate
+        self.timeout = timeout
+        self.lock = threading.RLock()
+        self.ser = serial.Serial(port, baudrate, timeout=timeout)
+        state.port = port
+        logger.info(f'Connected to Serial port {port}')
+
+    def send(self, data: str) -> None:
+        with self.lock:
+            self.ser.write(data.encode())
+            self.ser.flush()
+
+    def flush(self) -> None:
+        with self.lock:
+            self.ser.flush()
+
+    def readline(self) -> str:
+        with self.lock:
+            return self.ser.readline().decode().strip()
+
+    def in_waiting(self) -> int:
+        with self.lock:
+            return self.ser.in_waiting
+
+    def is_connected(self) -> bool:
+        return self.ser is not None and self.ser.is_open
+
+    def close(self) -> None:
+        with self.lock:
+            if self.ser.is_open:
+                self.ser.close()
+
+###############################################################################
+# WebSocket Connection Implementation
+###############################################################################
+
+class WebSocketConnection(BaseConnection):
+    def __init__(self, url: str, timeout: int = 2):
+        self.url = url
+        self.timeout = timeout
+        self.lock = threading.RLock()
+        self.ws = None
+        self.connect()
+
+    def connect(self):
+        self.ws = websocket.create_connection(self.url, timeout=self.timeout)
+        state.port = self.url
+        logger.info(f'Connected to Websocket {self.url}')
+        
+    def send(self, data: str) -> None:
+        with self.lock:
+            self.ws.send(data)
+
+    def flush(self) -> None:
+        # WebSocket sends immediately; nothing to flush.
+        pass
+
+    def readline(self) -> str:
+        with self.lock:
+            data = self.ws.recv()
+            # Decode bytes to string if necessary
+            if isinstance(data, bytes):
+                data = data.decode('utf-8')
+            return data.strip()
+
+    def in_waiting(self) -> int:
+        return 0  # Not applicable for WebSocket
+
+    def is_connected(self) -> bool:
+        return self.ws is not None
+
+    def close(self) -> None:
+        with self.lock:
+            if self.ws:
+                self.ws.close()
+                
+def list_serial_ports():
+    """Return a list of available serial ports."""
+    ports = serial.tools.list_ports.comports()
+    available_ports = [port.device for port in ports if port.device not in IGNORE_PORTS]
+    logger.debug(f"Available serial ports: {available_ports}")
+    return available_ports
+
+def connect_device():
+    ports = list_serial_ports()
+    if not ports:
+        state.conn = WebSocketConnection('ws://fluidnc.local:81')
+    else:
+        state.conn = SerialConnection(ports[0])
+        
+
+def get_status_response() -> str:
+    """
+    Send a status query ('?') and return the response if available.
+    """
+    while True:
+        try:
+            state.conn.send('?')
+            response = state.conn.readline()
+            if "MPos" in response:
+                logger.debug(f"Status response: {response}")
+                return response
+        except Exception as e:
+            logger.error(f"Error getting status response: {e}")
+        time.sleep(1)
+        
+def parse_machine_position(response: str):
+    """
+    Parse the work position (MPos) from a status response.
+    Expected format: "<...|MPos:-994.869,-321.861,0.000|...>"
+    Returns a tuple (work_x, work_y) if found, else None.
+    """
+    if "MPos:" not in response:
+        return None
+    try:
+        wpos_section = next((part for part in response.split("|") if part.startswith("MPos:")), None)
+        if wpos_section:
+            wpos_str = wpos_section.split(":", 1)[1]
+            wpos_values = wpos_str.split(",")
+            work_x = float(wpos_values[0])
+            work_y = float(wpos_values[1])
+            return work_x, work_y
+    except Exception as e:
+        logger.error(f"Error parsing work position: {e}")
+    return None
+
+
+def send_grbl_coordinates(x, y, speed=600, timeout=5, home=False):
+    """
+    Send a G-code command to FluidNC and wait for an 'ok' response.
+    """
+    logger.debug(f"Sending G-code: X{x} Y{y} at F{speed}")
+    while True:
+        try:
+            gcode = f"$J=G91 Y{y} F{speed}" if home else f"G1 X{x} Y{y} F{speed}"
+            state.conn.send(gcode + "\n")
+            logger.debug(f"Sent command: {gcode}")
+            start_time = time.time()
+            while time.time() - start_time < timeout:
+                response = state.conn.readline()
+                logger.debug(f"Response: {response}")
+                if response.lower() == "ok":
+                    logger.debug("Command execution confirmed.")
+                    return
+            logger.warning(f"No 'ok' received for X{x} Y{y}. Retrying in 1s...")
+        except Exception as e:
+            logger.error(f"Error sending command: {e}")
+        time.sleep(1)
+        
+
+def get_machine_steps(timeout=10):
+    """
+    Send "$$" to retrieve machine settings and update state.
+    Returns True if the expected configuration is received, or False if it times out.
+    """
+    if not state.conn.is_connected():
+        logger.error("Connection is not established.")
+        return False
+
+    # Send the command once
+    state.conn.send("$$\n")
+    start_time = time.time()
+    x_steps_per_mm = y_steps_per_mm = gear_ratio = None
+
+    while time.time() - start_time < timeout:
+        try:
+            # Attempt to read a line from the connection
+            response = state.conn.readline()
+            logger.debug(response)
+            for line in response.splitlines():
+                logger.debug(f"Config response: {line}")
+                if line.startswith("$100="):
+                    x_steps_per_mm = float(line.split("=")[1])
+                    state.x_steps_per_mm = x_steps_per_mm
+                elif line.startswith("$101="):
+                    y_steps_per_mm = float(line.split("=")[1])
+                    state.y_steps_per_mm = y_steps_per_mm
+                elif line.startswith("$131="):
+                    gear_ratio = float(line.split("=")[1])
+                    state.gear_ratio = gear_ratio
+            
+            # If all parameters are received, exit early
+            if x_steps_per_mm is not None and y_steps_per_mm is not None and gear_ratio is not None:
+                return True
+
+        except Exception as e:
+            logger.error(f"Error getting machine steps: {e}")
+            return False
+
+        # Use a smaller sleep to poll more frequently
+        time.sleep(0.1)
+
+    logger.error("Timeout reached waiting for machine steps")
+    return False
+
+
+def home(retry=0):
+    """
+    Perform homing by checking device configuration and sending the appropriate commands.
+    """
+    logger.info(f"Homing with speed {state.speed}")
+    try:
+        state.conn.send("$config\n")
+        response = state.conn.readline().strip().lower()
+        logger.debug(f"Config response: {response}")
+    except Exception as e:
+        logger.error(f"Error during homing config: {e}")
+        response = ""
+    if "sensorless" in response:
+        logger.info("Using sensorless homing")
+        state.conn.send("$H\n")
+        state.conn.send("G1 Y0 F100\n")
+    elif "filename" in response or "error:3" in response:
+        logger.info("Using crash homing")
+        send_grbl_coordinates(0, -110/5, 300, home=True)
+    else:
+        if retry < 3:
+            time.sleep(1)
+            home(retry + 1)
+            return
+        else:
+            raise Exception("Couldn't get a valid response for homing after 3 retries")
+    state.current_theta = state.current_rho = 0
+    update_machine_position()
+
+def check_idle():
+    """
+    Continuously check if the device is idle.
+    """
+    logger.info("Checking idle")
+    while True:
+        response = get_status_response()
+        if response and "Idle" in response:
+            logger.info("Device is idle")
+            update_machine_position()
+            return True
+        time.sleep(1)
+
+
+def get_machine_position(timeout=5):
+    """
+    Query the device for its position.
+    """
+    start_time = time.time()
+    while time.time() - start_time < timeout:
+        try:
+            state.conn.send('?')
+            response = state.conn.readline()
+            logger.debug(f"Raw status response: {response}")
+            if "MPos" in response:
+                pos = parse_machine_position(response)
+                if pos:
+                    machine_x, machine_y = pos
+                    logger.debug(f"Machine position: X={machine_x}, Y={machine_y}")
+                    return machine_x, machine_y
+        except Exception as e:
+            logger.error(f"Error getting machine position: {e}")
+        time.sleep(0.1)
+    logger.warning("Timeout reached waiting for machine position")
+    return None, None
+
+def update_machine_position():
+    state.machine_x, state.machine_y = get_machine_position()
+    state.save()

+ 32 - 33
dune_weaver_flask/modules/core/pattern_manager.py

@@ -5,7 +5,7 @@ import random
 import logging
 from datetime import datetime
 from tqdm import tqdm
-from dune_weaver_flask.modules.serial import serial_manager
+from dune_weaver_flask.modules.connection import connection_manager
 from dune_weaver_flask.modules.core.state import state
 from math import pi
 
@@ -87,7 +87,7 @@ def schedule_checker(schedule_hours):
     if start_time <= now < end_time:
         if state.pause_requested:
             logger.info("Starting execution: Within schedule")
-            serial_manager.update_machine_position()
+            connection_manager.update_machine_position()
         state.pause_requested = False
         with state.pause_condition:
             state.pause_condition.notify_all()
@@ -95,7 +95,7 @@ def schedule_checker(schedule_hours):
         if not state.pause_requested:
             logger.info("Pausing execution: Outside schedule")
         state.pause_requested = True
-        serial_manager.update_machine_position()
+        connection_manager.update_machine_position()
         threading.Thread(target=wait_for_start_time, args=(schedule_hours,), daemon=True).start()
 
 def wait_for_start_time(schedule_hours):
@@ -158,7 +158,7 @@ def move_polar(theta, rho):
     
     # dynamic_speed = compute_dynamic_speed(rho, max_speed=state.speed)
     
-    serial_manager.send_grbl_coordinates(round(new_x_abs, 3), round(new_y_abs,3), state.speed)
+    connection_manager.send_grbl_coordinates(round(new_x_abs, 3), round(new_y_abs,3), state.speed)
     state.current_theta = theta
     state.current_rho = rho
     state.machine_x = new_x_abs
@@ -180,7 +180,7 @@ def resume_execution():
 def reset_theta():
     logger.info('Resetting Theta')
     state.current_theta = 0
-    serial_manager.update_machine_position()
+    connection_manager.update_machine_position()
 
 def set_speed(new_speed):
     state.speed = new_speed
@@ -204,35 +204,34 @@ def run_theta_rho_file(file_path, schedule_hours=None):
     # stop actions without resetting the playlist
     stop_actions(clear_playlist=False)
 
-    with serial_manager.serial_lock:
-        state.current_playing_file = file_path
-        state.execution_progress = (0, 0, None)
-        state.stop_requested = False
-        logger.info(f"Starting pattern execution: {file_path}")
-        logger.info(f"t: {state.current_theta}, r: {state.current_rho}")
-        reset_theta()
-        with tqdm(total=total_coordinates, unit="coords", desc=f"Executing Pattern {file_path}", dynamic_ncols=True, disable=None) as pbar:
-            for i, coordinate in enumerate(coordinates):
-                theta, rho = coordinate
-                if state.stop_requested:
-                    logger.info("Execution stopped by user after completing the current batch")
-                    break
+    state.current_playing_file = file_path
+    state.execution_progress = (0, 0, None)
+    state.stop_requested = False
+    logger.info(f"Starting pattern execution: {file_path}")
+    logger.info(f"t: {state.current_theta}, r: {state.current_rho}")
+    reset_theta()
+    with tqdm(total=total_coordinates, unit="coords", desc=f"Executing Pattern {file_path}", dynamic_ncols=True, disable=None) as pbar:
+        for i, coordinate in enumerate(coordinates):
+            theta, rho = coordinate
+            if state.stop_requested:
+                logger.info("Execution stopped by user after completing the current batch")
+                break
 
-                with state.pause_condition:
-                    while state.pause_requested:
-                        logger.info("Execution paused...")
-                        state.pause_condition.wait()
+            with state.pause_condition:
+                while state.pause_requested:
+                    logger.info("Execution paused...")
+                    state.pause_condition.wait()
 
-                schedule_checker(schedule_hours)
-                move_polar(theta, rho)
-                
-                if i != 0:
-                    pbar.update(1)
-                    estimated_remaining_time = (total_coordinates - i) / pbar.format_dict['rate'] if pbar.format_dict['rate'] and total_coordinates else 0
-                    elapsed_time = pbar.format_dict['elapsed']
-                    state.execution_progress = (i, total_coordinates, estimated_remaining_time, elapsed_time)
+            schedule_checker(schedule_hours)
+            move_polar(theta, rho)
+            
+            if i != 0:
+                pbar.update(1)
+                estimated_remaining_time = (total_coordinates - i) / pbar.format_dict['rate'] if pbar.format_dict['rate'] and total_coordinates else 0
+                elapsed_time = pbar.format_dict['elapsed']
+                state.execution_progress = (i, total_coordinates, estimated_remaining_time, elapsed_time)
 
-        serial_manager.check_idle()
+    connection_manager.check_idle()
 
     state.current_playing_file = None
     state.execution_progress = None
@@ -318,7 +317,7 @@ def stop_actions(clear_playlist = True):
             state.current_playlist_index = None
             state.playlist_mode = None
         state.pause_condition.notify_all()
-        serial_manager.update_machine_position()
+        connection_manager.update_machine_position()
 
 def get_status():
     """Get the current execution status."""
@@ -329,7 +328,7 @@ def get_status():
         state.is_clearing = False
 
     return {
-        "ser_port": serial_manager.get_port(),
+        "ser_port": state.port,
         "stop_requested": state.stop_requested,
         "pause_requested": state.pause_requested,
         "current_playing_file": state.current_playing_file,

+ 5 - 1
dune_weaver_flask/modules/core/state.py

@@ -30,6 +30,8 @@ class AppState:
         
         self.STATE_FILE = "state.json"
         self.mqtt_handler = None  # Will be set by the MQTT handler
+        self.conn = None
+        self.port = None
         self.load()
 
     @property
@@ -100,7 +102,8 @@ class AppState:
             "gear_ratio": self.gear_ratio,
             "current_playlist": self._current_playlist,
             "current_playlist_index": self.current_playlist_index,
-            "playlist_mode": self.playlist_mode
+            "playlist_mode": self.playlist_mode,
+            "port": self.port
         }
 
     def from_dict(self, data):
@@ -121,6 +124,7 @@ class AppState:
         self._current_playlist = data.get("current_playlist")
         self.current_playlist_index = data.get("current_playlist_index")
         self.playlist_mode = data.get("playlist_mode")
+        self.port = data.get("port", None)
 
     def save(self):
         """Save the current state to a JSON file."""

+ 28 - 16
dune_weaver_flask/modules/mqtt/handler.py

@@ -11,7 +11,6 @@ from .base import BaseMQTTHandler
 from dune_weaver_flask.modules.core.state import state
 from dune_weaver_flask.modules.core.pattern_manager import list_theta_rho_files
 from dune_weaver_flask.modules.core.playlist_manager import list_all_playlists
-from dune_weaver_flask.modules.serial.serial_manager import is_connected, get_port
 
 logger = logging.getLogger(__name__)
 
@@ -240,8 +239,8 @@ class MQTTHandler(BaseMQTTHandler):
             
     def _publish_serial_state(self):
         """Helper to publish serial state."""
-        serial_connected = is_connected()
-        serial_port = get_port() if serial_connected else None
+        serial_connected = state.conn.is_connected()
+        serial_port = state.port if serial_connected else None
         serial_status = f"connected to {serial_port}" if serial_connected else "disconnected"
         self.client.publish(self.serial_state_topic, serial_status, retain=True)
 
@@ -265,19 +264,32 @@ class MQTTHandler(BaseMQTTHandler):
 
     def on_connect(self, client, userdata, flags, rc):
         """Callback when connected to MQTT broker."""
-        logger.info(f"Connected to MQTT broker with result code {rc}")
-        # Subscribe to command topics
-        client.subscribe([
-            (self.command_topic, 0),
-            (self.pattern_select_topic, 0),
-            (self.playlist_select_topic, 0),
-            (self.speed_topic, 0),
-            (f"{self.device_id}/command/stop", 0),
-            (f"{self.device_id}/command/pause", 0),
-            (f"{self.device_id}/command/play", 0)
-        ])
-        # Publish discovery configurations
-        self.setup_ha_discovery()
+        if rc == 0:
+            logger.info("MQTT Connection Accepted.")
+            # Subscribe to command topics
+            client.subscribe([
+                (self.command_topic, 0),
+                (self.pattern_select_topic, 0),
+                (self.playlist_select_topic, 0),
+                (self.speed_topic, 0),
+                (f"{self.device_id}/command/stop", 0),
+                (f"{self.device_id}/command/pause", 0),
+                (f"{self.device_id}/command/play", 0)
+            ])
+            # Publish discovery configurations
+            self.setup_ha_discovery()
+        elif rc == 1:
+            logger.error("MQTT Connection Refused. Protocol level not supported.")
+        elif rc == 2:
+            logger.error("MQTT Connection Refused. The client-identifier is not allowed by the server.")
+        elif rc == 3:
+            logger.error("MQTT Connection Refused. The MQTT service is not available.")
+        elif rc == 4:
+            logger.error("MQTT Connection Refused. The data in the username or password is malformed.")
+        elif rc == 5:
+            logger.error("MQTT Connection Refused. The client is not authorized to connect.")
+        else:
+            logger.error(f"MQTT Connection Refused. Unknown error code: {rc}")
 
     def on_message(self, client, userdata, msg):
         """Callback when message is received."""

+ 3 - 5
dune_weaver_flask/modules/mqtt/utils.py

@@ -7,9 +7,7 @@ from dune_weaver_flask.modules.core.pattern_manager import (
     run_theta_rho_files, list_theta_rho_files
 )
 from dune_weaver_flask.modules.core.playlist_manager import get_playlist, run_playlist
-from dune_weaver_flask.modules.serial.serial_manager import (
-    is_connected, get_port, home
-)
+from dune_weaver_flask.modules.connection.connection_manager import home
 from dune_weaver_flask.modules.core.state import state
 
 def create_mqtt_callbacks() -> Dict[str, Callable]:
@@ -41,8 +39,8 @@ def get_mqtt_state():
     is_running = bool(state.current_playing_file) and not state.stop_requested
     
     # Get serial status
-    serial_connected = is_connected()
-    serial_port = get_port() if serial_connected else None
+    serial_connected = state.conn.is_connected
+    serial_port = state.port if serial_connected else None
     serial_status = f"connected to {serial_port}" if serial_connected else "disconnected"
     
     return {

+ 0 - 346
dune_weaver_flask/modules/serial/serial_manager.py

@@ -1,346 +0,0 @@
-import serial
-import serial.tools.list_ports
-import threading
-import time
-import logging
-from dune_weaver_flask.modules.core.state import state
-
-logger = logging.getLogger(__name__)
-
-# Global variables
-ser = None
-ser_port = None
-serial_lock = threading.RLock()
-IGNORE_PORTS = ['/dev/cu.debug-console', '/dev/cu.Bluetooth-Incoming-Port']
-
-# Device information
-arduino_table_name = None
-arduino_driver_type = 'Unknown'
-firmware_version = 'Unknown'
-
-
-def list_serial_ports():
-    """Return a list of available serial ports."""
-    ports = serial.tools.list_ports.comports()
-    available_ports = [port.device for port in ports if port.device not in IGNORE_PORTS]
-    logger.debug(f"Available serial ports: {available_ports}")
-    return available_ports
-
-def get_machine_steps():
-    """
-    Send "$$" to the serial port to retrieve machine settings and return values for $100 and $101.
-    Returns two floats: x_steps_per_mm and y_steps_per_mm, or (None, None) if not found.
-    """
-    if not is_connected():
-        logger.error("Serial connection is not established.")
-        return None, None
-
-    while True:
-        with serial_lock:
-            ser.write("$$\n".encode())
-            ser.flush()
-            time.sleep(1)  # Allow time for response
-
-            x_steps_per_mm = None
-            y_steps_per_mm = None
-            gear_ratio = None
-
-            while ser.in_waiting > 0:
-                line = ser.readline().decode().strip()
-                logger.debug(f"Config response: {line}")
-
-                if line.startswith("$100="):
-                    x_steps_per_mm = float(line.split("=")[1])
-                    state.x_steps_per_mm = x_steps_per_mm
-                elif line.startswith("$101="):
-                    y_steps_per_mm = float(line.split("=")[1])
-                    state.y_steps_per_mm = y_steps_per_mm
-                elif line.startswith("$131="):
-                    gear_ratio = float(line.split("=")[1])
-                    state.gear_ratio = gear_ratio
-
-                if x_steps_per_mm is not None and y_steps_per_mm is not None and gear_ratio is not None:
-                    return True
-                
-def device_init():
-    try:
-        if get_machine_steps():
-            logger.info(f"x_steps_per_mm: {state.x_steps_per_mm}, y_steps_per_mm: {state.y_steps_per_mm}, gear_ratio: {state.gear_ratio}")
-    except:
-        logger.fatal("Not GRBL firmware")
-        pass
-
-    machine_x, machine_y = get_machine_position()
-    if not machine_x or not machine_y or machine_x != state.machine_x or machine_y != state.machine_y:
-        logger.info(f'x, y; {machine_x}, {machine_y}')
-        logger.info(f'State x, y; {state.machine_x}, {state.machine_y}')
-        home()
-    else:
-        logger.info('Machine position known, skipping home')
-        logger.info(f'Theta: {state.current_theta}, rho: {state.current_rho}')
-        logger.info(f'State x, y; {state.machine_x}, {state.machine_y}')
-
-    time.sleep(2)  # Allow time for the connection to establish
-
-    try:
-        if get_machine_steps():
-            logger.info(f"x_steps_per_mm: {state.x_steps_per_mm}, x_steps_per_mm: {state.y_steps_per_mm}, gear_ratio: {state.gear_ratio}")
-            return True
-    except:
-        logger.info("Not GRBL firmware")
-        return False
-        
-
-def connect_to_serial(port=None, baudrate=115200):
-    """Automatically connect to the first available serial port or a specified port with a timeout."""
-    global ser, ser_port, arduino_table_name, arduino_driver_type, firmware_version
-    timeout = 5  # Timeout in seconds
-    start_time = time.time()
-
-    while time.time() - start_time < timeout:
-        try:
-            if port is None:
-                ports = list_serial_ports()
-                if not ports:
-                    logger.warning("No serial port connected")
-                    time.sleep(1) # Wait before retrying
-                    continue  # Retry connecting
-                port = ports[0]  # Auto-select the first available port
-
-            with serial_lock: 
-                if ser and ser.is_open:
-                    ser.close()
-                ser = serial.Serial(port, baudrate, timeout=2) # Timeout for individual serial operations
-                ser_port = port
-            
-            # Check if connection is established by trying a quick command
-            try:
-                ser.write(b"?\r") # Example GRBL command
-                response = ser.readline().decode('utf-8').strip()
-                if response: # Check if we get any response
-                   logger.info(f"Connected to serial port: {port}")
-                   device_init()
-                   break # Exit the loop, connection successful
-                else:
-                    logger.warning("No response from serial port. Retrying...")
-                    ser.close() # Close the port before retrying
-                    time.sleep(1) # Wait before retrying
-                    continue
-            except serial.SerialException as e:
-                logger.warning(f"Serial exception during test command: {e}. Retrying...")
-                ser.close()
-                time.sleep(1)
-                continue
-            except Exception as e:
-                logger.exception(f"Unexpected error during connection test: {e}")
-                ser.close()
-                time.sleep(1)
-                continue
-
-
-        except serial.SerialException as e:
-            logger.error(f"Failed to connect to serial port {port}: {e}")
-            ser_port = None
-            time.sleep(1) # Wait before retrying
-            continue  # Retry connecting
-
-    else: # If the loop finishes without breaking (timeout)
-        logger.error(f"Timeout reached. Could not connect to a serial port after {timeout} seconds.")
-        return False
-
-
-def disconnect_serial():
-    """Disconnect the current serial connection."""
-    global ser, ser_port
-    if ser and ser.is_open:
-        logger.info("Disconnecting serial connection")
-        ser.close()
-        ser = None
-    ser_port = None
-
-
-def restart_serial(port, baudrate=115200):
-    """Restart the serial connection."""
-    logger.info(f"Restarting serial connection on port {port}")
-    disconnect_serial()
-    return connect_to_serial(port, baudrate)
-
-
-def is_connected():
-    """Check if serial connection is established and open."""
-    return ser is not None and ser.is_open
-
-
-def get_port():
-    """Get the current serial port."""
-    return ser_port
-
-
-def get_status_response():
-    """
-    Send a status query ('?') and return the response string if available.
-    This helper centralizes the query logic used throughout the module.
-    """
-    while True:
-        with serial_lock:
-            ser.write('?'.encode())
-            ser.flush()
-            while ser.in_waiting > 0:
-                response = ser.readline().decode().strip()
-                if "MPos" in response:
-                    logger.debug(f"Status response: {response}")
-                    return response
-        time.sleep(1)
-
-
-
-def parse_machine_position(response):
-    """
-    Parse the work position (MPos) from a status response.
-    Expected format: "<...|MPos:-994.869,-321.861,0.000|...>"
-    Returns a tuple (work_x, work_y) if found, else None.
-    """
-    if "MPos:" not in response:
-        return None
-    try:
-        wpos_section = next((part for part in response.split("|") if part.startswith("MPos:")), None)
-        if wpos_section:
-            wpos_str = wpos_section.split(":", 1)[1]
-            wpos_values = wpos_str.split(",")
-            work_x = float(wpos_values[0])
-            work_y = float(wpos_values[1])
-            return work_x, work_y
-    except Exception as e:
-        logger.error(f"Error parsing work position: {e}")
-    return None
-
-
-def parse_buffer_info(response):
-    """
-    Parse the planner and serial buffer info from a status response.
-    Expected format: "<...|Bf:15,128|...>"
-    Returns a dictionary with keys 'planner_buffer' and 'serial_buffer' if found, else None.
-    """
-    if "|Bf:" in response:
-        try:
-            buffer_section = response.split("|Bf:")[1].split("|")[0]
-            planner_buffer, serial_buffer = map(int, buffer_section.split(","))
-            return {"planner_buffer": planner_buffer, "serial_buffer": serial_buffer}
-        except ValueError:
-            logger.warning("Failed to parse buffer info from response")
-    return None
-
-
-def send_grbl_coordinates(x, y, speed=600, timeout=2, home=False):
-    """
-    Send a G-code command to FluidNC and wait up to timeout seconds for an 'ok' response.
-    If no 'ok' is received, retry every retry_interval seconds until successful.
-    """
-    logger.debug(f"Sending G-code: X{x} Y{y} at F{speed}")
-    while True:
-        with serial_lock:
-            if home:
-                gcode = f"$J=G91 Y{y} F{speed}"
-            else:
-                gcode = f"G1 X{x} Y{y} F{speed}"
-            ser.write(f"{gcode}\n".encode())
-            ser.flush()
-            logger.debug(f"Sent command: {gcode}")
-
-            start_time = time.time()
-            while time.time() - start_time < timeout:
-                if ser.in_waiting > 0:
-                    response = ser.readline().decode().strip()
-                    logger.debug(f"Response: {response}")
-                    if response.lower() == "ok":
-                        logger.debug("Command execution confirmed.")
-                        return  # Exit function when 'ok' is received
-
-            logger.warning(f"No 'ok' received for X{x} Y{y}. Retrying in 1s...")
-
-        time.sleep(1)
-
-
-def home(retry = 0):
-    logger.info(f"Homing with speed {state.speed}")
-    
-    # Check config for sensorless homing
-    with serial_lock:
-        ser.flush()
-        ser.write("$config\n".encode())
-        response = ser.readline().decode().strip().lower()
-        logger.debug(f"Config response: {response}")
-                
-    if "sensorless" in response:
-        logger.info("Using sensorless homing")
-        with serial_lock:
-            ser.write("$H\n".encode())
-            ser.write("G1 Y0 F100\n".encode())
-            ser.flush()
-    # we check that we actually got a valid response, if not, we try again a couple of times
-    elif "filename" in response:
-        logger.info("Using crash homing")
-        send_grbl_coordinates(0, -110/5, state.speed, home=True)
-    # error:3 means that the controller is running grbl, which doesn't support $config
-    # we just use crash homing for grbl
-    elif "error:3" in response:
-        logger.info("Grbl detected, Using crash homing")
-        send_grbl_coordinates(0, -110/5, state.speed, home=True)
-    else:
-        # we wait a bit and cal again increasing retry times
-        # if we are over the third retry, we give up
-        if retry < 3:
-            time.sleep(1)
-            home(retry+1)
-            return
-        else:
-            # after 3 retries we're still not getting a good response
-            # raise an exception
-            raise Exception("Couldn't get a valid response for homing after 3 retries")
-    
-    
-    state.current_theta = state.current_rho = 0
-    update_machine_position()
-
-def check_idle():
-    """
-    Continuously check if the machine is in the 'Idle' state.
-    """
-    logger.info("Checking idle")
-    while True:
-        response = get_status_response()
-        if response and "Idle" in response:
-            logger.info("Table is idle")
-            update_machine_position()
-            return True  # Exit once 'Idle' is confirmed
-        time.sleep(1)
-        
-def get_machine_position(timeout=3):
-    """
-    Send status queries for up to `timeout` seconds to obtain a valid machine (work) position.
-    Returns a tuple (machine_x, machine_y) if a valid response is received,
-    otherwise returns (None, None) after timeout.
-    """
-    start_time = time.time()
-    while time.time() - start_time < timeout:
-        with serial_lock:
-            ser.write('?'.encode())
-            ser.flush()
-            # Check if there is any response available
-            while ser.in_waiting > 0:
-                response = ser.readline().decode().strip()
-                logger.debug(f"Raw status response: {response}")
-                if "MPos" in response:
-                    pos = parse_machine_position(response)
-                    if pos:
-                        machine_x, machine_y = pos
-                        logger.debug(f"Machine position: X={machine_x}, Y={machine_y}")
-                        return machine_x, machine_y
-        # Short sleep before trying again
-        time.sleep(0.1)
-    logger.warning("Timeout reached waiting for machine position")
-    return None, None
-
-def update_machine_position():
-    state.machine_x, state.machine_y = get_machine_position()
-    state.save()

+ 0 - 0
dune_weaver_flask/modules/serial/__init__.py → dune_weaver_flask/modules/update/__init__.py


+ 0 - 1
dune_weaver_flask/modules/firmware/firmware_manager.py → dune_weaver_flask/modules/update/update_manager.py

@@ -1,7 +1,6 @@
 import os
 import subprocess
 import logging
-from ..serial import serial_manager
 
 # Configure logging
 logger = logging.getLogger(__name__)

+ 2 - 2
dune_weaver_flask/static/js/main.js

@@ -632,7 +632,7 @@ async function loadSerialPorts() {
 
 async function connectSerial() {
     const port = document.getElementById('serial_ports').value;
-    const response = await fetch('/connect_serial', {
+    const response = await fetch('/connect', {
         method: 'POST',
         headers: { 'Content-Type': 'application/json' },
         body: JSON.stringify({ port })
@@ -649,7 +649,7 @@ async function connectSerial() {
 }
 
 async function disconnectSerial() {
-    const response = await fetch('/disconnect_serial', { method: 'POST' });
+    const response = await fetch('/disconnect', { method: 'POST' });
     const result = await response.json();
     if (result.success) {
         logMessage('Serial port disconnected.', LOG_TYPE.SUCCESS);