Project_SylDa/docs/superpowers/specs/2026-08-04-auto-detect-port-fix.md

5.1 KiB

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:

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:

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:

// 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:

pio run -e s3lite-client -t upload --upload-port /dev/serial/by-id/...