diff options
Diffstat (limited to 'DESIGN.md')
| -rw-r--r-- | DESIGN.md | 171 |
1 files changed, 171 insertions, 0 deletions
diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..c1ac093 --- /dev/null +++ b/DESIGN.md | |||
| @@ -0,0 +1,171 @@ | |||
| 1 | # Design: WiFi Beacon Price Advertising via Vendor IE | ||
| 2 | |||
| 3 | ## Problem | ||
| 4 | |||
| 5 | TollGate client mode (`tollgate_client.c`) can only discover upstream TollGate pricing **after** connecting to its WiFi and hitting `GET :2121/`. There is no way to compare prices of nearby TollGates before choosing which one to connect to. The Nostr-based wifistr approach requires internet access (expensive WebSocket connections to relays), creating a chicken-and-egg problem. | ||
| 6 | |||
| 7 | ## Solution | ||
| 8 | |||
| 9 | Use IEEE 802.11 **Vendor-Specific Information Elements (IEs)** injected into WiFi beacon frames to broadcast price data over the air. Any nearby device can passively receive this data during a standard WiFi scan — no connection, no internet, no WebSocket required. | ||
| 10 | |||
| 11 | This approach is cross-platform compatible: | ||
| 12 | - **ESP32**: `esp_wifi_set_vendor_ie()` + `esp_wifi_set_vendor_ie_cb()` + `esp_wifi_scan_start()` | ||
| 13 | - **OpenWRT**: hostapd `vendor_elements` config parameter | ||
| 14 | - **Ubuntu/Linux**: `iw scan` / `nl80211` / `scapy` | ||
| 15 | |||
| 16 | ## Architecture | ||
| 17 | |||
| 18 | ### Wire Format (Phase 1: Binary Struct) | ||
| 19 | |||
| 20 | The vendor IE payload uses a fixed binary struct for maximum simplicity and portability. | ||
| 21 | |||
| 22 | ``` | ||
| 23 | Vendor IE (Element ID = 0xDD): | ||
| 24 | vendor_oui[3] = 0xC0, 0xFF, 0xEE (OpenTollGate OUI, placeholder) | ||
| 25 | vendor_oui_type = 0x01 (Price Advertisement v1) | ||
| 26 | payload: | ||
| 27 | uint8_t version = 1 (protocol version) | ||
| 28 | uint8_t metric = 0=milliseconds, 1=bytes | ||
| 29 | uint16_t price_per_step (sats, little-endian) | ||
| 30 | uint32_t step_size (ms or bytes, little-endian) | ||
| 31 | uint8_t mint_hash[4] (first 4 bytes of SHA-256(mint_url)) | ||
| 32 | uint8_t geohash_len (0-9) | ||
| 33 | char geohash[9] (null-padded to 9 bytes) | ||
| 34 | uint8_t npub_hash[4] (first 4 bytes of SHA-256(npub_hex)) | ||
| 35 | ``` | ||
| 36 | |||
| 37 | Total payload: 1+1+2+4+4+1+9+4 = **26 bytes**. Well under the 255-byte vendor IE limit. | ||
| 38 | |||
| 39 | ### Sender Flow | ||
| 40 | |||
| 41 | ``` | ||
| 42 | tollgate_main.c: start_services() | ||
| 43 | → beacon_price_start() | ||
| 44 | → Build tollgate_price_payload_t from config (price, step_size, metric, mint_url hash, geohash, npub hash) | ||
| 45 | → Wrap in vendor_ie_data_t (element_id=0xDD, oui=0xC0FFEE, oui_type=0x01) | ||
| 46 | → esp_wifi_set_vendor_ie(true, BEACON|PROBE_RESP, ID_0, &ie) | ||
| 47 | ``` | ||
| 48 | |||
| 49 | Every beacon frame (typically every 100ms) now carries the price data. | ||
| 50 | |||
| 51 | ### Receiver Flow | ||
| 52 | |||
| 53 | ``` | ||
| 54 | tollgate_main.c: main loop | ||
| 55 | → market_tick() | ||
| 56 | → Every 30s: esp_wifi_scan_start(NULL, false) // non-blocking all-channel scan | ||
| 57 | → During scan, vendor IE callback fires for each received beacon: | ||
| 58 | → Check OUI == 0xC0FFEE && oui_type == 0x01 | ||
| 59 | → Parse tollgate_price_payload_t | ||
| 60 | → Store in market_entry_t indexed by BSSID | ||
| 61 | → On scan complete event: esp_wifi_scan_get_ap_records() → correlate SSID/RSSI by BSSID | ||
| 62 | → Sort entries by effective price | ||
| 63 | ``` | ||
| 64 | |||
| 65 | ### Future Phases | ||
| 66 | |||
| 67 | - **Phase 2**: CBOR-encoded Nostr events in vendor IEs for cryptographic verification (BIP-340 Schnorr signatures on price data) | ||
| 68 | - **Phase 3**: Nostr relay subscription for wide-area market discovery (requires internet) | ||
| 69 | - **Phase 4**: `client_auto_switch` — automatically disconnect from expensive upstream and reconnect to cheapest | ||
| 70 | |||
| 71 | ## Cross-Platform Reference | ||
| 72 | |||
| 73 | ### OpenWRT / hostapd (Sender) | ||
| 74 | |||
| 75 | ```uci | ||
| 76 | config wifi-iface 'tollgate' | ||
| 77 | option vendor_elements 'dd20c0ffee0101001...hex...' | ||
| 78 | ``` | ||
| 79 | |||
| 80 | Where the hex is the binary payload encoded as hex string. | ||
| 81 | |||
| 82 | ### Linux (Receiver) | ||
| 83 | |||
| 84 | ```bash | ||
| 85 | iw dev wlan0 scan | grep -A1 "Vendor specific" | ||
| 86 | # Or: scapy/python with Dot11Beacon parsing | ||
| 87 | ``` | ||
| 88 | |||
| 89 | --- | ||
| 90 | |||
| 91 | ## Implementation Checklist | ||
| 92 | |||
| 93 | ### Phase 1: Vendor IE Transmitter | ||
| 94 | - [x] Create `main/beacon_price.h` — payload struct, API declarations | ||
| 95 | - [x] Create `main/beacon_price.c` — `beacon_price_start()`, `beacon_price_stop()` | ||
| 96 | - [x] Compute `mint_hash` and `npub_hash` using SHA-256 | ||
| 97 | |||
| 98 | ### Phase 2: Vendor IE Receiver + Market Scanner | ||
| 99 | - [x] Create `main/market.h` — `market_entry_t`, `market_t`, API declarations | ||
| 100 | - [x] Create `main/market.c` — vendor IE callback, scan trigger, entry storage, ranking | ||
| 101 | - [x] BSSID correlation between vendor IE callback and scan results | ||
| 102 | |||
| 103 | ### Phase 3: Config Additions | ||
| 104 | - [x] Add `market_enabled`, `market_scan_interval_s`, `client_auto_switch` to `config.h` | ||
| 105 | - [x] Parse new fields from config.json in `config.c` | ||
| 106 | |||
| 107 | ### Phase 4: Main Loop Integration | ||
| 108 | - [x] Call `beacon_price_start()` / `beacon_price_stop()` in `tollgate_main.c` | ||
| 109 | - [x] Call `market_init()` in `start_services()` | ||
| 110 | - [x] Call `market_tick()` in main loop | ||
| 111 | - [x] Add `beacon_price.c` and `market.c` to `CMakeLists.txt` | ||
| 112 | |||
| 113 | ### Phase 5: Client Market Consultation | ||
| 114 | - [x] In `tollgate_client.c`, log price comparison when connecting to upstream | ||
| 115 | - [x] Warn if cheaper alternative exists in market snapshot | ||
| 116 | |||
| 117 | ### Phase 6: API Endpoint | ||
| 118 | - [x] Add `GET /market` handler in `tollgate_api.c` | ||
| 119 | - [x] Return JSON array of discovered TollGates with prices | ||
| 120 | |||
| 121 | ### Phase 7: Unit Tests | ||
| 122 | - [x] `tests/unit/test_beacon_price.c` — encode/decode roundtrip, struct packing | ||
| 123 | - [x] `tests/unit/test_market.c` — ranking, geohash filtering, entry management | ||
| 124 | |||
| 125 | ### Phase 8: Integration Tests | ||
| 126 | - [x] `tests/integration/test-market.mjs` — GET /market endpoint validation | ||
| 127 | - [x] `tests/integration/test-price-discovery.mjs` — two-board price discovery | ||
| 128 | - [x] Add Makefile targets for new tests | ||
| 129 | |||
| 130 | ### Phase 9: ESP-IDF Build | ||
| 131 | - [x] Fix format specifiers (`%u` → `%lu` + cast for `uint32_t` on xtensa) | ||
| 132 | - [x] Copy local-only files (`display.c/h`, `font.c/h`) to worktree | ||
| 133 | - [x] Apply nucula `save_proofs()` private→public patch | ||
| 134 | - [x] `idf.py build` succeeds | ||
| 135 | - [x] Symlink missing components (`axs15231b`, `qrcode`) from main repo | ||
| 136 | |||
| 137 | ### Phase 10: Hardware Mutex (per-board, shared across worktrees) | ||
| 138 | - [x] Rewrite Makefile with per-board locks (`lock-a`, `lock-b`, `unlock-a`, `unlock-b`) | ||
| 139 | - [x] Shared `LOCK_DIR` at `/home/c03rad0r/physical-router-test-automation/locks` | ||
| 140 | - [x] `require_lock_a` / `require_lock_b` macros | ||
| 141 | - [x] `acquire_lock` function macro (same pattern as `physical-router-test-automation/esp32/Makefile`) | ||
| 142 | - [x] Per-board flash/monitor/reset/serial-log/erase-nvs targets | ||
| 143 | - [x] `connect-a` / `connect-b` / `disconnect` WiFi targets | ||
| 144 | - [x] `_connect-a-if-needed` / `_connect-b-if-needed` auto-connect helpers | ||
| 145 | - [x] All integration tests require `lock-a` + `_connect-a-if-needed` | ||
| 146 | - [x] `test-price-discovery` requires both `lock-a` AND `lock-b` | ||
| 147 | - [x] Board port mapping matches `boards.env` (A=ACM1, B=ACM2) | ||
| 148 | - [x] Remove old single-lock `hardware.lock` from `.gitignore` | ||
| 149 | |||
| 150 | ### Phase 11: Debugging & Hardening | ||
| 151 | - [x] Add WiFi disconnect reason code to log output (`tollgate_main.c:58`) | ||
| 152 | - [x] Add `esp_wifi_set_country_code("DE")` — was missing, defaults to CN | ||
| 153 | - [x] Commit both fixes to `feature/price-discovery` | ||
| 154 | - [x] Document findings in `SESSION_NOTES.md` | ||
| 155 | |||
| 156 | ### Final | ||
| 157 | - [x] `make test-unit` passes (all 13 test suites, 45 new assertions) | ||
| 158 | - [x] `idf.py build` passes (ESP32-S3 firmware) | ||
| 159 | - [x] Commit to `feature/price-discovery` branch | ||
| 160 | - [x] Hardware flash + integration test on Board B (`test-market`: 4 passed, 0 failed) | ||
| 161 | - [x] Hardware flash + integration test on Board A (`test-market`: 4 passed, 0 failed) | ||
| 162 | - [ ] Two-board price discovery test (`test-price-discovery`) — blocked by WiFi STA issue (reason=211 NO_AP_FOUND) | ||
| 163 | - [ ] Merge to master | ||
| 164 | |||
| 165 | ## Blockers | ||
| 166 | |||
| 167 | ### WiFi STA Connectivity (reason=211) | ||
| 168 | Both boards fail to find `EnterSSID-2.4GHz` during STA scan despite the router being visible from the laptop at 100% signal. Root cause unclear — may be RF/environmental, APSTA co-channel limitation, or ESP32 scan sensitivity issue. Board B obtained STA IP once during testing, proving the firmware code is correct. See `SESSION_NOTES.md` for detailed analysis. | ||
| 169 | |||
| 170 | ### Multi-Session Hardware Conflict | ||
| 171 | Other LLM sessions (`esp32-tollgate`, `esp32-tollgate-arch`, `esp32-tollgate-display`) flash boards concurrently without respecting the lock system, overwriting our firmware within seconds of flashing. | ||