upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/DESIGN.md
blob: c1ac0938c28a69ae429cfe6996d0d974c7bfc4e2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# Design: WiFi Beacon Price Advertising via Vendor IE

## Problem

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.

## Solution

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.

This approach is cross-platform compatible:
- **ESP32**: `esp_wifi_set_vendor_ie()` + `esp_wifi_set_vendor_ie_cb()` + `esp_wifi_scan_start()`
- **OpenWRT**: hostapd `vendor_elements` config parameter
- **Ubuntu/Linux**: `iw scan` / `nl80211` / `scapy`

## Architecture

### Wire Format (Phase 1: Binary Struct)

The vendor IE payload uses a fixed binary struct for maximum simplicity and portability.

```
Vendor IE (Element ID = 0xDD):
  vendor_oui[3]     = 0xC0, 0xFF, 0xEE   (OpenTollGate OUI, placeholder)
  vendor_oui_type   = 0x01               (Price Advertisement v1)
  payload:
    uint8_t  version            = 1       (protocol version)
    uint8_t  metric             = 0=milliseconds, 1=bytes
    uint16_t price_per_step               (sats, little-endian)
    uint32_t step_size                    (ms or bytes, little-endian)
    uint8_t  mint_hash[4]                (first 4 bytes of SHA-256(mint_url))
    uint8_t  geohash_len                  (0-9)
    char     geohash[9]                  (null-padded to 9 bytes)
    uint8_t  npub_hash[4]               (first 4 bytes of SHA-256(npub_hex))
```

Total payload: 1+1+2+4+4+1+9+4 = **26 bytes**. Well under the 255-byte vendor IE limit.

### Sender Flow

```
tollgate_main.c: start_services()
  → beacon_price_start()
    → Build tollgate_price_payload_t from config (price, step_size, metric, mint_url hash, geohash, npub hash)
    → Wrap in vendor_ie_data_t (element_id=0xDD, oui=0xC0FFEE, oui_type=0x01)
    → esp_wifi_set_vendor_ie(true, BEACON|PROBE_RESP, ID_0, &ie)
```

Every beacon frame (typically every 100ms) now carries the price data.

### Receiver Flow

```
tollgate_main.c: main loop
  → market_tick()
    → Every 30s: esp_wifi_scan_start(NULL, false)  // non-blocking all-channel scan
    → During scan, vendor IE callback fires for each received beacon:
      → Check OUI == 0xC0FFEE && oui_type == 0x01
      → Parse tollgate_price_payload_t
      → Store in market_entry_t indexed by BSSID
    → On scan complete event: esp_wifi_scan_get_ap_records() → correlate SSID/RSSI by BSSID
    → Sort entries by effective price
```

### Future Phases

- **Phase 2**: CBOR-encoded Nostr events in vendor IEs for cryptographic verification (BIP-340 Schnorr signatures on price data)
- **Phase 3**: Nostr relay subscription for wide-area market discovery (requires internet)
- **Phase 4**: `client_auto_switch` — automatically disconnect from expensive upstream and reconnect to cheapest

## Cross-Platform Reference

### OpenWRT / hostapd (Sender)

```uci
config wifi-iface 'tollgate'
    option vendor_elements 'dd20c0ffee0101001...hex...'
```

Where the hex is the binary payload encoded as hex string.

### Linux (Receiver)

```bash
iw dev wlan0 scan | grep -A1 "Vendor specific"
# Or: scapy/python with Dot11Beacon parsing
```

---

## Implementation Checklist

### Phase 1: Vendor IE Transmitter
- [x] Create `main/beacon_price.h` — payload struct, API declarations
- [x] Create `main/beacon_price.c` — `beacon_price_start()`, `beacon_price_stop()`
- [x] Compute `mint_hash` and `npub_hash` using SHA-256

### Phase 2: Vendor IE Receiver + Market Scanner
- [x] Create `main/market.h` — `market_entry_t`, `market_t`, API declarations
- [x] Create `main/market.c` — vendor IE callback, scan trigger, entry storage, ranking
- [x] BSSID correlation between vendor IE callback and scan results

### Phase 3: Config Additions
- [x] Add `market_enabled`, `market_scan_interval_s`, `client_auto_switch` to `config.h`
- [x] Parse new fields from config.json in `config.c`

### Phase 4: Main Loop Integration
- [x] Call `beacon_price_start()` / `beacon_price_stop()` in `tollgate_main.c`
- [x] Call `market_init()` in `start_services()`
- [x] Call `market_tick()` in main loop
- [x] Add `beacon_price.c` and `market.c` to `CMakeLists.txt`

### Phase 5: Client Market Consultation
- [x] In `tollgate_client.c`, log price comparison when connecting to upstream
- [x] Warn if cheaper alternative exists in market snapshot

### Phase 6: API Endpoint
- [x] Add `GET /market` handler in `tollgate_api.c`
- [x] Return JSON array of discovered TollGates with prices

### Phase 7: Unit Tests
- [x] `tests/unit/test_beacon_price.c` — encode/decode roundtrip, struct packing
- [x] `tests/unit/test_market.c` — ranking, geohash filtering, entry management

### Phase 8: Integration Tests
- [x] `tests/integration/test-market.mjs` — GET /market endpoint validation
- [x] `tests/integration/test-price-discovery.mjs` — two-board price discovery
- [x] Add Makefile targets for new tests

### Phase 9: ESP-IDF Build
- [x] Fix format specifiers (`%u` → `%lu` + cast for `uint32_t` on xtensa)
- [x] Copy local-only files (`display.c/h`, `font.c/h`) to worktree
- [x] Apply nucula `save_proofs()` private→public patch
- [x] `idf.py build` succeeds
- [x] Symlink missing components (`axs15231b`, `qrcode`) from main repo

### Phase 10: Hardware Mutex (per-board, shared across worktrees)
- [x] Rewrite Makefile with per-board locks (`lock-a`, `lock-b`, `unlock-a`, `unlock-b`)
- [x] Shared `LOCK_DIR` at `/home/c03rad0r/physical-router-test-automation/locks`
- [x] `require_lock_a` / `require_lock_b` macros
- [x] `acquire_lock` function macro (same pattern as `physical-router-test-automation/esp32/Makefile`)
- [x] Per-board flash/monitor/reset/serial-log/erase-nvs targets
- [x] `connect-a` / `connect-b` / `disconnect` WiFi targets
- [x] `_connect-a-if-needed` / `_connect-b-if-needed` auto-connect helpers
- [x] All integration tests require `lock-a` + `_connect-a-if-needed`
- [x] `test-price-discovery` requires both `lock-a` AND `lock-b`
- [x] Board port mapping matches `boards.env` (A=ACM1, B=ACM2)
- [x] Remove old single-lock `hardware.lock` from `.gitignore`

### Phase 11: Debugging & Hardening
- [x] Add WiFi disconnect reason code to log output (`tollgate_main.c:58`)
- [x] Add `esp_wifi_set_country_code("DE")` — was missing, defaults to CN
- [x] Commit both fixes to `feature/price-discovery`
- [x] Document findings in `SESSION_NOTES.md`

### Final
- [x] `make test-unit` passes (all 13 test suites, 45 new assertions)
- [x] `idf.py build` passes (ESP32-S3 firmware)
- [x] Commit to `feature/price-discovery` branch
- [x] Hardware flash + integration test on Board B (`test-market`: 4 passed, 0 failed)
- [x] Hardware flash + integration test on Board A (`test-market`: 4 passed, 0 failed)
- [ ] Two-board price discovery test (`test-price-discovery`) — blocked by WiFi STA issue (reason=211 NO_AP_FOUND)
- [ ] Merge to master

## Blockers

### WiFi STA Connectivity (reason=211)
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.

### Multi-Session Hardware Conflict
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.