From 5c687076f1243024c193a86685b7b11534ab5823 Mon Sep 17 00:00:00 2001 From: Lloyd Date: Fri, 22 May 2026 10:21:07 +0100 Subject: [PATCH] feat: add endpoint to discover available serial/USB modem device paths --- repeater/web/api_endpoints.py | 53 +++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/repeater/web/api_endpoints.py b/repeater/web/api_endpoints.py index 10b3733..5ccdd64 100644 --- a/repeater/web/api_endpoints.py +++ b/repeater/web/api_endpoints.py @@ -137,6 +137,7 @@ logger = logging.getLogger("HTTPServer") # GET /api/needs_setup - Check if repeater needs initial setup # GET /api/hardware_options - Get available hardware configurations # GET /api/radio_presets - Get radio preset configurations +# GET /api/serial_ports - Discover available serial/USB modem device paths # POST /api/setup_wizard - Complete initial setup wizard # Backup & Restore @@ -414,6 +415,50 @@ class APIEndpoints: logger.error(f"Error loading radio presets: {e}") return {"error": str(e)} + @cherrypy.expose + @cherrypy.tools.json_out() + def serial_ports(self): + """Discover available serial/USB modem device paths.""" + try: + devices = [] + + # Preferred: pyserial provides stable metadata (VID/PID, product, serial number). + try: + from serial.tools import list_ports + + for port in list_ports.comports(): + label_parts = [port.device] + if getattr(port, "description", None): + label_parts.append(str(port.description)) + if getattr(port, "hwid", None) and str(port.hwid) != "n/a": + label_parts.append(str(port.hwid)) + devices.append( + { + "device": str(port.device), + "description": " - ".join(label_parts), + } + ) + except Exception: + # Fallback for environments where pyserial is unavailable. + import glob + + for pattern in ("/dev/ttyACM*", "/dev/ttyUSB*", "/dev/ttyS*", "/dev/serial/by-id/*"): + for dev in glob.glob(pattern): + devices.append({"device": str(dev), "description": str(dev)}) + + # De-duplicate by device path while preserving the first description. + dedup = {} + for item in devices: + dev = item.get("device") + if dev and dev not in dedup: + dedup[dev] = item + + sorted_devices = sorted(dedup.values(), key=lambda x: x["device"]) + return self._success(sorted_devices) + except Exception as e: + logger.error(f"Error discovering serial ports: {e}") + return self._error(str(e)) + @cherrypy.expose @cherrypy.tools.json_out() @cherrypy.tools.json_in() @@ -681,6 +726,14 @@ class APIEndpoints: def stats(self): try: stats = self.stats_getter() if self.stats_getter else {} + # Include active radio configuration in stats so UI can hydrate + # directly from this endpoint without additional config fetches. + stats["radio_type"] = self.config.get("radio_type") + stats["sx1262"] = self.config.get("sx1262", {}) + stats["ch341"] = self.config.get("ch341", {}) + stats["kiss"] = self.config.get("kiss", {}) + stats["pymc_usb"] = self.config.get("pymc_usb", {}) + stats["pymc_tcp"] = self.config.get("pymc_tcp", {}) stats["version"] = __version__ try: import pymc_core