From 30fc7532ae03587980e881d367fa9f266cbcdd14 Mon Sep 17 00:00:00 2001 From: eddy Date: Tue, 4 Aug 2026 18:59:14 +0200 Subject: [PATCH 1/6] docs: add design spec for issue #16 auto-detect port fix --- .../specs/2026-08-04-auto-detect-port-fix.md | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-04-auto-detect-port-fix.md diff --git a/docs/superpowers/specs/2026-08-04-auto-detect-port-fix.md b/docs/superpowers/specs/2026-08-04-auto-detect-port-fix.md new file mode 100644 index 0000000..fb05d3b --- /dev/null +++ b/docs/superpowers/specs/2026-08-04-auto-detect-port-fix.md @@ -0,0 +1,152 @@ +# 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/... +``` From 2030b42ba27243e0dfa9f73efc18c74dbae1ec82 Mon Sep 17 00:00:00 2001 From: eddy Date: Tue, 4 Aug 2026 19:01:57 +0200 Subject: [PATCH 2/6] docs: add implementation plan for issue #16 --- .../plans/2026-08-04-auto-detect-port-fix.md | 181 ++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-04-auto-detect-port-fix.md diff --git a/docs/superpowers/plans/2026-08-04-auto-detect-port-fix.md b/docs/superpowers/plans/2026-08-04-auto-detect-port-fix.md new file mode 100644 index 0000000..dfa14fa --- /dev/null +++ b/docs/superpowers/plans/2026-08-04-auto-detect-port-fix.md @@ -0,0 +1,181 @@ +# Auto-detect Upload Port Fix Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Fix auto-detect upload port failure when active log output interferes with ID? response detection. + +**Architecture:** Two-part fix — script-side filtering continues reading until ID match, firmware-side serial mute pauses all output during ID? response. + +**Tech Stack:** Python 3 + pyserial (script), ESP32-S3 + Arduino + ESP-IDF logging (firmware) + +--- + +## File Structure + +| File | Action | Purpose | +|------|--------|---------| +| `tools/pio_upload_port.py` | Modify | Filter ID responses in query_id() | +| `src/main.cpp` | Modify | Add serial mute for server | +| `src/client.cpp` | Modify | Add serial mute for client | + +--- + +### Task 1: Script-side ID filtering + +**Files:** +- Modify: `tools/pio_upload_port.py:46-61` + +- [ ] **Step 1: Update query_id() to filter for ID response** + +Replace lines 46-61 in `tools/pio_upload_port.py`: + +```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 +``` + +- [ ] **Step 2: Commit** + +```bash +git add tools/pio_upload_port.py +git commit -m "fix(script): filter ID response in query_id()" +``` + +--- + +### Task 2: Server firmware serial mute + +**Files:** +- Modify: `src/main.cpp` + +- [ ] **Step 1: Add serialMuted flag and mute helper** + +After line 17 (`static const char *TAG = "server";`), add: + +```cpp +static bool serialMuted = false; + +static void muteSerial() { + serialMuted = true; + esp_log_level_set("*", ESP_LOG_NONE); +} + +static void unmuteSerial() { + serialMuted = false; + esp_log_level_set("*", CONFIG_LOG_DEFAULT_LEVEL); +} +``` + +- [ ] **Step 2: Update ID? handler to use mute** + +Replace lines 591-592: + +```cpp + } else if (serialBuf == "ID?") { + Serial.flush(); + muteSerial(); + Serial.println("SylDa_Server"); + delay(50); + unmuteSerial(); +``` + +- [ ] **Step 3: Commit** + +```bash +git add src/main.cpp +git commit -m "fix(server): add serial mute during ID? response" +``` + +--- + +### Task 3: Client firmware serial mute + +**Files:** +- Modify: `src/client.cpp` + +- [ ] **Step 1: Add serialMuted flag and mute helper** + +After line 15 (`static const char *TAG = "client";`), add: + +```cpp +static bool serialMuted = false; + +static void muteSerial() { + serialMuted = true; + esp_log_level_set("*", ESP_LOG_NONE); +} + +static void unmuteSerial() { + serialMuted = false; + esp_log_level_set("*", CONFIG_LOG_DEFAULT_LEVEL); +} +``` + +- [ ] **Step 2: Update ID? handler to use mute** + +Replace lines 425-426: + +```cpp + } else if (serialBuf == "ID?") { + Serial.flush(); + muteSerial(); + Serial.println("SylDa_Client"); + delay(50); + unmuteSerial(); +``` + +- [ ] **Step 3: Commit** + +```bash +git add src/client.cpp +git commit -m "fix(client): add serial mute during ID? response" +``` + +--- + +### Task 4: Build and verify + +- [ ] **Step 1: Build both environments** + +```bash +pio run -e s3lite-server +pio run -e s3lite-client +``` + +Expected: Both compile without errors. + +- [ ] **Step 2: Verify firmware version bump** + +Check `version.txt` exists with correct format. + +- [ ] **Step 3: Push branch** + +```bash +git push -u origin fix/issue-16-auto-detect-port +``` + +--- + +## Testing Checklist + +After implementation, test with actual hardware: + +1. Connect both server and client via USB +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 upload completes +5. Test during active ECDH handshake (worst case) From 99fd6b21d445702abaab55faed884d666c8288d6 Mon Sep 17 00:00:00 2001 From: eddy Date: Tue, 4 Aug 2026 19:02:51 +0200 Subject: [PATCH 3/6] fix(script): filter ID response in query_id() --- tools/pio_upload_port.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/pio_upload_port.py b/tools/pio_upload_port.py index 0e94c81..43c9e25 100644 --- a/tools/pio_upload_port.py +++ b/tools/pio_upload_port.py @@ -52,7 +52,7 @@ def query_id(port, timeout=5): deadline = time.time() + timeout while time.time() < deadline: line = ser.readline().decode("utf-8", errors="ignore").strip() - if line: + if "SylDa_Server" in line or "SylDa_Client" in line: ser.close() return line ser.close() From ae5028503fd81ba4f87b6a194175113d211cc8ea Mon Sep 17 00:00:00 2001 From: eddy Date: Tue, 4 Aug 2026 19:03:21 +0200 Subject: [PATCH 4/6] fix(server): add serial mute during ID? response --- src/main.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/main.cpp b/src/main.cpp index 5b6543e..332ff60 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -16,6 +16,18 @@ static const char *TAG = "server"; +static bool serialMuted = false; + +static void muteSerial() { + serialMuted = true; + esp_log_level_set("*", ESP_LOG_NONE); +} + +static void unmuteSerial() { + serialMuted = false; + esp_log_level_set("*", CONFIG_LOG_DEFAULT_LEVEL); +} + #define RELAY1_PIN 1 #define RELAY2_PIN 2 #define TOUCH1_PIN 8 @@ -589,7 +601,11 @@ void loop() { Serial.println("NO_KEY"); } } else if (serialBuf == "ID?") { + Serial.flush(); + muteSerial(); Serial.println("SylDa_Server"); + delay(50); + unmuteSerial(); } else if (serialBuf == "VERSION?") { Serial.flush(); Serial.print("BUILD_NR:"); From 3c6b378b9b20f271e00806ebf0efa6c65c5c5876 Mon Sep 17 00:00:00 2001 From: eddy Date: Tue, 4 Aug 2026 19:03:54 +0200 Subject: [PATCH 5/6] fix(client): add serial mute during ID? response --- src/client.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/client.cpp b/src/client.cpp index 4a85039..3d4298c 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -13,6 +13,18 @@ static const char *TAG = "client"; +static bool serialMuted = false; + +static void muteSerial() { + serialMuted = true; + esp_log_level_set("*", ESP_LOG_NONE); +} + +static void unmuteSerial() { + serialMuted = false; + esp_log_level_set("*", CONFIG_LOG_DEFAULT_LEVEL); +} + #define TOUCH1_PIN 8 #define TOUCH2_PIN 6 #define BUTTON_PIN 41 @@ -423,7 +435,11 @@ void loop() { Serial.println("NO_KEY"); } } else if (serialBuf == "ID?") { + Serial.flush(); + muteSerial(); Serial.println("SylDa_Client"); + delay(50); + unmuteSerial(); } else if (serialBuf == "VERSION?") { Serial.print("BUILD_NR:"); Serial.println(FIRMWARE_VERSION_CLIENT); From 7b59d36bd7715bf89a17618ad64a9eb02e242c3b Mon Sep 17 00:00:00 2001 From: eddy Date: Tue, 4 Aug 2026 19:04:37 +0200 Subject: [PATCH 6/6] fix: cast CONFIG_LOG_DEFAULT_LEVEL to esp_log_level_t --- src/client.cpp | 2 +- src/main.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client.cpp b/src/client.cpp index 3d4298c..15746fc 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -22,7 +22,7 @@ static void muteSerial() { static void unmuteSerial() { serialMuted = false; - esp_log_level_set("*", CONFIG_LOG_DEFAULT_LEVEL); + esp_log_level_set("*", (esp_log_level_t)CONFIG_LOG_DEFAULT_LEVEL); } #define TOUCH1_PIN 8 diff --git a/src/main.cpp b/src/main.cpp index 332ff60..f795bb5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -25,7 +25,7 @@ static void muteSerial() { static void unmuteSerial() { serialMuted = false; - esp_log_level_set("*", CONFIG_LOG_DEFAULT_LEVEL); + esp_log_level_set("*", (esp_log_level_t)CONFIG_LOG_DEFAULT_LEVEL); } #define RELAY1_PIN 1