diff --git a/platformio.ini b/platformio.ini index f02579d..9e57f2e 100644 --- a/platformio.ini +++ b/platformio.ini @@ -11,11 +11,12 @@ build_flags = lib_deps = fastled/FastLED@^3 h2zero/NimBLE-Arduino@^2 -upload_port = /dev/serial/by-id/usb-Espressif_USB_JTAG_serial_debug_unit_44:1B:F6:F4:CF:14-if00 upload_speed = 115200 upload_flags = --no-stub build_src_filter = + - -extra_scripts = pre:tools/pio_version_pre.py +extra_scripts = + pre:tools/pio_version_pre.py + post:tools/pio_upload_port.py [env:s3lite-client] platform = espressif32@6.7.0 @@ -31,8 +32,9 @@ build_flags = lib_deps = fastled/FastLED@^3 h2zero/NimBLE-Arduino@^2 -upload_port = /dev/ttyACM2 upload_speed = 115200 upload_flags = --no-stub build_src_filter = + - -extra_scripts = pre:tools/pio_version_pre.py +extra_scripts = + pre:tools/pio_version_pre.py + post:tools/pio_upload_port.py diff --git a/src/client.cpp b/src/client.cpp index 0dc079c..dfed816 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -263,6 +263,8 @@ void loop() { } else { Serial.println("NO_KEY"); } + } else if (serialBuf == "ID?") { + Serial.println("SylDa_Client"); } else if (serialBuf == "VERSION?") { Serial.print("BUILD_NR:"); Serial.println(FIRMWARE_VERSION_CLIENT); diff --git a/src/main.cpp b/src/main.cpp index e12cbd5..473712c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -485,6 +485,8 @@ void loop() { } else { Serial.println("NO_KEY"); } + } else if (serialBuf == "ID?") { + Serial.println("SylDa_Server"); } else if (serialBuf == "VERSION?") { Serial.print("BUILD_NR:"); Serial.println(FIRMWARE_VERSION); diff --git a/tools/pio_upload_port.py b/tools/pio_upload_port.py new file mode 100644 index 0000000..cd66b77 --- /dev/null +++ b/tools/pio_upload_port.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""POST-script: auto-detect upload_port by querying connected ESP32s. + +Each ESP32 running SylDa firmware responds to the serial command "ID?" +with either "SylDa_Server" or "SylDa_Client". This script scans +/dev/serial/by-id/ for Espressif devices, queries each one, and sets +UPLOAD_PORT for the matching environment. + +If no device responds (e.g. first flash), the user is prompted to +select a port manually. +""" +Import("env") + +if env.IsIntegrationDump(): + Return() + +ENV_TO_ID = { + "s3lite-server": "SylDa_Server", + "s3lite-client": "SylDa_Client", +} + +import glob +import os +import sys +import time + +try: + import serial +except ImportError: + print(" WARNING: pyserial not installed, skipping auto-detect") + print(" Install: pip install pyserial") + Return() + + +def find_esp32_ports(): + by_id = "/dev/serial/by-id" + if not os.path.isdir(by_id): + return [] + ports = [] + for entry in sorted(os.listdir(by_id)): + if "Espressif" in entry or "espressif" in entry: + ports.append(os.path.join(by_id, entry)) + return ports + + +def query_id(port, timeout=5): + try: + ser = serial.Serial(port, 115200, timeout=0.5) + time.sleep(0.1) + ser.reset_input_buffer() + ser.write(b"ID?\n") + deadline = time.time() + timeout + while time.time() < deadline: + line = ser.readline().decode("utf-8", errors="ignore").strip() + if line: + ser.close() + return line + ser.close() + except (serial.SerialException, OSError): + pass + return None + + +def detect_port(source, target, env): + env_name = env.subst("$PIOENV") + expected_id = ENV_TO_ID.get(env_name) + + if not expected_id: + print(f" Unknown environment '{env_name}', skipping auto-detect") + return + + ports = find_esp32_ports() + + if not ports: + print(f" No ESP32 serial ports found in /dev/serial/by-id/") + print(f" Connect an ESP32 and try again") + sys.exit(1) + + if len(ports) == 1: + port = ports[0] + resp = query_id(port, timeout=5) + if resp and expected_id in resp: + print(f" Auto-detected {expected_id} on {port}") + else: + if resp: + print(f" Found '{resp}' on {port} (expected {expected_id})") + print(f" Only one ESP32 connected, using {port}") + env["UPLOAD_PORT"] = port + return + + for port in ports: + resp = query_id(port, timeout=5) + if resp and expected_id in resp: + print(f" Found {expected_id} on {port}") + env["UPLOAD_PORT"] = port + return + + print(f"\n Cannot auto-detect {expected_id} (no ID? response matched)") + print(f" Available ESP32 ports:") + for i, port in enumerate(ports, 1): + resp = query_id(port, timeout=3) + label = f" ({resp})" if resp else "" + print(f" {i}: {port}{label}") + + try: + choice = input(f" Select port for {env_name} [{expected_id}] (1-{len(ports)}): ") + idx = int(choice) - 1 + if 0 <= idx < len(ports): + env["UPLOAD_PORT"] = ports[idx] + return + except (ValueError, EOFError): + pass + + print(" Invalid choice, aborting upload") + sys.exit(1) + + +env.AddPreAction("upload", detect_port)