# Auto-detect Upload Port Fix **Issue:** #16 **Date:** 2026-08-04 **Status:** Approved ## Problem The post-script `tools/pio_upload_port.py` sends `ID?` to the ESP and reads the first non-empty response to determine device type (server/client). The client responds correctly with `SylDa_Client`, but during upload there was active log output (ECDH handshake) on the serial port. The `query_id()` function reads the first log message instead of the ID response, causing auto-detect to fail. Root cause: `query_id()` blindly takes the first non-empty line, even if it's log output. ## Solution Two-part fix for maximum robustness: 1. **Script-side:** Filter responses — keep reading until a line containing `SylDa_Server` or `SylDa_Client` appears 2. **Firmware-side:** Pause all serial output during `ID?` response No protocol changes. Both parts work independently but together provide robustness. ## Architecture ``` ┌─────────────────────┐ ID?\n ┌─────────────────────┐ │ pio_upload_port │ ─────────────▶│ Firmware (ESP32) │ │ (Python script) │ │ │ │ │◀─────────────│ serialMuted=true │ │ query_id() reads │ SylDa_* │ Serial.flush() │ │ until match │ │ response │ └─────────────────────┘ │ serialMuted=false │ └─────────────────────┘ ``` ## Components ### 1. Script: `tools/pio_upload_port.py` **Change:** `query_id()` function filters for expected ID instead of returning first non-empty line. **Before:** ```python def query_id(port, timeout=5): # ... while time.time() < deadline: line = ser.readline().decode("utf-8", errors="ignore").strip() if line: ser.close() return line # Returns first non-empty, even if log # ... ``` **After:** ```python 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 "SylDa_Server" in line or "SylDa_Client" in line: ser.close() return line ser.close() except (serial.SerialException, OSError): pass return None ``` **No other changes needed** — the rest of the script already uses `expected_id in resp` checks. ### 2. Firmware: `src/client.cpp` and `src/server.cpp` **Changes:** 1. Global `serialMuted` flag 2. Serial mute during `ID?` handling 3. Wrapper macros for existing Serial calls **Implementation:** ```cpp // Global flag (file scope) static bool serialMuted = false; // Wrapper macros #define SERIAL_PRINTLN(msg) if (!serialMuted) Serial.println(msg) #define SERIAL_PRINT(msg) if (!serialMuted) Serial.print(msg) // In serial command handling } else if (serialBuf == "ID?") { Serial.flush(); serialMuted = true; Serial.println("SylDa_Client"); // or SylDa_Server delay(50); // Ensure response transmitted serialMuted = false; } ``` **Existing `Serial.println()` calls:** Replace with `SERIAL_PRINTLN()` macro. **ESP_LOG macros:** Unchanged — these use IDF logging, not Arduino Serial. ### 3. Server firmware Identical implementation to client, with `SylDa_Server` response. ## Data Flow 1. Script opens serial port, sends `ID?\n` 2. Firmware receives command: - `Serial.flush()` empties TX buffer - `serialMuted = true` blocks new output - `Serial.println("SylDa_Client")` — only output - `delay(50)` ensures transmission - `serialMuted = false` releases mute 3. Script reads lines until `SylDa_*` appears (timeout 5s) 4. Match → port selected ## Error Handling | Scenario | Behavior | |----------|----------| | No response | Script timeout → manual port selection (existing) | | Wrong response | Script detects mismatch → manual selection (existing) | | Multiple ports, both respond | Script matches correct one, selects it | | Firmware mute stuck | Impossible — mute released immediately after response | | Buffer overflow | `Serial.flush()` prevents this | ## Testing 1. Connect both server and client simultaneously 2. Run `pio run -e s3lite-client -t upload` — should auto-detect client 3. Run `pio run -e s3lite-server -t upload` — should auto-detect server 4. Verify log output resumes after `ID?` response 5. Test with active ECDH handshake in progress ## Files Changed - `tools/pio_upload_port.py` — `query_id()` filtering - `src/client.cpp` — serial mute implementation - `src/server.cpp` — serial mute implementation ## Rollback If issues arise, revert to manual port selection: ```bash pio run -e s3lite-client -t upload --upload-port /dev/serial/by-id/... ```