docs: add design spec for issue #16 auto-detect port fix
This commit is contained in:
parent
09b6ff6823
commit
30fc7532ae
1 changed files with 152 additions and 0 deletions
152
docs/superpowers/specs/2026-08-04-auto-detect-port-fix.md
Normal file
152
docs/superpowers/specs/2026-08-04-auto-detect-port-fix.md
Normal file
|
|
@ -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/...
|
||||
```
|
||||
Loading…
Reference in a new issue