upleb.uk

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

summaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/MULTI_MINT_DESIGN.md511
-rw-r--r--docs/REBASE-SQUASH-MERGE-PLAN.md92
-rw-r--r--docs/WPA-AUTO-DETECT-PLAN.md121
3 files changed, 724 insertions, 0 deletions
diff --git a/docs/MULTI_MINT_DESIGN.md b/docs/MULTI_MINT_DESIGN.md
new file mode 100644
index 0000000..f4db06b
--- /dev/null
+++ b/docs/MULTI_MINT_DESIGN.md
@@ -0,0 +1,511 @@
1# Multi-Mint Support — Design Document
2
3**Branch**: `feature/multi-mint-support`
4**Date**: 2026-05-18
5**Status**: Implementation Phase
6
7---
8
9## 1. Overview
10
11Extend the ESP32 TollGate firmware to accept Cashu ecash payments from **multiple mints** instead of a single hardcoded mint URL. The system must:
12
13- Accept tokens from any of 4 configured mints
14- Track mint reachability via periodic health probes
15- Only accept payments from mints that are currently reachable (successful swap)
16- Expose all reachable mints in the discovery endpoint and captive portal
17- Manage per-mint wallets with independent keysets and proof storage
18
19### Supported Mints
20
21| Mint | URL |
22|------|-----|
23| Minibits | `https://mint.minibits.cash/Bitcoin` |
24| CoinOS | `https://mint.coinos.io` |
25| 21mint | `https://21mint.me` |
26| LNVoltz | `https://mint.lnvoltz.com` |
27
28All verified reachable via `GET /v1/info` (HTTP 200).
29
30---
31
32## 2. Architecture
33
34```
35┌─────────────────────────────────────────────────────┐
36│ config.json │
37│ "accepted_mints": ["url1", "url2", "url3", "url4"] │
38└──────────────────────┬──────────────────────────────┘
39
40 ┌────────────┼────────────────┐
41 ▼ ▼ ▼
42 ┌──────────┐ ┌──────────┐ ┌───────────────┐
43 │ Config │ │ Health │ │ Multi-Wallet │
44 │ Layer │ │ Tracker │ │ (Nucula) │
45 │ │ │ │ │ │
46 │ accepted_ │ │ probe │ │ Wallet[0] → │
47 │ mints[] │ │ every │ │ mint A │
48 │ │ │ 5min │ │ Wallet[1] → │
49 │ │ │ │ │ mint B │
50 │ │ │ recovery │ │ ... │
51 │ │ │ thresh=3 │ │ │
52 └─────┬─────┘ └────┬─────┘ └───────┬───────┘
53 │ │ │
54 ▼ ▼ ▼
55 ┌─────────────────────────────────────────────────┐
56 │ cashu_is_mint_accepted() │
57 │ in config AND reachable → accept │
58 └────────────────────┬────────────────────────────┘
59
60 ┌─────────────┼──────────────┐
61 ▼ ▼ ▼
62 ┌──────────┐ ┌───────────┐ ┌───────────┐
63 │Discovery │ │ Captive │ │ Payment │
64 │ Endpoint │ │ Portal │ │ Handler │
65 │ │ │ │ │ │
66 │ 1 tag │ │ mint list │ │ find right│
67 │ per │ │ with │ │ wallet, │
68 │ reachable│ │ indicators│ │ receive() │
69 │ mint │ │ │ │ │
70 └──────────┘ └───────────┘ └───────────┘
71```
72
73---
74
75## 3. Phase Details
76
77### Phase 1: Config Layer — Multi-Mint Array
78
79**Files**: `main/config.h`, `main/config.c`
80
81**Changes**:
82
83- Increase `TOLLGATE_MAX_MINT_URLS` from `3` to `8`
84- Add to `tollgate_config_t`:
85 ```c
86 char accepted_mints[TOLLGATE_MAX_MINT_URLS][256];
87 int accepted_mint_count;
88 ```
89- Keep existing `mint_url[256]` for backward compatibility
90- Parse new `"accepted_mints"` JSON array from config.json
91- If `"accepted_mints"` absent, populate from `"mint_url"` (backward compat)
92- Update default config.json generation to include `"accepted_mints"`
93
94**Config.json format** (new):
95```json
96{
97 "nsec": "...",
98 "accepted_mints": [
99 "https://mint.minibits.cash/Bitcoin",
100 "https://mint.coinos.io",
101 "https://21mint.me",
102 "https://mint.lnvoltz.com"
103 ],
104 "mint_url": "https://mint.minibits.cash/Bitcoin"
105}
106```
107
108The `"mint_url"` field is kept as fallback / primary mint identifier.
109
110---
111
112### Phase 2: Mint Acceptance — Multi-Mint Check
113
114**Files**: `main/cashu.c`, `main/cashu.h`
115
116Replace single-mint check in `cashu_is_mint_accepted()`:
117
118```c
119bool cashu_is_mint_accepted(const char *mint_url) {
120 if (!mint_url || mint_url[0] == '\0') return false;
121 const tollgate_config_t *cfg = tollgate_config_get();
122 for (int i = 0; i < cfg->accepted_mint_count; i++) {
123 if (strstr(mint_url, cfg->accepted_mints[i]) != NULL)
124 return true;
125 }
126 return false;
127}
128```
129
130This is the config-only check. Phase 4 adds health gating.
131
132---
133
134### Phase 3: Mint Health Tracker
135
136**New files**: `main/mint_health.h`, `main/mint_health.c`
137
138**Data structures**:
139
140```c
141#define MINT_HEALTH_MAX 8
142#define MINT_HEALTH_PROBE_INTERVAL_S 300
143#define MINT_HEALTH_PROBE_TIMEOUT_MS 15000
144#define MINT_HEALTH_RECOVERY_THRESHOLD 3
145
146typedef struct {
147 char url[256];
148 bool reachable;
149 uint8_t consecutive_successes;
150 int64_t last_probe_ms;
151 int last_http_status;
152} mint_status_t;
153
154typedef void (*mint_health_changed_cb)(void);
155```
156
157**Public API**:
158
159```c
160esp_err_t mint_health_init(const char urls[][256], int count);
161void mint_health_start(void);
162void mint_health_stop(void);
163const mint_status_t *mint_health_get_all(int *out_count);
164bool mint_health_is_reachable(const char *url);
165void mint_health_mark_unreachable(const char *url);
166void mint_health_register_callback(mint_health_changed_cb cb);
167```
168
169**Probing logic** (FreeRTOS task):
170
171| Parameter | Value | Rationale |
172|-----------|-------|-----------|
173| Endpoint | `GET {url}/v1/info` | Lightweight, no auth required |
174| Timeout | 15 seconds | ESP32 resource-constrained, 30s too long |
175| Interval | 5 minutes (`vTaskDelay`) | Matches Go reference |
176| Failure | Immediate | Single failed probe → unreachable |
177| Recovery | 3 consecutive successes | 15 min sustained health (matches Go) |
178| Initial | Success → reachable immediately | Set `consecutive_successes = threshold` |
179
180**Thread safety**: Single FreeRTOS mutex protecting the status array. Callbacks dispatched after releasing the mutex.
181
182**Reference**: Modeled after Go `MintHealthTracker` in `tollgate-module-basic-go/src/merchant/mint_health_tracker.go`.
183
184---
185
186### Phase 4: Health-Aware Acceptance
187
188**Files**: `main/cashu.c`
189
190Update `cashu_is_mint_accepted()` to gate on health:
191
192```c
193bool cashu_is_mint_accepted(const char *mint_url) {
194 if (!mint_url || mint_url[0] == '\0') return false;
195 const tollgate_config_t *cfg = tollgate_config_get();
196 for (int i = 0; i < cfg->accepted_mint_count; i++) {
197 if (strstr(mint_url, cfg->accepted_mints[i]) != NULL)
198 return mint_health_is_reachable(mint_url);
199 }
200 return false;
201}
202```
203
204On cold start with no internet: no mints reachable → no tokens accepted (matches Go degraded behavior). Once first probe succeeds, that mint becomes reachable and tokens are accepted.
205
206---
207
208### Phase 5: Multi-Mint Discovery Endpoint
209
210**File**: `main/tollgate_api.c`
211
212Replace single `price_per_step` tag in `api_get_discovery()` with one per reachable mint:
213
214```c
215int count;
216const mint_status_t *mints = mint_health_get_all(&count);
217for (int i = 0; i < count; i++) {
218 if (!mints[i].reachable) continue;
219 cJSON *price_tag = cJSON_CreateArray();
220 cJSON_AddItemToArray(price_tag, cJSON_CreateString("price_per_step"));
221 cJSON_AddItemToArray(price_tag, cJSON_CreateString("cashu"));
222 cJSON_AddItemToArray(price_tag, cJSON_CreateString(price_str));
223 cJSON_AddItemToArray(price_tag, cJSON_CreateString("sat"));
224 cJSON_AddItemToArray(price_tag, cJSON_CreateString(mints[i].url));
225 cJSON_AddItemToArray(price_tag, cJSON_CreateString("1"));
226 cJSON_AddItemToArray(tags, price_tag);
227}
228```
229
230If no mints are reachable, include a single tag with the primary `mint_url` as fallback (degraded mode signal).
231
232---
233
234### Phase 6: Multi-Mint Captive Portal UI
235
236**File**: `main/captive_portal.c`
237
238**Changes**:
239
2401. Replace `__MINT_URL__` template placeholder with `__MINT_LIST__`
2412. Generate HTML list of reachable mints with green dot indicators
2423. Unreachable mints shown greyed out (informative but not selectable)
2434. New API endpoint `GET /api/mints` → JSON array of mint status
244
245**Portal mint list HTML**:
246```html
247<div class="mints">
248 <div class="mints-title">SUPPORTED MINTS</div>
249 <div class="mint-item reachable">
250 <span class="mint-dot green"></span>
251 <span class="mint-url">mint.minibits.cash/Bitcoin</span>
252 </div>
253 <div class="mint-item unreachable">
254 <span class="mint-dot grey"></span>
255 <span class="mint-url">mint.coinos.io</span>
256 </div>
257</div>
258```
259
260**Auto-refresh**: JS polls `GET /api/mints` every 30s to update indicators.
261
262---
263
264### Phase 7: Multi-Mint Wallet (Nucula)
265
266**Files**: `components/nucula_lib/nucula_wallet.h`, `components/nucula_lib/nucula_wallet.cpp`
267
268**Approach**: Multi-wallet — one `cashu::Wallet` instance per mint.
269
270**Why multi-wallet vs refactoring Wallet class**:
271- Each mint has its own keysets, proofs, NVS slot — natural isolation
272- No risk of cross-mint proof confusion
273- `cashu::Wallet` class unchanged — zero regression risk
274- NVS slot allocation already supported: `Wallet(url, ctx, nvs_slot)`
275- `MAX_MINTS = 3` constant already defined in `wallet.hpp`
276
277**Internal structure**:
278```cpp
279static const int MAX_WALLETS = 4;
280static cashu::Wallet *s_wallets[MAX_WALLETS];
281static int s_wallet_count = 0;
282```
283
284**API changes**:
285
286| Old | New | Behavior |
287|-----|-----|----------|
288| `nucula_wallet_init(url)` | `nucula_wallet_init_multi(urls, count)` | Create wallet per mint |
289| `nucula_wallet_init(url)` | Keep as compat wrapper | Creates single-wallet array |
290| `nucula_wallet_receive(token)` | Same | Decode mint from token, route to correct wallet |
291| `nucula_wallet_balance()` | Same | Sum across all wallets |
292| `nucula_wallet_send(amount, ...)` | Same | Select wallet with sufficient balance |
293| `nucula_wallet_swap_all()` | Same | Swap all wallets |
294| `nucula_wallet_proof_count()` | Same | Sum across all wallets |
295
296**Token routing in `receive()`**:
2971. Decode token to extract `mint_url` from the token JSON
2982. Find matching wallet by URL
2993. Call `wallet->receive(token, proofs_out)` on that wallet
3004. If no matching wallet found, try first wallet as fallback
301
302**NVS slot mapping**:
303
304| Mint index | NVS slot | NVS keys |
305|-----------|----------|----------|
306| 0 | 0 | `url_0`, `proofs_0`, `kn_0`, `k_0_0`..`k_0_9` |
307| 1 | 1 | `url_1`, `proofs_1`, `kn_1`, `k_1_0`..`k_1_9` |
308| 2 | 2 | `url_2`, `proofs_2`, `kn_2`, `k_2_0`..`k_2_9` |
309| 3 | 3 | `url_3`, `proofs_3`, `kn_3`, `k_3_0`..`k_3_9` |
310
311---
312
313### Phase 8: Service Startup Integration
314
315**File**: `main/tollgate_main.c`
316
317**Changes to `start_services()`**:
318
319```
3201. firewall_init()
3212. session_manager_init()
3223. mint_health_init(cfg->accepted_mints, cfg->accepted_mint_count)
3234. mint_health_start() ← async probing begins
3245. nucula_wallet_init_multi(cfg->accepted_mints, cfg->accepted_mint_count)
3256. lightning_payout_init()
3267. dns_server_start()
3278. captive_portal_start()
3289. tollgate_api_start()
32910. wifistr_publish()
33011. cvm_server_start()
331```
332
333**Health callback**: When reachable set changes, trigger wifistr re-publish to update Nostr kind 38787 event with current mint list.
334
335---
336
337## 4. Data Flow
338
339### Payment Flow (Multi-Mint)
340
341```
342Client POST cashuA token
343
344
345api_post_payment()
346 ├── cashu_decode_token() → extract mint_url from token
347 ├── cashu_is_mint_accepted(mint_url)
348 │ ├── Check in cfg->accepted_mints[] → config match
349 │ └── Check mint_health_is_reachable(mint_url) → health gate
350 ├── cashu_check_proof_states(mint_url, token) → POST {mint_url}/v1/checkstate
351 ├── session_create(client_ip, allotment)
352 └── nucula_wallet_receive(token_str)
353 ├── Decode token → extract mint_url
354 ├── Find wallet for that mint_url
355 └── wallet->receive(token, proofs_out)
356```
357
358### Health Probe Flow
359
360```
361mint_health_task (FreeRTOS, 5min interval)
362
363 for each mint in accepted_mints[]:
364
365 ├── GET {url}/v1/info (15s timeout)
366
367 ├── Success?
368 │ ├── YES → consecutive_successes++
369 │ │ if >= RECOVERY_THRESHOLD → mark reachable
370 │ └── NO → mark unreachable, reset consecutive_successes = 0
371
372 └── Reachable set changed? → fire callback
373```
374
375---
376
377## 5. Error Handling
378
379| Scenario | Behavior |
380|----------|----------|
381| No internet at boot | No mints reachable, no tokens accepted until probe succeeds |
382| All mints unreachable | Discovery shows primary mint (degraded), portal shows "Checking mints..." |
383| Mint goes down mid-operation | `cashu_check_proof_states` fails → 502 Bad Gateway to client |
384| Wallet init fails for one mint | Skip that mint, log error, continue with others |
385| NVS full for multi-wallet | Fallback to single wallet, log warning |
386| Probe timeout | Treat as unreachable (same as connection refused) |
387
388---
389
390## 6. Memory Budget
391
392| Component | Estimated RAM | Notes |
393|-----------|--------------|-------|
394| `mint_status_t[8]` | ~2 KB | 256-byte URLs + metadata |
395| Health probe task stack | 8 KB | HTTP client needs stack |
396| `cashu::Wallet` per mint | ~4 KB each | Keysets + proofs in NVS, not RAM |
397| 4 wallets total | ~16 KB | Within ESP32-S3 512KB SRAM budget |
398| Health task TLS | ~40 KB | esp_http_client TLS buffer |
399| **Total new overhead** | **~66 KB** | Acceptable with 512KB SRAM + 8MB PSRAM |
400
401---
402
403## 7. Testing Strategy
404
405### Unit Tests (host, `tests/unit/`)
406
407| Test File | Covers |
408|-----------|--------|
409| `test_cashu.c` | Multi-mint acceptance (config-only) |
410| `test_mint_health.c` | Health state machine, recovery, callbacks |
411| `test_config.c` | Config parsing of `accepted_mints` array |
412
413### Integration Tests (device)
414
4151. Flash to Board A, verify discovery shows multiple mints
4162. Send token from each mint, verify accepted
4173. Block one mint at firewall level, verify becomes unreachable
4184. Verify recovery after unblocking
419
420### E2E Tests (Playwright)
421
4221. Captive portal shows mint list with indicators
4232. Pay with token from mint A → success
4243. Pay with token from unreachable mint → error shown in portal
425
426---
427
428## 8. Risks and Mitigations
429
430| Risk | Likelihood | Impact | Mitigation |
431|------|-----------|--------|------------|
432| TLS memory pressure with 4 wallets | Medium | High | Each wallet shares single TLS context; only probe makes concurrent HTTP |
433| NVS key namespace collision | Low | High | Use distinct `nvs_slot` per wallet (0-3) |
434| Keyset loading OOM on multiple mints | Medium | Medium | Cap keysets per wallet at `MAX_KEYSETS=10` |
435| Health probe blocks other tasks | Low | Medium | Dedicated FreeRTOS task, low priority |
436| Backward compatibility break | Low | High | `mint_url` field still works as fallback |
437
438---
439
440## 9. Backward Compatibility
441
442- Existing `config.json` with only `"mint_url"` → works (populates `accepted_mints[0]` from it)
443- Existing SPIFFS images → no change needed
444- NVS data → compatible (single wallet stays at slot 0)
445- API endpoints → same paths, discovery just has more tags
446- Captive portal → same UI flow, more mints shown
447
448---
449
450## 10. Git Worktree Strategy
451
452Multiple LLM sessions work on this repo simultaneously. To avoid conflicts:
453
454### Setup
455
456```
457# Main worktree stays on master for other sessions
458git -C /home/c03rad0r/esp32-tollgate checkout master
459
460# Dedicated worktree for this feature
461git -C /home/c03rad0r/esp32-tollgate worktree add /home/c03rad0r/esp32-tollgate-multi-mint feature/multi-mint-support
462```
463
464### Worktree Locations
465
466| Path | Branch | Purpose |
467|------|--------|---------|
468| `/home/c03rad0r/esp32-tollgate` | `master` | Main worktree, shared with other sessions |
469| `/home/c03rad0r/esp32-tollgate-multi-mint` | `feature/multi-mint-support` | This feature's isolated workspace |
470
471### Conflict Avoidance Rules
472
473| Rule | Why |
474|------|-----|
475| All edits happen in `/home/c03rad0r/esp32-tollgate-multi-mint` | Other sessions keep their own checkout untouched |
476| Push after every green test | Other sessions can `git pull` to see progress |
477| Never modify `master` directly | Merge only when feature is complete and tested |
478| `git pull --rebase` before push | Avoid merge commits if others pushed to same branch |
479
480### Cleanup (after merge)
481
482```
483git -C /home/c03rad0r/esp32-tollgate worktree remove /home/c03rad0r/esp32-tollgate-multi-mint
484```
485
486---
487
488## 11. Implementation Checklist
489
490- [x] Create feature branch `feature/multi-mint-support`
491- [x] Write design document `docs/MULTI_MINT_DESIGN.md`
492- [x] Set up git worktree at `/home/c03rad0r/esp32-tollgate-multi-mint`
493- [x] Phase 1: Config layer (`config.h`, `config.c`) — multi-mint array
494- [x] Phase 2: Multi-mint acceptance (`cashu.c`) — iterate accepted_mints
495- [x] Phase 3: Mint health tracker (`mint_health.h`, `mint_health.c`) — FreeRTOS probing task
496- [x] Phase 4: Health-aware acceptance integration — gate on reachability
497- [x] Phase 5: Multi-mint discovery endpoint (`tollgate_api.c`) — one tag per reachable mint
498- [x] Phase 6: Multi-mint captive portal UI (`captive_portal.c`) — mint list with indicators
499- [x] Phase 7: Multi-mint wallet (`nucula_wallet.h`, `nucula_wallet.cpp`) — multi-wallet approach
500- [x] Phase 8: Service startup integration (`tollgate_main.c`) — init health + multi-wallet
501- [x] Unit tests: update `test_cashu.c` for multi-mint acceptance (14/14 pass)
502- [x] Unit tests: all 256 existing tests pass
503- [x] Build verification (ESP-IDF compiles cleanly, no errors)
504- [ ] Unit tests: `test_mint_health.c` — health state machine, recovery, callbacks
505- [ ] Flash Board A and verify multi-mint discovery
506- [ ] Flash Board B and verify multi-mint discovery
507- [ ] Payment test with token from each supported mint
508- [ ] Health probe test (verify reachable/unreachable transitions)
509- [ ] Captive portal multi-mint display verification
510- [ ] Push after every passing test (blocked: Nostr relay down)
511- [ ] Merge to master
diff --git a/docs/REBASE-SQUASH-MERGE-PLAN.md b/docs/REBASE-SQUASH-MERGE-PLAN.md
new file mode 100644
index 0000000..f4bd98f
--- /dev/null
+++ b/docs/REBASE-SQUASH-MERGE-PLAN.md
@@ -0,0 +1,92 @@
1# Multi-Mint Support — Rebase, Backup, Squash & Merge Plan
2
3## Goal
4Rebase `feature/multi-mint-support` onto `master`, create a backup branch, squash all 20 commits into one clean commit, then merge to master.
5
6## Current State
7- **Branch**: `feature/multi-mint-support` in worktree `/home/c03rad0r/esp32-tollgate-multi-mint`
8- **Commits on branch**: 20 (since `master` at `77031f0`)
9- **Remote**: `origin` → Nostr relay `relay.ngit.dev` (currently down)
10- **Worktree**: shared repo — other sessions use other worktrees on different branches
11
12## Procedure
13
14### Phase 1: Pre-flight
151. Verify working tree is clean (no uncommitted changes)
162. Verify build passes
173. Verify unit tests pass (75/75)
18
19### Phase 2: Backup
204. Create backup branch `backup/multi-mint-support-pre-rebase` at current HEAD
215. Create backup branch `backup/multi-mint-support-pre-squash` (same point, used after rebase)
22
23### Phase 3: Rebase
246. `git rebase master` — rebase all 20 commits onto master
257. Resolve any conflicts
268. Verify build + tests still pass after rebase
27
28### Phase 4: Post-rebase Backup
299. Create `backup/multi-mint-support-rebased` at the rebased HEAD
3010. This preserves every individual commit even after squashing
31
32### Phase 5: Squash
3311. `git reset --soft master` — soft reset to master, keeping all changes staged
3412. `git commit -m "feat: multi-mint Cashu wallet with health tracking, WPA auto-detect, CVM"` — single clean commit
3513. Verify build + tests pass after squash
36
37### Phase 6: Merge
3814. Merge to master (fast-forward since squashed branch sits on top)
3915. Verify master builds and tests pass
40
41## Checklist
42
43### Pre-flight
44- [ ] Working tree clean
45- [ ] Build passes (`idf.py build`)
46- [ ] Unit tests pass (`make test-unit`)
47
48### Backup
49- [ ] `backup/multi-mint-support-pre-rebase` created at current HEAD (`3aa372c`)
50
51### Rebase
52- [ ] `git rebase master` completed
53- [ ] Conflicts resolved (if any)
54- [ ] Build passes after rebase
55- [ ] Unit tests pass after rebase
56
57### Post-rebase Backup
58- [ ] `backup/multi-mint-support-rebased` created at rebased HEAD
59
60### Squash
61- [ ] `git reset --soft master` done
62- [ ] Single commit created with clean message
63- [ ] Build passes after squash
64- [ ] Unit tests pass after squash
65
66### Merge
67- [ ] Merged to master (fast-forward)
68- [ ] Master builds and tests pass
69- [ ] Worktree updated
70
71## Remaining Work After Merge
721. **Push to Nostr relay** — blocked until `relay.ngit.dev` recovers
732. **NVS keyset storage** — `ESP_ERR_NVS_NOT_ENOUGH_SPACE` errors; factory partition at `0x10000` limits NVS to 24KB. Options:
74 - Store keysets in SPIFFS instead of NVS
75 - Compress keyset data
76 - Only cache active keysets
773. **Board A crash** — hardware-specific (~50s uptime), not software. Possible causes:
78 - Bad power supply on QinHeng UART adapter (serial `5A84017819`)
79 - Failing flash chip on that ESP32-S3 board
80 - Swap physical boards between UART adapters to isolate
814. **Integration test WiFi stability** — `test-multi-mint-*` targets fail on early steps because WiFi disconnects during 30s probe wait. Fix: `_connect-b-if-needed` should run before each curl call
825. **Display AXS15231B `ESP_ERR_NO_MEM`** — SPI flush fails every ~1s (307KB PSRAM framebuffer). The `display_enabled` config field allows disabling, but proper fix needs:
83 - Reduce framebuffer (partial refresh instead of full-screen)
84 - Or use SPI DMA with larger chunk sizes
856. **Health probe recovery threshold** — 3 consecutive successes × 300s interval = 15min before a mint is marked reachable. Consider reducing `MINT_HEALTH_RECOVERY_THRESHOLD` to 1 for initial probes
867. **Makefile WPA auto-detect** — `detect-wpa-security` + `generate-spiffs` + `flash-spiffs-{a,b,c}` targets added to `physical-router-test-automation/esp32/Makefile`. Needs separate commit/merge there
87
88## Backup Branch Names
89| Branch | Purpose | Created At |
90|--------|---------|------------|
91| `backup/multi-mint-support-pre-rebase` | Full history before rebase | Before `git rebase master` |
92| `backup/multi-mint-support-rebased` | All 20 commits after rebase | After `git rebase master` |
diff --git a/docs/WPA-AUTO-DETECT-PLAN.md b/docs/WPA-AUTO-DETECT-PLAN.md
new file mode 100644
index 0000000..dbbc0c8
--- /dev/null
+++ b/docs/WPA-AUTO-DETECT-PLAN.md
@@ -0,0 +1,121 @@
1# WPA Auto-Detect + STA Connectivity Fix
2
3## Problem
4
5`config.c:322` hardcodes `WIFI_AUTH_WPA3_PSK` as the STA auth threshold. The home
6router (`EnterSSID-2.4GHz`) uses **WPA2**, so the ESP32 silently refuses
7association and never gets internet. This blocks health probes, real payments,
8and all downstream testing.
9
10Additionally, concurrent HTTP client connections at boot (wallet init + health probes
11+ CVM + wifistr) caused an lwip `mem_free` assertion crash.
12
13## Solution
14
15### 1. Runtime WPA Threshold (Firmware)
16
17Add `wifi_auth_mode` field to `tollgate_config_t`. Parse it from `config.json`
18as a string (`"WPA2"`, `"WPA3"`, `"WPA2_WPA3"`). Map to ESP-IDF
19`wifi_auth_mode_t` enum at runtime. Default to `WIFI_AUTH_WPA2_PSK` which
20accepts both WPA2 and WPA3 networks.
21
22### 2. Makefile Auto-Detect (Build Time)
23
24Add Makefile targets that scan WiFi with `nmcli`, detect WPA2 vs WPA3, and
25generate a SPIFFS image with the correct `wifi_auth_mode` baked into
26`config.json`.
27
28### 3. Reduced Probe Interval (Testing)
29
30Temporarily reduce `MINT_HEALTH_PROBE_INTERVAL_S` from 300 to 30 so health
31probes actually fire during short board uptime windows.
32
33### 4. Boot Sequence Stabilization
34
35- 3-second delay before starting services after IP obtained (DNS stabilization)
36- 5-second delay before initial health probes (DNS resolution readiness)
37
38## Files Changed
39
40| File | Change |
41|------|--------|
42| `main/config.h` | Add `wifi_auth_mode` field to `tollgate_config_t` |
43| `main/config.c` | Parse `wifi_auth_mode` from config.json; use it in `tollgate_config_get_wifi()` |
44| `main/mint_health.h` | Reduce probe interval 300 → 30 |
45| `main/mint_health.c` | Add 5s DNS stabilization delay before initial probes |
46| `main/tollgate_main.c` | Add 3s delay in services_start_task before starting services |
47| `physical-router-test-automation/esp32/Makefile` | Add `detect-wpa-security`, `generate-spiffs`, `flash-spiffs-{a,b,c}` targets |
48
49## Hardware Verification (Board A, 2026-05-19)
50
51### STA Connectivity
52- `STA auth threshold: WPA2 → 3` confirmed in serial log
53- `Got IP:192.168.2.16, GW:192.168.2.1` — connected to home router via WPA2
54- SNTP time sync started
55- No lwip crashes
56
57### Health Probes
58- `Initial probe OK: https://mint.minibits.cash/Bitcoin (reachable)`
59- `Initial probe OK: https://mint.coinos.io (reachable)`
60- `Initial probe OK: https://21mint.me (reachable)`
61- `Initial probe OK: https://mint.lnvoltz.com (reachable)`
62- All 4 accepted mints confirmed reachable via `GET /v1/info`
63
64### API Endpoints
65- `GET /:2121` (discovery) — kind=10021, metric=milliseconds, only reachable mint in price_per_step tag
66- `GET /mints` — 4 mints with boolean `reachable` field (3 false, 1 true initially)
67- `GET /wallet` — balance=0, proof_count=0
68- `GET /usage` — returns data
69- `GET /whoami` — ip + mac
70
71### Multi-Wallet
72- 4/4 wallets initialized with real keysets from live mints
73- Keyset load confirmed for minibits, coinos, 21mint, lnvoltz
74- NVS save errors for some keysets (ESP_ERR_NVS_NOT_ENOUGH_SPACE) — non-critical
75
76## Checklist
77
78### Firmware Changes
79- [x] Add `wifi_auth_mode` string field (16 bytes) to `tollgate_config_t` in `config.h`
80- [x] Parse `wifi_auth_mode` from `config.json` in `config.c` with default `"WPA2"`
81- [x] Map `wifi_auth_mode` string to `wifi_auth_mode_t` in `tollgate_config_get_wifi()`
82- [x] Remove hardcoded `WIFI_AUTH_WPA3_PSK` at `config.c:322`
83- [x] Reduce `MINT_HEALTH_PROBE_INTERVAL_S` from 300 to 30 in `mint_health.h`
84- [x] Add boot sequence delays to prevent lwip crash
85
86### Makefile Auto-Detect
87- [x] Add `detect-wpa-security` target (nmcli scan → extract WPA mode for SSID)
88- [x] Add `generate-spiffs` target (create config.json → spiffsgen.py)
89- [x] Add `flash-spiffs-a`, `flash-spiffs-b`, `flash-spiffs-c` targets
90- [ ] Wire `flash-{a,b,c}` to auto-generate SPIFFS before flashing (optional)
91
92### Build & Test
93- [x] Build firmware — `idf.py build` passes
94- [x] Unit tests pass — 75/75 (61 + 14 mint_health)
95- [x] Wait for board unlock (no force-unlock) — Board A was available
96- [x] Lock board, flash firmware + SPIFFS
97- [x] Verify STA connects via serial (`Got IP:192.168.2.16`)
98- [x] Verify health probes fire and mints show `reachable: true`
99- [x] Run API endpoint tests (discovery, mints, wallet, usage, whoami)
100- [x] Run `make test-discovery-b`, `make test-mints-b`, `make test-multi-mint-b` — all pass
101- [x] All 4 mints confirmed reachable via health probes on Board B
102- [x] Discovery shows 4 `price_per_step` tags (one per reachable mint)
103- [x] Wallet has 40 sats balance from previous payment (proofs stored in NVS)
104- [ ] Test 6 previously-skipped scenarios (real payment, unreachable transition, etc.)
105
106### Commit
107- [x] Commit all changes with descriptive message (`2ad2ed4`)
108- [ ] Push when Nostr relay recovers (relay.ngit.dev still down)
109
110## Commits
111- `b387982` wip: disable display for stability testing
112- `d21fc93` docs: update WPA auto-detect plan with hardware verification results
113- `2ad2ed4` feat: WPA auto-detect, STA connectivity fix, lwip crash fix (feature/multi-mint-support)
114- `64e81b5` feat: WPA auto-detect SPIFFS generation + per-board flash targets (physical-router-test-automation)
115
116## Remaining Work
1171. Push commits when Nostr relay recovers
1182. Test 6 skipped scenarios with stable board (reachable↔unreachable transitions, real payment, etc.)
1193. Revert `MINT_HEALTH_PROBE_INTERVAL_S` from 30 to 300 before production
1204. Address NVS `ESP_ERR_NVS_NOT_ENOUGH_SPACE` errors for keyset storage
1215. Investigate display `ESP_ERR_NO_MEM` errors (307KB PSRAM framebuffer)