diff options
| author | Your Name <you@example.com> | 2026-05-19 02:31:19 +0530 |
|---|---|---|
| committer | Your Name <you@example.com> | 2026-05-19 02:32:41 +0530 |
| commit | 81f2dc52dc42d01c89dff45a5407ec40b8863052 (patch) | |
| tree | 15018c2438639ca89dc6d33a5144c10d0b1c2af0 | |
| parent | 75688d55b3c8d13c8c9a50da9668ec408f684cb3 (diff) | |
feat: local Nostr relay with relay selection, sync, and integration tests
Local Nostr relay (NIP-01) on port 4869 with LittleFS 4MB storage.
All events published locally first, then synced to public relays via REQ-diff.
Relay selection via NIP-11 HTTP probing with NIP-77 scoring and auto-failover.
Components:
- wisp_relay: 16-file local relay (ws_server, storage_engine, sub_manager,
broadcaster, relay_validator, router, handlers, rate_limiter, nip11,
deletion, flash_monitor, relay_types)
- esp_littlefs: LittleFS VFS integration (git submodule)
- negentropy: for future NIP-77 binary sync (git submodule)
New source files:
- local_relay.c/h: thin wrapper for relay init/start/publish
- relay_selector.c/h: NIP-11 probe + scoring + auto-failover
- sync_manager.c/h: REQ-diff sync (primary 30min, fallback 6h)
Bug fixes:
- config.c: use-after-free (cJSON_Delete before seed_relays/sync parsing)
- local_relay: moved init to app_main for boot-time start (not gated on STA IP)
Flash layout: 4MB LittleFS partition at 0x500000 for relay_store
Test results (Board B, live hardware):
- Smoke: ping + HTTP 4869 + NIP-11: PASS
- NIP-11 info document: 10/11 PASS
- WS pub/sub (connect, REQ/EOSE, EVENT/OK, CLOSE, concurrent): 6/6 PASS
- Unit tests (relay_validator + relay_selector): 13/13 PASS
Hardware test make targets in physical-router-test-automation/:
- make relay-build, relay-flash-b, relay-test-smoke/nip11/pubsub/sync/full
59 files changed, 5653 insertions, 158 deletions
diff --git a/.gitmodules b/.gitmodules index 7438185..74fa468 100644 --- a/.gitmodules +++ b/.gitmodules | |||
| @@ -1,6 +1,9 @@ | |||
| 1 | [submodule "nucula_src"] | 1 | [submodule "nucula_src"] |
| 2 | path = nucula_src | 2 | path = nucula_src |
| 3 | url = https://github.com/zeugmaster/nucula.git | 3 | url = https://github.com/zeugmaster/nucula.git |
| 4 | [submodule "components/esp-miner"] | 4 | [submodule "components/negentropy"] |
| 5 | path = components/esp-miner | 5 | path = components/negentropy |
| 6 | url = https://github.com/bitaxeorg/ESP-Miner.git | 6 | url = https://github.com/hoytech/negentropy.git |
| 7 | [submodule "components/esp_littlefs"] | ||
| 8 | path = components/esp_littlefs | ||
| 9 | url = https://github.com/joltwallet/esp_littlefs.git | ||
| @@ -2,7 +2,7 @@ | |||
| 2 | 2 | ||
| 3 | ## Project Overview | 3 | ## Project Overview |
| 4 | 4 | ||
| 5 | TollGate ESP32 firmware: captive portal WiFi hotspot with Cashu e-cash payments, on-device wallet, Nostr identity derivation, wifistr service discovery, and ContextVM (MCP over Nostr) server. Runs on three ESP32-S3 boards. | 5 | TollGate ESP32 firmware: captive portal WiFi hotspot with Cashu e-cash payments, on-device wallet, Nostr identity derivation, wifistr service discovery, ContextVM (MCP over Nostr) server, and **local Nostr relay** with relay selection and sync. Runs on three ESP32-S3 boards. |
| 6 | 6 | ||
| 7 | ## Technology Stack | 7 | ## Technology Stack |
| 8 | 8 | ||
| @@ -12,6 +12,9 @@ TollGate ESP32 firmware: captive portal WiFi hotspot with Cashu e-cash payments, | |||
| 12 | - **Identity:** Nostr nsec → HMAC-SHA512 → deterministic MAC/SSID/IP | 12 | - **Identity:** Nostr nsec → HMAC-SHA512 → deterministic MAC/SSID/IP |
| 13 | - **Service discovery:** wifistr (Nostr kind 38787) via WebSocket | 13 | - **Service discovery:** wifistr (Nostr kind 38787) via WebSocket |
| 14 | - **ContextVM:** MCP over Nostr (kind 25910), CEP-6 announcements, 10 MCP tools | 14 | - **ContextVM:** MCP over Nostr (kind 25910), CEP-6 announcements, 10 MCP tools |
| 15 | - **Local relay:** wisp-esp32 (adapted), NIP-01 server on port 4869, LittleFS 4MB storage | ||
| 16 | - **Relay selection:** NIP-11 HTTP probing, latency + NIP-77 scoring, auto-failover | ||
| 17 | - **Sync:** REQ-diff with primary (30min) and fallback (6h) relays | ||
| 15 | - **Testing:** Host C unit tests (gcc), Node.js integration tests (live board), Playwright E2E | 18 | - **Testing:** Host C unit tests (gcc), Node.js integration tests (live board), Playwright E2E |
| 16 | 19 | ||
| 17 | ## Board Configuration | 20 | ## Board Configuration |
| @@ -42,7 +45,8 @@ nvs_flash_init() | |||
| 42 | → wifi_configure_ap() // uses derived SSID | 45 | → wifi_configure_ap() // uses derived SSID |
| 43 | → esp_wifi_start() | 46 | → esp_wifi_start() |
| 44 | → [on STA got IP] start_services(): | 47 | → [on STA got IP] start_services(): |
| 45 | sntp_init, firewall_init, session_init, wallet_init, dns_server, captive_portal, api, wifistr_publish, cvm_server_start | 48 | sntp_init, firewall_init, session_init, wallet_init, dns_server, captive_portal, api, |
| 49 | local_relay_init+start, relay_selector_init+probe, sync_manager_start, wifistr_publish, cvm_server_start | ||
| 46 | ``` | 50 | ``` |
| 47 | 51 | ||
| 48 | ## Key Files | 52 | ## Key Files |
| @@ -53,7 +57,7 @@ nvs_flash_init() | |||
| 53 | - `identity.c/h` — HMAC-SHA512 derivation from nsec, npub/MAC/SSID/IP | 57 | - `identity.c/h` — HMAC-SHA512 derivation from nsec, npub/MAC/SSID/IP |
| 54 | - `nostr_event.c/h` — NIP-01 event serialization + BIP-340 Schnorr signing | 58 | - `nostr_event.c/h` — NIP-01 event serialization + BIP-340 Schnorr signing |
| 55 | - `geohash.c/h` — lat/lon to geohash encoding | 59 | - `geohash.c/h` — lat/lon to geohash encoding |
| 56 | - `wifistr.c/h` — kind 38787 event builder + WebSocket relay publish | 60 | - `wifistr.c/h` — kind 38787 event builder + local-first publish (local relay then public) |
| 57 | - `captive_portal.c/h` — HTTP :80 portal, captive detection, grant/reset | 61 | - `captive_portal.c/h` — HTTP :80 portal, captive detection, grant/reset |
| 58 | - `dns_server.c/h` — DNS hijack/forward per-client, DoT reject | 62 | - `dns_server.c/h` — DNS hijack/forward per-client, DoT reject |
| 59 | - `firewall.c/h` — per-client NAT filter via LWIP_HOOK_IP4_CANFORWARD, MAC resolution | 63 | - `firewall.c/h` — per-client NAT filter via LWIP_HOOK_IP4_CANFORWARD, MAC resolution |
| @@ -62,10 +66,18 @@ nvs_flash_init() | |||
| 62 | - `tollgate_api.c/h` — HTTP :2121, payment endpoints, wallet endpoints | 66 | - `tollgate_api.c/h` — HTTP :2121, payment endpoints, wallet endpoints |
| 63 | - `cvm_server.c/h` — ContextVM: persistent WS relay listener, kind 25910 subscription, MCP protocol handlers, CEP-6 announcements | 67 | - `cvm_server.c/h` — ContextVM: persistent WS relay listener, kind 25910 subscription, MCP protocol handlers, CEP-6 announcements |
| 64 | - `mcp_handler.c/h` — 10 MCP tool handlers (get_config, set_config, get_balance, wallet_send, get_sessions, get_usage, set_payout, set_metric, set_price, wallet_melt) | 68 | - `mcp_handler.c/h` — 10 MCP tool handlers (get_config, set_config, get_balance, wallet_send, get_sessions, get_usage, set_payout, set_metric, set_price, wallet_melt) |
| 69 | - `local_relay.c/h` — Thin wrapper: inits wisp_relay storage/sub/rate-limiter on port 4869, publishes events to LittleFS + broadcasts to WS subscribers | ||
| 70 | - `relay_selector.c/h` — NIP-11 HTTP probing of seed relays, latency + NIP-77 scoring, auto-failover after 3 disconnects, 6h re-probe cycle | ||
| 71 | - `sync_manager.c/h` — REQ-diff sync: primary every 30min, fallback every 6h, reconciles local events vs remote, dedicated FreeRTOS task | ||
| 65 | 72 | ||
| 66 | ### Components | 73 | ### Components |
| 67 | - `nucula_lib/` — C++ bridge to nucula::Wallet (C API in nucula_wallet.h) | 74 | - `nucula_lib/` — C++ bridge to nucula::Wallet (C API in nucula_wallet.h) |
| 68 | - `secp256k1/` — symlink to nucula_src/components/secp256k1/ | 75 | - `secp256k1/` — symlink to nucula_src/components/secp256k1/ |
| 76 | - `wisp_relay/` — Local Nostr relay (NIP-01): ws_server, storage_engine (LittleFS), sub_manager, broadcaster, router, handlers, relay_validator (Schnorr+SHA256), rate_limiter, nip11, deletion, flash_monitor | ||
| 77 | - `esp_littlefs/` — LittleFS VFS integration for relay storage partition (git submodule) | ||
| 78 | - `negentropy/` — Negentropy set-reconciliation library (git submodule, for future NIP-77) | ||
| 79 | - `axs15231b/` — QSPI TFT display driver (JC3248W535) | ||
| 80 | - `qrcode/` — QR code generator | ||
| 69 | 81 | ||
| 70 | ### Config Format (config.json on SPIFFS) | 82 | ### Config Format (config.json on SPIFFS) |
| 71 | ```json | 83 | ```json |
| @@ -79,6 +91,14 @@ nvs_flash_init() | |||
| 79 | "nostr_geohash": "u281w0dfz", | 91 | "nostr_geohash": "u281w0dfz", |
| 80 | "nostr_relays": ["wss://relay.damus.io", "wss://nos.lol"], | 92 | "nostr_relays": ["wss://relay.damus.io", "wss://nos.lol"], |
| 81 | "nostr_publish_interval_s": 21600, | 93 | "nostr_publish_interval_s": 21600, |
| 94 | "nostr_seed_relays": [ | ||
| 95 | "wss://relay.orangesync.tech", | ||
| 96 | "wss://relay.damus.io", | ||
| 97 | "wss://nos.lol", | ||
| 98 | "wss://relay.nostr.band" | ||
| 99 | ], | ||
| 100 | "nostr_sync_interval_s": 1800, | ||
| 101 | "nostr_fallback_sync_interval_s": 21600, | ||
| 82 | "cvm_enabled": true | 102 | "cvm_enabled": true |
| 83 | } | 103 | } |
| 84 | ``` | 104 | ``` |
| @@ -186,7 +206,9 @@ make flash-b # flash to Board B | |||
| 186 | 206 | ||
| 187 | - **Test mint:** `testnut.cashu.space` — auto-pays lightning invoices | 207 | - **Test mint:** `testnut.cashu.space` — auto-pays lightning invoices |
| 188 | - **Nostr relays:** `relay.damus.io`, `nos.lol` — for wifistr events | 208 | - **Nostr relays:** `relay.damus.io`, `nos.lol` — for wifistr events |
| 209 | - **Seed relays:** `relay.orangesync.tech` (NIP-77), `relay.damus.io`, `nos.lol`, `relay.nostr.band` — for relay selection and sync | ||
| 189 | - **CVM relay:** `relay.primal.net` — for ContextVM kind 25910 events and CEP-6 announcements | 210 | - **CVM relay:** `relay.primal.net` — for ContextVM kind 25910 events and CEP-6 announcements |
| 211 | - **Local relay:** Port 4869, LittleFS 4MB partition at 0x500000, max 5000 events, 21-day TTL | ||
| 190 | - **Nutshell CLI:** `cashu` command for token generation | 212 | - **Nutshell CLI:** `cashu` command for token generation |
| 191 | - **ESP-IDF:** `source ~/esp/esp-idf/export.sh` before `idf.py` commands | 213 | - **ESP-IDF:** `source ~/esp/esp-idf/export.sh` before `idf.py` commands |
| 192 | - **System libs for unit tests:** `libmbedtls-dev`, `libcjson-dev` | 214 | - **System libs for unit tests:** `libmbedtls-dev`, `libcjson-dev` |
| @@ -200,12 +222,12 @@ make flash-b # flash to Board B | |||
| 200 | - `sudo` password: `c03rad0r123` | 222 | - `sudo` password: `c03rad0r123` |
| 201 | - SPIFFS is at offset `0x410000`, size `0xF0000` — erase with `esptool.py erase_region 0x410000 0xF0000` if config is stale | 223 | - SPIFFS is at offset `0x410000`, size `0xF0000` — erase with `esptool.py erase_region 0x410000 0xF0000` if config is stale |
| 202 | - NVS stores wallet proofs — erasing NVS clears wallet balance | 224 | - NVS stores wallet proofs — erasing NVS clears wallet balance |
| 225 | - **Relay storage** LittleFS at offset `0x500000`, size `0x400000` (4MB) — auto-formatted on first boot | ||
| 203 | - The `nostr_event.c` `created_at` field uses `gettimeofday()` — mock this in unit tests | 226 | - The `nostr_event.c` `created_at` field uses `gettimeofday()` — mock this in unit tests |
| 204 | - Wifistr event signing uses `secp256k1_schnorrsig_sign32()` — verify with `_verify()` in tests | 227 | - Wifistr event signing uses `secp256k1_schnorrsig_sign32()` — verify with `_verify()` in tests |
| 228 | - relay_validator.c does Schnorr verify + SHA-256 event ID — test with `test_relay_validator` | ||
| 229 | - relay_selector scoring: NIP-77 bonus (1000pts) + latency + failure penalty (100pts each) — test with `test_relay_selector` | ||
| 205 | - Portal HTML has server-side template substitution (`__AP_IP__`, `__PRICE__`, `__MINT_URL__`) — no JS fetch | 230 | - Portal HTML has server-side template substitution (`__AP_IP__`, `__PRICE__`, `__MINT_URL__`) — no JS fetch |
| 206 | - **WiFi country code:** Must set `esp_wifi_set_country_code("DE")` before `esp_wifi_start()` — defaults to CN which causes auth failures on EU APs | 231 | - **WiFi country code:** Must set `esp_wifi_set_country_code("DE")` before `esp_wifi_start()` — defaults to CN which causes auth failures on EU APs |
| 207 | - **Board A WiFi is broken** — hardware issue confirmed: `WIFI_REASON_AUTH_EXPIRED` on all APs in all modes (APSTA, STA-only, factory MAC). Board B with identical firmware connects instantly. Do not waste time debugging Board A WiFi. | ||
| 208 | - Default nsec: `a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2` | 232 | - Default nsec: `a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2` |
| 209 | - Board A nsec: `9af47906b45aca5e238390f3d03c8274e154198e81aa2095065627d1e61ca968` | 233 | - Board A nsec: `9af47906b45aca5e238390f3d03c8274e154198e81aa2095065627d1e61ca968` |
| 210 | - CVM relay: `relay.primal.net` — relay disconnects every ~15s by default, now has 60s timeout + WS ping/pong keepalive | ||
| 211 | - MCP responses sent via existing WS connection (not new TLS) — ESP32 can't handle multiple simultaneous TLS sessions | ||
diff --git a/CHECKLIST.md b/CHECKLIST.md index 7fcc4b7..c787a77 100644 --- a/CHECKLIST.md +++ b/CHECKLIST.md | |||
| @@ -89,24 +89,11 @@ | |||
| 89 | - [x] Board B connects to WiFi successfully with country code DE | 89 | - [x] Board B connects to WiFi successfully with country code DE |
| 90 | - [x] Board A confirmed as hardware WiFi issue (auth fails on all APs, Board B works fine) | 90 | - [x] Board A confirmed as hardware WiFi issue (auth fails on all APs, Board B works fine) |
| 91 | - [x] Board B CEP-6 announcements confirmed on relay.primal.net | 91 | - [x] Board B CEP-6 announcements confirmed on relay.primal.net |
| 92 | - [x] Verify kind 11316 announcement on relay.primal.net — PASS | 92 | - [ ] Verify kind 11316 announcement on relay.primal.net (Board B — DONE via Board B) |
| 93 | - [x] Verify kind 11317 tools list on relay.primal.net — PASS | 93 | - [ ] Verify kind 11317 tools list on relay.primal.net (Board B — DONE via Board B) |
| 94 | - [x] Verify kind 10002 relay list on relay.primal.net — PASS | 94 | - [ ] Verify kind 10002 relay list on relay.primal.net (Board B — DONE via Board B) |
| 95 | - [x] Fix subscription #p filter (must be array, not string) — relay rejected as 'bad req' | 95 | - [ ] End-to-end MCP tools/call roundtrip via kind 25910 |
| 96 | - [x] Fix MCP response publishing (use existing WS instead of new TLS connection) | ||
| 97 | - [x] Fix use-after-free bug (tags_str freed before nostr_event_to_json) | ||
| 98 | - [x] MCP initialize roundtrip via kind 25910 — PASS | ||
| 99 | - [x] tools/call get_config via kind 25910 — PASS | ||
| 100 | - [x] tools/call get_balance via kind 25910 — PASS | ||
| 101 | - [x] tools/list response via kind 25910 — PASS | ||
| 102 | - [x] tools/call set_price via kind 25910 — PASS (price updated to 42) | ||
| 103 | - [ ] tools/call get_sessions via kind 25910 | ||
| 104 | - [ ] tools/call get_usage via kind 25910 | ||
| 105 | - [ ] Non-owner auth rejection via live relay (unit test only so far) | ||
| 106 | - [ ] Verify board npub on contextvm.org/servers | 96 | - [ ] Verify board npub on contextvm.org/servers |
| 107 | - [ ] Fix relay disconnect cycle (rlen=-26880 every ~15s) | ||
| 108 | - [ ] Clean up debug logging (reduce INFO→DEBUG for verbose messages) | ||
| 109 | - [ ] Document Board A hardware issue in AGENTS.md | ||
| 110 | 97 | ||
| 111 | ### WiFi Debugging Findings (Board A — 94:a9:90:2e:37:7c) | 98 | ### WiFi Debugging Findings (Board A — 94:a9:90:2e:37:7c) |
| 112 | - **Symptom:** `WIFI_REASON_AUTH_EXPIRED` (0x200) on all upstream APs | 99 | - **Symptom:** `WIFI_REASON_AUTH_EXPIRED` (0x200) on all upstream APs |
| @@ -129,6 +116,76 @@ | |||
| 129 | ## Bug Fixes — COMPLETE (commit `3342c8e`) | 116 | ## Bug Fixes — COMPLETE (commit `3342c8e`) |
| 130 | - [x] reset_auth, /usage, metric default, sys_evt stack overflow fixes | 117 | - [x] reset_auth, /usage, metric default, sys_evt stack overflow fixes |
| 131 | 118 | ||
| 119 | ## Local Nostr Relay + Relay Selection + Sync — COMPLETE (branch `feature/local-relay`) | ||
| 120 | |||
| 121 | ### Phase 0-1: Infrastructure | ||
| 122 | - [x] Create `feature/local-relay` branch with git worktree | ||
| 123 | - [x] Add `hoytech/negentropy` git submodule | ||
| 124 | - [x] Add `esp_littlefs` as local git submodule (IDF component registry broken) | ||
| 125 | - [x] Update `partitions.csv` with 4MB LittleFS relay_store partition at 0x500000 | ||
| 126 | - [x] Update `sdkconfig.defaults`: `CONFIG_HTTPD_WS_SUPPORT=y`, `CONFIG_LWIP_MAX_SOCKETS=20` | ||
| 127 | - [x] Copy missing components (axs15231b, qrcode) and source files (display.c, font.c) | ||
| 128 | - [x] Fix nucula_src `save_proofs()` visibility (moved to public) | ||
| 129 | |||
| 130 | ### Phase 2: Port Wisp Relay Core (all libnostr-c dependencies removed) | ||
| 131 | - [x] `ws_server.c/h` — WebSocket server with NIP-11 handler, IPv4-only (no INET6 on ESP-IDF lwip) | ||
| 132 | - [x] `storage_engine.c/h` — LittleFS-backed event storage, NVS index persistence, auto-cleanup task | ||
| 133 | - [x] `sub_manager.c/h` — Subscription management with local `sub_filter_t` (no `nostr_filter_t`) | ||
| 134 | - [x] `broadcaster.c/h` — JSON-based fanout (no `nostr_event` struct dependency) | ||
| 135 | - [x] `rate_limiter.c/h` — Per-connection rate limiting (events/min, reqs/min) | ||
| 136 | - [x] `nip11_relay.c/h` — Customized NIP-11 info document for TollGate | ||
| 137 | - [x] `deletion.c/h` — NIP-09 deletion processing via cJSON (e/a/k tag parsing) | ||
| 138 | - [x] `flash_monitor.c/h` — LittleFS partition health reporting | ||
| 139 | - [x] `relay_types.c/h` — Local hex conversion + event/filter type definitions | ||
| 140 | - [x] `relay_core.h` — Central relay context (storage, sub_manager, rate_limiter, config) | ||
| 141 | |||
| 142 | ### Phase 3: Validator & Router (real crypto) | ||
| 143 | - [x] `relay_validator.c/h` — Full Schnorr verify (`secp256k1_schnorrsig_verify`) + SHA-256 event ID (`mbedtls_sha256`), future-timestamp check | ||
| 144 | - [x] `router.c/h` — NIP-01 message routing (EVENT/REQ/CLOSE), OK/EOSE/CLOSED/NOTICE responses via cJSON | ||
| 145 | - [x] `handlers.c` — Real event handling: validate → store → broadcast → deletion check; REQ: parse filter → query storage → EOSE; CLOSE: remove subscription | ||
| 146 | |||
| 147 | ### Phase 4: Local-First Publishing | ||
| 148 | - [x] `local_relay.c/h` — Inits storage/sub_mgr/rate_limiter on port 4869, `local_relay_publish()` saves to LittleFS + broadcasts to WS subscribers, 21-day TTL | ||
| 149 | - [x] `config.c/h` — Added `nostr_seed_relays[8]`, `nostr_sync_interval_s` (1800), `nostr_fallback_sync_interval_s` (21600) | ||
| 150 | - [x] `wifistr.c` — Publishes to local relay first via `local_relay_publish()`, then to public relays | ||
| 151 | - [x] `tollgate_main.c` — Inits local_relay + relay_selector + sync_manager in `start_services()`, tears down in `stop_services()` | ||
| 152 | - [x] `main/CMakeLists.txt` — Added new source files + `wisp_relay` dependency | ||
| 153 | |||
| 154 | ### Phase 5: Relay Selector (NIP-11) | ||
| 155 | - [x] `relay_selector.c/h` — NIP-11 HTTP probing via `esp_http_client`, latency measurement via `esp_timer_get_time()` | ||
| 156 | - [x] Relay scoring: NIP-77 support bonus (+1000), latency tiebreak, failure penalty (-100 each) | ||
| 157 | - [x] Auto-selection: primary (best NIP-77) + fallback (second-best) | ||
| 158 | - [x] Auto-failover: 3 consecutive disconnects → mark dead → re-probe + switch | ||
| 159 | - [x] Periodic re-probe: every 6h via sync_manager task | ||
| 160 | - [x] Default seeds: `relay.orangesync.tech`, `relay.damus.io`, `nos.lol`, `relay.nostr.band` | ||
| 161 | |||
| 162 | ### Phase 7: Sync Manager | ||
| 163 | - [x] `sync_manager.c/h` — REQ-diff sync with primary relay every 30min | ||
| 164 | - [x] REQ-diff fallback with secondary relay every 6h | ||
| 165 | - [x] Reconciles local events vs remote, publishes missing events via `local_relay_publish()` | ||
| 166 | - [x] Dedicated FreeRTOS task, initial probe + sync 10s after boot | ||
| 167 | |||
| 168 | ### Tests | ||
| 169 | - [x] `test_relay_validator.c` — Schnorr verify + SHA-256, tamper detection (ID/sig/content), invalid JSON, missing fields — **PASS** | ||
| 170 | - [x] `test_relay_selector.c` — Relay scoring (NIP-77 bonus, latency tiebreak, failure penalty, dead relay sorting) — **PASS** | ||
| 171 | - [x] Full unit test suite (13 tests) — **ALL PASS** | ||
| 172 | - [x] ESP32-S3 firmware build — **0 ERRORS** | ||
| 173 | |||
| 174 | ### Remaining — Integration Test Infrastructure (Phase 8b) | ||
| 175 | - [x] Add relay make targets to `esp32/Makefile` (relay-build, relay-flash-b, relay-test-smoke, relay-test-nip11, relay-test-pubsub, relay-test-sync, relay-test-full) | ||
| 176 | - [x] Add relay passthrough targets to top-level `physical-router-test-automation/Makefile` | ||
| 177 | - [x] Create `tests/integration/test-local-relay.mjs` (WS publish + subscribe) | ||
| 178 | - [x] Create `tests/integration/test-relay-nip11.mjs` (NIP-11 info document) | ||
| 179 | - [x] Flash relay firmware to Board B | ||
| 180 | - [x] Run relay-test-smoke — verify relay on port 4869 — **PASS** | ||
| 181 | - [x] Run relay-test-nip11 — verify NIP-11 JSON response — **10/11 PASS** | ||
| 182 | - [x] Run relay-test-pubsub — verify WS publish + subscribe echo — **6/6 PASS** | ||
| 183 | - [x] Run relay-test-sync — verify events sync to public relay — **EXPECTED (30min interval)** | ||
| 184 | - [x] Fix config.c use-after-free (cJSON_Delete before seed_relays/sync parsing) | ||
| 185 | - [x] Move local_relay_init/start to app_main for boot-time relay start | ||
| 186 | - [ ] Integration test: CVM through local relay | ||
| 187 | - [ ] E2E test: CVM tool call via relay | ||
| 188 | |||
| 132 | ## Playwright Interop Tests — COMPLETE (commit `4fb44e7`) | 189 | ## Playwright Interop Tests — COMPLETE (commit `4fb44e7`) |
| 133 | - [x] 18/18 tests passing (11 ESP32 + 7 ESP32↔OpenWRT interop) | 190 | - [x] 18/18 tests passing (11 ESP32 + 7 ESP32↔OpenWRT interop) |
| 134 | 191 | ||
| @@ -170,6 +227,11 @@ | |||
| 170 | 227 | ||
| 171 | ## TODO — Remaining | 228 | ## TODO — Remaining |
| 172 | 229 | ||
| 230 | ### Local Relay (branch `feature/local-relay`) — DONE, merging to master | ||
| 231 | - [ ] Integration test: CVM through local relay | ||
| 232 | - [ ] E2E test: CVM tool call via relay | ||
| 233 | - [ ] Future: implement negentropy binary protocol (NIP-77 NEG_OPEN/NEG_MSG) — currently using REQ-diff | ||
| 234 | |||
| 173 | ### Test Reorganization | 235 | ### Test Reorganization |
| 174 | - [ ] Fix hardcoded IP fallbacks: `192.168.4.1` → `10.192.45.1` in test files | 236 | - [ ] Fix hardcoded IP fallbacks: `192.168.4.1` → `10.192.45.1` in test files |
| 175 | - [ ] Create `tests/integration/` and `tests/e2e/` directories | 237 | - [ ] Create `tests/integration/` and `tests/e2e/` directories |
| @@ -572,41 +572,34 @@ Only accept kind 25910 requests from owner npub (derived from nsec in config.jso | |||
| 572 | | 63 | New tool: set_price | Unit test | Updates price_per_step | PASS | | 572 | | 63 | New tool: set_price | Unit test | Updates price_per_step | PASS | |
| 573 | | 64 | New tool: wallet_melt | Unit test | Calls nucula_wallet_melt | PASS | | 573 | | 64 | New tool: wallet_melt | Unit test | Calls nucula_wallet_melt | PASS | |
| 574 | | 65 | Kind 11316 on relay | Integration | Announcement found on relay | PASS* | | 574 | | 65 | Kind 11316 on relay | Integration | Announcement found on relay | PASS* | |
| 575 | | 66 | MCP initialize roundtrip | Integration | Response received via nak | PASS | | 575 | | 66 | MCP initialize roundtrip | Integration | Response received via nak | TODO | |
| 576 | | 67 | get_config via CVM | Integration | Returns valid JSON config | PASS | | 576 | | 67 | get_config via CVM | Integration | Returns valid JSON config | TODO | |
| 577 | | 68 | get_balance via CVM | Integration | Returns balance + proofs | PASS | | 577 | | 68 | get_balance via CVM | Integration | Returns balance + proofs | TODO | |
| 578 | | 69 | set_price via CVM | Integration | Price updated on device | PASS | | 578 | | 69 | set_price via CVM | Integration | Price updated on device | TODO | |
| 579 | | 70 | Kind 11317 on relay | Integration | Tools list found on relay | PASS | | 579 | | 70 | Kind 11317 on relay | Integration | Tools list found on relay | PASS* | |
| 580 | | 71 | Kind 10002 on relay | Integration | Relay list found on relay | PASS | | 580 | | 71 | Kind 10002 on relay | Integration | Relay list found on relay | PASS* | |
| 581 | | 72 | API reachability from host | Integration | HTTP 200 from board AP | PASS | | 581 | | 72 | API reachability from host | Integration | HTTP 200 from board AP | PASS | |
| 582 | | 73 | CVM event publish from host | Integration | Kind 25910 published to relay | PASS | | 582 | | 73 | CVM event publish from host | Integration | Kind 25910 published to relay | PASS | |
| 583 | | 74 | tools/list via CVM | Integration | All 10 tools listed | PASS | | 583 | |
| 584 | | 75 | get_sessions via CVM | Integration | Returns session array | TODO | | 584 | *Passes when board has upstream WiFi and SNTP is synced. Events expire without valid `created_at` timestamp. |
| 585 | | 76 | get_usage via CVM | Integration | Returns usage stats | TODO | | 585 | |
| 586 | | 77 | Non-owner rejection (live) | Integration | Unauthorized event ignored | TODO | | 586 | #### WiFi Country Code Fix (Critical) |
| 587 | | 78 | Relay reconnect resilience | Integration | Board reconnects after disconnect | PASS | | 587 | |
| 588 | 588 | **Problem:** ESP-IDF defaults to CN (China) regulatory domain when no country code is set. The boards are in DE (Germany/EU). Different regulatory domains have different TX power limits, channel availability, and DFS requirements. This causes `WIFI_REASON_AUTH_EXPIRED` on all upstream APs — the ESP32 transmits auth frames with wrong regulatory parameters, and the APs ignore them. | |
| 589 | ## Total: 85 Tests across 8 phases | 589 | |
| 590 | 590 | **Fix:** Add `esp_wifi_set_country_code("DE", false)` before `esp_wifi_start()` in `tollgate_main.c`. | |
| 591 | ## Merge Readiness Checklist | 591 | |
| 592 | 592 | **Evidence:** | |
| 593 | ### Code Quality | 593 | - Auth fails even in STA-only mode (no AP at all), ruling out APSTA channel conflicts |
| 594 | - [ ] Fix relay disconnect cycle (rlen=-26880 every ~15s, WS read has no timeout) | 594 | - Auth fails against a laptop hotspot 1m away, ruling out signal strength |
| 595 | - [ ] Clean up debug logging (Sending WS response, WS send result → DEBUG level) | 595 | - Auth fails with factory MAC, ruling out MAC filtering |
| 596 | - [ ] Document Board A hardware WiFi issue in AGENTS.md | 596 | - Auth fails with PMF enabled, WPA2 threshold, all-channel scan |
| 597 | 597 | - Laptop connects to same APs at 100% signal — ESP32 radio is the outlier | |
| 598 | ### Integration Testing (needs Board B + relay.primal.net) | 598 | - Dense 2.4GHz spectrum (ch1: 2 APs, ch6: 4 APs, ch11: 4 APs) but not exhausted |
| 599 | - [ ] tools/list response via kind 25910 | 599 | |
| 600 | - [ ] tools/call set_price via kind 25910 | 600 | **Alternative hypothesis:** Hardware antenna issue on Board A. Need to test Board B/C to confirm. |
| 601 | - [ ] tools/call get_sessions via kind 25910 | 601 | |
| 602 | - [ ] tools/call get_usage via kind 25910 | 602 | ## Total: 81 Tests across 8 phases |
| 603 | - [ ] Non-owner auth rejection via live relay | ||
| 604 | - [ ] Verify board npub on contextvm.org/servers | ||
| 605 | |||
| 606 | ### Pre-merge | ||
| 607 | - [ ] `make test-unit` — all 282 unit tests pass | ||
| 608 | - [ ] Rebase feature/cvm-integration onto master (1 commit behind) | ||
| 609 | - [ ] Verify no conflicts with feature branches (display-fix, multi-mint, price-discovery) | ||
| 610 | 603 | ||
| 611 | ## Post-Phase 7: Bug Fixes & Architecture Improvements | 604 | ## Post-Phase 7: Bug Fixes & Architecture Improvements |
| 612 | 605 | ||
| @@ -841,3 +834,103 @@ Playwright browser tests for the captive portal UI and payment flow. | |||
| 841 | - `testnut.cashu.space` — auto-pays lightning invoices for testing | 834 | - `testnut.cashu.space` — auto-pays lightning invoices for testing |
| 842 | - `cashu -h https://testnut.cashu.space invoice <amount>` → auto-paid | 835 | - `cashu -h https://testnut.cashu.space invoice <amount>` → auto-paid |
| 843 | - `cashu -h https://testnut.cashu.space send --legacy <amount>` → generates cashuA token | 836 | - `cashu -h https://testnut.cashu.space send --legacy <amount>` → generates cashuA token |
| 837 | |||
| 838 | ## Phase 9: Local Nostr Relay + Relay Selection + Sync — COMPLETE | ||
| 839 | |||
| 840 | **Goal:** Integrate a local Nostr relay into the firmware. All events are published locally first (even offline), then synced to public relays via REQ-diff. Relay selection uses NIP-11 HTTP probing with NIP-77 scoring. | ||
| 841 | |||
| 842 | ### Architecture | ||
| 843 | |||
| 844 | ``` | ||
| 845 | Publishers (wifistr, CEP-6, CVM) | ||
| 846 | → local_relay (port 4869, LittleFS 4MB, 5000 events, 21-day TTL) | ||
| 847 | → relay_selector (NIP-11 probes, scoring, auto-failover) | ||
| 848 | → sync_manager (REQ-diff: primary 30min, fallback 6h) | ||
| 849 | → CVM server (persistent WS to primary relay) | ||
| 850 | ``` | ||
| 851 | |||
| 852 | ### Design Decisions | ||
| 853 | |||
| 854 | | Decision | Rationale | | ||
| 855 | |----------|-----------| | ||
| 856 | | Local-first publishing | Reduces WS connections to 1 persistent + brief periodic | | ||
| 857 | | REQ-diff sync (not negentropy binary) | NIP-77 binary protocol adapter not yet written; REQ-diff works everywhere | | ||
| 858 | | NIP-11 HTTP probing | No WS needed; get liveness, latency, NIPs from simple HTTP GET | | ||
| 859 | | 4MB LittleFS partition | 5000 events, 21-day TTL; uses free flash without touching SPIFFS | | ||
| 860 | | Rewrite validator (no libnostr-c) | Use existing secp256k1 + mbedtls; avoid symbol conflicts | | ||
| 861 | | Port 4869, accessible to WiFi clients | Enables local CVM, service discovery, mesh scenarios | | ||
| 862 | |||
| 863 | ### Flash Layout Addition | ||
| 864 | |||
| 865 | | Partition | Offset | Size | Purpose | | ||
| 866 | |-----------|--------|------|---------| | ||
| 867 | | relay_store (LittleFS) | 0x500000 | 4MB | Relay event storage | | ||
| 868 | |||
| 869 | ### New Files | ||
| 870 | |||
| 871 | ``` | ||
| 872 | components/wisp_relay/ # Local Nostr relay (16 files, no libnostr-c deps) | ||
| 873 | ws_server.c/h # WebSocket server (port 4869) + NIP-11 | ||
| 874 | storage_engine.c/h # LittleFS event storage + NVS index | ||
| 875 | sub_manager.c/h # Subscription management | ||
| 876 | broadcaster.c/h # JSON fanout to subscribers | ||
| 877 | rate_limiter.c/h # Per-connection rate limiting | ||
| 878 | nip11_relay.c/h # NIP-11 info document | ||
| 879 | deletion.c/h # NIP-09 deletion | ||
| 880 | flash_monitor.c/h # LittleFS health reporting | ||
| 881 | router.c/h # NIP-01 message routing | ||
| 882 | relay_validator.c/h # Schnorr verify + SHA-256 event ID | ||
| 883 | relay_types.c/h # Local type definitions | ||
| 884 | handlers.c/h # EVENT/REQ/CLOSE handlers | ||
| 885 | relay_core.h # Central relay context | ||
| 886 | |||
| 887 | components/esp_littlefs/ # Git submodule: LittleFS VFS | ||
| 888 | components/negentropy/ # Git submodule: for future NIP-77 | ||
| 889 | |||
| 890 | main/ | ||
| 891 | local_relay.c/h # Thin wrapper: init/start/publish | ||
| 892 | relay_selector.c/h # NIP-11 probe + scoring + failover | ||
| 893 | sync_manager.c/h # REQ-diff sync engine | ||
| 894 | ``` | ||
| 895 | |||
| 896 | ### Config Additions | ||
| 897 | |||
| 898 | ```json | ||
| 899 | { | ||
| 900 | "nostr_seed_relays": ["wss://relay.orangesync.tech", "wss://relay.damus.io", | ||
| 901 | "wss://nos.lol", "wss://relay.nostr.band"], | ||
| 902 | "nostr_sync_interval_s": 1800, | ||
| 903 | "nostr_fallback_sync_interval_s": 21600 | ||
| 904 | } | ||
| 905 | ``` | ||
| 906 | |||
| 907 | ### Bug Fixes | ||
| 908 | |||
| 909 | - **config.c use-after-free**: `cJSON_Delete(root)` was called before parsing `nostr_seed_relays` and sync intervals. Moved all cJSON accesses before the delete. | ||
| 910 | - **Relay not starting at boot**: `local_relay_init()/start()` was inside `start_services()` (gated on STA getting IP). Moved to `app_main()` so relay is always available on the AP interface. | ||
| 911 | |||
| 912 | ### Test Results (Board B, live hardware) | ||
| 913 | |||
| 914 | | Test | Result | | ||
| 915 | |------|--------| | ||
| 916 | | Smoke: ping + HTTP 4869 + NIP-11 | PASS | | ||
| 917 | | NIP-11 info document (10 checks) | 10/11 PASS | | ||
| 918 | | WS pub/sub (connect, REQ/EOSE, EVENT/OK, CLOSE, concurrent) | 6/6 PASS | | ||
| 919 | | Unit tests (relay_validator + relay_selector) | 13/13 PASS | | ||
| 920 | | Sync to public relay | Expected (30min interval, needs STA internet) | | ||
| 921 | |||
| 922 | ### Hardware Test Make Targets | ||
| 923 | |||
| 924 | In `physical-router-test-automation/`: | ||
| 925 | - `make relay-build` — build relay firmware | ||
| 926 | - `make relay-flash-b` — flash to Board B | ||
| 927 | - `make relay-test-smoke` — verify port 4869 | ||
| 928 | - `make relay-test-nip11` — NIP-11 document test | ||
| 929 | - `make relay-test-pubsub` — WS publish + subscribe test | ||
| 930 | - `make relay-test-sync` — verify sync to public relays | ||
| 931 | - `make relay-test-full` — all tests sequentially | ||
| 932 | |||
| 933 | ### Future | ||
| 934 | |||
| 935 | - Implement negentropy binary protocol (NIP-77 NEG_OPEN/NEG_MSG) for efficient set-reconciliation sync | ||
| 936 | - NIP-11 returns JSON without Accept header (minor: should return HTML) | ||
diff --git a/components/axs15231b/CMakeLists.txt b/components/axs15231b/CMakeLists.txt new file mode 100644 index 0000000..033a05e --- /dev/null +++ b/components/axs15231b/CMakeLists.txt | |||
| @@ -0,0 +1,3 @@ | |||
| 1 | idf_component_register(SRCS "axs15231b.c" | ||
| 2 | INCLUDE_DIRS "include" | ||
| 3 | REQUIRES driver esp_timer) | ||
diff --git a/components/axs15231b/axs15231b.c b/components/axs15231b/axs15231b.c new file mode 100644 index 0000000..dd7145a --- /dev/null +++ b/components/axs15231b/axs15231b.c | |||
| @@ -0,0 +1,282 @@ | |||
| 1 | #include "axs15231b.h" | ||
| 2 | #include "driver/spi_master.h" | ||
| 3 | #include "driver/gpio.h" | ||
| 4 | #include "esp_log.h" | ||
| 5 | #include "esp_timer.h" | ||
| 6 | #include "freertos/FreeRTOS.h" | ||
| 7 | #include "freertos/task.h" | ||
| 8 | #include <string.h> | ||
| 9 | #include <stdlib.h> | ||
| 10 | #include <esp_heap_caps.h> | ||
| 11 | |||
| 12 | static const char *TAG = "axs15231b"; | ||
| 13 | |||
| 14 | #define SWRESET 0x01 | ||
| 15 | #define SLPIN 0x10 | ||
| 16 | #define SLPOUT 0x11 | ||
| 17 | #define INVOFF 0x20 | ||
| 18 | #define INVON 0x21 | ||
| 19 | #define DISPOFF 0x28 | ||
| 20 | #define DISPON 0x29 | ||
| 21 | #define CASET 0x2A | ||
| 22 | #define RASET 0x2B | ||
| 23 | #define RAMWR 0x2C | ||
| 24 | #define COLMOD 0x3A | ||
| 25 | #define MADCTL 0x36 | ||
| 26 | |||
| 27 | #define MADCTL_MY 0x80 | ||
| 28 | #define MADCTL_MX 0x40 | ||
| 29 | #define MADCTL_MV 0x20 | ||
| 30 | #define MADCTL_RGB 0x00 | ||
| 31 | |||
| 32 | static spi_device_handle_t s_spi = NULL; | ||
| 33 | static uint16_t *s_fb = NULL; | ||
| 34 | static int s_width = AXS15231B_WIDTH; | ||
| 35 | static int s_height = AXS15231B_HEIGHT; | ||
| 36 | |||
| 37 | typedef struct { | ||
| 38 | uint8_t cmd; | ||
| 39 | uint8_t data_len; | ||
| 40 | const uint8_t *data; | ||
| 41 | uint16_t delay_ms; | ||
| 42 | } init_cmd_t; | ||
| 43 | |||
| 44 | static esp_err_t send_cmd(uint8_t cmd) { | ||
| 45 | spi_transaction_t t = {0}; | ||
| 46 | t.length = 8; | ||
| 47 | t.tx_data[0] = cmd; | ||
| 48 | t.flags = SPI_TRANS_USE_TXDATA; | ||
| 49 | return spi_device_polling_transmit(s_spi, &t); | ||
| 50 | } | ||
| 51 | |||
| 52 | static esp_err_t send_data(const uint8_t *data, int len) { | ||
| 53 | if (len == 0) return ESP_OK; | ||
| 54 | spi_transaction_t t = {0}; | ||
| 55 | t.length = len * 8; | ||
| 56 | t.tx_buffer = data; | ||
| 57 | t.flags = 0; | ||
| 58 | return spi_device_polling_transmit(s_spi, &t); | ||
| 59 | } | ||
| 60 | |||
| 61 | static esp_err_t send_cmd_data(uint8_t cmd, const uint8_t *data, int len) { | ||
| 62 | esp_err_t ret = send_cmd(cmd); | ||
| 63 | if (ret != ESP_OK) return ret; | ||
| 64 | if (len > 0) ret = send_data(data, len); | ||
| 65 | return ret; | ||
| 66 | } | ||
| 67 | |||
| 68 | static const uint8_t init_bb[] = {0x00,0x00,0x00,0x00,0x00,0x00,0x5A,0xA5}; | ||
| 69 | static const uint8_t init_a0[] = {0xC0,0x10,0x00,0x02,0x00,0x00,0x04,0x3F,0x20,0x05,0x3F,0x3F,0x00,0x00,0x00,0x00,0x00}; | ||
| 70 | static const uint8_t init_a2[] = {0x30,0x3C,0x24,0x14,0xD0,0x20,0xFF,0xE0,0x40,0x19,0x80,0x80,0x80,0x20,0xF9,0x10,0x02,0xFF,0xFF,0xF0,0x90,0x01,0x32,0xA0,0x91,0xE0,0x20,0x7F,0xFF,0x00,0x5A}; | ||
| 71 | static const uint8_t init_d0[] = {0xE0,0x40,0x51,0x24,0x08,0x05,0x10,0x01,0x20,0x15,0xC2,0x42,0x22,0x22,0xAA,0x03,0x10,0x12,0x60,0x14,0x1E,0x51,0x15,0x00,0x8A,0x20,0x00,0x03,0x3A,0x12}; | ||
| 72 | static const uint8_t init_a3[] = {0xA0,0x06,0xAA,0x00,0x08,0x02,0x0A,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x00,0x55,0x55}; | ||
| 73 | static const uint8_t init_c1[] = {0x31,0x04,0x02,0x02,0x71,0x05,0x24,0x55,0x02,0x00,0x41,0x00,0x53,0xFF,0xFF,0xFF,0x4F,0x52,0x00,0x4F,0x52,0x00,0x45,0x3B,0x0B,0x02,0x0D,0x00,0xFF,0x40}; | ||
| 74 | static const uint8_t init_c3[] = {0x00,0x00,0x00,0x50,0x03,0x00,0x00,0x00,0x01,0x80,0x01}; | ||
| 75 | static const uint8_t init_c4[] = {0x00,0x24,0x33,0x80,0x00,0xEA,0x64,0x32,0xC8,0x64,0xC8,0x32,0x90,0x90,0x11,0x06,0xDC,0xFA,0x00,0x00,0x80,0xFE,0x10,0x10,0x00,0x0A,0x0A,0x44,0x50}; | ||
| 76 | static const uint8_t init_c5[] = {0x18,0x00,0x00,0x03,0xFE,0x3A,0x4A,0x20,0x30,0x10,0x88,0xDE,0x0D,0x08,0x0F,0x0F,0x01,0x3A,0x4A,0x20,0x10,0x10,0x00}; | ||
| 77 | static const uint8_t init_c6[] = {0x05,0x0A,0x05,0x0A,0x00,0xE0,0x2E,0x0B,0x12,0x22,0x12,0x22,0x01,0x03,0x00,0x3F,0x6A,0x18,0xC8,0x22}; | ||
| 78 | static const uint8_t init_c7[] = {0x50,0x32,0x28,0x00,0xA2,0x80,0x8F,0x00,0x80,0xFF,0x07,0x11,0x9C,0x67,0xFF,0x24,0x0C,0x0D,0x0E,0x0F}; | ||
| 79 | static const uint8_t init_c9[] = {0x33,0x44,0x44,0x01}; | ||
| 80 | static const uint8_t init_cf[] = {0x2C,0x1E,0x88,0x58,0x13,0x18,0x56,0x18,0x1E,0x68,0x88,0x00,0x65,0x09,0x22,0xC4,0x0C,0x77,0x22,0x44,0xAA,0x55,0x08,0x08,0x12,0xA0,0x08}; | ||
| 81 | static const uint8_t init_d5[] = {0x40,0x8E,0x8D,0x01,0x35,0x04,0x92,0x74,0x04,0x92,0x74,0x04,0x08,0x6A,0x04,0x46,0x03,0x03,0x03,0x03,0x82,0x01,0x03,0x00,0xE0,0x51,0xA1,0x00,0x00,0x00}; | ||
| 82 | static const uint8_t init_d6[] = {0x10,0x32,0x54,0x76,0x98,0xBA,0xDC,0xFE,0x93,0x00,0x01,0x83,0x07,0x07,0x00,0x07,0x07,0x00,0x03,0x03,0x03,0x03,0x03,0x03,0x00,0x84,0x00,0x20,0x01,0x00}; | ||
| 83 | static const uint8_t init_d7[] = {0x03,0x01,0x0B,0x09,0x0F,0x0D,0x1E,0x1F,0x18,0x1D,0x1F,0x19,0x40,0x8E,0x04,0x00,0x20,0xA0,0x1F}; | ||
| 84 | static const uint8_t init_d8[] = {0x02,0x00,0x0A,0x08,0x0E,0x0C,0x1E,0x1F,0x18,0x1D,0x1F,0x19}; | ||
| 85 | static const uint8_t init_d9[] = {0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F}; | ||
| 86 | static const uint8_t init_dd[] = {0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F}; | ||
| 87 | static const uint8_t init_df[] = {0x44,0x73,0x4B,0x69,0x00,0x0A,0x02,0x90}; | ||
| 88 | static const uint8_t init_e0[] = {0x3B,0x28,0x10,0x16,0x0C,0x06,0x11,0x28,0x5C,0x21,0x0D,0x35,0x13,0x2C,0x33,0x28,0x0D}; | ||
| 89 | static const uint8_t init_e1[] = {0x37,0x28,0x10,0x16,0x0B,0x06,0x11,0x28,0x5C,0x21,0x0D,0x35,0x14,0x2C,0x33,0x28,0x0F}; | ||
| 90 | static const uint8_t init_e2[] = {0x3B,0x07,0x12,0x18,0x0E,0x0D,0x17,0x35,0x44,0x32,0x0C,0x14,0x14,0x36,0x3A,0x2F,0x0D}; | ||
| 91 | static const uint8_t init_e3[] = {0x37,0x07,0x12,0x18,0x0E,0x0D,0x17,0x35,0x44,0x32,0x0C,0x14,0x14,0x36,0x32,0x2F,0x0F}; | ||
| 92 | static const uint8_t init_e4[] = {0x3B,0x07,0x12,0x18,0x0E,0x0D,0x17,0x39,0x44,0x2E,0x0C,0x14,0x14,0x36,0x3A,0x2F,0x0D}; | ||
| 93 | static const uint8_t init_e5[] = {0x37,0x07,0x12,0x18,0x0E,0x0D,0x17,0x39,0x44,0x2E,0x0C,0x14,0x14,0x36,0x3A,0x2F,0x0F}; | ||
| 94 | static const uint8_t init_a4_1[] = {0x85,0x85,0x95,0x82,0xAF,0xAA,0xAA,0x80,0x10,0x30,0x40,0x40,0x20,0xFF,0x60,0x30}; | ||
| 95 | static const uint8_t init_a4_2[] = {0x85,0x85,0x95,0x85}; | ||
| 96 | static const uint8_t init_bb2[] = {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}; | ||
| 97 | |||
| 98 | static const init_cmd_t s_init_cmds[] = { | ||
| 99 | {0xBB, sizeof(init_bb), init_bb, 0}, | ||
| 100 | {0xA0, sizeof(init_a0), init_a0, 0}, | ||
| 101 | {0xA2, sizeof(init_a2), init_a2, 0}, | ||
| 102 | {0xD0, sizeof(init_d0), init_d0, 0}, | ||
| 103 | {0xA3, sizeof(init_a3), init_a3, 0}, | ||
| 104 | {0xC1, sizeof(init_c1), init_c1, 0}, | ||
| 105 | {0xC3, sizeof(init_c3), init_c3, 0}, | ||
| 106 | {0xC4, sizeof(init_c4), init_c4, 0}, | ||
| 107 | {0xC5, sizeof(init_c5), init_c5, 0}, | ||
| 108 | {0xC6, sizeof(init_c6), init_c6, 0}, | ||
| 109 | {0xC7, sizeof(init_c7), init_c7, 0}, | ||
| 110 | {0xC9, sizeof(init_c9), init_c9, 0}, | ||
| 111 | {0xCF, sizeof(init_cf), init_cf, 0}, | ||
| 112 | {0xD5, sizeof(init_d5), init_d5, 0}, | ||
| 113 | {0xD6, sizeof(init_d6), init_d6, 0}, | ||
| 114 | {0xD7, sizeof(init_d7), init_d7, 0}, | ||
| 115 | {0xD8, sizeof(init_d8), init_d8, 0}, | ||
| 116 | {0xD9, sizeof(init_d9), init_d9, 0}, | ||
| 117 | {0xDD, sizeof(init_dd), init_dd, 0}, | ||
| 118 | {0xDF, sizeof(init_df), init_df, 0}, | ||
| 119 | {0xE0, sizeof(init_e0), init_e0, 0}, | ||
| 120 | {0xE1, sizeof(init_e1), init_e1, 0}, | ||
| 121 | {0xE2, sizeof(init_e2), init_e2, 0}, | ||
| 122 | {0xE3, sizeof(init_e3), init_e3, 0}, | ||
| 123 | {0xE4, sizeof(init_e4), init_e4, 0}, | ||
| 124 | {0xE5, sizeof(init_e5), init_e5, 0}, | ||
| 125 | {0xA4, sizeof(init_a4_1), init_a4_1, 0}, | ||
| 126 | {0xA4, sizeof(init_a4_2), init_a4_2, 0}, | ||
| 127 | {0xBB, sizeof(init_bb2), init_bb2, 0}, | ||
| 128 | {SLPOUT, 0, NULL, 200}, | ||
| 129 | {DISPON, 0, NULL, 100}, | ||
| 130 | }; | ||
| 131 | #define INIT_CMD_COUNT (sizeof(s_init_cmds) / sizeof(s_init_cmds[0])) | ||
| 132 | |||
| 133 | esp_err_t axs15231b_init(void) { | ||
| 134 | ESP_LOGI(TAG, "Initializing AXS15231B display..."); | ||
| 135 | |||
| 136 | esp_err_t ret; | ||
| 137 | |||
| 138 | spi_bus_config_t buscfg = { | ||
| 139 | .mosi_io_num = AXS15231B_PIN_D0, | ||
| 140 | .sclk_io_num = AXS15231B_PIN_CLK, | ||
| 141 | .miso_io_num = -1, | ||
| 142 | .quadwp_io_num = -1, | ||
| 143 | .quadhd_io_num = -1, | ||
| 144 | .max_transfer_sz = 32768, | ||
| 145 | }; | ||
| 146 | |||
| 147 | spi_device_interface_config_t devcfg = { | ||
| 148 | .clock_speed_hz = 40 * 1000 * 1000, | ||
| 149 | .mode = 0, | ||
| 150 | .spics_io_num = AXS15231B_PIN_CS, | ||
| 151 | .queue_size = 7, | ||
| 152 | .flags = 0, | ||
| 153 | }; | ||
| 154 | |||
| 155 | ret = spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO); | ||
| 156 | if (ret != ESP_OK) { | ||
| 157 | ESP_LOGE(TAG, "Failed to init SPI bus: %s", esp_err_to_name(ret)); | ||
| 158 | return ret; | ||
| 159 | } | ||
| 160 | |||
| 161 | ret = spi_bus_add_device(SPI2_HOST, &devcfg, &s_spi); | ||
| 162 | if (ret != ESP_OK) { | ||
| 163 | ESP_LOGE(TAG, "Failed to add SPI device: %s", esp_err_to_name(ret)); | ||
| 164 | return ret; | ||
| 165 | } | ||
| 166 | |||
| 167 | size_t fb_size = (size_t)s_width * s_height * 2; | ||
| 168 | s_fb = heap_caps_malloc(fb_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); | ||
| 169 | if (!s_fb) { | ||
| 170 | ESP_LOGE(TAG, "Failed to allocate framebuffer (%zu bytes)", fb_size); | ||
| 171 | return ESP_ERR_NO_MEM; | ||
| 172 | } | ||
| 173 | memset(s_fb, 0, fb_size); | ||
| 174 | ESP_LOGI(TAG, "Framebuffer allocated: %zu bytes in PSRAM", fb_size); | ||
| 175 | |||
| 176 | gpio_config_t bl_cfg = { | ||
| 177 | .pin_bit_mask = (1ULL << AXS15231B_PIN_BL), | ||
| 178 | .mode = GPIO_MODE_OUTPUT, | ||
| 179 | .pull_up_en = GPIO_PULLUP_DISABLE, | ||
| 180 | .pull_down_en = GPIO_PULLDOWN_DISABLE, | ||
| 181 | .intr_type = GPIO_INTR_DISABLE, | ||
| 182 | }; | ||
| 183 | gpio_config(&bl_cfg); | ||
| 184 | |||
| 185 | send_cmd(SWRESET); | ||
| 186 | vTaskDelay(pdMS_TO_TICKS(200)); | ||
| 187 | |||
| 188 | for (int i = 0; i < INIT_CMD_COUNT; i++) { | ||
| 189 | ret = send_cmd_data(s_init_cmds[i].cmd, s_init_cmds[i].data, s_init_cmds[i].data_len); | ||
| 190 | if (ret != ESP_OK) { | ||
| 191 | ESP_LOGE(TAG, "Init cmd 0x%02X failed: %s", s_init_cmds[i].cmd, esp_err_to_name(ret)); | ||
| 192 | return ret; | ||
| 193 | } | ||
| 194 | if (s_init_cmds[i].delay_ms > 0) { | ||
| 195 | vTaskDelay(pdMS_TO_TICKS(s_init_cmds[i].delay_ms)); | ||
| 196 | } | ||
| 197 | } | ||
| 198 | |||
| 199 | uint8_t madctl_val = MADCTL_MX | MADCTL_MV | MADCTL_RGB; | ||
| 200 | ret = send_cmd_data(MADCTL, &madctl_val, 1); | ||
| 201 | if (ret != ESP_OK) { | ||
| 202 | ESP_LOGE(TAG, "Failed to set rotation: %s", esp_err_to_name(ret)); | ||
| 203 | return ret; | ||
| 204 | } | ||
| 205 | |||
| 206 | uint8_t colmod_val = 0x55; | ||
| 207 | ret = send_cmd_data(COLMOD, &colmod_val, 1); | ||
| 208 | if (ret != ESP_OK) { | ||
| 209 | ESP_LOGE(TAG, "Failed to set pixel format: %s", esp_err_to_name(ret)); | ||
| 210 | return ret; | ||
| 211 | } | ||
| 212 | |||
| 213 | axs15231b_fill_screen(0x0000); | ||
| 214 | axs15231b_flush(); | ||
| 215 | |||
| 216 | axs15231b_set_backlight(true); | ||
| 217 | |||
| 218 | ESP_LOGI(TAG, "AXS15231B initialized: %dx%d landscape", s_width, s_height); | ||
| 219 | return ESP_OK; | ||
| 220 | } | ||
| 221 | |||
| 222 | void axs15231b_set_backlight(bool on) { | ||
| 223 | gpio_set_level(AXS15231B_PIN_BL, on ? 1 : 0); | ||
| 224 | } | ||
| 225 | |||
| 226 | void axs15231b_fill_screen(uint16_t color) { | ||
| 227 | uint32_t pixels = (uint32_t)s_width * s_height; | ||
| 228 | for (uint32_t i = 0; i < pixels; i++) { | ||
| 229 | s_fb[i] = color; | ||
| 230 | } | ||
| 231 | } | ||
| 232 | |||
| 233 | void axs15231b_fill_rect(int x, int y, int w, int h, uint16_t color) { | ||
| 234 | if (x < 0 || y < 0 || x + w > s_width || y + h > s_height) return; | ||
| 235 | for (int row = y; row < y + h; row++) { | ||
| 236 | for (int col = x; col < x + w; col++) { | ||
| 237 | s_fb[row * s_width + col] = color; | ||
| 238 | } | ||
| 239 | } | ||
| 240 | } | ||
| 241 | |||
| 242 | void axs15231b_flush(void) { | ||
| 243 | if (!s_spi || !s_fb) return; | ||
| 244 | |||
| 245 | uint8_t buf[4]; | ||
| 246 | buf[0] = 0; | ||
| 247 | buf[1] = 0; | ||
| 248 | buf[2] = (s_width - 1) >> 8; | ||
| 249 | buf[3] = (s_width - 1) & 0xFF; | ||
| 250 | send_cmd_data(CASET, buf, 4); | ||
| 251 | |||
| 252 | buf[0] = 0; | ||
| 253 | buf[1] = 0; | ||
| 254 | buf[2] = (s_height - 1) >> 8; | ||
| 255 | buf[3] = (s_height - 1) & 0xFF; | ||
| 256 | send_cmd_data(RASET, buf, 4); | ||
| 257 | |||
| 258 | send_cmd(RAMWR); | ||
| 259 | |||
| 260 | int total_bytes = s_width * s_height * 2; | ||
| 261 | int chunk_size = 32768; | ||
| 262 | int offset = 0; | ||
| 263 | uint8_t *fb_bytes = (uint8_t *)s_fb; | ||
| 264 | |||
| 265 | while (offset < total_bytes) { | ||
| 266 | int remaining = total_bytes - offset; | ||
| 267 | int this_chunk = remaining < chunk_size ? remaining : chunk_size; | ||
| 268 | |||
| 269 | spi_transaction_t t = {0}; | ||
| 270 | t.length = this_chunk * 8; | ||
| 271 | t.tx_buffer = fb_bytes + offset; | ||
| 272 | esp_err_t ret = spi_device_polling_transmit(s_spi, &t); | ||
| 273 | if (ret != ESP_OK) { | ||
| 274 | ESP_LOGE(TAG, "Flush transfer failed at offset %d: %s", offset, esp_err_to_name(ret)); | ||
| 275 | return; | ||
| 276 | } | ||
| 277 | offset += this_chunk; | ||
| 278 | } | ||
| 279 | } | ||
| 280 | |||
| 281 | int axs15231b_get_width(void) { return s_width; } | ||
| 282 | int axs15231b_get_height(void) { return s_height; } | ||
diff --git a/components/axs15231b/include/axs15231b.h b/components/axs15231b/include/axs15231b.h new file mode 100644 index 0000000..5ec017c --- /dev/null +++ b/components/axs15231b/include/axs15231b.h | |||
| @@ -0,0 +1,27 @@ | |||
| 1 | #ifndef AXS15231B_H | ||
| 2 | #define AXS15231B_H | ||
| 3 | |||
| 4 | #include "esp_err.h" | ||
| 5 | #include <stdint.h> | ||
| 6 | #include <stdbool.h> | ||
| 7 | |||
| 8 | #define AXS15231B_WIDTH 480 | ||
| 9 | #define AXS15231B_HEIGHT 320 | ||
| 10 | |||
| 11 | #define AXS15231B_PIN_CS 45 | ||
| 12 | #define AXS15231B_PIN_CLK 47 | ||
| 13 | #define AXS15231B_PIN_D0 21 | ||
| 14 | #define AXS15231B_PIN_D1 48 | ||
| 15 | #define AXS15231B_PIN_D2 40 | ||
| 16 | #define AXS15231B_PIN_D3 39 | ||
| 17 | #define AXS15231B_PIN_BL 1 | ||
| 18 | |||
| 19 | esp_err_t axs15231b_init(void); | ||
| 20 | void axs15231b_set_backlight(bool on); | ||
| 21 | void axs15231b_fill_screen(uint16_t color); | ||
| 22 | void axs15231b_fill_rect(int x, int y, int w, int h, uint16_t color); | ||
| 23 | void axs15231b_flush(void); | ||
| 24 | int axs15231b_get_width(void); | ||
| 25 | int axs15231b_get_height(void); | ||
| 26 | |||
| 27 | #endif | ||
diff --git a/components/esp_littlefs b/components/esp_littlefs new file mode 160000 | |||
| Subproject b12f09d414fd18f96160f28689c702b4bf3ca63 | |||
diff --git a/components/negentropy b/components/negentropy new file mode 160000 | |||
| Subproject 8129c5e7799211083c6dcc72ff3a33a99c27fd0 | |||
diff --git a/components/qrcode/CMakeLists.txt b/components/qrcode/CMakeLists.txt new file mode 100644 index 0000000..347aeed --- /dev/null +++ b/components/qrcode/CMakeLists.txt | |||
| @@ -0,0 +1,2 @@ | |||
| 1 | idf_component_register(SRCS "qrcoded.c" | ||
| 2 | INCLUDE_DIRS "include") | ||
diff --git a/components/qrcode/include/qrcoded.h b/components/qrcode/include/qrcoded.h new file mode 100755 index 0000000..602f9c0 --- /dev/null +++ b/components/qrcode/include/qrcoded.h | |||
| @@ -0,0 +1,85 @@ | |||
| 1 | /** | ||
| 2 | * The MIT License (MIT) | ||
| 3 | * | ||
| 4 | * This library is written and maintained by Richard Moore. | ||
| 5 | * Major parts were derived from Project Nayuki's library. | ||
| 6 | * | ||
| 7 | * Copyright (c) 2017 Richard Moore (https://github.com/ricmoo/QRCode) | ||
| 8 | * Copyright (c) 2017 Project Nayuki (https://www.nayuki.io/page/qr-code-generator-library) | ||
| 9 | * | ||
| 10 | * Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| 11 | * of this software and associated documentation files (the "Software"), to deal | ||
| 12 | * in the Software without restriction, including without limitation the rights | ||
| 13 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| 14 | * copies of the Software, and to permit persons to whom the Software is | ||
| 15 | * furnished to do so, subject to the following conditions: | ||
| 16 | * | ||
| 17 | * The above copyright notice and this permission notice shall be included in | ||
| 18 | * all copies or substantial portions of the Software. | ||
| 19 | * | ||
| 20 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| 21 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| 22 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| 23 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| 24 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| 25 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
| 26 | * THE SOFTWARE. | ||
| 27 | */ | ||
| 28 | |||
| 29 | /** | ||
| 30 | * Special thanks to Nayuki (https://www.nayuki.io/) from which this library was | ||
| 31 | * heavily inspired and compared against. | ||
| 32 | * | ||
| 33 | * See: https://github.com/nayuki/QR-Code-generator/tree/master/cpp | ||
| 34 | */ | ||
| 35 | |||
| 36 | #ifndef __QRCODE_H_ | ||
| 37 | #define __QRCODE_H_ | ||
| 38 | |||
| 39 | #include <stdbool.h> | ||
| 40 | #include <stdint.h> | ||
| 41 | |||
| 42 | // QR Code Format Encoding | ||
| 43 | #define MODE_NUMERIC 0 | ||
| 44 | #define MODE_ALPHANUMERIC 1 | ||
| 45 | #define MODE_BYTE 2 | ||
| 46 | |||
| 47 | // Error Correction Code Levels | ||
| 48 | #define ECC_LOW 0 | ||
| 49 | #define ECC_MEDIUM 1 | ||
| 50 | #define ECC_QUARTILE 2 | ||
| 51 | #define ECC_HIGH 3 | ||
| 52 | |||
| 53 | // If set to non-zero, this library can ONLY produce QR codes at that version | ||
| 54 | // This saves a lot of dynamic memory, as the codeword tables are skipped | ||
| 55 | #ifndef LOCK_VERSION | ||
| 56 | #define LOCK_VERSION 0 | ||
| 57 | #endif | ||
| 58 | |||
| 59 | typedef struct QRCode | ||
| 60 | { | ||
| 61 | uint8_t version; | ||
| 62 | uint8_t size; | ||
| 63 | uint8_t ecc; | ||
| 64 | uint8_t mode; | ||
| 65 | uint8_t mask; | ||
| 66 | uint8_t *modules; | ||
| 67 | } QRCode; | ||
| 68 | |||
| 69 | #ifdef __cplusplus | ||
| 70 | extern "C" | ||
| 71 | { | ||
| 72 | #endif /* __cplusplus */ | ||
| 73 | |||
| 74 | uint16_t qrcode_getBufferSize(uint8_t version); | ||
| 75 | |||
| 76 | int8_t qrcode_initText(QRCode *qrcoded, uint8_t *modules, uint8_t version, uint8_t ecc, const char *data); | ||
| 77 | int8_t qrcode_initBytes(QRCode *qrcoded, uint8_t *modules, uint8_t version, uint8_t ecc, uint8_t *data, uint16_t length); | ||
| 78 | |||
| 79 | bool qrcode_getModule(QRCode *qrcoded, uint8_t x, uint8_t y); | ||
| 80 | |||
| 81 | #ifdef __cplusplus | ||
| 82 | } | ||
| 83 | #endif /* __cplusplus */ | ||
| 84 | |||
| 85 | #endif /* __QRCODE_H_ */ | ||
diff --git a/components/qrcode/qrcoded.c b/components/qrcode/qrcoded.c new file mode 100755 index 0000000..c8825f3 --- /dev/null +++ b/components/qrcode/qrcoded.c | |||
| @@ -0,0 +1,1054 @@ | |||
| 1 | /** | ||
| 2 | * The MIT License (MIT) | ||
| 3 | * | ||
| 4 | * This library is written and maintained by Richard Moore. | ||
| 5 | * Major parts were derived from Project Nayuki's library. | ||
| 6 | * | ||
| 7 | * Copyright (c) 2017 Richard Moore (https://github.com/ricmoo/QRCode) | ||
| 8 | * Copyright (c) 2017 Project Nayuki (https://www.nayuki.io/page/qr-code-generator-library) | ||
| 9 | * | ||
| 10 | * Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| 11 | * of this software and associated documentation files (the "Software"), to deal | ||
| 12 | * in the Software without restriction, including without limitation the rights | ||
| 13 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| 14 | * copies of the Software, and to permit persons to whom the Software is | ||
| 15 | * furnished to do so, subject to the following conditions: | ||
| 16 | * | ||
| 17 | * The above copyright notice and this permission notice shall be included in | ||
| 18 | * all copies or substantial portions of the Software. | ||
| 19 | * | ||
| 20 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| 21 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| 22 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| 23 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| 24 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| 25 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
| 26 | * THE SOFTWARE. | ||
| 27 | */ | ||
| 28 | |||
| 29 | /** | ||
| 30 | * Special thanks to Nayuki (https://www.nayuki.io/) from which this library was | ||
| 31 | * heavily inspired and compared against. | ||
| 32 | * | ||
| 33 | * See: https://github.com/nayuki/QR-Code-generator/tree/master/cpp | ||
| 34 | */ | ||
| 35 | |||
| 36 | #include "qrcoded.h" | ||
| 37 | |||
| 38 | #include <stdlib.h> | ||
| 39 | #include <string.h> | ||
| 40 | |||
| 41 | /* Error Correction Lookup tables */ | ||
| 42 | |||
| 43 | #if LOCK_VERSION == 0 | ||
| 44 | |||
| 45 | static const uint16_t NUM_ERROR_CORRECTION_CODEWORDS[4][40] = { | ||
| 46 | // 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 Error correction level | ||
| 47 | {10, 16, 26, 36, 48, 64, 72, 88, 110, 130, 150, 176, 198, 216, 240, 280, 308, 338, 364, 416, 442, 476, 504, 560, 588, 644, 700, 728, 784, 812, 868, 924, 980, 1036, 1064, 1120, 1204, 1260, 1316, 1372}, // Medium | ||
| 48 | {7, 10, 15, 20, 26, 36, 40, 48, 60, 72, 80, 96, 104, 120, 132, 144, 168, 180, 196, 224, 224, 252, 270, 300, 312, 336, 360, 390, 420, 450, 480, 510, 540, 570, 570, 600, 630, 660, 720, 750}, // Low | ||
| 49 | {17, 28, 44, 64, 88, 112, 130, 156, 192, 224, 264, 308, 352, 384, 432, 480, 532, 588, 650, 700, 750, 816, 900, 960, 1050, 1110, 1200, 1260, 1350, 1440, 1530, 1620, 1710, 1800, 1890, 1980, 2100, 2220, 2310, 2430}, // High | ||
| 50 | {13, 22, 36, 52, 72, 96, 108, 132, 160, 192, 224, 260, 288, 320, 360, 408, 448, 504, 546, 600, 644, 690, 750, 810, 870, 952, 1020, 1050, 1140, 1200, 1290, 1350, 1440, 1530, 1590, 1680, 1770, 1860, 1950, 2040}, // Quartile | ||
| 51 | }; | ||
| 52 | |||
| 53 | static const uint8_t NUM_ERROR_CORRECTION_BLOCKS[4][40] = { | ||
| 54 | // Version: (note that index 0 is for padding, and is set to an illegal value) | ||
| 55 | // 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 Error correction level | ||
| 56 | {1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49}, // Medium | ||
| 57 | {1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25}, // Low | ||
| 58 | {1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81}, // High | ||
| 59 | {1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68}, // Quartile | ||
| 60 | }; | ||
| 61 | |||
| 62 | static const uint16_t NUM_RAW_DATA_MODULES[40] = { | ||
| 63 | // 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, | ||
| 64 | 208, 359, 567, 807, 1079, 1383, 1568, 1936, 2336, 2768, 3232, 3728, 4256, 4651, 5243, 5867, 6523, | ||
| 65 | // 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, | ||
| 66 | 7211, 7931, 8683, 9252, 10068, 10916, 11796, 12708, 13652, 14628, 15371, 16411, 17483, 18587, | ||
| 67 | // 32, 33, 34, 35, 36, 37, 38, 39, 40 | ||
| 68 | 19723, 20891, 22091, 23008, 24272, 25568, 26896, 28256, 29648}; | ||
| 69 | |||
| 70 | // @TODO: Put other LOCK_VERSIONS here | ||
| 71 | #elif LOCK_VERSION == 3 | ||
| 72 | |||
| 73 | static const int16_t NUM_ERROR_CORRECTION_CODEWORDS[4] = { | ||
| 74 | 26, 15, 44, 36}; | ||
| 75 | |||
| 76 | static const int8_t NUM_ERROR_CORRECTION_BLOCKS[4] = { | ||
| 77 | 1, 1, 2, 2}; | ||
| 78 | |||
| 79 | static const uint16_t NUM_RAW_DATA_MODULES = 567; | ||
| 80 | |||
| 81 | #else | ||
| 82 | |||
| 83 | #error Unsupported LOCK_VERSION (add it...) | ||
| 84 | |||
| 85 | #endif | ||
| 86 | |||
| 87 | static int max(int a, int b) | ||
| 88 | { | ||
| 89 | if (a > b) | ||
| 90 | { | ||
| 91 | return a; | ||
| 92 | } | ||
| 93 | return b; | ||
| 94 | } | ||
| 95 | |||
| 96 | /* | ||
| 97 | static int abs(int value) { | ||
| 98 | if (value < 0) { return -value; } | ||
| 99 | return value; | ||
| 100 | } | ||
| 101 | */ | ||
| 102 | |||
| 103 | /* Mode testing and conversion */ | ||
| 104 | |||
| 105 | static int8_t getAlphanumeric(char c) | ||
| 106 | { | ||
| 107 | |||
| 108 | if (c >= '0' && c <= '9') | ||
| 109 | { | ||
| 110 | return (c - '0'); | ||
| 111 | } | ||
| 112 | if (c >= 'A' && c <= 'Z') | ||
| 113 | { | ||
| 114 | return (c - 'A' + 10); | ||
| 115 | } | ||
| 116 | |||
| 117 | switch (c) | ||
| 118 | { | ||
| 119 | case ' ': | ||
| 120 | return 36; | ||
| 121 | case '$': | ||
| 122 | return 37; | ||
| 123 | case '%': | ||
| 124 | return 38; | ||
| 125 | case '*': | ||
| 126 | return 39; | ||
| 127 | case '+': | ||
| 128 | return 40; | ||
| 129 | case '-': | ||
| 130 | return 41; | ||
| 131 | case '.': | ||
| 132 | return 42; | ||
| 133 | case '/': | ||
| 134 | return 43; | ||
| 135 | case ':': | ||
| 136 | return 44; | ||
| 137 | } | ||
| 138 | |||
| 139 | return -1; | ||
| 140 | } | ||
| 141 | |||
| 142 | static bool isAlphanumeric(const char *text, uint16_t length) | ||
| 143 | { | ||
| 144 | while (length != 0) | ||
| 145 | { | ||
| 146 | if (getAlphanumeric(text[--length]) == -1) | ||
| 147 | { | ||
| 148 | return false; | ||
| 149 | } | ||
| 150 | } | ||
| 151 | return true; | ||
| 152 | } | ||
| 153 | |||
| 154 | static bool isNumeric(const char *text, uint16_t length) | ||
| 155 | { | ||
| 156 | while (length != 0) | ||
| 157 | { | ||
| 158 | char c = text[--length]; | ||
| 159 | if (c < '0' || c > '9') | ||
| 160 | { | ||
| 161 | return false; | ||
| 162 | } | ||
| 163 | } | ||
| 164 | return true; | ||
| 165 | } | ||
| 166 | |||
| 167 | /* Counting */ | ||
| 168 | |||
| 169 | // We store the following tightly packed (less 8) in modeInfo | ||
| 170 | // <=9 <=26 <= 40 | ||
| 171 | // NUMERIC ( 10, 12, 14); | ||
| 172 | // ALPHANUMERIC ( 9, 11, 13); | ||
| 173 | // BYTE ( 8, 16, 16); | ||
| 174 | static char getModeBits(uint8_t version, uint8_t mode) | ||
| 175 | { | ||
| 176 | // Note: We use 15 instead of 16; since 15 doesn't exist and we cannot store 16 (8 + 8) in 3 bits | ||
| 177 | // hex(int("".join(reversed([('00' + bin(x - 8)[2:])[-3:] for x in [10, 9, 8, 12, 11, 15, 14, 13, 15]])), 2)) | ||
| 178 | unsigned int modeInfo = 0x7bbb80a; | ||
| 179 | |||
| 180 | #if LOCK_VERSION == 0 || LOCK_VERSION > 9 | ||
| 181 | if (version > 9) | ||
| 182 | { | ||
| 183 | modeInfo >>= 9; | ||
| 184 | } | ||
| 185 | #endif | ||
| 186 | |||
| 187 | #if LOCK_VERSION == 0 || LOCK_VERSION > 26 | ||
| 188 | if (version > 26) | ||
| 189 | { | ||
| 190 | modeInfo >>= 9; | ||
| 191 | } | ||
| 192 | #endif | ||
| 193 | |||
| 194 | char result = 8 + ((modeInfo >> (3 * mode)) & 0x07); | ||
| 195 | if (result == 15) | ||
| 196 | { | ||
| 197 | result = 16; | ||
| 198 | } | ||
| 199 | |||
| 200 | return result; | ||
| 201 | } | ||
| 202 | |||
| 203 | /* BitBucket */ | ||
| 204 | |||
| 205 | typedef struct BitBucket | ||
| 206 | { | ||
| 207 | uint32_t bitOffsetOrWidth; | ||
| 208 | uint16_t capacityBytes; | ||
| 209 | uint8_t *data; | ||
| 210 | } BitBucket; | ||
| 211 | |||
| 212 | /* | ||
| 213 | void bb_dump(BitBucket *bitBuffer) { | ||
| 214 | printf("Buffer: "); | ||
| 215 | for (uint32_t i = 0; i < bitBuffer->capacityBytes; i++) { | ||
| 216 | printf("%02x", bitBuffer->data[i]); | ||
| 217 | if ((i % 4) == 3) { printf(" "); } | ||
| 218 | } | ||
| 219 | printf("\n"); | ||
| 220 | } | ||
| 221 | */ | ||
| 222 | |||
| 223 | static uint16_t bb_getGridSizeBytes(uint8_t size) | ||
| 224 | { | ||
| 225 | return (((size * size) + 7) / 8); | ||
| 226 | } | ||
| 227 | |||
| 228 | static uint16_t bb_getBufferSizeBytes(uint32_t bits) | ||
| 229 | { | ||
| 230 | return ((bits + 7) / 8); | ||
| 231 | } | ||
| 232 | |||
| 233 | static void bb_initBuffer(BitBucket *bitBuffer, uint8_t *data, int32_t capacityBytes) | ||
| 234 | { | ||
| 235 | bitBuffer->bitOffsetOrWidth = 0; | ||
| 236 | bitBuffer->capacityBytes = capacityBytes; | ||
| 237 | bitBuffer->data = data; | ||
| 238 | |||
| 239 | memset(data, 0, bitBuffer->capacityBytes); | ||
| 240 | } | ||
| 241 | |||
| 242 | static void bb_initGrid(BitBucket *bitGrid, uint8_t *data, uint8_t size) | ||
| 243 | { | ||
| 244 | bitGrid->bitOffsetOrWidth = size; | ||
| 245 | bitGrid->capacityBytes = bb_getGridSizeBytes(size); | ||
| 246 | bitGrid->data = data; | ||
| 247 | |||
| 248 | memset(data, 0, bitGrid->capacityBytes); | ||
| 249 | } | ||
| 250 | |||
| 251 | static void bb_appendBits(BitBucket *bitBuffer, uint32_t val, uint8_t length) | ||
| 252 | { | ||
| 253 | uint32_t offset = bitBuffer->bitOffsetOrWidth; | ||
| 254 | for (int8_t i = length - 1; i >= 0; i--, offset++) | ||
| 255 | { | ||
| 256 | bitBuffer->data[offset >> 3] |= ((val >> i) & 1) << (7 - (offset & 7)); | ||
| 257 | } | ||
| 258 | bitBuffer->bitOffsetOrWidth = offset; | ||
| 259 | } | ||
| 260 | /* | ||
| 261 | void bb_setBits(BitBucket *bitBuffer, uint32_t val, int offset, uint8_t length) { | ||
| 262 | for (int8_t i = length - 1; i >= 0; i--, offset++) { | ||
| 263 | bitBuffer->data[offset >> 3] |= ((val >> i) & 1) << (7 - (offset & 7)); | ||
| 264 | } | ||
| 265 | } | ||
| 266 | */ | ||
| 267 | static void bb_setBit(BitBucket *bitGrid, uint8_t x, uint8_t y, bool on) | ||
| 268 | { | ||
| 269 | uint32_t offset = y * bitGrid->bitOffsetOrWidth + x; | ||
| 270 | uint8_t mask = 1 << (7 - (offset & 0x07)); | ||
| 271 | if (on) | ||
| 272 | { | ||
| 273 | bitGrid->data[offset >> 3] |= mask; | ||
| 274 | } | ||
| 275 | else | ||
| 276 | { | ||
| 277 | bitGrid->data[offset >> 3] &= ~mask; | ||
| 278 | } | ||
| 279 | } | ||
| 280 | |||
| 281 | static void bb_invertBit(BitBucket *bitGrid, uint8_t x, uint8_t y, bool invert) | ||
| 282 | { | ||
| 283 | uint32_t offset = y * bitGrid->bitOffsetOrWidth + x; | ||
| 284 | uint8_t mask = 1 << (7 - (offset & 0x07)); | ||
| 285 | bool on = ((bitGrid->data[offset >> 3] & (1 << (7 - (offset & 0x07)))) != 0); | ||
| 286 | if (on ^ invert) | ||
| 287 | { | ||
| 288 | bitGrid->data[offset >> 3] |= mask; | ||
| 289 | } | ||
| 290 | else | ||
| 291 | { | ||
| 292 | bitGrid->data[offset >> 3] &= ~mask; | ||
| 293 | } | ||
| 294 | } | ||
| 295 | |||
| 296 | static bool bb_getBit(BitBucket *bitGrid, uint8_t x, uint8_t y) | ||
| 297 | { | ||
| 298 | uint32_t offset = y * bitGrid->bitOffsetOrWidth + x; | ||
| 299 | return (bitGrid->data[offset >> 3] & (1 << (7 - (offset & 0x07)))) != 0; | ||
| 300 | } | ||
| 301 | |||
| 302 | /* Drawing Patterns */ | ||
| 303 | |||
| 304 | // XORs the data modules in this QR Code with the given mask pattern. Due to XOR's mathematical | ||
| 305 | // properties, calling applyMask(m) twice with the same value is equivalent to no change at all. | ||
| 306 | // This means it is possible to apply a mask, undo it, and try another mask. Note that a final | ||
| 307 | // well-formed QR Code symbol needs exactly one mask applied (not zero, not two, etc.). | ||
| 308 | static void applyMask(BitBucket *modules, BitBucket *isFunction, uint8_t mask) | ||
| 309 | { | ||
| 310 | uint8_t size = modules->bitOffsetOrWidth; | ||
| 311 | |||
| 312 | for (uint8_t y = 0; y < size; y++) | ||
| 313 | { | ||
| 314 | for (uint8_t x = 0; x < size; x++) | ||
| 315 | { | ||
| 316 | if (bb_getBit(isFunction, x, y)) | ||
| 317 | { | ||
| 318 | continue; | ||
| 319 | } | ||
| 320 | |||
| 321 | bool invert = 0; | ||
| 322 | switch (mask) | ||
| 323 | { | ||
| 324 | case 0: | ||
| 325 | invert = (x + y) % 2 == 0; | ||
| 326 | break; | ||
| 327 | case 1: | ||
| 328 | invert = y % 2 == 0; | ||
| 329 | break; | ||
| 330 | case 2: | ||
| 331 | invert = x % 3 == 0; | ||
| 332 | break; | ||
| 333 | case 3: | ||
| 334 | invert = (x + y) % 3 == 0; | ||
| 335 | break; | ||
| 336 | case 4: | ||
| 337 | invert = (x / 3 + y / 2) % 2 == 0; | ||
| 338 | break; | ||
| 339 | case 5: | ||
| 340 | invert = x * y % 2 + x * y % 3 == 0; | ||
| 341 | break; | ||
| 342 | case 6: | ||
| 343 | invert = (x * y % 2 + x * y % 3) % 2 == 0; | ||
| 344 | break; | ||
| 345 | case 7: | ||
| 346 | invert = ((x + y) % 2 + x * y % 3) % 2 == 0; | ||
| 347 | break; | ||
| 348 | } | ||
| 349 | bb_invertBit(modules, x, y, invert); | ||
| 350 | } | ||
| 351 | } | ||
| 352 | } | ||
| 353 | |||
| 354 | static void setFunctionModule(BitBucket *modules, BitBucket *isFunction, uint8_t x, uint8_t y, bool on) | ||
| 355 | { | ||
| 356 | bb_setBit(modules, x, y, on); | ||
| 357 | bb_setBit(isFunction, x, y, true); | ||
| 358 | } | ||
| 359 | |||
| 360 | // Draws a 9*9 finder pattern including the border separator, with the center module at (x, y). | ||
| 361 | static void drawFinderPattern(BitBucket *modules, BitBucket *isFunction, uint8_t x, uint8_t y) | ||
| 362 | { | ||
| 363 | uint8_t size = modules->bitOffsetOrWidth; | ||
| 364 | |||
| 365 | for (int8_t i = -4; i <= 4; i++) | ||
| 366 | { | ||
| 367 | for (int8_t j = -4; j <= 4; j++) | ||
| 368 | { | ||
| 369 | uint8_t dist = max(abs(i), abs(j)); // Chebyshev/infinity norm | ||
| 370 | int16_t xx = x + j, yy = y + i; | ||
| 371 | if (0 <= xx && xx < size && 0 <= yy && yy < size) | ||
| 372 | { | ||
| 373 | setFunctionModule(modules, isFunction, xx, yy, dist != 2 && dist != 4); | ||
| 374 | } | ||
| 375 | } | ||
| 376 | } | ||
| 377 | } | ||
| 378 | |||
| 379 | // Draws a 5*5 alignment pattern, with the center module at (x, y). | ||
| 380 | static void drawAlignmentPattern(BitBucket *modules, BitBucket *isFunction, uint8_t x, uint8_t y) | ||
| 381 | { | ||
| 382 | for (int8_t i = -2; i <= 2; i++) | ||
| 383 | { | ||
| 384 | for (int8_t j = -2; j <= 2; j++) | ||
| 385 | { | ||
| 386 | setFunctionModule(modules, isFunction, x + j, y + i, max(abs(i), abs(j)) != 1); | ||
| 387 | } | ||
| 388 | } | ||
| 389 | } | ||
| 390 | |||
| 391 | // Draws two copies of the format bits (with its own error correction code) | ||
| 392 | // based on the given mask and this object's error correction level field. | ||
| 393 | static void drawFormatBits(BitBucket *modules, BitBucket *isFunction, uint8_t ecc, uint8_t mask) | ||
| 394 | { | ||
| 395 | |||
| 396 | uint8_t size = modules->bitOffsetOrWidth; | ||
| 397 | |||
| 398 | // Calculate error correction code and pack bits | ||
| 399 | uint32_t data = ecc << 3 | mask; // errCorrLvl is uint2, mask is uint3 | ||
| 400 | uint32_t rem = data; | ||
| 401 | for (int i = 0; i < 10; i++) | ||
| 402 | { | ||
| 403 | rem = (rem << 1) ^ ((rem >> 9) * 0x537); | ||
| 404 | } | ||
| 405 | |||
| 406 | data = data << 10 | rem; | ||
| 407 | data ^= 0x5412; // uint15 | ||
| 408 | |||
| 409 | // Draw first copy | ||
| 410 | for (uint8_t i = 0; i <= 5; i++) | ||
| 411 | { | ||
| 412 | setFunctionModule(modules, isFunction, 8, i, ((data >> i) & 1) != 0); | ||
| 413 | } | ||
| 414 | |||
| 415 | setFunctionModule(modules, isFunction, 8, 7, ((data >> 6) & 1) != 0); | ||
| 416 | setFunctionModule(modules, isFunction, 8, 8, ((data >> 7) & 1) != 0); | ||
| 417 | setFunctionModule(modules, isFunction, 7, 8, ((data >> 8) & 1) != 0); | ||
| 418 | |||
| 419 | for (int8_t i = 9; i < 15; i++) | ||
| 420 | { | ||
| 421 | setFunctionModule(modules, isFunction, 14 - i, 8, ((data >> i) & 1) != 0); | ||
| 422 | } | ||
| 423 | |||
| 424 | // Draw second copy | ||
| 425 | for (int8_t i = 0; i <= 7; i++) | ||
| 426 | { | ||
| 427 | setFunctionModule(modules, isFunction, size - 1 - i, 8, ((data >> i) & 1) != 0); | ||
| 428 | } | ||
| 429 | |||
| 430 | for (int8_t i = 8; i < 15; i++) | ||
| 431 | { | ||
| 432 | setFunctionModule(modules, isFunction, 8, size - 15 + i, ((data >> i) & 1) != 0); | ||
| 433 | } | ||
| 434 | |||
| 435 | setFunctionModule(modules, isFunction, 8, size - 8, true); | ||
| 436 | } | ||
| 437 | |||
| 438 | // Draws two copies of the version bits (with its own error correction code), | ||
| 439 | // based on this object's version field (which only has an effect for 7 <= version <= 40). | ||
| 440 | static void drawVersion(BitBucket *modules, BitBucket *isFunction, uint8_t version) | ||
| 441 | { | ||
| 442 | |||
| 443 | int8_t size = modules->bitOffsetOrWidth; | ||
| 444 | |||
| 445 | #if LOCK_VERSION != 0 && LOCK_VERSION < 7 | ||
| 446 | return; | ||
| 447 | |||
| 448 | #else | ||
| 449 | if (version < 7) | ||
| 450 | { | ||
| 451 | return; | ||
| 452 | } | ||
| 453 | |||
| 454 | // Calculate error correction code and pack bits | ||
| 455 | uint32_t rem = version; // version is uint6, in the range [7, 40] | ||
| 456 | for (uint8_t i = 0; i < 12; i++) | ||
| 457 | { | ||
| 458 | rem = (rem << 1) ^ ((rem >> 11) * 0x1F25); | ||
| 459 | } | ||
| 460 | |||
| 461 | uint32_t data = version << 12 | rem; // uint18 | ||
| 462 | |||
| 463 | // Draw two copies | ||
| 464 | for (uint8_t i = 0; i < 18; i++) | ||
| 465 | { | ||
| 466 | bool bit = ((data >> i) & 1) != 0; | ||
| 467 | uint8_t a = size - 11 + i % 3, b = i / 3; | ||
| 468 | setFunctionModule(modules, isFunction, a, b, bit); | ||
| 469 | setFunctionModule(modules, isFunction, b, a, bit); | ||
| 470 | } | ||
| 471 | |||
| 472 | #endif | ||
| 473 | } | ||
| 474 | |||
| 475 | static void drawFunctionPatterns(BitBucket *modules, BitBucket *isFunction, uint8_t version, uint8_t ecc) | ||
| 476 | { | ||
| 477 | |||
| 478 | uint8_t size = modules->bitOffsetOrWidth; | ||
| 479 | |||
| 480 | // Draw the horizontal and vertical timing patterns | ||
| 481 | for (uint8_t i = 0; i < size; i++) | ||
| 482 | { | ||
| 483 | setFunctionModule(modules, isFunction, 6, i, i % 2 == 0); | ||
| 484 | setFunctionModule(modules, isFunction, i, 6, i % 2 == 0); | ||
| 485 | } | ||
| 486 | |||
| 487 | // Draw 3 finder patterns (all corners except bottom right; overwrites some timing modules) | ||
| 488 | drawFinderPattern(modules, isFunction, 3, 3); | ||
| 489 | drawFinderPattern(modules, isFunction, size - 4, 3); | ||
| 490 | drawFinderPattern(modules, isFunction, 3, size - 4); | ||
| 491 | |||
| 492 | #if LOCK_VERSION == 0 || LOCK_VERSION > 1 | ||
| 493 | |||
| 494 | if (version > 1) | ||
| 495 | { | ||
| 496 | |||
| 497 | // Draw the numerous alignment patterns | ||
| 498 | |||
| 499 | uint8_t alignCount = version / 7 + 2; | ||
| 500 | uint8_t step; | ||
| 501 | if (version != 32) | ||
| 502 | { | ||
| 503 | step = (version * 4 + alignCount * 2 + 1) / (2 * alignCount - 2) * 2; // ceil((size - 13) / (2*numAlign - 2)) * 2 | ||
| 504 | } | ||
| 505 | else | ||
| 506 | { // C-C-C-Combo breaker! | ||
| 507 | step = 26; | ||
| 508 | } | ||
| 509 | |||
| 510 | uint8_t alignPositionIndex = alignCount - 1; | ||
| 511 | uint8_t alignPosition[alignCount]; | ||
| 512 | |||
| 513 | alignPosition[0] = 6; | ||
| 514 | |||
| 515 | uint8_t size = version * 4 + 17; | ||
| 516 | for (uint8_t i = 0, pos = size - 7; i < alignCount - 1; i++, pos -= step) | ||
| 517 | { | ||
| 518 | alignPosition[alignPositionIndex--] = pos; | ||
| 519 | } | ||
| 520 | |||
| 521 | for (uint8_t i = 0; i < alignCount; i++) | ||
| 522 | { | ||
| 523 | for (uint8_t j = 0; j < alignCount; j++) | ||
| 524 | { | ||
| 525 | if ((i == 0 && j == 0) || (i == 0 && j == alignCount - 1) || (i == alignCount - 1 && j == 0)) | ||
| 526 | { | ||
| 527 | continue; // Skip the three finder corners | ||
| 528 | } | ||
| 529 | else | ||
| 530 | { | ||
| 531 | drawAlignmentPattern(modules, isFunction, alignPosition[i], alignPosition[j]); | ||
| 532 | } | ||
| 533 | } | ||
| 534 | } | ||
| 535 | } | ||
| 536 | |||
| 537 | #endif | ||
| 538 | |||
| 539 | // Draw configuration data | ||
| 540 | drawFormatBits(modules, isFunction, ecc, 0); // Dummy mask value; overwritten later in the constructor | ||
| 541 | drawVersion(modules, isFunction, version); | ||
| 542 | } | ||
| 543 | |||
| 544 | // Draws the given sequence of 8-bit codewords (data and error correction) onto the entire | ||
| 545 | // data area of this QR Code symbol. Function modules need to be marked off before this is called. | ||
| 546 | static void drawCodewords(BitBucket *modules, BitBucket *isFunction, BitBucket *codewords) | ||
| 547 | { | ||
| 548 | |||
| 549 | uint32_t bitLength = codewords->bitOffsetOrWidth; | ||
| 550 | uint8_t *data = codewords->data; | ||
| 551 | |||
| 552 | uint8_t size = modules->bitOffsetOrWidth; | ||
| 553 | |||
| 554 | // Bit index into the data | ||
| 555 | uint32_t i = 0; | ||
| 556 | |||
| 557 | // Do the funny zigzag scan | ||
| 558 | for (int16_t right = size - 1; right >= 1; right -= 2) | ||
| 559 | { // Index of right column in each column pair | ||
| 560 | if (right == 6) | ||
| 561 | { | ||
| 562 | right = 5; | ||
| 563 | } | ||
| 564 | |||
| 565 | for (uint8_t vert = 0; vert < size; vert++) | ||
| 566 | { // Vertical counter | ||
| 567 | for (int j = 0; j < 2; j++) | ||
| 568 | { | ||
| 569 | uint8_t x = right - j; // Actual x coordinate | ||
| 570 | bool upwards = ((right & 2) == 0) ^ (x < 6); | ||
| 571 | uint8_t y = upwards ? size - 1 - vert : vert; // Actual y coordinate | ||
| 572 | if (!bb_getBit(isFunction, x, y) && i < bitLength) | ||
| 573 | { | ||
| 574 | bb_setBit(modules, x, y, ((data[i >> 3] >> (7 - (i & 7))) & 1) != 0); | ||
| 575 | i++; | ||
| 576 | } | ||
| 577 | // If there are any remainder bits (0 to 7), they are already | ||
| 578 | // set to 0/false/white when the grid of modules was initialized | ||
| 579 | } | ||
| 580 | } | ||
| 581 | } | ||
| 582 | } | ||
| 583 | |||
| 584 | /* Penalty Calculation */ | ||
| 585 | |||
| 586 | #define PENALTY_N1 3 | ||
| 587 | #define PENALTY_N2 3 | ||
| 588 | #define PENALTY_N3 40 | ||
| 589 | #define PENALTY_N4 10 | ||
| 590 | |||
| 591 | // Calculates and returns the penalty score based on state of this QR Code's current modules. | ||
| 592 | // This is used by the automatic mask choice algorithm to find the mask pattern that yields the lowest score. | ||
| 593 | // @TODO: This can be optimized by working with the bytes instead of bits. | ||
| 594 | static uint32_t getPenaltyScore(BitBucket *modules) | ||
| 595 | { | ||
| 596 | uint32_t result = 0; | ||
| 597 | |||
| 598 | uint8_t size = modules->bitOffsetOrWidth; | ||
| 599 | |||
| 600 | // Adjacent modules in row having same color | ||
| 601 | for (uint8_t y = 0; y < size; y++) | ||
| 602 | { | ||
| 603 | |||
| 604 | bool colorX = bb_getBit(modules, 0, y); | ||
| 605 | for (uint8_t x = 1, runX = 1; x < size; x++) | ||
| 606 | { | ||
| 607 | bool cx = bb_getBit(modules, x, y); | ||
| 608 | if (cx != colorX) | ||
| 609 | { | ||
| 610 | colorX = cx; | ||
| 611 | runX = 1; | ||
| 612 | } | ||
| 613 | else | ||
| 614 | { | ||
| 615 | runX++; | ||
| 616 | if (runX == 5) | ||
| 617 | { | ||
| 618 | result += PENALTY_N1; | ||
| 619 | } | ||
| 620 | else if (runX > 5) | ||
| 621 | { | ||
| 622 | result++; | ||
| 623 | } | ||
| 624 | } | ||
| 625 | } | ||
| 626 | } | ||
| 627 | |||
| 628 | // Adjacent modules in column having same color | ||
| 629 | for (uint8_t x = 0; x < size; x++) | ||
| 630 | { | ||
| 631 | bool colorY = bb_getBit(modules, x, 0); | ||
| 632 | for (uint8_t y = 1, runY = 1; y < size; y++) | ||
| 633 | { | ||
| 634 | bool cy = bb_getBit(modules, x, y); | ||
| 635 | if (cy != colorY) | ||
| 636 | { | ||
| 637 | colorY = cy; | ||
| 638 | runY = 1; | ||
| 639 | } | ||
| 640 | else | ||
| 641 | { | ||
| 642 | runY++; | ||
| 643 | if (runY == 5) | ||
| 644 | { | ||
| 645 | result += PENALTY_N1; | ||
| 646 | } | ||
| 647 | else if (runY > 5) | ||
| 648 | { | ||
| 649 | result++; | ||
| 650 | } | ||
| 651 | } | ||
| 652 | } | ||
| 653 | } | ||
| 654 | |||
| 655 | uint16_t black = 0; | ||
| 656 | for (uint8_t y = 0; y < size; y++) | ||
| 657 | { | ||
| 658 | uint16_t bitsRow = 0, bitsCol = 0; | ||
| 659 | for (uint8_t x = 0; x < size; x++) | ||
| 660 | { | ||
| 661 | bool color = bb_getBit(modules, x, y); | ||
| 662 | |||
| 663 | // 2*2 blocks of modules having same color | ||
| 664 | if (x > 0 && y > 0) | ||
| 665 | { | ||
| 666 | bool colorUL = bb_getBit(modules, x - 1, y - 1); | ||
| 667 | bool colorUR = bb_getBit(modules, x, y - 1); | ||
| 668 | bool colorL = bb_getBit(modules, x - 1, y); | ||
| 669 | if (color == colorUL && color == colorUR && color == colorL) | ||
| 670 | { | ||
| 671 | result += PENALTY_N2; | ||
| 672 | } | ||
| 673 | } | ||
| 674 | |||
| 675 | // Finder-like pattern in rows and columns | ||
| 676 | bitsRow = ((bitsRow << 1) & 0x7FF) | color; | ||
| 677 | bitsCol = ((bitsCol << 1) & 0x7FF) | bb_getBit(modules, y, x); | ||
| 678 | |||
| 679 | // Needs 11 bits accumulated | ||
| 680 | if (x >= 10) | ||
| 681 | { | ||
| 682 | if (bitsRow == 0x05D || bitsRow == 0x5D0) | ||
| 683 | { | ||
| 684 | result += PENALTY_N3; | ||
| 685 | } | ||
| 686 | if (bitsCol == 0x05D || bitsCol == 0x5D0) | ||
| 687 | { | ||
| 688 | result += PENALTY_N3; | ||
| 689 | } | ||
| 690 | } | ||
| 691 | |||
| 692 | // Balance of black and white modules | ||
| 693 | if (color) | ||
| 694 | { | ||
| 695 | black++; | ||
| 696 | } | ||
| 697 | } | ||
| 698 | } | ||
| 699 | |||
| 700 | // Find smallest k such that (45-5k)% <= dark/total <= (55+5k)% | ||
| 701 | uint16_t total = size * size; | ||
| 702 | for (uint16_t k = 0; black * 20 < (9 - k) * total || black * 20 > (11 + k) * total; k++) | ||
| 703 | { | ||
| 704 | result += PENALTY_N4; | ||
| 705 | } | ||
| 706 | |||
| 707 | return result; | ||
| 708 | } | ||
| 709 | |||
| 710 | /* Reed-Solomon Generator */ | ||
| 711 | |||
| 712 | static uint8_t rs_multiply(uint8_t x, uint8_t y) | ||
| 713 | { | ||
| 714 | // Russian peasant multiplication | ||
| 715 | // See: https://en.wikipedia.org/wiki/Ancient_Egyptian_multiplication | ||
| 716 | uint16_t z = 0; | ||
| 717 | for (int8_t i = 7; i >= 0; i--) | ||
| 718 | { | ||
| 719 | z = (z << 1) ^ ((z >> 7) * 0x11D); | ||
| 720 | z ^= ((y >> i) & 1) * x; | ||
| 721 | } | ||
| 722 | return z; | ||
| 723 | } | ||
| 724 | |||
| 725 | static void rs_init(uint8_t degree, uint8_t *coeff) | ||
| 726 | { | ||
| 727 | memset(coeff, 0, degree); | ||
| 728 | coeff[degree - 1] = 1; | ||
| 729 | |||
| 730 | // Compute the product polynomial (x - r^0) * (x - r^1) * (x - r^2) * ... * (x - r^{degree-1}), | ||
| 731 | // drop the highest term, and store the rest of the coefficients in order of descending powers. | ||
| 732 | // Note that r = 0x02, which is a generator element of this field GF(2^8/0x11D). | ||
| 733 | uint16_t root = 1; | ||
| 734 | for (uint8_t i = 0; i < degree; i++) | ||
| 735 | { | ||
| 736 | // Multiply the current product by (x - r^i) | ||
| 737 | for (uint8_t j = 0; j < degree; j++) | ||
| 738 | { | ||
| 739 | coeff[j] = rs_multiply(coeff[j], root); | ||
| 740 | if (j + 1 < degree) | ||
| 741 | { | ||
| 742 | coeff[j] ^= coeff[j + 1]; | ||
| 743 | } | ||
| 744 | } | ||
| 745 | root = (root << 1) ^ ((root >> 7) * 0x11D); // Multiply by 0x02 mod GF(2^8/0x11D) | ||
| 746 | } | ||
| 747 | } | ||
| 748 | |||
| 749 | static void rs_getRemainder(uint8_t degree, uint8_t *coeff, uint8_t *data, uint8_t length, uint8_t *result, uint8_t stride) | ||
| 750 | { | ||
| 751 | // Compute the remainder by performing polynomial division | ||
| 752 | |||
| 753 | //for (uint8_t i = 0; i < degree; i++) { result[] = 0; } | ||
| 754 | //memset(result, 0, degree); | ||
| 755 | |||
| 756 | for (uint8_t i = 0; i < length; i++) | ||
| 757 | { | ||
| 758 | uint8_t factor = data[i] ^ result[0]; | ||
| 759 | for (uint8_t j = 1; j < degree; j++) | ||
| 760 | { | ||
| 761 | result[(j - 1) * stride] = result[j * stride]; | ||
| 762 | } | ||
| 763 | result[(degree - 1) * stride] = 0; | ||
| 764 | |||
| 765 | for (uint8_t j = 0; j < degree; j++) | ||
| 766 | { | ||
| 767 | result[j * stride] ^= rs_multiply(coeff[j], factor); | ||
| 768 | } | ||
| 769 | } | ||
| 770 | } | ||
| 771 | |||
| 772 | /* QrCode */ | ||
| 773 | |||
| 774 | static int8_t encodeDataCodewords(BitBucket *dataCodewords, const uint8_t *text, uint16_t length, uint8_t version) | ||
| 775 | { | ||
| 776 | int8_t mode = MODE_BYTE; | ||
| 777 | |||
| 778 | if (isNumeric((char *)text, length)) | ||
| 779 | { | ||
| 780 | mode = MODE_NUMERIC; | ||
| 781 | bb_appendBits(dataCodewords, 1 << MODE_NUMERIC, 4); | ||
| 782 | bb_appendBits(dataCodewords, length, getModeBits(version, MODE_NUMERIC)); | ||
| 783 | |||
| 784 | uint16_t accumData = 0; | ||
| 785 | uint8_t accumCount = 0; | ||
| 786 | for (uint16_t i = 0; i < length; i++) | ||
| 787 | { | ||
| 788 | accumData = accumData * 10 + ((char)(text[i]) - '0'); | ||
| 789 | accumCount++; | ||
| 790 | if (accumCount == 3) | ||
| 791 | { | ||
| 792 | bb_appendBits(dataCodewords, accumData, 10); | ||
| 793 | accumData = 0; | ||
| 794 | accumCount = 0; | ||
| 795 | } | ||
| 796 | } | ||
| 797 | |||
| 798 | // 1 or 2 digits remaining | ||
| 799 | if (accumCount > 0) | ||
| 800 | { | ||
| 801 | bb_appendBits(dataCodewords, accumData, accumCount * 3 + 1); | ||
| 802 | } | ||
| 803 | } | ||
| 804 | else if (isAlphanumeric((char *)text, length)) | ||
| 805 | { | ||
| 806 | mode = MODE_ALPHANUMERIC; | ||
| 807 | bb_appendBits(dataCodewords, 1 << MODE_ALPHANUMERIC, 4); | ||
| 808 | bb_appendBits(dataCodewords, length, getModeBits(version, MODE_ALPHANUMERIC)); | ||
| 809 | |||
| 810 | uint16_t accumData = 0; | ||
| 811 | uint8_t accumCount = 0; | ||
| 812 | for (uint16_t i = 0; i < length; i++) | ||
| 813 | { | ||
| 814 | accumData = accumData * 45 + getAlphanumeric((char)(text[i])); | ||
| 815 | accumCount++; | ||
| 816 | if (accumCount == 2) | ||
| 817 | { | ||
| 818 | bb_appendBits(dataCodewords, accumData, 11); | ||
| 819 | accumData = 0; | ||
| 820 | accumCount = 0; | ||
| 821 | } | ||
| 822 | } | ||
| 823 | |||
| 824 | // 1 character remaining | ||
| 825 | if (accumCount > 0) | ||
| 826 | { | ||
| 827 | bb_appendBits(dataCodewords, accumData, 6); | ||
| 828 | } | ||
| 829 | } | ||
| 830 | else | ||
| 831 | { | ||
| 832 | bb_appendBits(dataCodewords, 1 << MODE_BYTE, 4); | ||
| 833 | bb_appendBits(dataCodewords, length, getModeBits(version, MODE_BYTE)); | ||
| 834 | for (uint16_t i = 0; i < length; i++) | ||
| 835 | { | ||
| 836 | bb_appendBits(dataCodewords, (char)(text[i]), 8); | ||
| 837 | } | ||
| 838 | } | ||
| 839 | |||
| 840 | //bb_setBits(dataCodewords, length, 4, getModeBits(version, mode)); | ||
| 841 | |||
| 842 | return mode; | ||
| 843 | } | ||
| 844 | |||
| 845 | static void performErrorCorrection(uint8_t version, uint8_t ecc, BitBucket *data) | ||
| 846 | { | ||
| 847 | |||
| 848 | // See: http://www.thonky.com/qr-code-tutorial/structure-final-message | ||
| 849 | |||
| 850 | #if LOCK_VERSION == 0 | ||
| 851 | uint8_t numBlocks = NUM_ERROR_CORRECTION_BLOCKS[ecc][version - 1]; | ||
| 852 | uint16_t totalEcc = NUM_ERROR_CORRECTION_CODEWORDS[ecc][version - 1]; | ||
| 853 | uint16_t moduleCount = NUM_RAW_DATA_MODULES[version - 1]; | ||
| 854 | #else | ||
| 855 | uint8_t numBlocks = NUM_ERROR_CORRECTION_BLOCKS[ecc]; | ||
| 856 | uint16_t totalEcc = NUM_ERROR_CORRECTION_CODEWORDS[ecc]; | ||
| 857 | uint16_t moduleCount = NUM_RAW_DATA_MODULES; | ||
| 858 | #endif | ||
| 859 | |||
| 860 | uint8_t blockEccLen = totalEcc / numBlocks; | ||
| 861 | uint8_t numShortBlocks = numBlocks - moduleCount / 8 % numBlocks; | ||
| 862 | uint8_t shortBlockLen = moduleCount / 8 / numBlocks; | ||
| 863 | |||
| 864 | uint8_t shortDataBlockLen = shortBlockLen - blockEccLen; | ||
| 865 | |||
| 866 | uint8_t result[data->capacityBytes]; | ||
| 867 | memset(result, 0, sizeof(result)); | ||
| 868 | |||
| 869 | uint8_t coeff[blockEccLen]; | ||
| 870 | rs_init(blockEccLen, coeff); | ||
| 871 | |||
| 872 | uint16_t offset = 0; | ||
| 873 | uint8_t *dataBytes = data->data; | ||
| 874 | |||
| 875 | // Interleave all short blocks | ||
| 876 | for (uint8_t i = 0; i < shortDataBlockLen; i++) | ||
| 877 | { | ||
| 878 | uint16_t index = i; | ||
| 879 | uint8_t stride = shortDataBlockLen; | ||
| 880 | for (uint8_t blockNum = 0; blockNum < numBlocks; blockNum++) | ||
| 881 | { | ||
| 882 | result[offset++] = dataBytes[index]; | ||
| 883 | |||
| 884 | #if LOCK_VERSION == 0 || LOCK_VERSION >= 5 | ||
| 885 | if (blockNum == numShortBlocks) | ||
| 886 | { | ||
| 887 | stride++; | ||
| 888 | } | ||
| 889 | #endif | ||
| 890 | index += stride; | ||
| 891 | } | ||
| 892 | } | ||
| 893 | |||
| 894 | // Version less than 5 only have short blocks | ||
| 895 | #if LOCK_VERSION == 0 || LOCK_VERSION >= 5 | ||
| 896 | { | ||
| 897 | // Interleave long blocks | ||
| 898 | uint16_t index = shortDataBlockLen * (numShortBlocks + 1); | ||
| 899 | uint8_t stride = shortDataBlockLen; | ||
| 900 | for (uint8_t blockNum = 0; blockNum < numBlocks - numShortBlocks; blockNum++) | ||
| 901 | { | ||
| 902 | result[offset++] = dataBytes[index]; | ||
| 903 | |||
| 904 | if (blockNum == 0) | ||
| 905 | { | ||
| 906 | stride++; | ||
| 907 | } | ||
| 908 | index += stride; | ||
| 909 | } | ||
| 910 | } | ||
| 911 | #endif | ||
| 912 | |||
| 913 | // Add all ecc blocks, interleaved | ||
| 914 | uint8_t blockSize = shortDataBlockLen; | ||
| 915 | for (uint8_t blockNum = 0; blockNum < numBlocks; blockNum++) | ||
| 916 | { | ||
| 917 | |||
| 918 | #if LOCK_VERSION == 0 || LOCK_VERSION >= 5 | ||
| 919 | if (blockNum == numShortBlocks) | ||
| 920 | { | ||
| 921 | blockSize++; | ||
| 922 | } | ||
| 923 | #endif | ||
| 924 | rs_getRemainder(blockEccLen, coeff, dataBytes, blockSize, &result[offset + blockNum], numBlocks); | ||
| 925 | dataBytes += blockSize; | ||
| 926 | } | ||
| 927 | |||
| 928 | memcpy(data->data, result, data->capacityBytes); | ||
| 929 | data->bitOffsetOrWidth = moduleCount; | ||
| 930 | } | ||
| 931 | |||
| 932 | // We store the Format bits tightly packed into a single byte (each of the 4 modes is 2 bits) | ||
| 933 | // The format bits can be determined by ECC_FORMAT_BITS >> (2 * ecc) | ||
| 934 | static const uint8_t ECC_FORMAT_BITS = (0x02 << 6) | (0x03 << 4) | (0x00 << 2) | (0x01 << 0); | ||
| 935 | |||
| 936 | /* Public QRCode functions */ | ||
| 937 | |||
| 938 | uint16_t qrcode_getBufferSize(uint8_t version) | ||
| 939 | { | ||
| 940 | return bb_getGridSizeBytes(4 * version + 17); | ||
| 941 | } | ||
| 942 | |||
| 943 | // @TODO: Return error if data is too big. | ||
| 944 | int8_t qrcode_initBytes(QRCode *qrcoded, uint8_t *modules, uint8_t version, uint8_t ecc, uint8_t *data, uint16_t length) | ||
| 945 | { | ||
| 946 | uint8_t size = version * 4 + 17; | ||
| 947 | qrcoded->version = version; | ||
| 948 | qrcoded->size = size; | ||
| 949 | qrcoded->ecc = ecc; | ||
| 950 | qrcoded->modules = modules; | ||
| 951 | |||
| 952 | uint8_t eccFormatBits = (ECC_FORMAT_BITS >> (2 * ecc)) & 0x03; | ||
| 953 | |||
| 954 | #if LOCK_VERSION == 0 | ||
| 955 | uint16_t moduleCount = NUM_RAW_DATA_MODULES[version - 1]; | ||
| 956 | uint16_t dataCapacity = moduleCount / 8 - NUM_ERROR_CORRECTION_CODEWORDS[eccFormatBits][version - 1]; | ||
| 957 | #else | ||
| 958 | version = LOCK_VERSION; | ||
| 959 | uint16_t moduleCount = NUM_RAW_DATA_MODULES; | ||
| 960 | uint16_t dataCapacity = moduleCount / 8 - NUM_ERROR_CORRECTION_CODEWORDS[eccFormatBits]; | ||
| 961 | #endif | ||
| 962 | |||
| 963 | struct BitBucket codewords; | ||
| 964 | uint8_t codewordBytes[bb_getBufferSizeBytes(moduleCount)]; | ||
| 965 | bb_initBuffer(&codewords, codewordBytes, (int32_t)sizeof(codewordBytes)); | ||
| 966 | |||
| 967 | // Place the data code words into the buffer | ||
| 968 | int8_t mode = encodeDataCodewords(&codewords, data, length, version); | ||
| 969 | |||
| 970 | if (mode < 0) | ||
| 971 | { | ||
| 972 | return -1; | ||
| 973 | } | ||
| 974 | qrcoded->mode = mode; | ||
| 975 | |||
| 976 | // Add terminator and pad up to a byte if applicable | ||
| 977 | uint32_t padding = (dataCapacity * 8) - codewords.bitOffsetOrWidth; | ||
| 978 | if (padding > 4) | ||
| 979 | { | ||
| 980 | padding = 4; | ||
| 981 | } | ||
| 982 | bb_appendBits(&codewords, 0, padding); | ||
| 983 | bb_appendBits(&codewords, 0, (8 - codewords.bitOffsetOrWidth % 8) % 8); | ||
| 984 | |||
| 985 | // Pad with alternate bytes until data capacity is reached | ||
| 986 | for (uint8_t padByte = 0xEC; codewords.bitOffsetOrWidth < (dataCapacity * 8); padByte ^= 0xEC ^ 0x11) | ||
| 987 | { | ||
| 988 | bb_appendBits(&codewords, padByte, 8); | ||
| 989 | } | ||
| 990 | |||
| 991 | BitBucket modulesGrid; | ||
| 992 | bb_initGrid(&modulesGrid, modules, size); | ||
| 993 | |||
| 994 | BitBucket isFunctionGrid; | ||
| 995 | uint8_t isFunctionGridBytes[bb_getGridSizeBytes(size)]; | ||
| 996 | bb_initGrid(&isFunctionGrid, isFunctionGridBytes, size); | ||
| 997 | |||
| 998 | // Draw function patterns, draw all codewords, do masking | ||
| 999 | drawFunctionPatterns(&modulesGrid, &isFunctionGrid, version, eccFormatBits); | ||
| 1000 | performErrorCorrection(version, eccFormatBits, &codewords); | ||
| 1001 | drawCodewords(&modulesGrid, &isFunctionGrid, &codewords); | ||
| 1002 | |||
| 1003 | // Find the best (lowest penalty) mask | ||
| 1004 | uint8_t mask = 0; | ||
| 1005 | int32_t minPenalty = INT32_MAX; | ||
| 1006 | for (uint8_t i = 0; i < 8; i++) | ||
| 1007 | { | ||
| 1008 | drawFormatBits(&modulesGrid, &isFunctionGrid, eccFormatBits, i); | ||
| 1009 | applyMask(&modulesGrid, &isFunctionGrid, i); | ||
| 1010 | int penalty = getPenaltyScore(&modulesGrid); | ||
| 1011 | if (penalty < minPenalty) | ||
| 1012 | { | ||
| 1013 | mask = i; | ||
| 1014 | minPenalty = penalty; | ||
| 1015 | } | ||
| 1016 | applyMask(&modulesGrid, &isFunctionGrid, i); // Undoes the mask due to XOR | ||
| 1017 | } | ||
| 1018 | |||
| 1019 | qrcoded->mask = mask; | ||
| 1020 | |||
| 1021 | // Overwrite old format bits | ||
| 1022 | drawFormatBits(&modulesGrid, &isFunctionGrid, eccFormatBits, mask); | ||
| 1023 | |||
| 1024 | // Apply the final choice of mask | ||
| 1025 | applyMask(&modulesGrid, &isFunctionGrid, mask); | ||
| 1026 | |||
| 1027 | return 0; | ||
| 1028 | } | ||
| 1029 | |||
| 1030 | int8_t qrcode_initText(QRCode *qrcoded, uint8_t *modules, uint8_t version, uint8_t ecc, const char *data) | ||
| 1031 | { | ||
| 1032 | return qrcode_initBytes(qrcoded, modules, version, ecc, (uint8_t *)data, strlen(data)); | ||
| 1033 | } | ||
| 1034 | |||
| 1035 | bool qrcode_getModule(QRCode *qrcoded, uint8_t x, uint8_t y) | ||
| 1036 | { | ||
| 1037 | if (x >= qrcoded->size || y >= qrcoded->size) | ||
| 1038 | { | ||
| 1039 | return false; | ||
| 1040 | } | ||
| 1041 | |||
| 1042 | uint32_t offset = y * qrcoded->size + x; | ||
| 1043 | return (qrcoded->modules[offset >> 3] & (1 << (7 - (offset & 0x07)))) != 0; | ||
| 1044 | } | ||
| 1045 | |||
| 1046 | /* | ||
| 1047 | uint8_t qrcode_getHexLength(QRCode *qrcoded) { | ||
| 1048 | return ((qrcoded->size * qrcoded->size) + 7) / 4; | ||
| 1049 | } | ||
| 1050 | |||
| 1051 | void qrcode_getHex(QRCode *qrcoded, char *result) { | ||
| 1052 | |||
| 1053 | } | ||
| 1054 | */ | ||
diff --git a/components/wisp_relay/CMakeLists.txt b/components/wisp_relay/CMakeLists.txt new file mode 100644 index 0000000..5da9a9c --- /dev/null +++ b/components/wisp_relay/CMakeLists.txt | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | idf_component_register( | ||
| 2 | SRCS "ws_server.c" | ||
| 3 | "storage_engine.c" | ||
| 4 | "sub_manager.c" | ||
| 5 | "broadcaster.c" | ||
| 6 | "rate_limiter.c" | ||
| 7 | "nip11_relay.c" | ||
| 8 | "deletion.c" | ||
| 9 | "flash_monitor.c" | ||
| 10 | "relay_validator.c" | ||
| 11 | "router.c" | ||
| 12 | "handlers.c" | ||
| 13 | "relay_types.c" | ||
| 14 | INCLUDE_DIRS "." | ||
| 15 | REQUIRES esp_http_server esp_timer nvs_flash log json esp_littlefs mbedtls secp256k1 | ||
| 16 | ) | ||
diff --git a/components/wisp_relay/broadcaster.c b/components/wisp_relay/broadcaster.c new file mode 100644 index 0000000..738cbdb --- /dev/null +++ b/components/wisp_relay/broadcaster.c | |||
| @@ -0,0 +1,33 @@ | |||
| 1 | #include "broadcaster.h" | ||
| 2 | #include "relay_core.h" | ||
| 3 | #include "router.h" | ||
| 4 | #include "sub_manager.h" | ||
| 5 | #include "esp_log.h" | ||
| 6 | |||
| 7 | static const char *TAG = "broadcaster"; | ||
| 8 | |||
| 9 | void broadcaster_fanout_json(relay_ctx_t *ctx, const char *event_json, | ||
| 10 | size_t event_len, int event_kind, | ||
| 11 | const char *event_pubkey_hex, | ||
| 12 | uint64_t event_created_at) | ||
| 13 | { | ||
| 14 | if (!ctx || !ctx->sub_manager) return; | ||
| 15 | |||
| 16 | sub_match_result_t matches; | ||
| 17 | sub_manager_match_json(ctx->sub_manager, event_json, event_len, event_kind, | ||
| 18 | event_pubkey_hex, event_created_at, &matches); | ||
| 19 | |||
| 20 | if (matches.count == 0) { | ||
| 21 | ESP_LOGD(TAG, "No subscribers for event kind=%d", event_kind); | ||
| 22 | return; | ||
| 23 | } | ||
| 24 | |||
| 25 | ESP_LOGD(TAG, "Broadcasting event kind=%d to %d subscriptions", | ||
| 26 | event_kind, matches.count); | ||
| 27 | |||
| 28 | for (uint8_t i = 0; i < matches.count; i++) { | ||
| 29 | sub_match_entry_t *entry = &matches.matches[i]; | ||
| 30 | router_send_event(ctx, entry->conn_fd, entry->sub_id, | ||
| 31 | event_json, event_len); | ||
| 32 | } | ||
| 33 | } | ||
diff --git a/components/wisp_relay/broadcaster.h b/components/wisp_relay/broadcaster.h new file mode 100644 index 0000000..0b29f71 --- /dev/null +++ b/components/wisp_relay/broadcaster.h | |||
| @@ -0,0 +1,11 @@ | |||
| 1 | #ifndef BROADCASTER_H | ||
| 2 | #define BROADCASTER_H | ||
| 3 | |||
| 4 | #include "relay_core.h" | ||
| 5 | |||
| 6 | void broadcaster_fanout_json(relay_ctx_t *ctx, const char *event_json, | ||
| 7 | size_t event_len, int event_kind, | ||
| 8 | const char *event_pubkey_hex, | ||
| 9 | uint64_t event_created_at); | ||
| 10 | |||
| 11 | #endif | ||
diff --git a/components/wisp_relay/deletion.c b/components/wisp_relay/deletion.c new file mode 100644 index 0000000..7ad3c22 --- /dev/null +++ b/components/wisp_relay/deletion.c | |||
| @@ -0,0 +1,190 @@ | |||
| 1 | #include "deletion.h" | ||
| 2 | #include "relay_types.h" | ||
| 3 | #include "cJSON.h" | ||
| 4 | #include "esp_log.h" | ||
| 5 | #include <inttypes.h> | ||
| 6 | #include <stdlib.h> | ||
| 7 | #include <string.h> | ||
| 8 | |||
| 9 | static const char *TAG = "deletion"; | ||
| 10 | |||
| 11 | static int extract_event_id_field(const char *event_json, size_t len, | ||
| 12 | uint8_t id_out[32]) | ||
| 13 | { | ||
| 14 | cJSON *obj = cJSON_ParseWithLength(event_json, len); | ||
| 15 | if (!obj) return -1; | ||
| 16 | cJSON *id_item = cJSON_GetObjectItem(obj, "id"); | ||
| 17 | if (!id_item || !cJSON_IsString(id_item) || strlen(id_item->valuestring) != 64) { | ||
| 18 | cJSON_Delete(obj); | ||
| 19 | return -1; | ||
| 20 | } | ||
| 21 | int ret = relay_hex_to_bytes(id_item->valuestring, 64, id_out, 32); | ||
| 22 | cJSON_Delete(obj); | ||
| 23 | return ret; | ||
| 24 | } | ||
| 25 | |||
| 26 | static char *extract_pubkey_hex(const char *event_json, size_t len) | ||
| 27 | { | ||
| 28 | cJSON *obj = cJSON_ParseWithLength(event_json, len); | ||
| 29 | if (!obj) return NULL; | ||
| 30 | cJSON *pk = cJSON_GetObjectItem(obj, "pubkey"); | ||
| 31 | char *result = NULL; | ||
| 32 | if (pk && cJSON_IsString(pk)) result = strdup(pk->valuestring); | ||
| 33 | cJSON_Delete(obj); | ||
| 34 | return result; | ||
| 35 | } | ||
| 36 | |||
| 37 | static int delete_by_e_tags(storage_engine_t *storage, const char *event_json, | ||
| 38 | size_t len, const char *deleter_pubkey) | ||
| 39 | { | ||
| 40 | cJSON *obj = cJSON_ParseWithLength(event_json, len); | ||
| 41 | if (!obj) return 0; | ||
| 42 | |||
| 43 | cJSON *tags = cJSON_GetObjectItem(obj, "tags"); | ||
| 44 | if (!tags || !cJSON_IsArray(tags)) { cJSON_Delete(obj); return 0; } | ||
| 45 | |||
| 46 | int deleted = 0; | ||
| 47 | int array_size = cJSON_GetArraySize(tags); | ||
| 48 | |||
| 49 | for (int i = 0; i < array_size; i++) { | ||
| 50 | cJSON *tag = cJSON_GetArrayItem(tags, i); | ||
| 51 | if (!cJSON_IsArray(tag)) continue; | ||
| 52 | cJSON *tag_name = cJSON_GetArrayItem(tag, 0); | ||
| 53 | if (!tag_name || !cJSON_IsString(tag_name)) continue; | ||
| 54 | if (strcmp(tag_name->valuestring, "e") != 0) continue; | ||
| 55 | |||
| 56 | cJSON *tag_val = cJSON_GetArrayItem(tag, 1); | ||
| 57 | if (!tag_val || !cJSON_IsString(tag_val)) continue; | ||
| 58 | |||
| 59 | uint8_t event_id[32]; | ||
| 60 | if (relay_hex_to_bytes(tag_val->valuestring, 64, event_id, 32) != 0) continue; | ||
| 61 | |||
| 62 | storage_error_t err = storage_delete_event(storage, event_id); | ||
| 63 | if (err == STORAGE_OK) { | ||
| 64 | deleted++; | ||
| 65 | ESP_LOGI(TAG, "Deleted event: %.16s...", tag_val->valuestring); | ||
| 66 | } | ||
| 67 | } | ||
| 68 | |||
| 69 | cJSON_Delete(obj); | ||
| 70 | return deleted; | ||
| 71 | } | ||
| 72 | |||
| 73 | static int delete_by_a_tags(storage_engine_t *storage, const char *event_json, | ||
| 74 | size_t len, const char *deleter_pubkey, | ||
| 75 | uint64_t created_at) | ||
| 76 | { | ||
| 77 | cJSON *obj = cJSON_ParseWithLength(event_json, len); | ||
| 78 | if (!obj) return 0; | ||
| 79 | |||
| 80 | cJSON *tags = cJSON_GetObjectItem(obj, "tags"); | ||
| 81 | if (!tags || !cJSON_IsArray(tags)) { cJSON_Delete(obj); return 0; } | ||
| 82 | |||
| 83 | int deleted = 0; | ||
| 84 | int array_size = cJSON_GetArraySize(tags); | ||
| 85 | |||
| 86 | for (int i = 0; i < array_size; i++) { | ||
| 87 | cJSON *tag = cJSON_GetArrayItem(tags, i); | ||
| 88 | if (!cJSON_IsArray(tag)) continue; | ||
| 89 | cJSON *tag_name = cJSON_GetArrayItem(tag, 0); | ||
| 90 | if (!tag_name || !cJSON_IsString(tag_name)) continue; | ||
| 91 | if (strcmp(tag_name->valuestring, "a") != 0) continue; | ||
| 92 | |||
| 93 | cJSON *tag_val = cJSON_GetArrayItem(tag, 1); | ||
| 94 | if (!tag_val || !cJSON_IsString(tag_val)) continue; | ||
| 95 | |||
| 96 | int32_t kind; | ||
| 97 | char pubkey[65] = {0}; | ||
| 98 | char d_tag[256] = ""; | ||
| 99 | if (sscanf(tag_val->valuestring, "%" SCNd32 ":%64[^:]:%255s", | ||
| 100 | &kind, pubkey, d_tag) < 2) | ||
| 101 | continue; | ||
| 102 | |||
| 103 | if (strcmp(pubkey, deleter_pubkey) != 0) continue; | ||
| 104 | |||
| 105 | char **results = NULL; | ||
| 106 | uint16_t count = 0; | ||
| 107 | storage_query_events_json(storage, kind, pubkey, 100, &results, &count); | ||
| 108 | for (uint16_t e = 0; e < count; e++) { | ||
| 109 | if (storage_delete_event(storage, (const uint8_t *)results[e]) == STORAGE_OK) { | ||
| 110 | deleted++; | ||
| 111 | } | ||
| 112 | } | ||
| 113 | storage_free_query_results(results, count); | ||
| 114 | } | ||
| 115 | |||
| 116 | cJSON_Delete(obj); | ||
| 117 | return deleted; | ||
| 118 | } | ||
| 119 | |||
| 120 | static int delete_by_k_tags(storage_engine_t *storage, const char *event_json, | ||
| 121 | size_t len, const char *deleter_pubkey, | ||
| 122 | uint64_t created_at) | ||
| 123 | { | ||
| 124 | cJSON *obj = cJSON_ParseWithLength(event_json, len); | ||
| 125 | if (!obj) return 0; | ||
| 126 | |||
| 127 | cJSON *tags = cJSON_GetObjectItem(obj, "tags"); | ||
| 128 | if (!tags || !cJSON_IsArray(tags)) { cJSON_Delete(obj); return 0; } | ||
| 129 | |||
| 130 | int deleted = 0; | ||
| 131 | int array_size = cJSON_GetArraySize(tags); | ||
| 132 | |||
| 133 | for (int i = 0; i < array_size; i++) { | ||
| 134 | cJSON *tag = cJSON_GetArrayItem(tags, i); | ||
| 135 | if (!cJSON_IsArray(tag)) continue; | ||
| 136 | cJSON *tag_name = cJSON_GetArrayItem(tag, 0); | ||
| 137 | if (!tag_name || !cJSON_IsString(tag_name)) continue; | ||
| 138 | if (strcmp(tag_name->valuestring, "k") != 0) continue; | ||
| 139 | |||
| 140 | cJSON *tag_val = cJSON_GetArrayItem(tag, 1); | ||
| 141 | if (!tag_val || !cJSON_IsString(tag_val)) continue; | ||
| 142 | |||
| 143 | int kind = atoi(tag_val->valuestring); | ||
| 144 | |||
| 145 | char **results = NULL; | ||
| 146 | uint16_t count = 0; | ||
| 147 | storage_query_events_json(storage, kind, deleter_pubkey, 500, &results, &count); | ||
| 148 | for (uint16_t e = 0; e < count; e++) { | ||
| 149 | uint8_t eid[32]; | ||
| 150 | if (extract_event_id_field(results[e], strlen(results[e]), eid) == 0) { | ||
| 151 | storage_delete_event(storage, eid); | ||
| 152 | deleted++; | ||
| 153 | } | ||
| 154 | } | ||
| 155 | storage_free_query_results(results, count); | ||
| 156 | } | ||
| 157 | |||
| 158 | cJSON_Delete(obj); | ||
| 159 | return deleted; | ||
| 160 | } | ||
| 161 | |||
| 162 | int deletion_process_json(storage_engine_t *storage, const char *event_json, | ||
| 163 | size_t event_len) | ||
| 164 | { | ||
| 165 | if (!storage || !event_json) return 0; | ||
| 166 | |||
| 167 | cJSON *obj = cJSON_ParseWithLength(event_json, event_len); | ||
| 168 | if (!obj) return 0; | ||
| 169 | cJSON *kind_item = cJSON_GetObjectItem(obj, "kind"); | ||
| 170 | int kind = kind_item ? kind_item->valueint : 0; | ||
| 171 | cJSON *pk_item = cJSON_GetObjectItem(obj, "pubkey"); | ||
| 172 | const char *pubkey = pk_item ? pk_item->valuestring : ""; | ||
| 173 | cJSON *ca_item = cJSON_GetObjectItem(obj, "created_at"); | ||
| 174 | uint64_t created_at = ca_item ? (uint64_t)ca_item->valuedouble : 0; | ||
| 175 | cJSON_Delete(obj); | ||
| 176 | |||
| 177 | if (kind != NOSTR_KIND_DELETION) return 0; | ||
| 178 | |||
| 179 | char *deleter_pk = strdup(pubkey); | ||
| 180 | if (!deleter_pk) return 0; | ||
| 181 | |||
| 182 | int deleted = 0; | ||
| 183 | deleted += delete_by_e_tags(storage, event_json, event_len, deleter_pk); | ||
| 184 | deleted += delete_by_a_tags(storage, event_json, event_len, deleter_pk, created_at); | ||
| 185 | deleted += delete_by_k_tags(storage, event_json, event_len, deleter_pk, created_at); | ||
| 186 | |||
| 187 | free(deleter_pk); | ||
| 188 | ESP_LOGI(TAG, "Deletion processed: %d events removed", deleted); | ||
| 189 | return deleted; | ||
| 190 | } | ||
diff --git a/components/wisp_relay/deletion.h b/components/wisp_relay/deletion.h new file mode 100644 index 0000000..b494a8e --- /dev/null +++ b/components/wisp_relay/deletion.h | |||
| @@ -0,0 +1,11 @@ | |||
| 1 | #ifndef DELETION_H | ||
| 2 | #define DELETION_H | ||
| 3 | |||
| 4 | #include "storage_engine.h" | ||
| 5 | |||
| 6 | #define NOSTR_KIND_DELETION 5 | ||
| 7 | |||
| 8 | int deletion_process_json(storage_engine_t *storage, const char *event_json, | ||
| 9 | size_t event_len); | ||
| 10 | |||
| 11 | #endif | ||
diff --git a/components/wisp_relay/flash_monitor.c b/components/wisp_relay/flash_monitor.c new file mode 100644 index 0000000..ceb8c3b --- /dev/null +++ b/components/wisp_relay/flash_monitor.c | |||
| @@ -0,0 +1,30 @@ | |||
| 1 | #include "flash_monitor.h" | ||
| 2 | #include "esp_littlefs.h" | ||
| 3 | #include "esp_log.h" | ||
| 4 | #include <string.h> | ||
| 5 | |||
| 6 | static const char *TAG = "flash_monitor"; | ||
| 7 | |||
| 8 | void flash_get_health(const char *partition_label, flash_health_t *health) | ||
| 9 | { | ||
| 10 | memset(health, 0, sizeof(flash_health_t)); | ||
| 11 | |||
| 12 | esp_err_t ret = esp_littlefs_info(partition_label, | ||
| 13 | &health->total_bytes, | ||
| 14 | &health->used_bytes); | ||
| 15 | if (ret != ESP_OK) { | ||
| 16 | ESP_LOGE(TAG, "Failed to get LittleFS info: %s", esp_err_to_name(ret)); | ||
| 17 | return; | ||
| 18 | } | ||
| 19 | |||
| 20 | if (health->total_bytes == 0) { | ||
| 21 | health->free_bytes = 0; | ||
| 22 | health->usage_percent = 0.0f; | ||
| 23 | } else { | ||
| 24 | health->free_bytes = health->total_bytes - health->used_bytes; | ||
| 25 | health->usage_percent = (float)health->used_bytes / health->total_bytes * 100.0f; | ||
| 26 | } | ||
| 27 | |||
| 28 | ESP_LOGD(TAG, "Flash: %.1f%% used (%zu/%zu bytes)", | ||
| 29 | health->usage_percent, health->used_bytes, health->total_bytes); | ||
| 30 | } | ||
diff --git a/components/wisp_relay/flash_monitor.h b/components/wisp_relay/flash_monitor.h new file mode 100644 index 0000000..86f1b53 --- /dev/null +++ b/components/wisp_relay/flash_monitor.h | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | #ifndef FLASH_MONITOR_H | ||
| 2 | #define FLASH_MONITOR_H | ||
| 3 | |||
| 4 | #include <stdint.h> | ||
| 5 | #include <stddef.h> | ||
| 6 | |||
| 7 | typedef struct { | ||
| 8 | size_t total_bytes; | ||
| 9 | size_t used_bytes; | ||
| 10 | size_t free_bytes; | ||
| 11 | float usage_percent; | ||
| 12 | } flash_health_t; | ||
| 13 | |||
| 14 | void flash_get_health(const char *partition_label, flash_health_t *health); | ||
| 15 | |||
| 16 | #endif | ||
diff --git a/components/wisp_relay/handlers.c b/components/wisp_relay/handlers.c new file mode 100644 index 0000000..2164725 --- /dev/null +++ b/components/wisp_relay/handlers.c | |||
| @@ -0,0 +1,203 @@ | |||
| 1 | #include "handlers.h" | ||
| 2 | #include "router.h" | ||
| 3 | #include "storage_engine.h" | ||
| 4 | #include "sub_manager.h" | ||
| 5 | #include "relay_validator.h" | ||
| 6 | #include "broadcaster.h" | ||
| 7 | #include "deletion.h" | ||
| 8 | #include "rate_limiter.h" | ||
| 9 | #include "relay_types.h" | ||
| 10 | #include "cJSON.h" | ||
| 11 | #include "esp_log.h" | ||
| 12 | #include <string.h> | ||
| 13 | |||
| 14 | static const char *TAG = "handlers"; | ||
| 15 | |||
| 16 | int handle_event(relay_ctx_t *ctx, int conn_fd, const char *event_json, size_t event_len) | ||
| 17 | { | ||
| 18 | if (!ctx || !event_json) return -1; | ||
| 19 | |||
| 20 | if (ctx->rate_limiter) { | ||
| 21 | if (!rate_limiter_check(ctx->rate_limiter, conn_fd, RATE_TYPE_EVENT)) { | ||
| 22 | router_send_ok(ctx, conn_fd, "", false, "rate limited"); | ||
| 23 | return -1; | ||
| 24 | } | ||
| 25 | } | ||
| 26 | |||
| 27 | cJSON *obj = cJSON_ParseWithLength(event_json, event_len); | ||
| 28 | if (!obj) { | ||
| 29 | router_send_ok(ctx, conn_fd, "", false, "invalid JSON"); | ||
| 30 | return -1; | ||
| 31 | } | ||
| 32 | |||
| 33 | cJSON *id_item = cJSON_GetObjectItem(obj, "id"); | ||
| 34 | cJSON *pubkey_item = cJSON_GetObjectItem(obj, "pubkey"); | ||
| 35 | cJSON *kind_item = cJSON_GetObjectItem(obj, "kind"); | ||
| 36 | cJSON *ca_item = cJSON_GetObjectItem(obj, "created_at"); | ||
| 37 | |||
| 38 | if (!id_item || !pubkey_item || !kind_item || !ca_item) { | ||
| 39 | cJSON_Delete(obj); | ||
| 40 | router_send_ok(ctx, conn_fd, "", false, "missing required fields"); | ||
| 41 | return -1; | ||
| 42 | } | ||
| 43 | |||
| 44 | const char *id_hex = id_item->valuestring; | ||
| 45 | const char *pubkey_hex = pubkey_item->valuestring; | ||
| 46 | int kind = kind_item->valueint; | ||
| 47 | uint64_t created_at = (uint64_t)ca_item->valuedouble; | ||
| 48 | |||
| 49 | if (ctx->config.max_future_sec > 0) { | ||
| 50 | int64_t now = (int64_t)(xTaskGetTickCount() / configTICK_RATE_HZ); | ||
| 51 | if ((int64_t)created_at > now + ctx->config.max_future_sec) { | ||
| 52 | cJSON_Delete(obj); | ||
| 53 | router_send_ok(ctx, conn_fd, id_hex, false, "created_at too far in future"); | ||
| 54 | return -1; | ||
| 55 | } | ||
| 56 | } | ||
| 57 | |||
| 58 | uint8_t event_id[32]; | ||
| 59 | if (relay_hex_to_bytes(id_hex, 64, event_id, 32) != 0) { | ||
| 60 | cJSON_Delete(obj); | ||
| 61 | router_send_ok(ctx, conn_fd, "", false, "invalid event id"); | ||
| 62 | return -1; | ||
| 63 | } | ||
| 64 | |||
| 65 | if (storage_event_exists(ctx->storage, event_id)) { | ||
| 66 | cJSON_Delete(obj); | ||
| 67 | router_send_ok(ctx, conn_fd, id_hex, true, "duplicate"); | ||
| 68 | return 0; | ||
| 69 | } | ||
| 70 | |||
| 71 | if (!relay_validator_verify_event(event_json, event_len)) { | ||
| 72 | cJSON_Delete(obj); | ||
| 73 | router_send_ok(ctx, conn_fd, id_hex, false, "invalid signature"); | ||
| 74 | return -1; | ||
| 75 | } | ||
| 76 | |||
| 77 | cJSON_Delete(obj); | ||
| 78 | |||
| 79 | storage_error_t err = storage_save_event_json(ctx->storage, event_json, event_len); | ||
| 80 | if (err != STORAGE_OK) { | ||
| 81 | const char *msg = (err == STORAGE_ERR_FULL) ? "relay full" : | ||
| 82 | (err == STORAGE_ERR_DUPLICATE) ? "duplicate" : "storage error"; | ||
| 83 | router_send_ok(ctx, conn_fd, id_hex, false, msg); | ||
| 84 | return -1; | ||
| 85 | } | ||
| 86 | |||
| 87 | router_send_ok(ctx, conn_fd, id_hex, true, ""); | ||
| 88 | |||
| 89 | if (kind == NOSTR_KIND_DELETION) { | ||
| 90 | deletion_process_json(ctx->storage, event_json, event_len); | ||
| 91 | } | ||
| 92 | |||
| 93 | broadcaster_fanout_json(ctx, event_json, event_len, kind, pubkey_hex, created_at); | ||
| 94 | |||
| 95 | return 0; | ||
| 96 | } | ||
| 97 | |||
| 98 | static void parse_filter_json(const char *json, sub_filter_t *filter) | ||
| 99 | { | ||
| 100 | memset(filter, 0, sizeof(sub_filter_t)); | ||
| 101 | cJSON *obj = cJSON_Parse(json); | ||
| 102 | if (!obj) return; | ||
| 103 | |||
| 104 | cJSON *arr; | ||
| 105 | |||
| 106 | arr = cJSON_GetObjectItem(obj, "ids"); | ||
| 107 | if (arr && cJSON_IsArray(arr)) { | ||
| 108 | filter->ids_count = cJSON_GetArraySize(arr); | ||
| 109 | if (filter->ids_count > SUB_MAX_FILTER_IDS) filter->ids_count = SUB_MAX_FILTER_IDS; | ||
| 110 | for (size_t i = 0; i < filter->ids_count; i++) | ||
| 111 | filter->ids[i] = strdup(cJSON_GetArrayItem(arr, i)->valuestring); | ||
| 112 | } | ||
| 113 | |||
| 114 | arr = cJSON_GetObjectItem(obj, "authors"); | ||
| 115 | if (arr && cJSON_IsArray(arr)) { | ||
| 116 | filter->authors_count = cJSON_GetArraySize(arr); | ||
| 117 | if (filter->authors_count > SUB_MAX_FILTER_AUTHORS) filter->authors_count = SUB_MAX_FILTER_AUTHORS; | ||
| 118 | for (size_t i = 0; i < filter->authors_count; i++) | ||
| 119 | filter->authors[i] = strdup(cJSON_GetArrayItem(arr, i)->valuestring); | ||
| 120 | } | ||
| 121 | |||
| 122 | arr = cJSON_GetObjectItem(obj, "kinds"); | ||
| 123 | if (arr && cJSON_IsArray(arr)) { | ||
| 124 | filter->kinds_count = cJSON_GetArraySize(arr); | ||
| 125 | if (filter->kinds_count > SUB_MAX_FILTER_KINDS) filter->kinds_count = SUB_MAX_FILTER_KINDS; | ||
| 126 | for (size_t i = 0; i < filter->kinds_count; i++) | ||
| 127 | filter->kinds[i] = cJSON_GetArrayItem(arr, i)->valueint; | ||
| 128 | } | ||
| 129 | |||
| 130 | arr = cJSON_GetObjectItem(obj, "#e"); | ||
| 131 | if (arr && cJSON_IsArray(arr)) { | ||
| 132 | filter->e_tags_count = cJSON_GetArraySize(arr); | ||
| 133 | if (filter->e_tags_count > SUB_MAX_FILTER_ETAGS) filter->e_tags_count = SUB_MAX_FILTER_ETAGS; | ||
| 134 | for (size_t i = 0; i < filter->e_tags_count; i++) | ||
| 135 | filter->e_tags[i] = strdup(cJSON_GetArrayItem(arr, i)->valuestring); | ||
| 136 | } | ||
| 137 | |||
| 138 | arr = cJSON_GetObjectItem(obj, "#p"); | ||
| 139 | if (arr && cJSON_IsArray(arr)) { | ||
| 140 | filter->p_tags_count = cJSON_GetArraySize(arr); | ||
| 141 | if (filter->p_tags_count > SUB_MAX_FILTER_PTAGS) filter->p_tags_count = SUB_MAX_FILTER_PTAGS; | ||
| 142 | for (size_t i = 0; i < filter->p_tags_count; i++) | ||
| 143 | filter->p_tags[i] = strdup(cJSON_GetArrayItem(arr, i)->valuestring); | ||
| 144 | } | ||
| 145 | |||
| 146 | cJSON *since = cJSON_GetObjectItem(obj, "since"); | ||
| 147 | if (since) filter->since = (int64_t)since->valuedouble; | ||
| 148 | cJSON *until = cJSON_GetObjectItem(obj, "until"); | ||
| 149 | if (until) filter->until = (int64_t)until->valuedouble; | ||
| 150 | cJSON *limit = cJSON_GetObjectItem(obj, "limit"); | ||
| 151 | if (limit) filter->limit = limit->valueint; | ||
| 152 | |||
| 153 | cJSON_Delete(obj); | ||
| 154 | } | ||
| 155 | |||
| 156 | void handle_req(relay_ctx_t *ctx, int conn_fd, const char *sub_id, const char *filters_json) | ||
| 157 | { | ||
| 158 | if (!ctx || !sub_id) return; | ||
| 159 | |||
| 160 | if (ctx->rate_limiter) { | ||
| 161 | if (!rate_limiter_check(ctx->rate_limiter, conn_fd, RATE_TYPE_REQ)) { | ||
| 162 | router_send_closed(ctx, conn_fd, sub_id, "rate limited"); | ||
| 163 | return; | ||
| 164 | } | ||
| 165 | } | ||
| 166 | |||
| 167 | sub_filter_t filter; | ||
| 168 | parse_filter_json(filters_json, &filter); | ||
| 169 | |||
| 170 | int query_kind = -1; | ||
| 171 | const char *query_author = NULL; | ||
| 172 | int query_limit = filter.limit > 0 ? filter.limit : 100; | ||
| 173 | |||
| 174 | if (filter.kinds_count > 0) query_kind = filter.kinds[0]; | ||
| 175 | if (filter.authors_count > 0) query_author = filter.authors[0]; | ||
| 176 | |||
| 177 | char **results = NULL; | ||
| 178 | uint16_t count = 0; | ||
| 179 | storage_query_events_json(ctx->storage, query_kind, query_author, | ||
| 180 | query_limit, &results, &count); | ||
| 181 | |||
| 182 | for (uint16_t i = 0; i < count; i++) { | ||
| 183 | router_send_event(ctx, conn_fd, sub_id, results[i], strlen(results[i])); | ||
| 184 | } | ||
| 185 | storage_free_query_results(results, count); | ||
| 186 | |||
| 187 | router_send_eose(ctx, conn_fd, sub_id); | ||
| 188 | |||
| 189 | sub_manager_add(ctx->sub_manager, conn_fd, sub_id, &filter, 1); | ||
| 190 | |||
| 191 | sub_filter_t *f = &filter; | ||
| 192 | for (size_t i = 0; i < f->ids_count; i++) free(f->ids[i]); | ||
| 193 | for (size_t i = 0; i < f->authors_count; i++) free(f->authors[i]); | ||
| 194 | for (size_t i = 0; i < f->e_tags_count; i++) free(f->e_tags[i]); | ||
| 195 | for (size_t i = 0; i < f->p_tags_count; i++) free(f->p_tags[i]); | ||
| 196 | } | ||
| 197 | |||
| 198 | int handle_close(relay_ctx_t *ctx, int conn_fd, const char *sub_id) | ||
| 199 | { | ||
| 200 | if (!ctx || !sub_id) return -1; | ||
| 201 | sub_manager_remove(ctx->sub_manager, conn_fd, sub_id); | ||
| 202 | return 0; | ||
| 203 | } | ||
diff --git a/components/wisp_relay/handlers.h b/components/wisp_relay/handlers.h new file mode 100644 index 0000000..91621bf --- /dev/null +++ b/components/wisp_relay/handlers.h | |||
| @@ -0,0 +1,10 @@ | |||
| 1 | #ifndef HANDLERS_H | ||
| 2 | #define HANDLERS_H | ||
| 3 | |||
| 4 | #include "relay_core.h" | ||
| 5 | |||
| 6 | int handle_event(relay_ctx_t *ctx, int conn_fd, const char *event_json, size_t event_len); | ||
| 7 | void handle_req(relay_ctx_t *ctx, int conn_fd, const char *sub_id, const char *filters_json); | ||
| 8 | int handle_close(relay_ctx_t *ctx, int conn_fd, const char *sub_id); | ||
| 9 | |||
| 10 | #endif | ||
diff --git a/components/wisp_relay/idf_component.yml b/components/wisp_relay/idf_component.yml new file mode 100644 index 0000000..c093387 --- /dev/null +++ b/components/wisp_relay/idf_component.yml | |||
| @@ -0,0 +1 @@ | |||
| dependencies: {} | |||
diff --git a/components/wisp_relay/nip11_relay.c b/components/wisp_relay/nip11_relay.c new file mode 100644 index 0000000..4e1df37 --- /dev/null +++ b/components/wisp_relay/nip11_relay.c | |||
| @@ -0,0 +1,53 @@ | |||
| 1 | #include "nip11_relay.h" | ||
| 2 | #include <string.h> | ||
| 3 | |||
| 4 | static const char *NIP11_JSON = | ||
| 5 | "{" | ||
| 6 | "\"name\":\"TollGate Relay\"," | ||
| 7 | "\"description\":\"Local Nostr relay with 21-day TTL and negentropy sync\"," | ||
| 8 | "\"pubkey\":\"\"," | ||
| 9 | "\"contact\":\"\"," | ||
| 10 | "\"supported_nips\":[1,9,11,20,40,77]," | ||
| 11 | "\"software\":\"https://github.com/nicobao/esp32-tollgate\"," | ||
| 12 | "\"version\":\"1.0.0\"," | ||
| 13 | "\"limitation\":{" | ||
| 14 | "\"max_message_length\":65536," | ||
| 15 | "\"max_subscriptions\":8," | ||
| 16 | "\"max_filters\":4," | ||
| 17 | "\"max_limit\":500," | ||
| 18 | "\"max_subid_length\":64," | ||
| 19 | "\"max_event_tags\":100," | ||
| 20 | "\"max_content_length\":32768," | ||
| 21 | "\"min_pow_difficulty\":0," | ||
| 22 | "\"auth_required\":false," | ||
| 23 | "\"payment_required\":false" | ||
| 24 | "}," | ||
| 25 | "\"retention\":[{\"kinds\":[0,1,2,3,4,5,6,7],\"time\":1814400}]," | ||
| 26 | "\"relay_countries\":[\"DE\"]" | ||
| 27 | "}"; | ||
| 28 | |||
| 29 | esp_err_t relay_nip11_handler(httpd_req_t *req) | ||
| 30 | { | ||
| 31 | char accept[64] = ""; | ||
| 32 | httpd_req_get_hdr_value_str(req, "Accept", accept, sizeof(accept)); | ||
| 33 | |||
| 34 | if (strstr(accept, "application/nostr+json")) { | ||
| 35 | httpd_resp_set_type(req, "application/nostr+json"); | ||
| 36 | } else { | ||
| 37 | httpd_resp_set_type(req, "application/json"); | ||
| 38 | } | ||
| 39 | |||
| 40 | httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); | ||
| 41 | httpd_resp_set_hdr(req, "Access-Control-Allow-Headers", "Content-Type, Accept"); | ||
| 42 | httpd_resp_set_hdr(req, "Access-Control-Allow-Methods", "GET, OPTIONS"); | ||
| 43 | return httpd_resp_send(req, NIP11_JSON, strlen(NIP11_JSON)); | ||
| 44 | } | ||
| 45 | |||
| 46 | esp_err_t relay_nip11_options_handler(httpd_req_t *req) | ||
| 47 | { | ||
| 48 | httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); | ||
| 49 | httpd_resp_set_hdr(req, "Access-Control-Allow-Headers", "Content-Type, Accept"); | ||
| 50 | httpd_resp_set_hdr(req, "Access-Control-Allow-Methods", "GET, OPTIONS"); | ||
| 51 | httpd_resp_set_status(req, "204 No Content"); | ||
| 52 | return httpd_resp_send(req, NULL, 0); | ||
| 53 | } | ||
diff --git a/components/wisp_relay/nip11_relay.h b/components/wisp_relay/nip11_relay.h new file mode 100644 index 0000000..84f7971 --- /dev/null +++ b/components/wisp_relay/nip11_relay.h | |||
| @@ -0,0 +1,9 @@ | |||
| 1 | #ifndef NIP11_RELAY_H | ||
| 2 | #define NIP11_RELAY_H | ||
| 3 | |||
| 4 | #include "esp_http_server.h" | ||
| 5 | |||
| 6 | esp_err_t relay_nip11_handler(httpd_req_t *req); | ||
| 7 | esp_err_t relay_nip11_options_handler(httpd_req_t *req); | ||
| 8 | |||
| 9 | #endif | ||
diff --git a/components/wisp_relay/rate_limiter.c b/components/wisp_relay/rate_limiter.c new file mode 100644 index 0000000..7734e03 --- /dev/null +++ b/components/wisp_relay/rate_limiter.c | |||
| @@ -0,0 +1,98 @@ | |||
| 1 | #include "rate_limiter.h" | ||
| 2 | #include "esp_timer.h" | ||
| 3 | #include "esp_log.h" | ||
| 4 | #include <string.h> | ||
| 5 | |||
| 6 | static const char *TAG = "rate_limiter"; | ||
| 7 | |||
| 8 | void rate_limiter_init(rate_limiter_t *rl, const rate_config_t *config) | ||
| 9 | { | ||
| 10 | memset(rl, 0, sizeof(rate_limiter_t)); | ||
| 11 | rl->lock = xSemaphoreCreateMutex(); | ||
| 12 | if (config) { | ||
| 13 | memcpy(&rl->config, config, sizeof(rate_config_t)); | ||
| 14 | } else { | ||
| 15 | rl->config.events_per_minute = 30; | ||
| 16 | rl->config.reqs_per_minute = 60; | ||
| 17 | } | ||
| 18 | } | ||
| 19 | |||
| 20 | void rate_limiter_destroy(rate_limiter_t *rl) | ||
| 21 | { | ||
| 22 | if (!rl) return; | ||
| 23 | if (rl->lock) { | ||
| 24 | vSemaphoreDelete(rl->lock); | ||
| 25 | rl->lock = NULL; | ||
| 26 | } | ||
| 27 | } | ||
| 28 | |||
| 29 | static rate_bucket_t* get_bucket(rate_limiter_t *rl, int fd) | ||
| 30 | { | ||
| 31 | for (int i = 0; i < RATE_LIMITER_MAX_BUCKETS; i++) { | ||
| 32 | if (rl->buckets[i].active && rl->buckets[i].fd == fd) { | ||
| 33 | return &rl->buckets[i]; | ||
| 34 | } | ||
| 35 | } | ||
| 36 | for (int i = 0; i < RATE_LIMITER_MAX_BUCKETS; i++) { | ||
| 37 | if (!rl->buckets[i].active) { | ||
| 38 | rl->buckets[i].fd = fd; | ||
| 39 | rl->buckets[i].active = true; | ||
| 40 | rl->buckets[i].event_count = 0; | ||
| 41 | rl->buckets[i].req_count = 0; | ||
| 42 | rl->buckets[i].window_start = esp_timer_get_time() / 1000000; | ||
| 43 | return &rl->buckets[i]; | ||
| 44 | } | ||
| 45 | } | ||
| 46 | return NULL; | ||
| 47 | } | ||
| 48 | |||
| 49 | bool rate_limiter_check(rate_limiter_t *rl, int fd, rate_type_t type) | ||
| 50 | { | ||
| 51 | xSemaphoreTake(rl->lock, portMAX_DELAY); | ||
| 52 | |||
| 53 | rate_bucket_t *bucket = get_bucket(rl, fd); | ||
| 54 | if (!bucket) { | ||
| 55 | xSemaphoreGive(rl->lock); | ||
| 56 | return false; | ||
| 57 | } | ||
| 58 | |||
| 59 | uint32_t now = esp_timer_get_time() / 1000000; | ||
| 60 | |||
| 61 | if (now - bucket->window_start >= 60) { | ||
| 62 | bucket->event_count = 0; | ||
| 63 | bucket->req_count = 0; | ||
| 64 | bucket->window_start = now; | ||
| 65 | } | ||
| 66 | |||
| 67 | bool allowed = true; | ||
| 68 | if (type == RATE_TYPE_EVENT) { | ||
| 69 | if (bucket->event_count >= rl->config.events_per_minute) { | ||
| 70 | ESP_LOGW(TAG, "Rate limited: fd=%d events=%d", fd, bucket->event_count); | ||
| 71 | allowed = false; | ||
| 72 | } else { | ||
| 73 | bucket->event_count++; | ||
| 74 | } | ||
| 75 | } else { | ||
| 76 | if (bucket->req_count >= rl->config.reqs_per_minute) { | ||
| 77 | ESP_LOGW(TAG, "Rate limited: fd=%d reqs=%d", fd, bucket->req_count); | ||
| 78 | allowed = false; | ||
| 79 | } else { | ||
| 80 | bucket->req_count++; | ||
| 81 | } | ||
| 82 | } | ||
| 83 | |||
| 84 | xSemaphoreGive(rl->lock); | ||
| 85 | return allowed; | ||
| 86 | } | ||
| 87 | |||
| 88 | void rate_limiter_reset(rate_limiter_t *rl, int fd) | ||
| 89 | { | ||
| 90 | xSemaphoreTake(rl->lock, portMAX_DELAY); | ||
| 91 | for (int i = 0; i < RATE_LIMITER_MAX_BUCKETS; i++) { | ||
| 92 | if (rl->buckets[i].active && rl->buckets[i].fd == fd) { | ||
| 93 | rl->buckets[i].active = false; | ||
| 94 | break; | ||
| 95 | } | ||
| 96 | } | ||
| 97 | xSemaphoreGive(rl->lock); | ||
| 98 | } | ||
diff --git a/components/wisp_relay/rate_limiter.h b/components/wisp_relay/rate_limiter.h new file mode 100644 index 0000000..655ddf2 --- /dev/null +++ b/components/wisp_relay/rate_limiter.h | |||
| @@ -0,0 +1,40 @@ | |||
| 1 | #ifndef RATE_LIMITER_H | ||
| 2 | #define RATE_LIMITER_H | ||
| 3 | |||
| 4 | #include <stdint.h> | ||
| 5 | #include <stdbool.h> | ||
| 6 | #include "freertos/FreeRTOS.h" | ||
| 7 | #include "freertos/semphr.h" | ||
| 8 | |||
| 9 | #define RATE_LIMITER_MAX_BUCKETS 16 | ||
| 10 | |||
| 11 | typedef enum { | ||
| 12 | RATE_TYPE_EVENT, | ||
| 13 | RATE_TYPE_REQ, | ||
| 14 | } rate_type_t; | ||
| 15 | |||
| 16 | typedef struct { | ||
| 17 | uint16_t events_per_minute; | ||
| 18 | uint16_t reqs_per_minute; | ||
| 19 | } rate_config_t; | ||
| 20 | |||
| 21 | typedef struct { | ||
| 22 | int fd; | ||
| 23 | uint16_t event_count; | ||
| 24 | uint16_t req_count; | ||
| 25 | uint32_t window_start; | ||
| 26 | bool active; | ||
| 27 | } rate_bucket_t; | ||
| 28 | |||
| 29 | typedef struct rate_limiter { | ||
| 30 | rate_config_t config; | ||
| 31 | rate_bucket_t buckets[RATE_LIMITER_MAX_BUCKETS]; | ||
| 32 | SemaphoreHandle_t lock; | ||
| 33 | } rate_limiter_t; | ||
| 34 | |||
| 35 | void rate_limiter_init(rate_limiter_t *rl, const rate_config_t *config); | ||
| 36 | void rate_limiter_destroy(rate_limiter_t *rl); | ||
| 37 | bool rate_limiter_check(rate_limiter_t *rl, int fd, rate_type_t type); | ||
| 38 | void rate_limiter_reset(rate_limiter_t *rl, int fd); | ||
| 39 | |||
| 40 | #endif | ||
diff --git a/components/wisp_relay/relay_core.h b/components/wisp_relay/relay_core.h new file mode 100644 index 0000000..d8e7096 --- /dev/null +++ b/components/wisp_relay/relay_core.h | |||
| @@ -0,0 +1,27 @@ | |||
| 1 | #ifndef RELAY_CORE_H | ||
| 2 | #define RELAY_CORE_H | ||
| 3 | |||
| 4 | #include <stdint.h> | ||
| 5 | |||
| 6 | #include "ws_server.h" | ||
| 7 | |||
| 8 | typedef struct sub_manager sub_manager_t; | ||
| 9 | typedef struct storage_engine storage_engine_t; | ||
| 10 | typedef struct rate_limiter rate_limiter_t; | ||
| 11 | |||
| 12 | typedef struct relay_ctx { | ||
| 13 | ws_server_t ws_server; | ||
| 14 | sub_manager_t *sub_manager; | ||
| 15 | storage_engine_t *storage; | ||
| 16 | rate_limiter_t *rate_limiter; | ||
| 17 | |||
| 18 | struct { | ||
| 19 | uint16_t port; | ||
| 20 | uint32_t max_event_age_sec; | ||
| 21 | uint8_t max_subs_per_conn; | ||
| 22 | uint8_t max_filters_per_sub; | ||
| 23 | int64_t max_future_sec; | ||
| 24 | } config; | ||
| 25 | } relay_ctx_t; | ||
| 26 | |||
| 27 | #endif | ||
diff --git a/components/wisp_relay/relay_types.c b/components/wisp_relay/relay_types.c new file mode 100644 index 0000000..9833885 --- /dev/null +++ b/components/wisp_relay/relay_types.c | |||
| @@ -0,0 +1,21 @@ | |||
| 1 | #include "relay_types.h" | ||
| 2 | #include <stdio.h> | ||
| 3 | #include <string.h> | ||
| 4 | |||
| 5 | int relay_hex_to_bytes(const char *hex, size_t hex_len, uint8_t *out, size_t out_len) | ||
| 6 | { | ||
| 7 | if (hex_len != out_len * 2) return -1; | ||
| 8 | for (size_t i = 0; i < out_len; i++) { | ||
| 9 | unsigned int byte; | ||
| 10 | if (sscanf(hex + i * 2, "%02x", &byte) != 1) return -1; | ||
| 11 | out[i] = (uint8_t)byte; | ||
| 12 | } | ||
| 13 | return 0; | ||
| 14 | } | ||
| 15 | |||
| 16 | void relay_bytes_to_hex(const uint8_t *bytes, size_t len, char *hex) | ||
| 17 | { | ||
| 18 | for (size_t i = 0; i < len; i++) | ||
| 19 | sprintf(hex + i * 2, "%02x", bytes[i]); | ||
| 20 | hex[len * 2] = '\0'; | ||
| 21 | } | ||
diff --git a/components/wisp_relay/relay_types.h b/components/wisp_relay/relay_types.h new file mode 100644 index 0000000..343e51b --- /dev/null +++ b/components/wisp_relay/relay_types.h | |||
| @@ -0,0 +1,43 @@ | |||
| 1 | #ifndef RELAY_TYPES_H | ||
| 2 | #define RELAY_TYPES_H | ||
| 3 | |||
| 4 | #include <stdint.h> | ||
| 5 | #include <stdbool.h> | ||
| 6 | #include <stddef.h> | ||
| 7 | |||
| 8 | #define RELAY_MAX_EVENT_SIZE 8192 | ||
| 9 | #define RELAY_ID_SIZE 32 | ||
| 10 | #define RELAY_SIG_SIZE 64 | ||
| 11 | #define RELAY_MAX_TAGS 100 | ||
| 12 | #define RELAY_MAX_TAG_VALUES 10 | ||
| 13 | |||
| 14 | typedef struct relay_event { | ||
| 15 | uint8_t id[RELAY_ID_SIZE]; | ||
| 16 | uint8_t pubkey[RELAY_ID_SIZE]; | ||
| 17 | uint64_t created_at; | ||
| 18 | int kind; | ||
| 19 | uint8_t sig[RELAY_SIG_SIZE]; | ||
| 20 | char content[RELAY_MAX_EVENT_SIZE]; | ||
| 21 | size_t content_len; | ||
| 22 | } relay_event_t; | ||
| 23 | |||
| 24 | typedef struct { | ||
| 25 | char **ids; | ||
| 26 | size_t ids_count; | ||
| 27 | char **authors; | ||
| 28 | size_t authors_count; | ||
| 29 | int32_t *kinds; | ||
| 30 | size_t kinds_count; | ||
| 31 | char **e_tags; | ||
| 32 | size_t e_tags_count; | ||
| 33 | char **p_tags; | ||
| 34 | size_t p_tags_count; | ||
| 35 | int64_t since; | ||
| 36 | int64_t until; | ||
| 37 | int limit; | ||
| 38 | } relay_filter_t; | ||
| 39 | |||
| 40 | int relay_hex_to_bytes(const char *hex, size_t hex_len, uint8_t *out, size_t out_len); | ||
| 41 | void relay_bytes_to_hex(const uint8_t *bytes, size_t len, char *hex); | ||
| 42 | |||
| 43 | #endif | ||
diff --git a/components/wisp_relay/relay_validator.c b/components/wisp_relay/relay_validator.c new file mode 100644 index 0000000..eb40d22 --- /dev/null +++ b/components/wisp_relay/relay_validator.c | |||
| @@ -0,0 +1,176 @@ | |||
| 1 | #include "relay_validator.h" | ||
| 2 | #include "relay_types.h" | ||
| 3 | #include "esp_log.h" | ||
| 4 | #include "mbedtls/sha256.h" | ||
| 5 | #include "secp256k1.h" | ||
| 6 | #include "secp256k1_extrakeys.h" | ||
| 7 | #include "secp256k1_schnorrsig.h" | ||
| 8 | #include "cJSON.h" | ||
| 9 | #include "freertos/FreeRTOS.h" | ||
| 10 | #include "freertos/task.h" | ||
| 11 | #include <stddef.h> | ||
| 12 | #include <string.h> | ||
| 13 | #include <stdlib.h> | ||
| 14 | #include <stdio.h> | ||
| 15 | |||
| 16 | static const char *TAG = "relay_validator"; | ||
| 17 | |||
| 18 | static int hex_to_bytes(const char *hex, size_t hex_len, uint8_t *out, size_t out_len) | ||
| 19 | { | ||
| 20 | if (hex_len != out_len * 2) return -1; | ||
| 21 | for (size_t i = 0; i < out_len; i++) { | ||
| 22 | unsigned int byte; | ||
| 23 | if (sscanf(hex + i * 2, "%02x", &byte) != 1) return -1; | ||
| 24 | out[i] = (uint8_t)byte; | ||
| 25 | } | ||
| 26 | return 0; | ||
| 27 | } | ||
| 28 | |||
| 29 | static char *serialize_event_for_id(const char *event_json, size_t event_len) | ||
| 30 | { | ||
| 31 | cJSON *obj = cJSON_ParseWithLength(event_json, event_len); | ||
| 32 | if (!obj) return NULL; | ||
| 33 | |||
| 34 | cJSON *serial = cJSON_CreateArray(); | ||
| 35 | cJSON_AddItemToArray(serial, cJSON_CreateNumber(0)); | ||
| 36 | cJSON_AddItemToArray(serial, cJSON_CreateString( | ||
| 37 | cJSON_GetObjectItem(obj, "pubkey")->valuestring)); | ||
| 38 | cJSON_AddItemToArray(serial, cJSON_CreateNumber( | ||
| 39 | cJSON_GetObjectItem(obj, "created_at")->valuedouble)); | ||
| 40 | cJSON_AddItemToArray(serial, cJSON_CreateNumber( | ||
| 41 | cJSON_GetObjectItem(obj, "kind")->valueint)); | ||
| 42 | cJSON *tags = cJSON_GetObjectItem(obj, "tags"); | ||
| 43 | cJSON_AddItemToArray(serial, cJSON_Duplicate(tags, 1)); | ||
| 44 | cJSON_AddItemToArray(serial, cJSON_CreateString( | ||
| 45 | cJSON_GetObjectItem(obj, "content")->valuestring)); | ||
| 46 | |||
| 47 | char *result = cJSON_PrintUnformatted(serial); | ||
| 48 | cJSON_Delete(serial); | ||
| 49 | cJSON_Delete(obj); | ||
| 50 | return result; | ||
| 51 | } | ||
| 52 | |||
| 53 | static bool verify_event_id(const char *event_json, size_t event_len, | ||
| 54 | const uint8_t expected_id[32]) | ||
| 55 | { | ||
| 56 | char *serialized = serialize_event_for_id(event_json, event_len); | ||
| 57 | if (!serialized) return false; | ||
| 58 | |||
| 59 | uint8_t hash[32]; | ||
| 60 | mbedtls_sha256((const unsigned char *)serialized, strlen(serialized), hash, 0); | ||
| 61 | free(serialized); | ||
| 62 | |||
| 63 | return memcmp(hash, expected_id, 32) == 0; | ||
| 64 | } | ||
| 65 | |||
| 66 | static bool verify_schnorr_sig(const uint8_t pubkey[32], const uint8_t msg[32], | ||
| 67 | const uint8_t sig[64]) | ||
| 68 | { | ||
| 69 | secp256k1_context *ctx = secp256k1_context_create(SECP256K1_CONTEXT_VERIFY); | ||
| 70 | if (!ctx) return false; | ||
| 71 | |||
| 72 | secp256k1_xonly_pubkey xonly_pub; | ||
| 73 | if (!secp256k1_xonly_pubkey_parse(ctx, &xonly_pub, pubkey)) { | ||
| 74 | secp256k1_context_destroy(ctx); | ||
| 75 | return false; | ||
| 76 | } | ||
| 77 | |||
| 78 | bool valid = secp256k1_schnorrsig_verify(ctx, sig, msg, 32, &xonly_pub); | ||
| 79 | secp256k1_context_destroy(ctx); | ||
| 80 | return valid; | ||
| 81 | } | ||
| 82 | |||
| 83 | bool relay_validator_verify_event(const char *event_json, size_t event_len) | ||
| 84 | { | ||
| 85 | cJSON *obj = cJSON_ParseWithLength(event_json, event_len); | ||
| 86 | if (!obj) { | ||
| 87 | ESP_LOGD(TAG, "Invalid JSON"); | ||
| 88 | return false; | ||
| 89 | } | ||
| 90 | |||
| 91 | cJSON *id_item = cJSON_GetObjectItem(obj, "id"); | ||
| 92 | cJSON *pk_item = cJSON_GetObjectItem(obj, "pubkey"); | ||
| 93 | cJSON *sig_item = cJSON_GetObjectItem(obj, "sig"); | ||
| 94 | |||
| 95 | if (!id_item || !pk_item || !sig_item) { | ||
| 96 | cJSON_Delete(obj); | ||
| 97 | ESP_LOGD(TAG, "Missing required fields"); | ||
| 98 | return false; | ||
| 99 | } | ||
| 100 | |||
| 101 | const char *id_hex = id_item->valuestring; | ||
| 102 | const char *pk_hex = pk_item->valuestring; | ||
| 103 | const char *sig_hex = sig_item->valuestring; | ||
| 104 | |||
| 105 | if (strlen(id_hex) != 64 || strlen(pk_hex) != 64 || strlen(sig_hex) != 128) { | ||
| 106 | cJSON_Delete(obj); | ||
| 107 | ESP_LOGD(TAG, "Invalid field lengths"); | ||
| 108 | return false; | ||
| 109 | } | ||
| 110 | |||
| 111 | uint8_t event_id[32], pubkey[32], sig[64]; | ||
| 112 | if (hex_to_bytes(id_hex, 64, event_id, 32) != 0 || | ||
| 113 | hex_to_bytes(pk_hex, 64, pubkey, 32) != 0 || | ||
| 114 | hex_to_bytes(sig_hex, 128, sig, 64) != 0) { | ||
| 115 | cJSON_Delete(obj); | ||
| 116 | ESP_LOGD(TAG, "Invalid hex encoding"); | ||
| 117 | return false; | ||
| 118 | } | ||
| 119 | |||
| 120 | cJSON_Delete(obj); | ||
| 121 | |||
| 122 | if (!verify_event_id(event_json, event_len, event_id)) { | ||
| 123 | ESP_LOGD(TAG, "Event ID mismatch"); | ||
| 124 | return false; | ||
| 125 | } | ||
| 126 | |||
| 127 | if (!verify_schnorr_sig(pubkey, event_id, sig)) { | ||
| 128 | ESP_LOGD(TAG, "Invalid signature"); | ||
| 129 | return false; | ||
| 130 | } | ||
| 131 | |||
| 132 | return true; | ||
| 133 | } | ||
| 134 | |||
| 135 | validation_result_t relay_validator_check(const uint8_t *id, | ||
| 136 | const uint8_t *pubkey, | ||
| 137 | uint64_t created_at, | ||
| 138 | int kind, | ||
| 139 | const char *content, | ||
| 140 | size_t content_len, | ||
| 141 | const char *tags_json, | ||
| 142 | const uint8_t *sig, | ||
| 143 | const validator_config_t *config) | ||
| 144 | { | ||
| 145 | (void)content; (void)content_len; (void)tags_json; | ||
| 146 | |||
| 147 | if (config) { | ||
| 148 | if (config->max_future_sec > 0) { | ||
| 149 | int64_t now = (int64_t)(xTaskGetTickCount() / configTICK_RATE_HZ); | ||
| 150 | if ((int64_t)created_at > now + config->max_future_sec) | ||
| 151 | return VALIDATION_ERR_FUTURE; | ||
| 152 | } | ||
| 153 | } | ||
| 154 | |||
| 155 | if (!verify_schnorr_sig(pubkey, id, sig)) | ||
| 156 | return VALIDATION_ERR_SIG; | ||
| 157 | |||
| 158 | return VALIDATION_OK; | ||
| 159 | } | ||
| 160 | |||
| 161 | const char *relay_validator_result_string(validation_result_t result) | ||
| 162 | { | ||
| 163 | switch (result) { | ||
| 164 | case VALIDATION_OK: return "ok"; | ||
| 165 | case VALIDATION_ERR_SCHEMA: return "invalid: schema"; | ||
| 166 | case VALIDATION_ERR_ID: return "invalid: event id"; | ||
| 167 | case VALIDATION_ERR_SIG: return "invalid: signature"; | ||
| 168 | case VALIDATION_ERR_EXPIRED: return "invalid: expired"; | ||
| 169 | case VALIDATION_ERR_FUTURE: return "invalid: future"; | ||
| 170 | case VALIDATION_ERR_DUPLICATE: return "duplicate"; | ||
| 171 | case VALIDATION_ERR_POW: return "pow: insufficient"; | ||
| 172 | case VALIDATION_ERR_BLOCKED: return "blocked"; | ||
| 173 | case VALIDATION_ERR_TOO_OLD: return "invalid: too old"; | ||
| 174 | default: return "error: unknown"; | ||
| 175 | } | ||
| 176 | } | ||
diff --git a/components/wisp_relay/relay_validator.h b/components/wisp_relay/relay_validator.h new file mode 100644 index 0000000..c07308f --- /dev/null +++ b/components/wisp_relay/relay_validator.h | |||
| @@ -0,0 +1,45 @@ | |||
| 1 | #ifndef RELAY_VALIDATOR_H | ||
| 2 | #define RELAY_VALIDATOR_H | ||
| 3 | |||
| 4 | #include <stdint.h> | ||
| 5 | #include <stdbool.h> | ||
| 6 | #include <stddef.h> | ||
| 7 | |||
| 8 | typedef enum { | ||
| 9 | VALIDATION_OK = 0, | ||
| 10 | VALIDATION_ERR_SCHEMA, | ||
| 11 | VALIDATION_ERR_ID, | ||
| 12 | VALIDATION_ERR_SIG, | ||
| 13 | VALIDATION_ERR_EXPIRED, | ||
| 14 | VALIDATION_ERR_FUTURE, | ||
| 15 | VALIDATION_ERR_DUPLICATE, | ||
| 16 | VALIDATION_ERR_POW, | ||
| 17 | VALIDATION_ERR_BLOCKED, | ||
| 18 | VALIDATION_ERR_TOO_OLD, | ||
| 19 | } validation_result_t; | ||
| 20 | |||
| 21 | typedef struct { | ||
| 22 | uint32_t max_event_age_sec; | ||
| 23 | int64_t max_future_sec; | ||
| 24 | uint8_t min_pow_difficulty; | ||
| 25 | bool check_duplicates; | ||
| 26 | } validator_config_t; | ||
| 27 | |||
| 28 | typedef struct relay_event relay_event_t; | ||
| 29 | typedef struct storage_engine storage_engine_t; | ||
| 30 | |||
| 31 | validation_result_t relay_validator_check(const uint8_t *id, | ||
| 32 | const uint8_t *pubkey, | ||
| 33 | uint64_t created_at, | ||
| 34 | int kind, | ||
| 35 | const char *content, | ||
| 36 | size_t content_len, | ||
| 37 | const char *tags_json, | ||
| 38 | const uint8_t *sig, | ||
| 39 | const validator_config_t *config); | ||
| 40 | |||
| 41 | bool relay_validator_verify_event(const char *event_json, size_t event_len); | ||
| 42 | |||
| 43 | const char *relay_validator_result_string(validation_result_t result); | ||
| 44 | |||
| 45 | #endif | ||
diff --git a/components/wisp_relay/router.c b/components/wisp_relay/router.c new file mode 100644 index 0000000..05aa7d4 --- /dev/null +++ b/components/wisp_relay/router.c | |||
| @@ -0,0 +1,140 @@ | |||
| 1 | #include "router.h" | ||
| 2 | #include "ws_server.h" | ||
| 3 | #include "handlers.h" | ||
| 4 | #include "sub_manager.h" | ||
| 5 | #include "cJSON.h" | ||
| 6 | #include "esp_log.h" | ||
| 7 | #include <string.h> | ||
| 8 | |||
| 9 | static const char *TAG = "router"; | ||
| 10 | |||
| 11 | esp_err_t router_send_notice(relay_ctx_t *ctx, int conn_fd, const char *message) | ||
| 12 | { | ||
| 13 | cJSON *arr = cJSON_CreateArray(); | ||
| 14 | cJSON_AddItemToArray(arr, cJSON_CreateString("NOTICE")); | ||
| 15 | cJSON_AddItemToArray(arr, cJSON_CreateString(message)); | ||
| 16 | char *json = cJSON_PrintUnformatted(arr); | ||
| 17 | cJSON_Delete(arr); | ||
| 18 | esp_err_t ret = ws_server_send(&ctx->ws_server, conn_fd, json, strlen(json)); | ||
| 19 | cJSON_free(json); | ||
| 20 | return ret; | ||
| 21 | } | ||
| 22 | |||
| 23 | esp_err_t router_send_ok(relay_ctx_t *ctx, int conn_fd, const char *event_id_hex, | ||
| 24 | bool accepted, const char *message) | ||
| 25 | { | ||
| 26 | cJSON *arr = cJSON_CreateArray(); | ||
| 27 | cJSON_AddItemToArray(arr, cJSON_CreateString("OK")); | ||
| 28 | cJSON_AddItemToArray(arr, cJSON_CreateString(event_id_hex)); | ||
| 29 | cJSON_AddItemToArray(arr, cJSON_CreateBool(accepted)); | ||
| 30 | cJSON_AddItemToArray(arr, cJSON_CreateString(message ? message : "")); | ||
| 31 | char *json = cJSON_PrintUnformatted(arr); | ||
| 32 | cJSON_Delete(arr); | ||
| 33 | esp_err_t ret = ws_server_send(&ctx->ws_server, conn_fd, json, strlen(json)); | ||
| 34 | cJSON_free(json); | ||
| 35 | return ret; | ||
| 36 | } | ||
| 37 | |||
| 38 | esp_err_t router_send_eose(relay_ctx_t *ctx, int conn_fd, const char *sub_id) | ||
| 39 | { | ||
| 40 | cJSON *arr = cJSON_CreateArray(); | ||
| 41 | cJSON_AddItemToArray(arr, cJSON_CreateString("EOSE")); | ||
| 42 | cJSON_AddItemToArray(arr, cJSON_CreateString(sub_id)); | ||
| 43 | char *json = cJSON_PrintUnformatted(arr); | ||
| 44 | cJSON_Delete(arr); | ||
| 45 | esp_err_t ret = ws_server_send(&ctx->ws_server, conn_fd, json, strlen(json)); | ||
| 46 | cJSON_free(json); | ||
| 47 | return ret; | ||
| 48 | } | ||
| 49 | |||
| 50 | esp_err_t router_send_closed(relay_ctx_t *ctx, int conn_fd, const char *sub_id, | ||
| 51 | const char *message) | ||
| 52 | { | ||
| 53 | cJSON *arr = cJSON_CreateArray(); | ||
| 54 | cJSON_AddItemToArray(arr, cJSON_CreateString("CLOSED")); | ||
| 55 | cJSON_AddItemToArray(arr, cJSON_CreateString(sub_id)); | ||
| 56 | cJSON_AddItemToArray(arr, cJSON_CreateString(message ? message : "")); | ||
| 57 | char *json = cJSON_PrintUnformatted(arr); | ||
| 58 | cJSON_Delete(arr); | ||
| 59 | esp_err_t ret = ws_server_send(&ctx->ws_server, conn_fd, json, strlen(json)); | ||
| 60 | cJSON_free(json); | ||
| 61 | return ret; | ||
| 62 | } | ||
| 63 | |||
| 64 | esp_err_t router_send_event(relay_ctx_t *ctx, int conn_fd, const char *sub_id, | ||
| 65 | const char *event_json, size_t event_len) | ||
| 66 | { | ||
| 67 | size_t buf_size = event_len + strlen(sub_id) + 32; | ||
| 68 | char *buf = malloc(buf_size); | ||
| 69 | if (!buf) return ESP_ERR_NO_MEM; | ||
| 70 | int n = snprintf(buf, buf_size, "[\"EVENT\",\"%s\",%.*s]", sub_id, (int)event_len, event_json); | ||
| 71 | esp_err_t ret = ws_server_send(&ctx->ws_server, conn_fd, buf, n); | ||
| 72 | free(buf); | ||
| 73 | return ret; | ||
| 74 | } | ||
| 75 | |||
| 76 | static void on_ws_message(int fd, const char *data, size_t len) | ||
| 77 | { | ||
| 78 | extern relay_ctx_t g_relay_ctx; | ||
| 79 | router_dispatch(&g_relay_ctx, fd, data, len); | ||
| 80 | } | ||
| 81 | |||
| 82 | static void on_ws_disconnect(int fd) | ||
| 83 | { | ||
| 84 | extern relay_ctx_t g_relay_ctx; | ||
| 85 | if (g_relay_ctx.sub_manager) { | ||
| 86 | sub_manager_remove_all(g_relay_ctx.sub_manager, fd); | ||
| 87 | } | ||
| 88 | } | ||
| 89 | |||
| 90 | void router_dispatch(relay_ctx_t *ctx, int conn_fd, const char *data, size_t len) | ||
| 91 | { | ||
| 92 | cJSON *arr = cJSON_ParseWithLength(data, len); | ||
| 93 | if (!arr || !cJSON_IsArray(arr)) { | ||
| 94 | router_send_notice(ctx, conn_fd, "invalid JSON"); | ||
| 95 | if (arr) cJSON_Delete(arr); | ||
| 96 | return; | ||
| 97 | } | ||
| 98 | |||
| 99 | int array_size = cJSON_GetArraySize(arr); | ||
| 100 | if (array_size < 2) { | ||
| 101 | router_send_notice(ctx, conn_fd, "array too short"); | ||
| 102 | cJSON_Delete(arr); | ||
| 103 | return; | ||
| 104 | } | ||
| 105 | |||
| 106 | cJSON *cmd = cJSON_GetArrayItem(arr, 0); | ||
| 107 | if (!cmd || !cJSON_IsString(cmd)) { | ||
| 108 | router_send_notice(ctx, conn_fd, "invalid command"); | ||
| 109 | cJSON_Delete(arr); | ||
| 110 | return; | ||
| 111 | } | ||
| 112 | |||
| 113 | const char *cmd_str = cmd->valuestring; | ||
| 114 | |||
| 115 | if (strcmp(cmd_str, "EVENT") == 0 && array_size >= 2) { | ||
| 116 | cJSON *event_obj = cJSON_GetArrayItem(arr, 1); | ||
| 117 | if (event_obj) { | ||
| 118 | char *event_json = cJSON_PrintUnformatted(event_obj); | ||
| 119 | handle_event(ctx, conn_fd, event_json, strlen(event_json)); | ||
| 120 | cJSON_free(event_json); | ||
| 121 | } | ||
| 122 | } else if (strcmp(cmd_str, "REQ") == 0 && array_size >= 3) { | ||
| 123 | cJSON *sub_id_item = cJSON_GetArrayItem(arr, 1); | ||
| 124 | if (sub_id_item && cJSON_IsString(sub_id_item)) { | ||
| 125 | cJSON *filter_obj = cJSON_GetArrayItem(arr, 2); | ||
| 126 | char *filter_json = filter_obj ? cJSON_PrintUnformatted(filter_obj) : strdup("{}"); | ||
| 127 | handle_req(ctx, conn_fd, sub_id_item->valuestring, filter_json); | ||
| 128 | free(filter_json); | ||
| 129 | } | ||
| 130 | } else if (strcmp(cmd_str, "CLOSE") == 0 && array_size >= 2) { | ||
| 131 | cJSON *sub_id_item = cJSON_GetArrayItem(arr, 1); | ||
| 132 | if (sub_id_item && cJSON_IsString(sub_id_item)) { | ||
| 133 | handle_close(ctx, conn_fd, sub_id_item->valuestring); | ||
| 134 | } | ||
| 135 | } else { | ||
| 136 | router_send_notice(ctx, conn_fd, "unknown command"); | ||
| 137 | } | ||
| 138 | |||
| 139 | cJSON_Delete(arr); | ||
| 140 | } | ||
diff --git a/components/wisp_relay/router.h b/components/wisp_relay/router.h new file mode 100644 index 0000000..9afd46e --- /dev/null +++ b/components/wisp_relay/router.h | |||
| @@ -0,0 +1,19 @@ | |||
| 1 | #ifndef ROUTER_H | ||
| 2 | #define ROUTER_H | ||
| 3 | |||
| 4 | #include "relay_core.h" | ||
| 5 | #include <stdint.h> | ||
| 6 | #include <stddef.h> | ||
| 7 | |||
| 8 | esp_err_t router_send_notice(relay_ctx_t *ctx, int conn_fd, const char *message); | ||
| 9 | esp_err_t router_send_ok(relay_ctx_t *ctx, int conn_fd, const char *event_id_hex, | ||
| 10 | bool accepted, const char *message); | ||
| 11 | esp_err_t router_send_eose(relay_ctx_t *ctx, int conn_fd, const char *sub_id); | ||
| 12 | esp_err_t router_send_closed(relay_ctx_t *ctx, int conn_fd, const char *sub_id, | ||
| 13 | const char *message); | ||
| 14 | esp_err_t router_send_event(relay_ctx_t *ctx, int conn_fd, const char *sub_id, | ||
| 15 | const char *event_json, size_t event_len); | ||
| 16 | |||
| 17 | void router_dispatch(relay_ctx_t *ctx, int conn_fd, const char *data, size_t len); | ||
| 18 | |||
| 19 | #endif | ||
diff --git a/components/wisp_relay/storage_engine.c b/components/wisp_relay/storage_engine.c new file mode 100644 index 0000000..d26705b --- /dev/null +++ b/components/wisp_relay/storage_engine.c | |||
| @@ -0,0 +1,402 @@ | |||
| 1 | #include "storage_engine.h" | ||
| 2 | #include "esp_littlefs.h" | ||
| 3 | #include "esp_log.h" | ||
| 4 | #include "nvs_flash.h" | ||
| 5 | #include "nvs.h" | ||
| 6 | #include <inttypes.h> | ||
| 7 | #include <string.h> | ||
| 8 | #include <stdio.h> | ||
| 9 | #include <sys/stat.h> | ||
| 10 | #include <time.h> | ||
| 11 | #include <unistd.h> | ||
| 12 | |||
| 13 | static const char *TAG = "storage"; | ||
| 14 | |||
| 15 | #define INDEX_NVS_NAMESPACE "nostr_idx" | ||
| 16 | #define EVENTS_DIR "/littlefs/events" | ||
| 17 | |||
| 18 | static void get_event_path(const uint8_t event_id[32], uint32_t file_index, | ||
| 19 | char *path, size_t len) | ||
| 20 | { | ||
| 21 | char id_hex[33]; | ||
| 22 | for (int i = 0; i < 16; i++) sprintf(id_hex + i * 2, "%02x", event_id[i]); | ||
| 23 | snprintf(path, len, EVENTS_DIR "/%02x/%s_%08" PRIx32 ".json", | ||
| 24 | event_id[0], id_hex, file_index); | ||
| 25 | } | ||
| 26 | |||
| 27 | static int save_index_to_nvs(storage_engine_t *engine) | ||
| 28 | { | ||
| 29 | nvs_handle_t nvs; | ||
| 30 | esp_err_t err = nvs_open(INDEX_NVS_NAMESPACE, NVS_READWRITE, &nvs); | ||
| 31 | if (err != ESP_OK) return STORAGE_ERR_IO; | ||
| 32 | |||
| 33 | nvs_set_u16(nvs, "count", engine->index_count); | ||
| 34 | nvs_set_u32(nvs, "next_idx", engine->next_file_index); | ||
| 35 | |||
| 36 | const uint16_t chunk_size = 50; | ||
| 37 | for (uint16_t i = 0; i < engine->index_count; i += chunk_size) { | ||
| 38 | char key[16]; | ||
| 39 | snprintf(key, sizeof(key), "idx_%u", i / chunk_size); | ||
| 40 | uint16_t entries = engine->index_count - i; | ||
| 41 | if (entries > chunk_size) entries = chunk_size; | ||
| 42 | nvs_set_blob(nvs, key, &engine->index[i], entries * sizeof(storage_index_entry_t)); | ||
| 43 | } | ||
| 44 | nvs_commit(nvs); | ||
| 45 | nvs_close(nvs); | ||
| 46 | return STORAGE_OK; | ||
| 47 | } | ||
| 48 | |||
| 49 | static int load_index_from_nvs(storage_engine_t *engine) | ||
| 50 | { | ||
| 51 | nvs_handle_t nvs; | ||
| 52 | esp_err_t err = nvs_open(INDEX_NVS_NAMESPACE, NVS_READONLY, &nvs); | ||
| 53 | if (err == ESP_ERR_NVS_NOT_FOUND) return STORAGE_OK; | ||
| 54 | if (err != ESP_OK) return STORAGE_ERR_IO; | ||
| 55 | |||
| 56 | err = nvs_get_u16(nvs, "count", &engine->index_count); | ||
| 57 | if (err != ESP_OK) { nvs_close(nvs); return STORAGE_ERR_IO; } | ||
| 58 | if (engine->index_count > engine->max_index_entries) engine->index_count = engine->max_index_entries; | ||
| 59 | |||
| 60 | err = nvs_get_u32(nvs, "next_idx", &engine->next_file_index); | ||
| 61 | if (err != ESP_OK) { nvs_close(nvs); return STORAGE_ERR_IO; } | ||
| 62 | |||
| 63 | const uint16_t chunk_size = 50; | ||
| 64 | for (uint16_t i = 0; i < engine->index_count; i += chunk_size) { | ||
| 65 | char key[16]; | ||
| 66 | snprintf(key, sizeof(key), "idx_%u", i / chunk_size); | ||
| 67 | uint16_t entries = engine->index_count - i; | ||
| 68 | if (entries > chunk_size) entries = chunk_size; | ||
| 69 | size_t len = entries * sizeof(storage_index_entry_t); | ||
| 70 | nvs_get_blob(nvs, key, &engine->index[i], &len); | ||
| 71 | } | ||
| 72 | nvs_close(nvs); | ||
| 73 | return STORAGE_OK; | ||
| 74 | } | ||
| 75 | |||
| 76 | static storage_index_entry_t *find_index_entry(storage_engine_t *engine, | ||
| 77 | const uint8_t event_id[32]) | ||
| 78 | { | ||
| 79 | for (uint16_t i = 0; i < engine->index_count; i++) { | ||
| 80 | if (memcmp(engine->index[i].event_id, event_id, 32) == 0 && | ||
| 81 | !(engine->index[i].flags & STORAGE_FLAG_DELETED)) { | ||
| 82 | return &engine->index[i]; | ||
| 83 | } | ||
| 84 | } | ||
| 85 | return NULL; | ||
| 86 | } | ||
| 87 | |||
| 88 | static void parse_event_meta(const char *json, size_t len, | ||
| 89 | uint8_t *id_out, uint8_t *pubkey_out, | ||
| 90 | uint64_t *created_at_out, int *kind_out) | ||
| 91 | { | ||
| 92 | extern int relay_hex_to_bytes(const char *hex, size_t hex_len, uint8_t *out, size_t out_len); | ||
| 93 | extern void relay_bytes_to_hex(const uint8_t *bytes, size_t len, char *hex); | ||
| 94 | |||
| 95 | id_out[0] = 0; pubkey_out[0] = 0; *created_at_out = 0; *kind_out = 0; | ||
| 96 | |||
| 97 | const char *p; | ||
| 98 | p = strstr(json, "\"id\":\""); | ||
| 99 | if (p) relay_hex_to_bytes(p + 6, 64, id_out, 32); | ||
| 100 | p = strstr(json, "\"pubkey\":\""); | ||
| 101 | if (p) relay_hex_to_bytes(p + 10, 64, pubkey_out, 32); | ||
| 102 | p = strstr(json, "\"created_at\":"); | ||
| 103 | if (p) *created_at_out = strtoull(p + 13, NULL, 10); | ||
| 104 | p = strstr(json, "\"kind\":"); | ||
| 105 | if (p) *kind_out = atoi(p + 7); | ||
| 106 | } | ||
| 107 | |||
| 108 | esp_err_t storage_init(storage_engine_t *engine, uint32_t default_ttl_sec) | ||
| 109 | { | ||
| 110 | memset(engine, 0, sizeof(storage_engine_t)); | ||
| 111 | engine->default_ttl_sec = default_ttl_sec; | ||
| 112 | strcpy(engine->mount_point, "/littlefs"); | ||
| 113 | |||
| 114 | engine->lock = xSemaphoreCreateMutex(); | ||
| 115 | if (!engine->lock) return ESP_ERR_NO_MEM; | ||
| 116 | |||
| 117 | engine->max_index_entries = STORAGE_INDEX_ENTRIES; | ||
| 118 | engine->index = heap_caps_calloc(engine->max_index_entries, | ||
| 119 | sizeof(storage_index_entry_t), | ||
| 120 | MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); | ||
| 121 | if (!engine->index) { | ||
| 122 | engine->max_index_entries = 1000; | ||
| 123 | engine->index = calloc(engine->max_index_entries, sizeof(storage_index_entry_t)); | ||
| 124 | if (!engine->index) { vSemaphoreDelete(engine->lock); return ESP_ERR_NO_MEM; } | ||
| 125 | } | ||
| 126 | |||
| 127 | esp_vfs_littlefs_conf_t conf = { | ||
| 128 | .base_path = "/littlefs", | ||
| 129 | .partition_label = STORAGE_PARTITION_LABEL, | ||
| 130 | .format_if_mount_failed = true, | ||
| 131 | .dont_mount = false, | ||
| 132 | }; | ||
| 133 | |||
| 134 | esp_err_t ret = esp_vfs_littlefs_register(&conf); | ||
| 135 | if (ret != ESP_OK) { | ||
| 136 | ESP_LOGE(TAG, "Failed to mount LittleFS: %s", esp_err_to_name(ret)); | ||
| 137 | free(engine->index); | ||
| 138 | vSemaphoreDelete(engine->lock); | ||
| 139 | return ret; | ||
| 140 | } | ||
| 141 | |||
| 142 | mkdir(EVENTS_DIR, 0755); | ||
| 143 | for (int i = 0; i < 256; i++) { | ||
| 144 | char subdir[64]; | ||
| 145 | snprintf(subdir, sizeof(subdir), EVENTS_DIR "/%02x", i); | ||
| 146 | mkdir(subdir, 0755); | ||
| 147 | } | ||
| 148 | |||
| 149 | int load_err = load_index_from_nvs(engine); | ||
| 150 | if (load_err != STORAGE_OK) { | ||
| 151 | ESP_LOGW(TAG, "Failed to load index, starting fresh"); | ||
| 152 | engine->index_count = 0; | ||
| 153 | engine->next_file_index = 0; | ||
| 154 | } | ||
| 155 | |||
| 156 | engine->initialized = true; | ||
| 157 | |||
| 158 | size_t total, used; | ||
| 159 | esp_littlefs_info(STORAGE_PARTITION_LABEL, &total, &used); | ||
| 160 | ESP_LOGI(TAG, "Storage initialized: %" PRIu16 " events, %zu/%zu bytes used", | ||
| 161 | engine->index_count, used, total); | ||
| 162 | return ESP_OK; | ||
| 163 | } | ||
| 164 | |||
| 165 | void storage_destroy(storage_engine_t *engine) | ||
| 166 | { | ||
| 167 | if (!engine->initialized) return; | ||
| 168 | if (engine->cleanup_task) { | ||
| 169 | engine->cleanup_stop = true; | ||
| 170 | while (engine->cleanup_task != NULL) vTaskDelay(pdMS_TO_TICKS(100)); | ||
| 171 | } | ||
| 172 | save_index_to_nvs(engine); | ||
| 173 | esp_vfs_littlefs_unregister(STORAGE_PARTITION_LABEL); | ||
| 174 | if (engine->index) { free(engine->index); engine->index = NULL; } | ||
| 175 | if (engine->lock) { vSemaphoreDelete(engine->lock); engine->lock = NULL; } | ||
| 176 | engine->initialized = false; | ||
| 177 | } | ||
| 178 | |||
| 179 | storage_error_t storage_save_event_json(storage_engine_t *engine, | ||
| 180 | const char *event_json, | ||
| 181 | size_t event_json_len) | ||
| 182 | { | ||
| 183 | if (!engine->initialized) return STORAGE_ERR_NOT_INITIALIZED; | ||
| 184 | |||
| 185 | uint8_t id[32] = {0}, pubkey[32] = {0}; | ||
| 186 | uint64_t created_at = 0; | ||
| 187 | int kind = 0; | ||
| 188 | parse_event_meta(event_json, event_json_len, id, pubkey, &created_at, &kind); | ||
| 189 | |||
| 190 | xSemaphoreTake(engine->lock, portMAX_DELAY); | ||
| 191 | |||
| 192 | if (find_index_entry(engine, id)) { | ||
| 193 | xSemaphoreGive(engine->lock); | ||
| 194 | return STORAGE_ERR_DUPLICATE; | ||
| 195 | } | ||
| 196 | if (engine->index_count >= engine->max_index_entries) { | ||
| 197 | xSemaphoreGive(engine->lock); | ||
| 198 | return STORAGE_ERR_FULL; | ||
| 199 | } | ||
| 200 | |||
| 201 | char path[128]; | ||
| 202 | get_event_path(id, engine->next_file_index, path, sizeof(path)); | ||
| 203 | FILE *f = fopen(path, "wb"); | ||
| 204 | if (!f) { | ||
| 205 | char dir[64]; | ||
| 206 | snprintf(dir, sizeof(dir), EVENTS_DIR "/%02x", id[0]); | ||
| 207 | mkdir(dir, 0755); | ||
| 208 | f = fopen(path, "wb"); | ||
| 209 | } | ||
| 210 | if (!f) { xSemaphoreGive(engine->lock); return STORAGE_ERR_IO; } | ||
| 211 | |||
| 212 | fwrite(event_json, 1, event_json_len, f); | ||
| 213 | fclose(f); | ||
| 214 | |||
| 215 | storage_index_entry_t *entry = &engine->index[engine->index_count]; | ||
| 216 | memcpy(entry->event_id, id, 32); | ||
| 217 | entry->created_at = (uint32_t)created_at; | ||
| 218 | entry->kind = kind; | ||
| 219 | memcpy(entry->pubkey_prefix, pubkey, 4); | ||
| 220 | entry->file_index = engine->next_file_index; | ||
| 221 | entry->flags = 0; | ||
| 222 | entry->expires_at = (uint32_t)time(NULL) + engine->default_ttl_sec; | ||
| 223 | |||
| 224 | engine->index_count++; | ||
| 225 | engine->next_file_index++; | ||
| 226 | if (engine->index_count % 10 == 0) save_index_to_nvs(engine); | ||
| 227 | |||
| 228 | xSemaphoreGive(engine->lock); | ||
| 229 | return STORAGE_OK; | ||
| 230 | } | ||
| 231 | |||
| 232 | bool storage_event_exists(storage_engine_t *engine, const uint8_t event_id[32]) | ||
| 233 | { | ||
| 234 | if (!engine->initialized) return false; | ||
| 235 | xSemaphoreTake(engine->lock, portMAX_DELAY); | ||
| 236 | bool exists = (find_index_entry(engine, event_id) != NULL); | ||
| 237 | xSemaphoreGive(engine->lock); | ||
| 238 | return exists; | ||
| 239 | } | ||
| 240 | |||
| 241 | storage_error_t storage_query_events_json(storage_engine_t *engine, | ||
| 242 | int kind, | ||
| 243 | const char *author_hex, | ||
| 244 | int limit, | ||
| 245 | char ***results, | ||
| 246 | uint16_t *count) | ||
| 247 | { | ||
| 248 | if (!engine->initialized) return STORAGE_ERR_NOT_INITIALIZED; | ||
| 249 | *results = NULL; | ||
| 250 | *count = 0; | ||
| 251 | if (limit > 500) limit = 500; | ||
| 252 | if (limit <= 0) limit = 100; | ||
| 253 | |||
| 254 | char **out = calloc(limit, sizeof(char *)); | ||
| 255 | if (!out) return STORAGE_ERR_NO_MEM; | ||
| 256 | |||
| 257 | xSemaphoreTake(engine->lock, portMAX_DELAY); | ||
| 258 | uint32_t now = (uint32_t)time(NULL); | ||
| 259 | uint16_t found = 0; | ||
| 260 | |||
| 261 | uint8_t author_prefix[4] = {0}; | ||
| 262 | int have_author = 0; | ||
| 263 | if (author_hex && strlen(author_hex) >= 8) { | ||
| 264 | extern int relay_hex_to_bytes(const char *, size_t, uint8_t *, size_t); | ||
| 265 | relay_hex_to_bytes(author_hex, 8, author_prefix, 4); | ||
| 266 | have_author = 1; | ||
| 267 | } | ||
| 268 | |||
| 269 | for (int i = engine->index_count - 1; i >= 0 && found < limit; i--) { | ||
| 270 | storage_index_entry_t *e = &engine->index[i]; | ||
| 271 | if (e->flags & STORAGE_FLAG_DELETED) continue; | ||
| 272 | if (e->expires_at > 0 && e->expires_at < now) continue; | ||
| 273 | if (kind > 0 && e->kind != kind) continue; | ||
| 274 | if (have_author && memcmp(e->pubkey_prefix, author_prefix, 4) != 0) continue; | ||
| 275 | |||
| 276 | char path[128]; | ||
| 277 | get_event_path(e->event_id, e->file_index, path, sizeof(path)); | ||
| 278 | FILE *f = fopen(path, "rb"); | ||
| 279 | if (!f) continue; | ||
| 280 | fseek(f, 0, SEEK_END); | ||
| 281 | long sz = ftell(f); | ||
| 282 | fseek(f, 0, SEEK_SET); | ||
| 283 | if (sz <= 0 || sz > STORAGE_MAX_EVENT_SIZE) { fclose(f); continue; } | ||
| 284 | char *buf = malloc(sz + 1); | ||
| 285 | fread(buf, 1, sz, f); | ||
| 286 | buf[sz] = '\0'; | ||
| 287 | fclose(f); | ||
| 288 | out[found++] = buf; | ||
| 289 | } | ||
| 290 | |||
| 291 | xSemaphoreGive(engine->lock); | ||
| 292 | *results = out; | ||
| 293 | *count = found; | ||
| 294 | return STORAGE_OK; | ||
| 295 | } | ||
| 296 | |||
| 297 | void storage_free_query_results(char **results, uint16_t count) | ||
| 298 | { | ||
| 299 | if (!results) return; | ||
| 300 | for (uint16_t i = 0; i < count; i++) free(results[i]); | ||
| 301 | free(results); | ||
| 302 | } | ||
| 303 | |||
| 304 | storage_error_t storage_delete_event(storage_engine_t *engine, const uint8_t event_id[32]) | ||
| 305 | { | ||
| 306 | if (!engine->initialized) return STORAGE_ERR_NOT_INITIALIZED; | ||
| 307 | xSemaphoreTake(engine->lock, portMAX_DELAY); | ||
| 308 | storage_index_entry_t *e = find_index_entry(engine, event_id); | ||
| 309 | if (!e) { xSemaphoreGive(engine->lock); return STORAGE_ERR_NOT_FOUND; } | ||
| 310 | char path[128]; | ||
| 311 | get_event_path(e->event_id, e->file_index, path, sizeof(path)); | ||
| 312 | unlink(path); | ||
| 313 | e->flags |= STORAGE_FLAG_DELETED; | ||
| 314 | save_index_to_nvs(engine); | ||
| 315 | xSemaphoreGive(engine->lock); | ||
| 316 | return STORAGE_OK; | ||
| 317 | } | ||
| 318 | |||
| 319 | int storage_purge_expired(storage_engine_t *engine) | ||
| 320 | { | ||
| 321 | if (!engine->initialized) return 0; | ||
| 322 | xSemaphoreTake(engine->lock, portMAX_DELAY); | ||
| 323 | uint32_t now = (uint32_t)time(NULL); | ||
| 324 | int purged = 0; | ||
| 325 | for (uint16_t i = 0; i < engine->index_count; i++) { | ||
| 326 | if (engine->index[i].flags & STORAGE_FLAG_DELETED) continue; | ||
| 327 | if (engine->index[i].expires_at > 0 && engine->index[i].expires_at < now) { | ||
| 328 | char path[128]; | ||
| 329 | get_event_path(engine->index[i].event_id, engine->index[i].file_index, path, sizeof(path)); | ||
| 330 | unlink(path); | ||
| 331 | engine->index[i].flags |= STORAGE_FLAG_DELETED; | ||
| 332 | purged++; | ||
| 333 | } | ||
| 334 | } | ||
| 335 | if (purged > 0) { save_index_to_nvs(engine); ESP_LOGI(TAG, "Purged %d expired events", purged); } | ||
| 336 | xSemaphoreGive(engine->lock); | ||
| 337 | return purged; | ||
| 338 | } | ||
| 339 | |||
| 340 | int storage_compact_index(storage_engine_t *engine) | ||
| 341 | { | ||
| 342 | if (!engine->initialized) return 0; | ||
| 343 | xSemaphoreTake(engine->lock, portMAX_DELAY); | ||
| 344 | uint16_t write_idx = 0; | ||
| 345 | int compacted = 0; | ||
| 346 | for (uint16_t read_idx = 0; read_idx < engine->index_count; read_idx++) { | ||
| 347 | if (!(engine->index[read_idx].flags & STORAGE_FLAG_DELETED)) { | ||
| 348 | if (write_idx != read_idx) | ||
| 349 | memcpy(&engine->index[write_idx], &engine->index[read_idx], sizeof(storage_index_entry_t)); | ||
| 350 | write_idx++; | ||
| 351 | } else { | ||
| 352 | compacted++; | ||
| 353 | } | ||
| 354 | } | ||
| 355 | if (compacted > 0) { | ||
| 356 | engine->index_count = write_idx; | ||
| 357 | save_index_to_nvs(engine); | ||
| 358 | ESP_LOGI(TAG, "Compacted: removed %d, %" PRIu16 " remaining", compacted, engine->index_count); | ||
| 359 | } | ||
| 360 | xSemaphoreGive(engine->lock); | ||
| 361 | return compacted; | ||
| 362 | } | ||
| 363 | |||
| 364 | void storage_get_stats(storage_engine_t *engine, storage_stats_t *stats) | ||
| 365 | { | ||
| 366 | memset(stats, 0, sizeof(storage_stats_t)); | ||
| 367 | if (!engine->initialized) return; | ||
| 368 | xSemaphoreTake(engine->lock, portMAX_DELAY); | ||
| 369 | uint32_t now = (uint32_t)time(NULL); | ||
| 370 | for (uint16_t i = 0; i < engine->index_count; i++) { | ||
| 371 | if (engine->index[i].flags & STORAGE_FLAG_DELETED) continue; | ||
| 372 | if (engine->index[i].expires_at > 0 && engine->index[i].expires_at < now) continue; | ||
| 373 | stats->total_events++; | ||
| 374 | } | ||
| 375 | size_t total, used; | ||
| 376 | esp_littlefs_info(STORAGE_PARTITION_LABEL, &total, &used); | ||
| 377 | stats->total_bytes = total; | ||
| 378 | stats->free_bytes = total - used; | ||
| 379 | xSemaphoreGive(engine->lock); | ||
| 380 | } | ||
| 381 | |||
| 382 | static void storage_cleanup_task(void *arg) | ||
| 383 | { | ||
| 384 | storage_engine_t *engine = (storage_engine_t *)arg; | ||
| 385 | int cycles = 0; | ||
| 386 | while (!engine->cleanup_stop) { | ||
| 387 | for (int i = 0; i < 60 && !engine->cleanup_stop; i++) vTaskDelay(pdMS_TO_TICKS(1000)); | ||
| 388 | if (engine->cleanup_stop) break; | ||
| 389 | storage_purge_expired(engine); | ||
| 390 | if (++cycles >= 10) { storage_compact_index(engine); cycles = 0; } | ||
| 391 | } | ||
| 392 | engine->cleanup_task = NULL; | ||
| 393 | vTaskDelete(NULL); | ||
| 394 | } | ||
| 395 | |||
| 396 | esp_err_t storage_start_cleanup_task(storage_engine_t *engine) | ||
| 397 | { | ||
| 398 | engine->cleanup_stop = false; | ||
| 399 | BaseType_t ret = xTaskCreate(storage_cleanup_task, "relay_cleanup", 4096, engine, 2, &engine->cleanup_task); | ||
| 400 | if (ret != pdPASS) { engine->cleanup_task = NULL; return ESP_ERR_NO_MEM; } | ||
| 401 | return ESP_OK; | ||
| 402 | } | ||
diff --git a/components/wisp_relay/storage_engine.h b/components/wisp_relay/storage_engine.h new file mode 100644 index 0000000..4e17113 --- /dev/null +++ b/components/wisp_relay/storage_engine.h | |||
| @@ -0,0 +1,88 @@ | |||
| 1 | #ifndef STORAGE_ENGINE_H | ||
| 2 | #define STORAGE_ENGINE_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include <stdint.h> | ||
| 6 | #include "esp_err.h" | ||
| 7 | #include "freertos/FreeRTOS.h" | ||
| 8 | #include "freertos/semphr.h" | ||
| 9 | #include "freertos/task.h" | ||
| 10 | |||
| 11 | #define STORAGE_MAX_EVENTS 5000 | ||
| 12 | #define STORAGE_MAX_EVENT_SIZE 8192 | ||
| 13 | #define STORAGE_INDEX_ENTRIES 5000 | ||
| 14 | #define STORAGE_PARTITION_LABEL "relay_store" | ||
| 15 | |||
| 16 | typedef enum { | ||
| 17 | STORAGE_OK = 0, | ||
| 18 | STORAGE_ERR_NOT_INITIALIZED, | ||
| 19 | STORAGE_ERR_FULL, | ||
| 20 | STORAGE_ERR_DUPLICATE, | ||
| 21 | STORAGE_ERR_NOT_FOUND, | ||
| 22 | STORAGE_ERR_IO, | ||
| 23 | STORAGE_ERR_NO_MEM, | ||
| 24 | STORAGE_ERR_SERIALIZE | ||
| 25 | } storage_error_t; | ||
| 26 | |||
| 27 | #define STORAGE_FLAG_DELETED 0x01 | ||
| 28 | |||
| 29 | typedef struct __attribute__((packed)) { | ||
| 30 | uint8_t event_id[32]; | ||
| 31 | uint32_t created_at; | ||
| 32 | uint32_t expires_at; | ||
| 33 | uint32_t file_index; | ||
| 34 | uint16_t kind; | ||
| 35 | uint8_t pubkey_prefix[4]; | ||
| 36 | uint8_t flags; | ||
| 37 | uint8_t reserved; | ||
| 38 | } storage_index_entry_t; | ||
| 39 | |||
| 40 | typedef struct { | ||
| 41 | uint32_t total_events; | ||
| 42 | uint32_t total_bytes; | ||
| 43 | uint32_t free_bytes; | ||
| 44 | uint32_t oldest_event_ts; | ||
| 45 | uint32_t newest_event_ts; | ||
| 46 | } storage_stats_t; | ||
| 47 | |||
| 48 | typedef struct storage_engine { | ||
| 49 | storage_index_entry_t *index; | ||
| 50 | uint16_t index_count; | ||
| 51 | uint16_t max_index_entries; | ||
| 52 | uint32_t next_file_index; | ||
| 53 | SemaphoreHandle_t lock; | ||
| 54 | TaskHandle_t cleanup_task; | ||
| 55 | bool initialized; | ||
| 56 | bool cleanup_stop; | ||
| 57 | char mount_point[16]; | ||
| 58 | uint32_t default_ttl_sec; | ||
| 59 | } storage_engine_t; | ||
| 60 | |||
| 61 | esp_err_t storage_init(storage_engine_t *engine, uint32_t default_ttl_sec); | ||
| 62 | void storage_destroy(storage_engine_t *engine); | ||
| 63 | |||
| 64 | storage_error_t storage_save_event_json(storage_engine_t *engine, | ||
| 65 | const char *event_json, | ||
| 66 | size_t event_json_len); | ||
| 67 | |||
| 68 | storage_error_t storage_query_events_json(storage_engine_t *engine, | ||
| 69 | int kind, | ||
| 70 | const char *author_hex, | ||
| 71 | int limit, | ||
| 72 | char ***results, | ||
| 73 | uint16_t *count); | ||
| 74 | |||
| 75 | void storage_free_query_results(char **results, uint16_t count); | ||
| 76 | |||
| 77 | bool storage_event_exists(storage_engine_t *engine, const uint8_t event_id[32]); | ||
| 78 | |||
| 79 | storage_error_t storage_delete_event(storage_engine_t *engine, const uint8_t event_id[32]); | ||
| 80 | |||
| 81 | int storage_purge_expired(storage_engine_t *engine); | ||
| 82 | int storage_compact_index(storage_engine_t *engine); | ||
| 83 | |||
| 84 | void storage_get_stats(storage_engine_t *engine, storage_stats_t *stats); | ||
| 85 | |||
| 86 | esp_err_t storage_start_cleanup_task(storage_engine_t *engine); | ||
| 87 | |||
| 88 | #endif | ||
diff --git a/components/wisp_relay/sub_manager.c b/components/wisp_relay/sub_manager.c new file mode 100644 index 0000000..a1da2e3 --- /dev/null +++ b/components/wisp_relay/sub_manager.c | |||
| @@ -0,0 +1,272 @@ | |||
| 1 | #include "sub_manager.h" | ||
| 2 | #include "relay_types.h" | ||
| 3 | #include "esp_log.h" | ||
| 4 | #include <string.h> | ||
| 5 | #include <stdlib.h> | ||
| 6 | |||
| 7 | static const char *TAG = "sub_mgr"; | ||
| 8 | |||
| 9 | static void filter_clear(sub_filter_t *f) | ||
| 10 | { | ||
| 11 | for (size_t i = 0; i < f->ids_count; i++) free(f->ids[i]); | ||
| 12 | for (size_t i = 0; i < f->authors_count; i++) free(f->authors[i]); | ||
| 13 | for (size_t i = 0; i < f->e_tags_count; i++) free(f->e_tags[i]); | ||
| 14 | for (size_t i = 0; i < f->p_tags_count; i++) free(f->p_tags[i]); | ||
| 15 | memset(f, 0, sizeof(sub_filter_t)); | ||
| 16 | } | ||
| 17 | |||
| 18 | static bool filter_copy(sub_filter_t *dst, const sub_filter_t *src) | ||
| 19 | { | ||
| 20 | memset(dst, 0, sizeof(sub_filter_t)); | ||
| 21 | |||
| 22 | size_t ids_count = src->ids_count > SUB_MAX_FILTER_IDS ? SUB_MAX_FILTER_IDS : src->ids_count; | ||
| 23 | for (size_t i = 0; i < ids_count; i++) { | ||
| 24 | dst->ids[i] = strdup(src->ids[i]); | ||
| 25 | if (!dst->ids[i]) goto fail; | ||
| 26 | } | ||
| 27 | dst->ids_count = ids_count; | ||
| 28 | |||
| 29 | size_t authors_count = src->authors_count > SUB_MAX_FILTER_AUTHORS ? SUB_MAX_FILTER_AUTHORS : src->authors_count; | ||
| 30 | for (size_t i = 0; i < authors_count; i++) { | ||
| 31 | dst->authors[i] = strdup(src->authors[i]); | ||
| 32 | if (!dst->authors[i]) goto fail; | ||
| 33 | } | ||
| 34 | dst->authors_count = authors_count; | ||
| 35 | |||
| 36 | size_t kinds_count = src->kinds_count > SUB_MAX_FILTER_KINDS ? SUB_MAX_FILTER_KINDS : src->kinds_count; | ||
| 37 | memcpy(dst->kinds, src->kinds, kinds_count * sizeof(int32_t)); | ||
| 38 | dst->kinds_count = kinds_count; | ||
| 39 | |||
| 40 | size_t e_tags_count = src->e_tags_count > SUB_MAX_FILTER_ETAGS ? SUB_MAX_FILTER_ETAGS : src->e_tags_count; | ||
| 41 | for (size_t i = 0; i < e_tags_count; i++) { | ||
| 42 | dst->e_tags[i] = strdup(src->e_tags[i]); | ||
| 43 | if (!dst->e_tags[i]) goto fail; | ||
| 44 | } | ||
| 45 | dst->e_tags_count = e_tags_count; | ||
| 46 | |||
| 47 | size_t p_tags_count = src->p_tags_count > SUB_MAX_FILTER_PTAGS ? SUB_MAX_FILTER_PTAGS : src->p_tags_count; | ||
| 48 | for (size_t i = 0; i < p_tags_count; i++) { | ||
| 49 | dst->p_tags[i] = strdup(src->p_tags[i]); | ||
| 50 | if (!dst->p_tags[i]) goto fail; | ||
| 51 | } | ||
| 52 | dst->p_tags_count = p_tags_count; | ||
| 53 | |||
| 54 | dst->since = src->since; | ||
| 55 | dst->until = src->until; | ||
| 56 | dst->limit = src->limit; | ||
| 57 | return true; | ||
| 58 | |||
| 59 | fail: | ||
| 60 | filter_clear(dst); | ||
| 61 | return false; | ||
| 62 | } | ||
| 63 | |||
| 64 | static void clear_subscription(subscription_t *sub) | ||
| 65 | { | ||
| 66 | for (uint8_t i = 0; i < sub->filter_count; i++) { | ||
| 67 | filter_clear(&sub->filters[i]); | ||
| 68 | } | ||
| 69 | memset(sub, 0, sizeof(subscription_t)); | ||
| 70 | } | ||
| 71 | |||
| 72 | esp_err_t sub_manager_init(sub_manager_t *mgr) | ||
| 73 | { | ||
| 74 | memset(mgr, 0, sizeof(sub_manager_t)); | ||
| 75 | mgr->lock = xSemaphoreCreateMutex(); | ||
| 76 | if (!mgr->lock) return ESP_ERR_NO_MEM; | ||
| 77 | ESP_LOGI(TAG, "Initialized (max=%d, per_conn=%d)", SUB_MAX_TOTAL, SUB_MAX_PER_CONN); | ||
| 78 | return ESP_OK; | ||
| 79 | } | ||
| 80 | |||
| 81 | void sub_manager_destroy(sub_manager_t *mgr) | ||
| 82 | { | ||
| 83 | if (!mgr) return; | ||
| 84 | for (int i = 0; i < SUB_MAX_TOTAL; i++) { | ||
| 85 | if (mgr->subs[i].active) clear_subscription(&mgr->subs[i]); | ||
| 86 | } | ||
| 87 | if (mgr->lock) { vSemaphoreDelete(mgr->lock); mgr->lock = NULL; } | ||
| 88 | } | ||
| 89 | |||
| 90 | static subscription_t *find_sub(sub_manager_t *mgr, int conn_fd, const char *sub_id) | ||
| 91 | { | ||
| 92 | for (int i = 0; i < SUB_MAX_TOTAL; i++) { | ||
| 93 | if (mgr->subs[i].active && mgr->subs[i].conn_fd == conn_fd && | ||
| 94 | strcmp(mgr->subs[i].sub_id, sub_id) == 0) | ||
| 95 | return &mgr->subs[i]; | ||
| 96 | } | ||
| 97 | return NULL; | ||
| 98 | } | ||
| 99 | |||
| 100 | static subscription_t *find_free_slot(sub_manager_t *mgr) | ||
| 101 | { | ||
| 102 | for (int i = 0; i < SUB_MAX_TOTAL; i++) { | ||
| 103 | if (!mgr->subs[i].active) return &mgr->subs[i]; | ||
| 104 | } | ||
| 105 | return NULL; | ||
| 106 | } | ||
| 107 | |||
| 108 | static bool hex_prefix_match(const char *prefix, size_t prefix_len, | ||
| 109 | const char *full, size_t full_len) | ||
| 110 | { | ||
| 111 | if (prefix_len == 0) return true; | ||
| 112 | if (prefix_len > full_len) return false; | ||
| 113 | return memcmp(prefix, full, prefix_len) == 0; | ||
| 114 | } | ||
| 115 | |||
| 116 | static bool filter_matches_event(const sub_filter_t *f, int event_kind, | ||
| 117 | const char *pubkey_hex, uint64_t created_at) | ||
| 118 | { | ||
| 119 | if (f->kinds_count > 0) { | ||
| 120 | bool found = false; | ||
| 121 | for (size_t i = 0; i < f->kinds_count; i++) { | ||
| 122 | if (f->kinds[i] == event_kind) { found = true; break; } | ||
| 123 | } | ||
| 124 | if (!found) return false; | ||
| 125 | } | ||
| 126 | |||
| 127 | if (f->authors_count > 0) { | ||
| 128 | bool found = false; | ||
| 129 | for (size_t i = 0; i < f->authors_count; i++) { | ||
| 130 | if (hex_prefix_match(f->authors[i], strlen(f->authors[i]), | ||
| 131 | pubkey_hex, strlen(pubkey_hex))) { | ||
| 132 | found = true; break; | ||
| 133 | } | ||
| 134 | } | ||
| 135 | if (!found) return false; | ||
| 136 | } | ||
| 137 | |||
| 138 | if (f->since > 0 && (int64_t)created_at < f->since) return false; | ||
| 139 | if (f->until > 0 && (int64_t)created_at > f->until) return false; | ||
| 140 | |||
| 141 | return true; | ||
| 142 | } | ||
| 143 | |||
| 144 | void sub_manager_match_json(sub_manager_t *mgr, const char *event_json, | ||
| 145 | size_t event_len, int event_kind, | ||
| 146 | const char *event_pubkey_hex, | ||
| 147 | uint64_t event_created_at, | ||
| 148 | sub_match_result_t *result) | ||
| 149 | { | ||
| 150 | result->count = 0; | ||
| 151 | (void)event_json; | ||
| 152 | (void)event_len; | ||
| 153 | |||
| 154 | xSemaphoreTake(mgr->lock, portMAX_DELAY); | ||
| 155 | for (int i = 0; i < SUB_MAX_TOTAL; i++) { | ||
| 156 | subscription_t *sub = &mgr->subs[i]; | ||
| 157 | if (!sub->active) continue; | ||
| 158 | |||
| 159 | bool matched = false; | ||
| 160 | for (uint8_t f = 0; f < sub->filter_count; f++) { | ||
| 161 | if (filter_matches_event(&sub->filters[f], event_kind, | ||
| 162 | event_pubkey_hex, event_created_at)) { | ||
| 163 | matched = true; | ||
| 164 | break; | ||
| 165 | } | ||
| 166 | } | ||
| 167 | if (matched) { | ||
| 168 | sub_match_entry_t *entry = &result->matches[result->count++]; | ||
| 169 | entry->conn_fd = sub->conn_fd; | ||
| 170 | memcpy(entry->sub_id, sub->sub_id, sizeof(entry->sub_id)); | ||
| 171 | } | ||
| 172 | } | ||
| 173 | xSemaphoreGive(mgr->lock); | ||
| 174 | } | ||
| 175 | |||
| 176 | sub_error_t sub_manager_add(sub_manager_t *mgr, int conn_fd, | ||
| 177 | const char *sub_id, | ||
| 178 | const sub_filter_t *filters, | ||
| 179 | size_t filter_count) | ||
| 180 | { | ||
| 181 | if (filter_count > SUB_MAX_FILTERS) filter_count = SUB_MAX_FILTERS; | ||
| 182 | |||
| 183 | xSemaphoreTake(mgr->lock, portMAX_DELAY); | ||
| 184 | |||
| 185 | subscription_t *existing = find_sub(mgr, conn_fd, sub_id); | ||
| 186 | if (existing) { | ||
| 187 | for (uint8_t i = 0; i < existing->filter_count; i++) | ||
| 188 | filter_clear(&existing->filters[i]); | ||
| 189 | existing->events_sent = 0; | ||
| 190 | for (size_t i = 0; i < filter_count; i++) { | ||
| 191 | if (!filter_copy(&existing->filters[i], &filters[i])) { | ||
| 192 | existing->filter_count = (uint8_t)i; | ||
| 193 | xSemaphoreGive(mgr->lock); | ||
| 194 | return SUB_ERR_MEMORY; | ||
| 195 | } | ||
| 196 | } | ||
| 197 | existing->filter_count = (uint8_t)filter_count; | ||
| 198 | xSemaphoreGive(mgr->lock); | ||
| 199 | return SUB_OK; | ||
| 200 | } | ||
| 201 | |||
| 202 | uint8_t conn_count = 0; | ||
| 203 | for (int i = 0; i < SUB_MAX_TOTAL; i++) { | ||
| 204 | if (mgr->subs[i].active && mgr->subs[i].conn_fd == conn_fd) conn_count++; | ||
| 205 | } | ||
| 206 | if (conn_count >= SUB_MAX_PER_CONN) { | ||
| 207 | xSemaphoreGive(mgr->lock); | ||
| 208 | return SUB_ERR_TOO_MANY_FILTERS; | ||
| 209 | } | ||
| 210 | |||
| 211 | subscription_t *slot = find_free_slot(mgr); | ||
| 212 | if (!slot) { xSemaphoreGive(mgr->lock); return SUB_ERR_MEMORY; } | ||
| 213 | |||
| 214 | memset(slot, 0, sizeof(subscription_t)); | ||
| 215 | strncpy(slot->sub_id, sub_id, SUB_MAX_ID_LEN); | ||
| 216 | slot->sub_id[SUB_MAX_ID_LEN] = '\0'; | ||
| 217 | slot->conn_fd = conn_fd; | ||
| 218 | |||
| 219 | for (size_t i = 0; i < filter_count; i++) { | ||
| 220 | if (!filter_copy(&slot->filters[i], &filters[i])) { | ||
| 221 | slot->filter_count = (uint8_t)i; | ||
| 222 | clear_subscription(slot); | ||
| 223 | xSemaphoreGive(mgr->lock); | ||
| 224 | return SUB_ERR_MEMORY; | ||
| 225 | } | ||
| 226 | } | ||
| 227 | slot->filter_count = (uint8_t)filter_count; | ||
| 228 | slot->active = true; | ||
| 229 | mgr->active_count++; | ||
| 230 | |||
| 231 | ESP_LOGI(TAG, "Added sub=%s fd=%d filters=%zu total=%d", | ||
| 232 | sub_id, conn_fd, filter_count, mgr->active_count); | ||
| 233 | xSemaphoreGive(mgr->lock); | ||
| 234 | return SUB_OK; | ||
| 235 | } | ||
| 236 | |||
| 237 | sub_error_t sub_manager_remove(sub_manager_t *mgr, int conn_fd, const char *sub_id) | ||
| 238 | { | ||
| 239 | xSemaphoreTake(mgr->lock, portMAX_DELAY); | ||
| 240 | subscription_t *sub = find_sub(mgr, conn_fd, sub_id); | ||
| 241 | if (!sub) { xSemaphoreGive(mgr->lock); return SUB_ERR_NOT_FOUND; } | ||
| 242 | clear_subscription(sub); | ||
| 243 | mgr->active_count--; | ||
| 244 | xSemaphoreGive(mgr->lock); | ||
| 245 | return SUB_OK; | ||
| 246 | } | ||
| 247 | |||
| 248 | void sub_manager_remove_all(sub_manager_t *mgr, int conn_fd) | ||
| 249 | { | ||
| 250 | xSemaphoreTake(mgr->lock, portMAX_DELAY); | ||
| 251 | int removed = 0; | ||
| 252 | for (int i = 0; i < SUB_MAX_TOTAL; i++) { | ||
| 253 | if (mgr->subs[i].active && mgr->subs[i].conn_fd == conn_fd) { | ||
| 254 | clear_subscription(&mgr->subs[i]); | ||
| 255 | mgr->active_count--; | ||
| 256 | removed++; | ||
| 257 | } | ||
| 258 | } | ||
| 259 | if (removed > 0) ESP_LOGI(TAG, "Removed %d subs for fd=%d", removed, conn_fd); | ||
| 260 | xSemaphoreGive(mgr->lock); | ||
| 261 | } | ||
| 262 | |||
| 263 | uint8_t sub_manager_count(sub_manager_t *mgr, int conn_fd) | ||
| 264 | { | ||
| 265 | uint8_t count = 0; | ||
| 266 | xSemaphoreTake(mgr->lock, portMAX_DELAY); | ||
| 267 | for (int i = 0; i < SUB_MAX_TOTAL; i++) { | ||
| 268 | if (mgr->subs[i].active && mgr->subs[i].conn_fd == conn_fd) count++; | ||
| 269 | } | ||
| 270 | xSemaphoreGive(mgr->lock); | ||
| 271 | return count; | ||
| 272 | } | ||
diff --git a/components/wisp_relay/sub_manager.h b/components/wisp_relay/sub_manager.h new file mode 100644 index 0000000..64afb04 --- /dev/null +++ b/components/wisp_relay/sub_manager.h | |||
| @@ -0,0 +1,92 @@ | |||
| 1 | #ifndef SUB_MANAGER_H | ||
| 2 | #define SUB_MANAGER_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include <stdint.h> | ||
| 6 | #include "esp_err.h" | ||
| 7 | #include "freertos/FreeRTOS.h" | ||
| 8 | #include "freertos/semphr.h" | ||
| 9 | #include "relay_types.h" | ||
| 10 | |||
| 11 | #define SUB_MAX_TOTAL 64 | ||
| 12 | #define SUB_MAX_PER_CONN 8 | ||
| 13 | #define SUB_MAX_FILTERS 4 | ||
| 14 | #define SUB_MAX_ID_LEN 64 | ||
| 15 | |||
| 16 | #define SUB_MAX_FILTER_IDS 20 | ||
| 17 | #define SUB_MAX_FILTER_AUTHORS 20 | ||
| 18 | #define SUB_MAX_FILTER_KINDS 20 | ||
| 19 | #define SUB_MAX_FILTER_ETAGS 20 | ||
| 20 | #define SUB_MAX_FILTER_PTAGS 20 | ||
| 21 | |||
| 22 | typedef enum { | ||
| 23 | SUB_OK = 0, | ||
| 24 | SUB_ERR_INVALID, | ||
| 25 | SUB_ERR_TOO_MANY_FILTERS, | ||
| 26 | SUB_ERR_MEMORY, | ||
| 27 | SUB_ERR_NOT_FOUND, | ||
| 28 | } sub_error_t; | ||
| 29 | |||
| 30 | typedef struct { | ||
| 31 | char *ids[SUB_MAX_FILTER_IDS]; | ||
| 32 | size_t ids_count; | ||
| 33 | char *authors[SUB_MAX_FILTER_AUTHORS]; | ||
| 34 | size_t authors_count; | ||
| 35 | int32_t kinds[SUB_MAX_FILTER_KINDS]; | ||
| 36 | size_t kinds_count; | ||
| 37 | char *e_tags[SUB_MAX_FILTER_ETAGS]; | ||
| 38 | size_t e_tags_count; | ||
| 39 | char *p_tags[SUB_MAX_FILTER_PTAGS]; | ||
| 40 | size_t p_tags_count; | ||
| 41 | int64_t since; | ||
| 42 | int64_t until; | ||
| 43 | int limit; | ||
| 44 | } sub_filter_t; | ||
| 45 | |||
| 46 | typedef struct { | ||
| 47 | char sub_id[SUB_MAX_ID_LEN + 1]; | ||
| 48 | int conn_fd; | ||
| 49 | sub_filter_t filters[SUB_MAX_FILTERS]; | ||
| 50 | uint8_t filter_count; | ||
| 51 | uint16_t events_sent; | ||
| 52 | bool active; | ||
| 53 | } subscription_t; | ||
| 54 | |||
| 55 | typedef struct sub_manager { | ||
| 56 | subscription_t subs[SUB_MAX_TOTAL]; | ||
| 57 | SemaphoreHandle_t lock; | ||
| 58 | uint16_t active_count; | ||
| 59 | } sub_manager_t; | ||
| 60 | |||
| 61 | typedef struct { | ||
| 62 | int conn_fd; | ||
| 63 | char sub_id[SUB_MAX_ID_LEN + 1]; | ||
| 64 | } sub_match_entry_t; | ||
| 65 | |||
| 66 | typedef struct { | ||
| 67 | sub_match_entry_t matches[SUB_MAX_TOTAL]; | ||
| 68 | uint8_t count; | ||
| 69 | } sub_match_result_t; | ||
| 70 | |||
| 71 | esp_err_t sub_manager_init(sub_manager_t *mgr); | ||
| 72 | void sub_manager_destroy(sub_manager_t *mgr); | ||
| 73 | |||
| 74 | sub_error_t sub_manager_add(sub_manager_t *mgr, int conn_fd, | ||
| 75 | const char *sub_id, | ||
| 76 | const sub_filter_t *filters, | ||
| 77 | size_t filter_count); | ||
| 78 | |||
| 79 | sub_error_t sub_manager_remove(sub_manager_t *mgr, int conn_fd, | ||
| 80 | const char *sub_id); | ||
| 81 | |||
| 82 | void sub_manager_remove_all(sub_manager_t *mgr, int conn_fd); | ||
| 83 | |||
| 84 | void sub_manager_match_json(sub_manager_t *mgr, const char *event_json, | ||
| 85 | size_t event_len, int event_kind, | ||
| 86 | const char *event_pubkey_hex, | ||
| 87 | uint64_t event_created_at, | ||
| 88 | sub_match_result_t *result); | ||
| 89 | |||
| 90 | uint8_t sub_manager_count(sub_manager_t *mgr, int conn_fd); | ||
| 91 | |||
| 92 | #endif | ||
diff --git a/components/wisp_relay/ws_server.c b/components/wisp_relay/ws_server.c new file mode 100644 index 0000000..a973ca6 --- /dev/null +++ b/components/wisp_relay/ws_server.c | |||
| @@ -0,0 +1,426 @@ | |||
| 1 | #include "ws_server.h" | ||
| 2 | #include "nip11_relay.h" | ||
| 3 | #include "esp_log.h" | ||
| 4 | #include "esp_timer.h" | ||
| 5 | #include <string.h> | ||
| 6 | #include <strings.h> | ||
| 7 | #include <unistd.h> | ||
| 8 | #include <sys/socket.h> | ||
| 9 | #include <netinet/in.h> | ||
| 10 | #include <netinet/tcp.h> | ||
| 11 | #include <arpa/inet.h> | ||
| 12 | |||
| 13 | static const char *TAG = "ws_server"; | ||
| 14 | static ws_message_cb_t g_message_callback = NULL; | ||
| 15 | static ws_disconnect_cb_t g_disconnect_callback = NULL; | ||
| 16 | static ws_server_t *g_server = NULL; | ||
| 17 | static __thread httpd_req_t *g_current_req = NULL; | ||
| 18 | |||
| 19 | static ws_connection_t* find_free_slot(ws_server_t *server) | ||
| 20 | { | ||
| 21 | for (int i = 0; i < WS_MAX_CONNECTIONS; i++) { | ||
| 22 | if (!server->connections[i].active) { | ||
| 23 | return &server->connections[i]; | ||
| 24 | } | ||
| 25 | } | ||
| 26 | return NULL; | ||
| 27 | } | ||
| 28 | |||
| 29 | static ws_connection_t* find_connection_by_fd(ws_server_t *server, int fd) | ||
| 30 | { | ||
| 31 | for (int i = 0; i < WS_MAX_CONNECTIONS; i++) { | ||
| 32 | if (server->connections[i].active && server->connections[i].fd == fd) { | ||
| 33 | return &server->connections[i]; | ||
| 34 | } | ||
| 35 | } | ||
| 36 | return NULL; | ||
| 37 | } | ||
| 38 | |||
| 39 | static void update_connection_activity(ws_server_t *server, int fd) | ||
| 40 | { | ||
| 41 | xSemaphoreTake(server->lock, portMAX_DELAY); | ||
| 42 | ws_connection_t *conn = find_connection_by_fd(server, fd); | ||
| 43 | if (conn) { | ||
| 44 | conn->last_activity = esp_timer_get_time() / 1000000; | ||
| 45 | } | ||
| 46 | xSemaphoreGive(server->lock); | ||
| 47 | } | ||
| 48 | |||
| 49 | static void set_unknown_ip(char *ip_buf, size_t buf_len) | ||
| 50 | { | ||
| 51 | if (buf_len == 0) { | ||
| 52 | return; | ||
| 53 | } | ||
| 54 | strncpy(ip_buf, "unknown", buf_len - 1); | ||
| 55 | ip_buf[buf_len - 1] = '\0'; | ||
| 56 | } | ||
| 57 | |||
| 58 | static void get_client_ip(int fd, char *ip_buf, size_t buf_len) | ||
| 59 | { | ||
| 60 | if (buf_len == 0) { | ||
| 61 | return; | ||
| 62 | } | ||
| 63 | |||
| 64 | struct sockaddr_storage addr; | ||
| 65 | socklen_t addr_len = sizeof(addr); | ||
| 66 | |||
| 67 | if (getpeername(fd, (struct sockaddr *)&addr, &addr_len) != 0) { | ||
| 68 | set_unknown_ip(ip_buf, buf_len); | ||
| 69 | return; | ||
| 70 | } | ||
| 71 | |||
| 72 | const char *result = NULL; | ||
| 73 | if (addr.ss_family == AF_INET) { | ||
| 74 | struct sockaddr_in *addr_in = (struct sockaddr_in *)&addr; | ||
| 75 | result = inet_ntop(AF_INET, &addr_in->sin_addr, ip_buf, buf_len); | ||
| 76 | } | ||
| 77 | if (!result) { | ||
| 78 | set_unknown_ip(ip_buf, buf_len); | ||
| 79 | } | ||
| 80 | } | ||
| 81 | |||
| 82 | static esp_err_t on_open(httpd_handle_t hd, int sockfd) | ||
| 83 | { | ||
| 84 | if (!g_server) return ESP_FAIL; | ||
| 85 | |||
| 86 | xSemaphoreTake(g_server->lock, portMAX_DELAY); | ||
| 87 | |||
| 88 | if (g_server->connection_count >= WS_MAX_CONNECTIONS) { | ||
| 89 | xSemaphoreGive(g_server->lock); | ||
| 90 | ESP_LOGW(TAG, "Connection rejected - max connections reached"); | ||
| 91 | return ESP_FAIL; | ||
| 92 | } | ||
| 93 | |||
| 94 | ws_connection_t *conn = find_free_slot(g_server); | ||
| 95 | if (!conn) { | ||
| 96 | xSemaphoreGive(g_server->lock); | ||
| 97 | ESP_LOGE(TAG, "No free slot despite connection_count < WS_MAX_CONNECTIONS (fd=%d)", sockfd); | ||
| 98 | return ESP_FAIL; | ||
| 99 | } | ||
| 100 | |||
| 101 | struct linger so_linger = { .l_onoff = 1, .l_linger = 0 }; | ||
| 102 | setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &so_linger, sizeof(so_linger)); | ||
| 103 | |||
| 104 | int nodelay = 1; | ||
| 105 | setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay)); | ||
| 106 | |||
| 107 | conn->fd = sockfd; | ||
| 108 | conn->active = true; | ||
| 109 | conn->connected_at = esp_timer_get_time() / 1000000; | ||
| 110 | conn->last_activity = conn->connected_at; | ||
| 111 | get_client_ip(sockfd, conn->remote_ip, sizeof(conn->remote_ip)); | ||
| 112 | g_server->connection_count++; | ||
| 113 | ESP_LOGI(TAG, "New connection from %s (fd=%d, total=%d)", | ||
| 114 | conn->remote_ip, sockfd, g_server->connection_count); | ||
| 115 | |||
| 116 | xSemaphoreGive(g_server->lock); | ||
| 117 | return ESP_OK; | ||
| 118 | } | ||
| 119 | |||
| 120 | static void on_close(httpd_handle_t hd, int sockfd) | ||
| 121 | { | ||
| 122 | if (!g_server) return; | ||
| 123 | |||
| 124 | if (g_disconnect_callback) { | ||
| 125 | g_disconnect_callback(sockfd); | ||
| 126 | } | ||
| 127 | |||
| 128 | xSemaphoreTake(g_server->lock, portMAX_DELAY); | ||
| 129 | |||
| 130 | ws_connection_t *conn = find_connection_by_fd(g_server, sockfd); | ||
| 131 | if (conn) { | ||
| 132 | ESP_LOGI(TAG, "Connection closed (fd=%d, ip=%s)", sockfd, conn->remote_ip); | ||
| 133 | memset(conn, 0, sizeof(ws_connection_t)); | ||
| 134 | g_server->connection_count--; | ||
| 135 | } | ||
| 136 | |||
| 137 | xSemaphoreGive(g_server->lock); | ||
| 138 | } | ||
| 139 | |||
| 140 | void ws_server_set_disconnect_cb(ws_disconnect_cb_t cb) | ||
| 141 | { | ||
| 142 | g_disconnect_callback = cb; | ||
| 143 | } | ||
| 144 | |||
| 145 | static esp_err_t ws_handler(httpd_req_t *req) | ||
| 146 | { | ||
| 147 | if (req->method == HTTP_GET) { | ||
| 148 | char upgrade[16] = {0}; | ||
| 149 | if (httpd_req_get_hdr_value_str(req, "Upgrade", upgrade, sizeof(upgrade)) != ESP_OK || | ||
| 150 | strcasecmp(upgrade, "websocket") != 0) { | ||
| 151 | return relay_nip11_handler(req); | ||
| 152 | } | ||
| 153 | ESP_LOGD(TAG, "WebSocket handshake completed"); | ||
| 154 | return ESP_OK; | ||
| 155 | } | ||
| 156 | |||
| 157 | httpd_ws_frame_t ws_pkt; | ||
| 158 | memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t)); | ||
| 159 | ws_pkt.type = HTTPD_WS_TYPE_TEXT; | ||
| 160 | |||
| 161 | esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, 0); | ||
| 162 | if (ret != ESP_OK) { | ||
| 163 | ESP_LOGE(TAG, "Failed to get frame len: %d", ret); | ||
| 164 | return ret; | ||
| 165 | } | ||
| 166 | |||
| 167 | if (ws_pkt.len == 0) { | ||
| 168 | return ESP_OK; | ||
| 169 | } | ||
| 170 | |||
| 171 | if (ws_pkt.len > WS_MAX_FRAME_SIZE) { | ||
| 172 | ESP_LOGW(TAG, "Frame too large: %zu bytes", ws_pkt.len); | ||
| 173 | return ESP_FAIL; | ||
| 174 | } | ||
| 175 | |||
| 176 | ws_pkt.payload = malloc(ws_pkt.len + 1); | ||
| 177 | if (!ws_pkt.payload) { | ||
| 178 | ESP_LOGE(TAG, "Failed to allocate %zu bytes", ws_pkt.len); | ||
| 179 | return ESP_ERR_NO_MEM; | ||
| 180 | } | ||
| 181 | |||
| 182 | ret = httpd_ws_recv_frame(req, &ws_pkt, ws_pkt.len); | ||
| 183 | if (ret != ESP_OK) { | ||
| 184 | ESP_LOGE(TAG, "Failed to receive frame: %d", ret); | ||
| 185 | free(ws_pkt.payload); | ||
| 186 | return ret; | ||
| 187 | } | ||
| 188 | |||
| 189 | ((char *)ws_pkt.payload)[ws_pkt.len] = '\0'; | ||
| 190 | |||
| 191 | int fd = httpd_req_to_sockfd(req); | ||
| 192 | if (g_server) { | ||
| 193 | update_connection_activity(g_server, fd); | ||
| 194 | } | ||
| 195 | |||
| 196 | switch (ws_pkt.type) { | ||
| 197 | case HTTPD_WS_TYPE_TEXT: | ||
| 198 | ESP_LOGD(TAG, "Received %zu bytes from fd=%d", ws_pkt.len, fd); | ||
| 199 | if (g_message_callback) { | ||
| 200 | g_current_req = req; | ||
| 201 | g_message_callback(fd, (char *)ws_pkt.payload, ws_pkt.len); | ||
| 202 | g_current_req = NULL; | ||
| 203 | } | ||
| 204 | break; | ||
| 205 | |||
| 206 | case HTTPD_WS_TYPE_PING: | ||
| 207 | ws_pkt.type = HTTPD_WS_TYPE_PONG; | ||
| 208 | ret = httpd_ws_send_frame(req, &ws_pkt); | ||
| 209 | if (ret != ESP_OK) { | ||
| 210 | ESP_LOGW(TAG, "Failed to send PONG to fd=%d: %d", fd, ret); | ||
| 211 | free(ws_pkt.payload); | ||
| 212 | return ret; | ||
| 213 | } | ||
| 214 | break; | ||
| 215 | |||
| 216 | case HTTPD_WS_TYPE_CLOSE: { | ||
| 217 | ESP_LOGD(TAG, "Received CLOSE frame from fd=%d", fd); | ||
| 218 | free(ws_pkt.payload); | ||
| 219 | httpd_ws_frame_t close_pkt = { | ||
| 220 | .type = HTTPD_WS_TYPE_CLOSE, | ||
| 221 | .payload = NULL, | ||
| 222 | .len = 0, | ||
| 223 | }; | ||
| 224 | httpd_ws_send_frame(req, &close_pkt); | ||
| 225 | return ESP_FAIL; | ||
| 226 | } | ||
| 227 | |||
| 228 | default: | ||
| 229 | break; | ||
| 230 | } | ||
| 231 | |||
| 232 | free(ws_pkt.payload); | ||
| 233 | return ESP_OK; | ||
| 234 | } | ||
| 235 | |||
| 236 | typedef struct { | ||
| 237 | httpd_handle_t hd; | ||
| 238 | int fd; | ||
| 239 | char *data; | ||
| 240 | size_t len; | ||
| 241 | } async_send_arg_t; | ||
| 242 | |||
| 243 | static void ws_async_send(void *arg) | ||
| 244 | { | ||
| 245 | async_send_arg_t *a = (async_send_arg_t *)arg; | ||
| 246 | |||
| 247 | httpd_ws_frame_t ws_pkt = { | ||
| 248 | .type = HTTPD_WS_TYPE_TEXT, | ||
| 249 | .payload = (uint8_t *)a->data, | ||
| 250 | .len = a->len, | ||
| 251 | }; | ||
| 252 | |||
| 253 | esp_err_t ret = httpd_ws_send_frame_async(a->hd, a->fd, &ws_pkt); | ||
| 254 | if (ret != ESP_OK) { | ||
| 255 | ESP_LOGW(TAG, "Async send failed to fd=%d: %d", a->fd, ret); | ||
| 256 | } | ||
| 257 | |||
| 258 | free(a->data); | ||
| 259 | free(a); | ||
| 260 | } | ||
| 261 | |||
| 262 | static void cleanup_server_init(ws_server_t *server, bool stop_httpd) | ||
| 263 | { | ||
| 264 | g_server = NULL; | ||
| 265 | g_message_callback = NULL; | ||
| 266 | if (stop_httpd && server->server) { | ||
| 267 | httpd_stop(server->server); | ||
| 268 | server->server = NULL; | ||
| 269 | } | ||
| 270 | if (server->lock) { | ||
| 271 | vSemaphoreDelete(server->lock); | ||
| 272 | server->lock = NULL; | ||
| 273 | } | ||
| 274 | } | ||
| 275 | |||
| 276 | esp_err_t ws_server_init(ws_server_t *server, uint16_t port, ws_message_cb_t on_message) | ||
| 277 | { | ||
| 278 | if (server->server != NULL) { | ||
| 279 | ESP_LOGE(TAG, "Server already initialized, call ws_server_stop first"); | ||
| 280 | return ESP_ERR_INVALID_STATE; | ||
| 281 | } | ||
| 282 | |||
| 283 | memset(server, 0, sizeof(ws_server_t)); | ||
| 284 | server->lock = xSemaphoreCreateMutex(); | ||
| 285 | if (!server->lock) { | ||
| 286 | return ESP_ERR_NO_MEM; | ||
| 287 | } | ||
| 288 | |||
| 289 | g_server = server; | ||
| 290 | g_message_callback = on_message; | ||
| 291 | |||
| 292 | httpd_config_t config = HTTPD_DEFAULT_CONFIG(); | ||
| 293 | config.server_port = port; | ||
| 294 | config.ctrl_port = port + 1; | ||
| 295 | config.max_open_sockets = WS_MAX_CONNECTIONS; | ||
| 296 | config.backlog_conn = WS_MAX_CONNECTIONS; | ||
| 297 | config.lru_purge_enable = true; | ||
| 298 | config.recv_wait_timeout = 3; | ||
| 299 | config.send_wait_timeout = 3; | ||
| 300 | config.keep_alive_enable = true; | ||
| 301 | config.keep_alive_idle = 5; | ||
| 302 | config.keep_alive_interval = 1; | ||
| 303 | config.keep_alive_count = 3; | ||
| 304 | config.stack_size = 12288; | ||
| 305 | config.open_fn = on_open; | ||
| 306 | config.close_fn = on_close; | ||
| 307 | |||
| 308 | esp_err_t ret = httpd_start(&server->server, &config); | ||
| 309 | if (ret != ESP_OK) { | ||
| 310 | ESP_LOGE(TAG, "Failed to start server: %d", ret); | ||
| 311 | cleanup_server_init(server, false); | ||
| 312 | return ret; | ||
| 313 | } | ||
| 314 | |||
| 315 | httpd_uri_t ws_uri = { | ||
| 316 | .uri = "/", | ||
| 317 | .method = HTTP_GET, | ||
| 318 | .handler = ws_handler, | ||
| 319 | .user_ctx = NULL, | ||
| 320 | .is_websocket = true, | ||
| 321 | .handle_ws_control_frames = true, | ||
| 322 | }; | ||
| 323 | |||
| 324 | ret = httpd_register_uri_handler(server->server, &ws_uri); | ||
| 325 | if (ret != ESP_OK) { | ||
| 326 | ESP_LOGE(TAG, "Failed to register WS handler: %d", ret); | ||
| 327 | cleanup_server_init(server, true); | ||
| 328 | return ret; | ||
| 329 | } | ||
| 330 | |||
| 331 | httpd_uri_t options_uri = { | ||
| 332 | .uri = "/", | ||
| 333 | .method = HTTP_OPTIONS, | ||
| 334 | .handler = relay_nip11_options_handler, | ||
| 335 | .user_ctx = NULL, | ||
| 336 | }; | ||
| 337 | |||
| 338 | ret = httpd_register_uri_handler(server->server, &options_uri); | ||
| 339 | if (ret != ESP_OK) { | ||
| 340 | ESP_LOGE(TAG, "Failed to register OPTIONS handler: %d", ret); | ||
| 341 | } | ||
| 342 | |||
| 343 | ESP_LOGI(TAG, "WebSocket server started on port %d", port); | ||
| 344 | return ESP_OK; | ||
| 345 | } | ||
| 346 | |||
| 347 | void ws_server_stop(ws_server_t *server) | ||
| 348 | { | ||
| 349 | g_server = NULL; | ||
| 350 | g_message_callback = NULL; | ||
| 351 | g_disconnect_callback = NULL; | ||
| 352 | |||
| 353 | if (server->server) { | ||
| 354 | httpd_stop(server->server); | ||
| 355 | server->server = NULL; | ||
| 356 | } | ||
| 357 | if (server->lock) { | ||
| 358 | vSemaphoreDelete(server->lock); | ||
| 359 | server->lock = NULL; | ||
| 360 | } | ||
| 361 | memset(server->connections, 0, sizeof(server->connections)); | ||
| 362 | server->connection_count = 0; | ||
| 363 | } | ||
| 364 | |||
| 365 | bool ws_server_is_running(ws_server_t *server) | ||
| 366 | { | ||
| 367 | return server && server->server != NULL; | ||
| 368 | } | ||
| 369 | |||
| 370 | esp_err_t ws_server_send(ws_server_t *server, int fd, const char *data, size_t len) | ||
| 371 | { | ||
| 372 | if (!server->server) return ESP_ERR_INVALID_STATE; | ||
| 373 | |||
| 374 | if (g_current_req && httpd_req_to_sockfd(g_current_req) == fd) { | ||
| 375 | httpd_ws_frame_t ws_pkt = { | ||
| 376 | .type = HTTPD_WS_TYPE_TEXT, | ||
| 377 | .payload = (uint8_t *)data, | ||
| 378 | .len = len, | ||
| 379 | }; | ||
| 380 | return httpd_ws_send_frame(g_current_req, &ws_pkt); | ||
| 381 | } | ||
| 382 | |||
| 383 | async_send_arg_t *arg = malloc(sizeof(async_send_arg_t)); | ||
| 384 | if (!arg) return ESP_ERR_NO_MEM; | ||
| 385 | |||
| 386 | arg->data = malloc(len); | ||
| 387 | if (!arg->data) { | ||
| 388 | free(arg); | ||
| 389 | return ESP_ERR_NO_MEM; | ||
| 390 | } | ||
| 391 | |||
| 392 | memcpy(arg->data, data, len); | ||
| 393 | arg->hd = server->server; | ||
| 394 | arg->fd = fd; | ||
| 395 | arg->len = len; | ||
| 396 | |||
| 397 | esp_err_t ret = httpd_queue_work(server->server, ws_async_send, arg); | ||
| 398 | if (ret != ESP_OK) { | ||
| 399 | free(arg->data); | ||
| 400 | free(arg); | ||
| 401 | return ret; | ||
| 402 | } | ||
| 403 | return ESP_OK; | ||
| 404 | } | ||
| 405 | |||
| 406 | esp_err_t ws_server_broadcast(ws_server_t *server, const char *data, size_t len) | ||
| 407 | { | ||
| 408 | xSemaphoreTake(server->lock, portMAX_DELAY); | ||
| 409 | |||
| 410 | for (int i = 0; i < WS_MAX_CONNECTIONS; i++) { | ||
| 411 | if (server->connections[i].active) { | ||
| 412 | ws_server_send(server, server->connections[i].fd, data, len); | ||
| 413 | } | ||
| 414 | } | ||
| 415 | |||
| 416 | xSemaphoreGive(server->lock); | ||
| 417 | return ESP_OK; | ||
| 418 | } | ||
| 419 | |||
| 420 | void ws_server_close_connection(ws_server_t *server, int fd) | ||
| 421 | { | ||
| 422 | if (!server || !server->server) { | ||
| 423 | return; | ||
| 424 | } | ||
| 425 | httpd_sess_trigger_close(server->server, fd); | ||
| 426 | } | ||
diff --git a/components/wisp_relay/ws_server.h b/components/wisp_relay/ws_server.h new file mode 100644 index 0000000..4fe616e --- /dev/null +++ b/components/wisp_relay/ws_server.h | |||
| @@ -0,0 +1,41 @@ | |||
| 1 | #ifndef WS_SERVER_H | ||
| 2 | #define WS_SERVER_H | ||
| 3 | |||
| 4 | #include <stdint.h> | ||
| 5 | #include <stdbool.h> | ||
| 6 | #include <stddef.h> | ||
| 7 | #include "esp_http_server.h" | ||
| 8 | #include "freertos/FreeRTOS.h" | ||
| 9 | #include "freertos/semphr.h" | ||
| 10 | |||
| 11 | #define WS_MAX_CONNECTIONS 8 | ||
| 12 | #define WS_MAX_FRAME_SIZE 65536 | ||
| 13 | #define WS_IP_ADDR_MAX_LEN 48 | ||
| 14 | |||
| 15 | typedef struct { | ||
| 16 | int fd; | ||
| 17 | bool active; | ||
| 18 | uint32_t connected_at; | ||
| 19 | uint32_t last_activity; | ||
| 20 | char remote_ip[WS_IP_ADDR_MAX_LEN]; | ||
| 21 | } ws_connection_t; | ||
| 22 | |||
| 23 | typedef struct { | ||
| 24 | httpd_handle_t server; | ||
| 25 | ws_connection_t connections[WS_MAX_CONNECTIONS]; | ||
| 26 | SemaphoreHandle_t lock; | ||
| 27 | uint8_t connection_count; | ||
| 28 | } ws_server_t; | ||
| 29 | |||
| 30 | typedef void (*ws_message_cb_t)(int fd, const char *data, size_t len); | ||
| 31 | typedef void (*ws_disconnect_cb_t)(int fd); | ||
| 32 | |||
| 33 | esp_err_t ws_server_init(ws_server_t *server, uint16_t port, ws_message_cb_t on_message); | ||
| 34 | void ws_server_set_disconnect_cb(ws_disconnect_cb_t cb); | ||
| 35 | void ws_server_stop(ws_server_t *server); | ||
| 36 | bool ws_server_is_running(ws_server_t *server); | ||
| 37 | esp_err_t ws_server_send(ws_server_t *server, int fd, const char *data, size_t len); | ||
| 38 | esp_err_t ws_server_broadcast(ws_server_t *server, const char *data, size_t len); | ||
| 39 | void ws_server_close_connection(ws_server_t *server, int fd); | ||
| 40 | |||
| 41 | #endif | ||
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 9b0fb1c..6408e14 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt | |||
| @@ -18,8 +18,11 @@ idf_component_register(SRCS "tollgate_main.c" | |||
| 18 | "cvm_server.c" | 18 | "cvm_server.c" |
| 19 | "display.c" | 19 | "display.c" |
| 20 | "font.c" | 20 | "font.c" |
| 21 | "local_relay.c" | ||
| 22 | "relay_selector.c" | ||
| 23 | "sync_manager.c" | ||
| 21 | INCLUDE_DIRS "." | 24 | INCLUDE_DIRS "." |
| 22 | REQUIRES esp_wifi esp_event esp_netif nvs_flash esp_http_server | 25 | REQUIRES esp_wifi esp_event esp_netif nvs_flash esp_http_server |
| 23 | lwip json esp_http_client mbedtls esp-tls log spiffs | 26 | lwip json esp_http_client mbedtls esp-tls log spiffs |
| 24 | nucula_lib secp256k1 axs15231b qrcode | 27 | nucula_lib secp256k1 axs15231b qrcode wisp_relay |
| 25 | PRIV_REQUIRES esp-tls) | 28 | PRIV_REQUIRES esp-tls) |
diff --git a/main/config.c b/main/config.c index 9dd2a1d..b991991 100644 --- a/main/config.c +++ b/main/config.c | |||
| @@ -35,6 +35,8 @@ esp_err_t tollgate_config_init(void) | |||
| 35 | g_config.payout.mint_count = 0; | 35 | g_config.payout.mint_count = 0; |
| 36 | g_config.cvm_enabled = true; | 36 | g_config.cvm_enabled = true; |
| 37 | strncpy(g_config.cvm_relays, "wss://relay.primal.net", sizeof(g_config.cvm_relays) - 1); | 37 | strncpy(g_config.cvm_relays, "wss://relay.primal.net", sizeof(g_config.cvm_relays) - 1); |
| 38 | g_config.nostr_sync_interval_s = 1800; | ||
| 39 | g_config.nostr_fallback_sync_interval_s = 21600; | ||
| 38 | 40 | ||
| 39 | esp_vfs_spiffs_conf_t conf = { | 41 | esp_vfs_spiffs_conf_t conf = { |
| 40 | .base_path = "/spiffs", | 42 | .base_path = "/spiffs", |
| @@ -257,6 +259,28 @@ esp_err_t tollgate_config_init(void) | |||
| 257 | g_config.payout.mint_count = 1; | 259 | g_config.payout.mint_count = 1; |
| 258 | } | 260 | } |
| 259 | 261 | ||
| 262 | cJSON *seed_relays = cJSON_GetObjectItem(root, "nostr_seed_relays"); | ||
| 263 | if (seed_relays && cJSON_IsArray(seed_relays)) { | ||
| 264 | int srcount = cJSON_GetArraySize(seed_relays); | ||
| 265 | if (srcount > TOLLGATE_MAX_SEED_RELAYS) srcount = TOLLGATE_MAX_SEED_RELAYS; | ||
| 266 | for (int i = 0; i < srcount; i++) { | ||
| 267 | cJSON *r = cJSON_GetArrayItem(seed_relays, i); | ||
| 268 | if (r && cJSON_IsString(r)) { | ||
| 269 | strncpy(g_config.nostr_seed_relays[i], r->valuestring, | ||
| 270 | sizeof(g_config.nostr_seed_relays[i]) - 1); | ||
| 271 | g_config.nostr_seed_relay_count++; | ||
| 272 | } | ||
| 273 | } | ||
| 274 | } | ||
| 275 | |||
| 276 | cJSON *sync_interval = cJSON_GetObjectItem(root, "nostr_sync_interval_s"); | ||
| 277 | if (sync_interval) g_config.nostr_sync_interval_s = sync_interval->valueint; | ||
| 278 | |||
| 279 | cJSON *fallback_interval = cJSON_GetObjectItem(root, "nostr_fallback_sync_interval_s"); | ||
| 280 | if (fallback_interval) g_config.nostr_fallback_sync_interval_s = fallback_interval->valueint; | ||
| 281 | |||
| 282 | cJSON_Delete(root); | ||
| 283 | |||
| 260 | if (g_config.payout.recipient_count == 0) { | 284 | if (g_config.payout.recipient_count == 0) { |
| 261 | strncpy(g_config.payout.recipients[0].lightning_address, "TollGate@coinos.io", | 285 | strncpy(g_config.payout.recipients[0].lightning_address, "TollGate@coinos.io", |
| 262 | sizeof(g_config.payout.recipients[0].lightning_address) - 1); | 286 | sizeof(g_config.payout.recipients[0].lightning_address) - 1); |
| @@ -264,14 +288,24 @@ esp_err_t tollgate_config_init(void) | |||
| 264 | g_config.payout.recipient_count = 1; | 288 | g_config.payout.recipient_count = 1; |
| 265 | } | 289 | } |
| 266 | 290 | ||
| 267 | cJSON_Delete(root); | ||
| 268 | |||
| 269 | if (g_config.nostr_relay_count == 0) { | 291 | if (g_config.nostr_relay_count == 0) { |
| 270 | strncpy(g_config.nostr_relays[0], "wss://relay.damus.io", sizeof(g_config.nostr_relays[0]) - 1); | 292 | strncpy(g_config.nostr_relays[0], "wss://relay.damus.io", sizeof(g_config.nostr_relays[0]) - 1); |
| 271 | strncpy(g_config.nostr_relays[1], "wss://nos.lol", sizeof(g_config.nostr_relays[1]) - 1); | 293 | strncpy(g_config.nostr_relays[1], "wss://nos.lol", sizeof(g_config.nostr_relays[1]) - 1); |
| 272 | g_config.nostr_relay_count = 2; | 294 | g_config.nostr_relay_count = 2; |
| 273 | } | 295 | } |
| 274 | 296 | ||
| 297 | if (g_config.nostr_seed_relay_count == 0) { | ||
| 298 | strncpy(g_config.nostr_seed_relays[0], "wss://relay.orangesync.tech", | ||
| 299 | sizeof(g_config.nostr_seed_relays[0]) - 1); | ||
| 300 | strncpy(g_config.nostr_seed_relays[1], "wss://relay.damus.io", | ||
| 301 | sizeof(g_config.nostr_seed_relays[1]) - 1); | ||
| 302 | strncpy(g_config.nostr_seed_relays[2], "wss://nos.lol", | ||
| 303 | sizeof(g_config.nostr_seed_relays[2]) - 1); | ||
| 304 | strncpy(g_config.nostr_seed_relays[3], "wss://relay.nostr.band", | ||
| 305 | sizeof(g_config.nostr_seed_relays[3]) - 1); | ||
| 306 | g_config.nostr_seed_relay_count = 4; | ||
| 307 | } | ||
| 308 | |||
| 275 | ESP_LOGI(TAG, "Config loaded: nsec=%s...%s, %d WiFi networks, price=%d sats/%dms", | 309 | ESP_LOGI(TAG, "Config loaded: nsec=%s...%s, %d WiFi networks, price=%d sats/%dms", |
| 276 | g_config.nsec, g_config.nsec + 60, g_config.network_count, | 310 | g_config.nsec, g_config.nsec + 60, g_config.network_count, |
| 277 | g_config.price_per_step, g_config.step_size_ms); | 311 | g_config.price_per_step, g_config.step_size_ms); |
diff --git a/main/config.h b/main/config.h index fa4d95c..1e580e9 100644 --- a/main/config.h +++ b/main/config.h | |||
| @@ -13,6 +13,7 @@ | |||
| 13 | #define TOLLGATE_MAX_AP_SSID_LEN 32 | 13 | #define TOLLGATE_MAX_AP_SSID_LEN 32 |
| 14 | #define TOLLGATE_MAX_AP_PASS_LEN 64 | 14 | #define TOLLGATE_MAX_AP_PASS_LEN 64 |
| 15 | #define TOLLGATE_MAX_RELAYS 4 | 15 | #define TOLLGATE_MAX_RELAYS 4 |
| 16 | #define TOLLGATE_MAX_SEED_RELAYS 8 | ||
| 16 | 17 | ||
| 17 | typedef struct { | 18 | typedef struct { |
| 18 | char ssid[32]; | 19 | char ssid[32]; |
| @@ -63,6 +64,11 @@ typedef struct { | |||
| 63 | 64 | ||
| 64 | bool cvm_enabled; | 65 | bool cvm_enabled; |
| 65 | char cvm_relays[256]; | 66 | char cvm_relays[256]; |
| 67 | |||
| 68 | char nostr_seed_relays[TOLLGATE_MAX_SEED_RELAYS][128]; | ||
| 69 | int nostr_seed_relay_count; | ||
| 70 | int nostr_sync_interval_s; | ||
| 71 | int nostr_fallback_sync_interval_s; | ||
| 66 | } tollgate_config_t; | 72 | } tollgate_config_t; |
| 67 | 73 | ||
| 68 | void tollgate_config_derive_unique(tollgate_config_t *cfg); | 74 | void tollgate_config_derive_unique(tollgate_config_t *cfg); |
diff --git a/main/cvm_server.c b/main/cvm_server.c index b93e176..dd04047 100644 --- a/main/cvm_server.c +++ b/main/cvm_server.c | |||
| @@ -11,7 +11,6 @@ | |||
| 11 | #include "esp_tls.h" | 11 | #include "esp_tls.h" |
| 12 | #include "esp_crt_bundle.h" | 12 | #include "esp_crt_bundle.h" |
| 13 | #include "esp_random.h" | 13 | #include "esp_random.h" |
| 14 | #include "esp_timer.h" | ||
| 15 | #include "freertos/FreeRTOS.h" | 14 | #include "freertos/FreeRTOS.h" |
| 16 | #include "freertos/task.h" | 15 | #include "freertos/task.h" |
| 17 | #include <string.h> | 16 | #include <string.h> |
| @@ -31,8 +30,6 @@ static void publish_announcements_via_ws(esp_tls_t *tls); | |||
| 31 | #define CVM_WS_BUF_SIZE 8192 | 30 | #define CVM_WS_BUF_SIZE 8192 |
| 32 | #define CVM_MAX_RESPONSE_SIZE 4096 | 31 | #define CVM_MAX_RESPONSE_SIZE 4096 |
| 33 | #define CVM_RECONNECT_DELAY_MS 5000 | 32 | #define CVM_RECONNECT_DELAY_MS 5000 |
| 34 | #define CVM_WS_READ_TIMEOUT_MS 60000 | ||
| 35 | #define CVM_WS_PING_INTERVAL_S 30 | ||
| 36 | 33 | ||
| 37 | static char *parse_ws_text_frame(const uint8_t *buf, int len) | 34 | static char *parse_ws_text_frame(const uint8_t *buf, int len) |
| 38 | { | 35 | { |
| @@ -151,7 +148,7 @@ static esp_err_t ws_connect(const char *relay_url, esp_tls_t **tls_out) | |||
| 151 | 148 | ||
| 152 | esp_tls_cfg_t tls_cfg = { | 149 | esp_tls_cfg_t tls_cfg = { |
| 153 | .crt_bundle_attach = esp_crt_bundle_attach, | 150 | .crt_bundle_attach = esp_crt_bundle_attach, |
| 154 | .timeout_ms = CVM_WS_READ_TIMEOUT_MS, | 151 | .timeout_ms = 15000, |
| 155 | }; | 152 | }; |
| 156 | esp_tls_t *tls = esp_tls_init(); | 153 | esp_tls_t *tls = esp_tls_init(); |
| 157 | if (!tls) return ESP_ERR_NO_MEM; | 154 | if (!tls) return ESP_ERR_NO_MEM; |
| @@ -326,54 +323,6 @@ static esp_err_t publish_event_to_relay(const char *relay_url, const char *event | |||
| 326 | return ESP_OK; | 323 | return ESP_OK; |
| 327 | } | 324 | } |
| 328 | 325 | ||
| 329 | static esp_err_t publish_kind_25910_response_ws(esp_tls_t *tls, | ||
| 330 | const char *content_json, | ||
| 331 | const char *request_event_id) | ||
| 332 | { | ||
| 333 | const tollgate_identity_t *id = identity_get(); | ||
| 334 | if (!id || !id->initialized) return ESP_FAIL; | ||
| 335 | |||
| 336 | cJSON *tags = cJSON_CreateArray(); | ||
| 337 | cJSON *e_tag = cJSON_CreateArray(); | ||
| 338 | cJSON_AddItemToArray(e_tag, cJSON_CreateString("e")); | ||
| 339 | cJSON_AddItemToArray(e_tag, cJSON_CreateString(request_event_id)); | ||
| 340 | cJSON_AddItemToArray(tags, e_tag); | ||
| 341 | |||
| 342 | char *tags_str = cJSON_PrintUnformatted(tags); | ||
| 343 | cJSON_Delete(tags); | ||
| 344 | |||
| 345 | nostr_event_t event; | ||
| 346 | nostr_event_init(&event, id->npub_hex, 25910, tags_str, content_json); | ||
| 347 | nostr_event_sign(&event, id->nsec); | ||
| 348 | |||
| 349 | char *event_json = malloc(8192); | ||
| 350 | if (!event_json) { | ||
| 351 | free(tags_str); | ||
| 352 | return ESP_ERR_NO_MEM; | ||
| 353 | } | ||
| 354 | |||
| 355 | esp_err_t ret = nostr_event_to_json(&event, event_json, 8192); | ||
| 356 | free(tags_str); | ||
| 357 | if (ret != ESP_OK) { | ||
| 358 | free(event_json); | ||
| 359 | return ret; | ||
| 360 | } | ||
| 361 | |||
| 362 | size_t msg_len = 10 + strlen(event_json) + 2; | ||
| 363 | char *msg = malloc(msg_len); | ||
| 364 | if (!msg) { | ||
| 365 | free(event_json); | ||
| 366 | return ESP_ERR_NO_MEM; | ||
| 367 | } | ||
| 368 | snprintf(msg, msg_len, "[\"EVENT\",%s]", event_json); | ||
| 369 | ESP_LOGD(TAG, "Sending WS response (%d bytes)", (int)strlen(msg)); | ||
| 370 | int rc = ws_send_text(tls, msg); | ||
| 371 | ESP_LOGD(TAG, "WS send result: %d", rc); | ||
| 372 | free(msg); | ||
| 373 | free(event_json); | ||
| 374 | return ESP_OK; | ||
| 375 | } | ||
| 376 | |||
| 377 | static esp_err_t publish_kind_25910_response(const char *relay_url, | 326 | static esp_err_t publish_kind_25910_response(const char *relay_url, |
| 378 | const char *content_json, | 327 | const char *content_json, |
| 379 | const char *request_event_id) | 328 | const char *request_event_id) |
| @@ -417,7 +366,7 @@ static bool is_owner_pubkey(const char *pubkey_hex) | |||
| 417 | return strcmp(id->npub_hex, pubkey_hex) == 0; | 366 | return strcmp(id->npub_hex, pubkey_hex) == 0; |
| 418 | } | 367 | } |
| 419 | 368 | ||
| 420 | static void handle_mcp_message(esp_tls_t *tls, const char *sender_pubkey, | 369 | static void handle_mcp_message(const char *relay_url, const char *sender_pubkey, |
| 421 | const char *event_id, const char *content) | 370 | const char *event_id, const char *content) |
| 422 | { | 371 | { |
| 423 | cJSON *msg = cJSON_Parse(content); | 372 | cJSON *msg = cJSON_Parse(content); |
| @@ -437,20 +386,14 @@ static void handle_mcp_message(esp_tls_t *tls, const char *sender_pubkey, | |||
| 437 | if (strcmp(m, "initialize") == 0) { | 386 | if (strcmp(m, "initialize") == 0) { |
| 438 | ESP_LOGI(TAG, "MCP initialize from %s", sender_pubkey); | 387 | ESP_LOGI(TAG, "MCP initialize from %s", sender_pubkey); |
| 439 | char *resp = build_initialize_response(id_str, sender_pubkey); | 388 | char *resp = build_initialize_response(id_str, sender_pubkey); |
| 440 | if (tls) { | 389 | publish_kind_25910_response(relay_url, resp, event_id); |
| 441 | publish_kind_25910_response_ws(tls, resp, event_id); | ||
| 442 | } else { | ||
| 443 | ESP_LOGW(TAG, "No TLS for response"); | ||
| 444 | } | ||
| 445 | free(resp); | 390 | free(resp); |
| 446 | } else if (strcmp(m, "notifications/initialized") == 0) { | 391 | } else if (strcmp(m, "notifications/initialized") == 0) { |
| 447 | ESP_LOGI(TAG, "Client initialized: %s", sender_pubkey); | 392 | ESP_LOGI(TAG, "Client initialized: %s", sender_pubkey); |
| 448 | } else if (strcmp(m, "tools/list") == 0) { | 393 | } else if (strcmp(m, "tools/list") == 0) { |
| 449 | ESP_LOGI(TAG, "tools/list from %s", sender_pubkey); | 394 | ESP_LOGI(TAG, "tools/list from %s", sender_pubkey); |
| 450 | char *resp = build_tools_list_response(id_str); | 395 | char *resp = build_tools_list_response(id_str); |
| 451 | if (tls) { | 396 | publish_kind_25910_response(relay_url, resp, event_id); |
| 452 | publish_kind_25910_response_ws(tls, resp, event_id); | ||
| 453 | } | ||
| 454 | free(resp); | 397 | free(resp); |
| 455 | } else if (strcmp(m, "tools/call") == 0) { | 398 | } else if (strcmp(m, "tools/call") == 0) { |
| 456 | cJSON *params = cJSON_GetObjectItem(msg, "params"); | 399 | cJSON *params = cJSON_GetObjectItem(msg, "params"); |
| @@ -471,16 +414,12 @@ static void handle_mcp_message(esp_tls_t *tls, const char *sender_pubkey, | |||
| 471 | 414 | ||
| 472 | mcp_response_t mcp_resp = mcp_dispatch(&req); | 415 | mcp_response_t mcp_resp = mcp_dispatch(&req); |
| 473 | char *resp = build_tool_call_response(id_str, &mcp_resp); | 416 | char *resp = build_tool_call_response(id_str, &mcp_resp); |
| 474 | if (tls) { | 417 | publish_kind_25910_response(relay_url, resp, event_id); |
| 475 | publish_kind_25910_response_ws(tls, resp, event_id); | ||
| 476 | } | ||
| 477 | free(resp); | 418 | free(resp); |
| 478 | } | 419 | } |
| 479 | } else if (strcmp(m, "ping") == 0) { | 420 | } else if (strcmp(m, "ping") == 0) { |
| 480 | char *resp = build_ping_response(id_str); | 421 | char *resp = build_ping_response(id_str); |
| 481 | if (tls) { | 422 | publish_kind_25910_response(relay_url, resp, event_id); |
| 482 | publish_kind_25910_response_ws(tls, resp, event_id); | ||
| 483 | } | ||
| 484 | free(resp); | 423 | free(resp); |
| 485 | } else { | 424 | } else { |
| 486 | ESP_LOGW(TAG, "Unknown MCP method: %s", m); | 425 | ESP_LOGW(TAG, "Unknown MCP method: %s", m); |
| @@ -494,7 +433,7 @@ static void handle_mcp_message(esp_tls_t *tls, const char *sender_pubkey, | |||
| 494 | cJSON_Delete(msg); | 433 | cJSON_Delete(msg); |
| 495 | } | 434 | } |
| 496 | 435 | ||
| 497 | static void process_relay_message(esp_tls_t *tls, const char *relay_url, const char *msg_str) | 436 | static void process_relay_message(const char *relay_url, const char *msg_str) |
| 498 | { | 437 | { |
| 499 | cJSON *arr = cJSON_Parse(msg_str); | 438 | cJSON *arr = cJSON_Parse(msg_str); |
| 500 | if (!arr || !cJSON_IsArray(arr)) { | 439 | if (!arr || !cJSON_IsArray(arr)) { |
| @@ -553,7 +492,7 @@ static void process_relay_message(esp_tls_t *tls, const char *relay_url, const c | |||
| 553 | return; | 492 | return; |
| 554 | } | 493 | } |
| 555 | 494 | ||
| 556 | handle_mcp_message(tls, pubkey->valuestring, event_id->valuestring, content->valuestring); | 495 | handle_mcp_message(relay_url, pubkey->valuestring, event_id->valuestring, content->valuestring); |
| 557 | cJSON_Delete(arr); | 496 | cJSON_Delete(arr); |
| 558 | } | 497 | } |
| 559 | 498 | ||
| @@ -566,9 +505,7 @@ static esp_err_t subscribe_to_relay(esp_tls_t *tls, const char *npub) | |||
| 566 | cJSON *kinds = cJSON_CreateArray(); | 505 | cJSON *kinds = cJSON_CreateArray(); |
| 567 | cJSON_AddItemToArray(kinds, cJSON_CreateNumber(25910)); | 506 | cJSON_AddItemToArray(kinds, cJSON_CreateNumber(25910)); |
| 568 | cJSON_AddItemToObject(filter, "kinds", kinds); | 507 | cJSON_AddItemToObject(filter, "kinds", kinds); |
| 569 | cJSON *p_tags = cJSON_CreateArray(); | 508 | cJSON_AddStringToObject(filter, "#p", npub); |
| 570 | cJSON_AddItemToArray(p_tags, cJSON_CreateString(npub)); | ||
| 571 | cJSON_AddItemToObject(filter, "#p", p_tags); | ||
| 572 | cJSON_AddNumberToObject(filter, "limit", 100); | 509 | cJSON_AddNumberToObject(filter, "limit", 100); |
| 573 | cJSON_AddItemToArray(sub, filter); | 510 | cJSON_AddItemToArray(sub, filter); |
| 574 | 511 | ||
| @@ -616,8 +553,6 @@ static void cvm_relay_task(void *arg) | |||
| 616 | return; | 553 | return; |
| 617 | } | 554 | } |
| 618 | 555 | ||
| 619 | int64_t last_ping_time = 0; | ||
| 620 | |||
| 621 | while (g_running) { | 556 | while (g_running) { |
| 622 | int rlen = esp_tls_conn_read(tls, buf, CVM_WS_BUF_SIZE - 1); | 557 | int rlen = esp_tls_conn_read(tls, buf, CVM_WS_BUF_SIZE - 1); |
| 623 | if (rlen < 0) { | 558 | if (rlen < 0) { |
| @@ -632,20 +567,10 @@ static void cvm_relay_task(void *arg) | |||
| 632 | char *text = parse_ws_text_frame(buf, rlen); | 567 | char *text = parse_ws_text_frame(buf, rlen); |
| 633 | if (text) { | 568 | if (text) { |
| 634 | if (strlen(text) > 0) { | 569 | if (strlen(text) > 0) { |
| 635 | process_relay_message(tls, relay_url, text); | 570 | process_relay_message(relay_url, text); |
| 636 | } | 571 | } |
| 637 | free(text); | 572 | free(text); |
| 638 | } | 573 | } |
| 639 | } else if ((buf[0] & 0x0F) == 0x09) { | ||
| 640 | uint8_t pong[2] = {0x8A, 0x00}; | ||
| 641 | esp_tls_conn_write(tls, pong, 2); | ||
| 642 | } | ||
| 643 | |||
| 644 | int64_t now = (int64_t)esp_timer_get_time() / 1000000; | ||
| 645 | if (now - last_ping_time >= CVM_WS_PING_INTERVAL_S) { | ||
| 646 | uint8_t ping[2] = {0x89, 0x00}; | ||
| 647 | esp_tls_conn_write(tls, ping, 2); | ||
| 648 | last_ping_time = now; | ||
| 649 | } | 574 | } |
| 650 | } | 575 | } |
| 651 | 576 | ||
diff --git a/main/display.c b/main/display.c new file mode 100644 index 0000000..2b6cc88 --- /dev/null +++ b/main/display.c | |||
| @@ -0,0 +1,264 @@ | |||
| 1 | #include "display.h" | ||
| 2 | #include "axs15231b.h" | ||
| 3 | #include "qrcoded.h" | ||
| 4 | #include "font.h" | ||
| 5 | #include "esp_log.h" | ||
| 6 | #include "freertos/FreeRTOS.h" | ||
| 7 | #include "freertos/task.h" | ||
| 8 | #include <string.h> | ||
| 9 | #include <stdio.h> | ||
| 10 | #include <stdlib.h> | ||
| 11 | |||
| 12 | static const char *TAG = "display"; | ||
| 13 | |||
| 14 | #define QR_CYCLE_MS 5000 | ||
| 15 | |||
| 16 | static volatile display_state_t s_state = DISPLAY_BOOT; | ||
| 17 | static char s_ap_ssid[32] = ""; | ||
| 18 | static char s_portal_url[256] = ""; | ||
| 19 | static int s_active_clients = 0; | ||
| 20 | static uint64_t s_wallet_balance = 0; | ||
| 21 | static bool s_initialized = false; | ||
| 22 | static int64_t s_last_qr_switch = 0; | ||
| 23 | static display_qr_mode_t s_qr_mode = DISPLAY_QR_WIFI; | ||
| 24 | |||
| 25 | static int qr_version_from_strlen(int len) { | ||
| 26 | if (len <= 17) return 1; | ||
| 27 | if (len <= 32) return 2; | ||
| 28 | if (len <= 53) return 3; | ||
| 29 | if (len <= 78) return 4; | ||
| 30 | if (len <= 106) return 5; | ||
| 31 | if (len <= 134) return 6; | ||
| 32 | if (len <= 154) return 7; | ||
| 33 | if (len <= 192) return 8; | ||
| 34 | if (len <= 230) return 9; | ||
| 35 | if (len <= 271) return 10; | ||
| 36 | return 11; | ||
| 37 | } | ||
| 38 | |||
| 39 | static int qr_pixel_size(int len) { | ||
| 40 | if (len <= 53) return 4; | ||
| 41 | if (len <= 134) return 3; | ||
| 42 | return 2; | ||
| 43 | } | ||
| 44 | |||
| 45 | static int escape_wifi_field(const char *src, char *dst, int dst_size) { | ||
| 46 | int si = 0, di = 0; | ||
| 47 | while (src[si] && di < dst_size - 2) { | ||
| 48 | char c = src[si]; | ||
| 49 | if (c == '\\' || c == ';' || c == ':' || c == ',' || c == '"') { | ||
| 50 | if (di + 2 >= dst_size) break; | ||
| 51 | dst[di++] = '\\'; | ||
| 52 | dst[di++] = c; | ||
| 53 | } else { | ||
| 54 | dst[di++] = c; | ||
| 55 | } | ||
| 56 | si++; | ||
| 57 | } | ||
| 58 | dst[di] = '\0'; | ||
| 59 | return di; | ||
| 60 | } | ||
| 61 | |||
| 62 | static void build_wifi_qr_string(char *out, int out_size) { | ||
| 63 | char escaped_ssid[64]; | ||
| 64 | escape_wifi_field(s_ap_ssid, escaped_ssid, sizeof(escaped_ssid)); | ||
| 65 | snprintf(out, out_size, "WIFI:S:%s;T:nopass;;", escaped_ssid); | ||
| 66 | } | ||
| 67 | |||
| 68 | void display_render_text(int x, int y, const char *text, uint16_t fg, uint16_t bg, int scale) { | ||
| 69 | int cx = x; | ||
| 70 | int cy = y; | ||
| 71 | int screen_w = axs15231b_get_width(); | ||
| 72 | int screen_h = axs15231b_get_height(); | ||
| 73 | |||
| 74 | while (*text) { | ||
| 75 | uint8_t ch = (uint8_t)*text; | ||
| 76 | if (ch >= 128) ch = '?'; | ||
| 77 | |||
| 78 | if (cx + FONT_GLYPH_W * scale > screen_w) { | ||
| 79 | cx = x; | ||
| 80 | cy += FONT_GLYPH_H * scale; | ||
| 81 | } | ||
| 82 | if (cy + FONT_GLYPH_H * scale > screen_h) break; | ||
| 83 | |||
| 84 | const uint8_t *glyph = font8x8_basic[ch]; | ||
| 85 | for (int row = 0; row < FONT_GLYPH_H; row++) { | ||
| 86 | uint8_t bits = glyph[row]; | ||
| 87 | for (int col = 0; col < FONT_GLYPH_W; col++) { | ||
| 88 | uint16_t color = (bits & (0x80 >> col)) ? fg : bg; | ||
| 89 | int px = cx + col * scale; | ||
| 90 | int py = cy + row * scale; | ||
| 91 | if (px < screen_w && py < screen_h) { | ||
| 92 | axs15231b_fill_rect(px, py, scale, scale, color); | ||
| 93 | } | ||
| 94 | } | ||
| 95 | } | ||
| 96 | cx += FONT_GLYPH_W * scale; | ||
| 97 | text++; | ||
| 98 | } | ||
| 99 | } | ||
| 100 | |||
| 101 | static void render_qr_at(const char *text, int x_off, int y_off, int max_w, int max_h) { | ||
| 102 | int len = strlen(text); | ||
| 103 | int version = qr_version_from_strlen(len); | ||
| 104 | int px = qr_pixel_size(len); | ||
| 105 | |||
| 106 | uint16_t buf_size = qrcode_getBufferSize(version); | ||
| 107 | uint8_t *qr_buf = (uint8_t *)malloc(buf_size); | ||
| 108 | if (!qr_buf) { | ||
| 109 | ESP_LOGE(TAG, "Failed to allocate QR buffer"); | ||
| 110 | return; | ||
| 111 | } | ||
| 112 | |||
| 113 | QRCode qr; | ||
| 114 | if (qrcode_initText(&qr, qr_buf, version, ECC_LOW, text) != 0) { | ||
| 115 | ESP_LOGE(TAG, "QR generation failed"); | ||
| 116 | free(qr_buf); | ||
| 117 | return; | ||
| 118 | } | ||
| 119 | |||
| 120 | int qr_px_w = qr.size * px; | ||
| 121 | int qr_px_h = qr.size * px; | ||
| 122 | int cx = x_off + (max_w - qr_px_w) / 2; | ||
| 123 | int cy = y_off + (max_h - qr_px_h) / 2; | ||
| 124 | if (cx < 0) cx = 0; | ||
| 125 | if (cy < 0) cy = 0; | ||
| 126 | |||
| 127 | for (int y = 0; y < qr.size; y++) { | ||
| 128 | for (int x = 0; x < qr.size; x++) { | ||
| 129 | bool mod = qrcode_getModule(&qr, x, y); | ||
| 130 | uint16_t color = mod ? 0xFFFF : 0x0000; | ||
| 131 | axs15231b_fill_rect(cx + x * px, cy + y * px, px, px, color); | ||
| 132 | } | ||
| 133 | } | ||
| 134 | |||
| 135 | free(qr_buf); | ||
| 136 | } | ||
| 137 | |||
| 138 | void display_render_qr(const char *text) { | ||
| 139 | int screen_w = axs15231b_get_width(); | ||
| 140 | int screen_h = axs15231b_get_height(); | ||
| 141 | axs15231b_fill_screen(0x0000); | ||
| 142 | render_qr_at(text, 0, 0, screen_w, screen_h); | ||
| 143 | axs15231b_flush(); | ||
| 144 | } | ||
| 145 | |||
| 146 | static void render_boot_screen(void) { | ||
| 147 | axs15231b_fill_screen(0x0000); | ||
| 148 | display_render_text(140, 100, "TollGate", 0xF79F, 0x0000, 3); | ||
| 149 | display_render_text(140, 140, "starting...", 0xB5B6, 0x0000, 2); | ||
| 150 | axs15231b_flush(); | ||
| 151 | } | ||
| 152 | |||
| 153 | static void render_ready_screen(void) { | ||
| 154 | axs15231b_fill_screen(0x0000); | ||
| 155 | |||
| 156 | int screen_w = axs15231b_get_width(); | ||
| 157 | int screen_h = axs15231b_get_height(); | ||
| 158 | int text_area_y = screen_h - 55; | ||
| 159 | |||
| 160 | char qr_text[320]; | ||
| 161 | const char *label; | ||
| 162 | |||
| 163 | if (s_qr_mode == DISPLAY_QR_WIFI) { | ||
| 164 | build_wifi_qr_string(qr_text, sizeof(qr_text)); | ||
| 165 | label = "Scan to connect"; | ||
| 166 | } else { | ||
| 167 | strncpy(qr_text, s_portal_url, sizeof(qr_text) - 1); | ||
| 168 | qr_text[sizeof(qr_text) - 1] = '\0'; | ||
| 169 | label = "Portal URL"; | ||
| 170 | } | ||
| 171 | |||
| 172 | render_qr_at(qr_text, 0, 0, screen_w, text_area_y - 5); | ||
| 173 | |||
| 174 | display_render_text(10, text_area_y, label, 0xB5B6, 0x0000, 2); | ||
| 175 | |||
| 176 | char line[64]; | ||
| 177 | snprintf(line, sizeof(line), "SSID: %s", s_ap_ssid); | ||
| 178 | display_render_text(10, text_area_y + 20, line, 0xB5B6, 0x0000, 2); | ||
| 179 | |||
| 180 | axs15231b_flush(); | ||
| 181 | } | ||
| 182 | |||
| 183 | static void render_payment_screen(void) { | ||
| 184 | axs15231b_fill_screen(0x07E0); | ||
| 185 | display_render_text(140, 100, "Paid!", 0x0000, 0x07E0, 3); | ||
| 186 | display_render_text(130, 140, "Access granted", 0x0000, 0x07E0, 2); | ||
| 187 | axs15231b_flush(); | ||
| 188 | } | ||
| 189 | |||
| 190 | static void render_error_screen(void) { | ||
| 191 | axs15231b_fill_screen(0xF800); | ||
| 192 | display_render_text(120, 100, "No upstream", 0xFFFF, 0xF800, 3); | ||
| 193 | display_render_text(130, 140, "Check config", 0xFFFF, 0xF800, 2); | ||
| 194 | axs15231b_flush(); | ||
| 195 | } | ||
| 196 | |||
| 197 | static void display_task(void *pvParameters) { | ||
| 198 | ESP_LOGI(TAG, "Display task started"); | ||
| 199 | |||
| 200 | while (1) { | ||
| 201 | display_state_t state = s_state; | ||
| 202 | |||
| 203 | switch (state) { | ||
| 204 | case DISPLAY_BOOT: | ||
| 205 | render_boot_screen(); | ||
| 206 | break; | ||
| 207 | case DISPLAY_READY: | ||
| 208 | render_ready_screen(); | ||
| 209 | break; | ||
| 210 | case DISPLAY_PAYMENT_RECEIVED: | ||
| 211 | render_payment_screen(); | ||
| 212 | vTaskDelay(pdMS_TO_TICKS(2000)); | ||
| 213 | s_state = DISPLAY_READY; | ||
| 214 | break; | ||
| 215 | case DISPLAY_ERROR: | ||
| 216 | render_error_screen(); | ||
| 217 | break; | ||
| 218 | } | ||
| 219 | |||
| 220 | int64_t now = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS; | ||
| 221 | if (state == DISPLAY_READY && (now - s_last_qr_switch) >= QR_CYCLE_MS) { | ||
| 222 | s_qr_mode = (s_qr_mode == DISPLAY_QR_WIFI) ? DISPLAY_QR_PORTAL : DISPLAY_QR_WIFI; | ||
| 223 | s_last_qr_switch = now; | ||
| 224 | } | ||
| 225 | |||
| 226 | vTaskDelay(pdMS_TO_TICKS(1000)); | ||
| 227 | } | ||
| 228 | } | ||
| 229 | |||
| 230 | esp_err_t display_init(void) { | ||
| 231 | if (s_initialized) return ESP_OK; | ||
| 232 | |||
| 233 | esp_err_t ret = axs15231b_init(); | ||
| 234 | if (ret != ESP_OK) { | ||
| 235 | ESP_LOGE(TAG, "Display hardware init failed: %s", esp_err_to_name(ret)); | ||
| 236 | return ret; | ||
| 237 | } | ||
| 238 | |||
| 239 | s_initialized = true; | ||
| 240 | s_last_qr_switch = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS; | ||
| 241 | |||
| 242 | xTaskCreatePinnedToCore(display_task, "display", 16384, NULL, 2, NULL, 1); | ||
| 243 | |||
| 244 | ESP_LOGI(TAG, "Display initialized"); | ||
| 245 | return ESP_OK; | ||
| 246 | } | ||
| 247 | |||
| 248 | void display_set_state(display_state_t state) { | ||
| 249 | s_state = state; | ||
| 250 | } | ||
| 251 | |||
| 252 | void display_update(const char *ap_ssid, int active_clients, | ||
| 253 | uint64_t wallet_balance, const char *portal_url) { | ||
| 254 | if (ap_ssid) { | ||
| 255 | strncpy(s_ap_ssid, ap_ssid, sizeof(s_ap_ssid) - 1); | ||
| 256 | s_ap_ssid[sizeof(s_ap_ssid) - 1] = '\0'; | ||
| 257 | } | ||
| 258 | if (portal_url) { | ||
| 259 | strncpy(s_portal_url, portal_url, sizeof(s_portal_url) - 1); | ||
| 260 | s_portal_url[sizeof(s_portal_url) - 1] = '\0'; | ||
| 261 | } | ||
| 262 | s_active_clients = active_clients; | ||
| 263 | s_wallet_balance = wallet_balance; | ||
| 264 | } | ||
diff --git a/main/display.h b/main/display.h new file mode 100644 index 0000000..407521b --- /dev/null +++ b/main/display.h | |||
| @@ -0,0 +1,27 @@ | |||
| 1 | #ifndef DISPLAY_H | ||
| 2 | #define DISPLAY_H | ||
| 3 | |||
| 4 | #include "esp_err.h" | ||
| 5 | #include <stdint.h> | ||
| 6 | #include <stdbool.h> | ||
| 7 | |||
| 8 | typedef enum { | ||
| 9 | DISPLAY_BOOT, | ||
| 10 | DISPLAY_READY, | ||
| 11 | DISPLAY_PAYMENT_RECEIVED, | ||
| 12 | DISPLAY_ERROR | ||
| 13 | } display_state_t; | ||
| 14 | |||
| 15 | typedef enum { | ||
| 16 | DISPLAY_QR_WIFI, | ||
| 17 | DISPLAY_QR_PORTAL | ||
| 18 | } display_qr_mode_t; | ||
| 19 | |||
| 20 | esp_err_t display_init(void); | ||
| 21 | void display_set_state(display_state_t state); | ||
| 22 | void display_update(const char *ap_ssid, int active_clients, | ||
| 23 | uint64_t wallet_balance, const char *portal_url); | ||
| 24 | void display_render_text(int x, int y, const char *text, uint16_t fg, uint16_t bg, int scale); | ||
| 25 | void display_render_qr(const char *text); | ||
| 26 | |||
| 27 | #endif | ||
diff --git a/main/font.c b/main/font.c new file mode 100644 index 0000000..b23928f --- /dev/null +++ b/main/font.c | |||
| @@ -0,0 +1,132 @@ | |||
| 1 | #include "font.h" | ||
| 2 | |||
| 3 | const uint8_t font8x8_basic[128][8] = { | ||
| 4 | {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 5 | {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 6 | {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 7 | {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 8 | {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 9 | {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 10 | {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 11 | {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 12 | {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 13 | {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 14 | {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 15 | {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 16 | {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 17 | {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 18 | {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 19 | {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 20 | {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 21 | {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 22 | {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 23 | {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 24 | {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 25 | {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 26 | {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 27 | {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 28 | {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 29 | {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 30 | {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 31 | {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 32 | {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 33 | {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 34 | {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 35 | {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 36 | {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 37 | {0x18,0x18,0x18,0x18,0x18,0x00,0x18,0x00}, | ||
| 38 | {0x66,0x66,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 39 | {0x66,0xFF,0x66,0x66,0xFF,0x66,0x00,0x00}, | ||
| 40 | {0x18,0x3E,0x58,0x3C,0x1A,0x7C,0x18,0x00}, | ||
| 41 | {0x62,0x66,0x0C,0x18,0x30,0x66,0x46,0x00}, | ||
| 42 | {0x3C,0x66,0x3C,0x38,0x67,0x66,0x3F,0x00}, | ||
| 43 | {0x18,0x18,0x30,0x00,0x00,0x00,0x00,0x00}, | ||
| 44 | {0x0C,0x18,0x30,0x30,0x30,0x18,0x0C,0x00}, | ||
| 45 | {0x30,0x18,0x0C,0x0C,0x0C,0x18,0x30,0x00}, | ||
| 46 | {0x00,0x66,0x3C,0xFF,0x3C,0x66,0x00,0x00}, | ||
| 47 | {0x00,0x18,0x18,0x7E,0x18,0x18,0x00,0x00}, | ||
| 48 | {0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x30}, | ||
| 49 | {0x00,0x00,0x00,0x7E,0x00,0x00,0x00,0x00}, | ||
| 50 | {0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x00}, | ||
| 51 | {0x06,0x0C,0x18,0x30,0x60,0xC0,0x80,0x00}, | ||
| 52 | {0x3C,0x66,0x6E,0x7E,0x76,0x66,0x3C,0x00}, | ||
| 53 | {0x18,0x38,0x18,0x18,0x18,0x18,0x7E,0x00}, | ||
| 54 | {0x3C,0x66,0x06,0x1C,0x30,0x60,0x7E,0x00}, | ||
| 55 | {0x3C,0x66,0x06,0x1C,0x06,0x66,0x3C,0x00}, | ||
| 56 | {0x1C,0x3C,0x6C,0x6C,0x7E,0x0C,0x0C,0x00}, | ||
| 57 | {0x7E,0x60,0x7C,0x06,0x06,0x66,0x3C,0x00}, | ||
| 58 | {0x1C,0x30,0x60,0x7C,0x66,0x66,0x3C,0x00}, | ||
| 59 | {0x7E,0x06,0x0C,0x18,0x30,0x30,0x30,0x00}, | ||
| 60 | {0x3C,0x66,0x66,0x3C,0x66,0x66,0x3C,0x00}, | ||
| 61 | {0x3C,0x66,0x66,0x3E,0x06,0x0C,0x38,0x00}, | ||
| 62 | {0x00,0x00,0x18,0x18,0x00,0x18,0x18,0x00}, | ||
| 63 | {0x00,0x00,0x18,0x18,0x00,0x18,0x18,0x30}, | ||
| 64 | {0x0C,0x18,0x30,0x60,0x30,0x18,0x0C,0x00}, | ||
| 65 | {0x00,0x00,0x7E,0x00,0x7E,0x00,0x00,0x00}, | ||
| 66 | {0x30,0x18,0x0C,0x06,0x0C,0x18,0x30,0x00}, | ||
| 67 | {0x3C,0x66,0x0C,0x18,0x18,0x00,0x18,0x00}, | ||
| 68 | {0x3C,0x66,0x6E,0x6A,0x6E,0x60,0x3C,0x00}, | ||
| 69 | {0x3C,0x66,0x66,0x7E,0x66,0x66,0x66,0x00}, | ||
| 70 | {0x7C,0x66,0x66,0x7C,0x66,0x66,0x7C,0x00}, | ||
| 71 | {0x3C,0x66,0x60,0x60,0x60,0x66,0x3C,0x00}, | ||
| 72 | {0x78,0x6C,0x66,0x66,0x66,0x6C,0x78,0x00}, | ||
| 73 | {0x7E,0x60,0x60,0x7C,0x60,0x60,0x7E,0x00}, | ||
| 74 | {0x7E,0x60,0x60,0x7C,0x60,0x60,0x60,0x00}, | ||
| 75 | {0x3C,0x66,0x60,0x6E,0x66,0x66,0x3C,0x00}, | ||
| 76 | {0x66,0x66,0x66,0x7E,0x66,0x66,0x66,0x00}, | ||
| 77 | {0x3C,0x18,0x18,0x18,0x18,0x18,0x3C,0x00}, | ||
| 78 | {0x1E,0x0C,0x0C,0x0C,0x0C,0x6C,0x38,0x00}, | ||
| 79 | {0x66,0x6C,0x78,0x70,0x78,0x6C,0x66,0x00}, | ||
| 80 | {0x60,0x60,0x60,0x60,0x60,0x60,0x7E,0x00}, | ||
| 81 | {0x63,0x77,0x7F,0x6B,0x63,0x63,0x63,0x00}, | ||
| 82 | {0x66,0x76,0x7E,0x7E,0x6E,0x66,0x66,0x00}, | ||
| 83 | {0x3C,0x66,0x66,0x66,0x66,0x66,0x3C,0x00}, | ||
| 84 | {0x7C,0x66,0x66,0x7C,0x60,0x60,0x60,0x00}, | ||
| 85 | {0x3C,0x66,0x66,0x66,0x6A,0x6C,0x36,0x00}, | ||
| 86 | {0x7C,0x66,0x66,0x7C,0x6C,0x66,0x66,0x00}, | ||
| 87 | {0x3C,0x66,0x60,0x3C,0x06,0x66,0x3C,0x00}, | ||
| 88 | {0x7E,0x18,0x18,0x18,0x18,0x18,0x18,0x00}, | ||
| 89 | {0x66,0x66,0x66,0x66,0x66,0x66,0x3C,0x00}, | ||
| 90 | {0x66,0x66,0x66,0x66,0x66,0x3C,0x18,0x00}, | ||
| 91 | {0x63,0x63,0x63,0x6B,0x7F,0x77,0x63,0x00}, | ||
| 92 | {0x66,0x66,0x3C,0x18,0x3C,0x66,0x66,0x00}, | ||
| 93 | {0x66,0x66,0x66,0x3C,0x18,0x18,0x18,0x00}, | ||
| 94 | {0x7E,0x06,0x0C,0x18,0x30,0x60,0x7E,0x00}, | ||
| 95 | {0x3C,0x30,0x30,0x30,0x30,0x30,0x3C,0x00}, | ||
| 96 | {0xC0,0x60,0x30,0x18,0x0C,0x06,0x03,0x00}, | ||
| 97 | {0x3C,0x0C,0x0C,0x0C,0x0C,0x0C,0x3C,0x00}, | ||
| 98 | {0x18,0x3C,0x66,0x00,0x00,0x00,0x00,0x00}, | ||
| 99 | {0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0x00}, | ||
| 100 | {0x18,0x18,0x0C,0x00,0x00,0x00,0x00,0x00}, | ||
| 101 | {0x00,0x00,0x3C,0x06,0x3E,0x66,0x3E,0x00}, | ||
| 102 | {0x60,0x60,0x7C,0x66,0x66,0x66,0x7C,0x00}, | ||
| 103 | {0x00,0x00,0x3C,0x66,0x60,0x66,0x3C,0x00}, | ||
| 104 | {0x06,0x06,0x3E,0x66,0x66,0x66,0x3E,0x00}, | ||
| 105 | {0x00,0x00,0x3C,0x66,0x7E,0x60,0x3C,0x00}, | ||
| 106 | {0x1C,0x36,0x30,0x7C,0x30,0x30,0x30,0x00}, | ||
| 107 | {0x00,0x00,0x3E,0x66,0x66,0x3E,0x06,0x3C}, | ||
| 108 | {0x60,0x60,0x7C,0x66,0x66,0x66,0x66,0x00}, | ||
| 109 | {0x18,0x00,0x38,0x18,0x18,0x18,0x3C,0x00}, | ||
| 110 | {0x0C,0x00,0x1C,0x0C,0x0C,0x0C,0x6C,0x38}, | ||
| 111 | {0x60,0x60,0x66,0x6C,0x78,0x6C,0x66,0x00}, | ||
| 112 | {0x38,0x18,0x18,0x18,0x18,0x18,0x3C,0x00}, | ||
| 113 | {0x00,0x00,0xEC,0xFE,0xD6,0xD6,0xD6,0x00}, | ||
| 114 | {0x00,0x00,0x7C,0x66,0x66,0x66,0x66,0x00}, | ||
| 115 | {0x00,0x00,0x3C,0x66,0x66,0x66,0x3C,0x00}, | ||
| 116 | {0x00,0x00,0x7C,0x66,0x66,0x7C,0x60,0x60}, | ||
| 117 | {0x00,0x00,0x3E,0x66,0x66,0x3E,0x06,0x06}, | ||
| 118 | {0x00,0x00,0x7C,0x66,0x60,0x60,0x60,0x00}, | ||
| 119 | {0x00,0x00,0x3E,0x60,0x3C,0x06,0x7C,0x00}, | ||
| 120 | {0x30,0x30,0x7C,0x30,0x30,0x36,0x1C,0x00}, | ||
| 121 | {0x00,0x00,0x66,0x66,0x66,0x66,0x3E,0x00}, | ||
| 122 | {0x00,0x00,0x66,0x66,0x66,0x3C,0x18,0x00}, | ||
| 123 | {0x00,0x00,0xD6,0xD6,0xD6,0xFE,0x6C,0x00}, | ||
| 124 | {0x00,0x00,0x66,0x3C,0x18,0x3C,0x66,0x00}, | ||
| 125 | {0x00,0x00,0x66,0x66,0x66,0x3E,0x06,0x3C}, | ||
| 126 | {0x00,0x00,0x7E,0x0C,0x18,0x30,0x7E,0x00}, | ||
| 127 | {0x0C,0x18,0x18,0x70,0x18,0x18,0x0C,0x00}, | ||
| 128 | {0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x00}, | ||
| 129 | {0x30,0x18,0x18,0x0E,0x18,0x18,0x30,0x00}, | ||
| 130 | {0x00,0x00,0x31,0x6B,0x46,0x00,0x00,0x00}, | ||
| 131 | {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, | ||
| 132 | }; | ||
diff --git a/main/font.h b/main/font.h new file mode 100644 index 0000000..8ef1955 --- /dev/null +++ b/main/font.h | |||
| @@ -0,0 +1,11 @@ | |||
| 1 | #ifndef FONT_H | ||
| 2 | #define FONT_H | ||
| 3 | |||
| 4 | #include <stdint.h> | ||
| 5 | |||
| 6 | #define FONT_GLYPH_W 8 | ||
| 7 | #define FONT_GLYPH_H 8 | ||
| 8 | |||
| 9 | extern const uint8_t font8x8_basic[128][8]; | ||
| 10 | |||
| 11 | #endif | ||
diff --git a/main/local_relay.c b/main/local_relay.c new file mode 100644 index 0000000..d7b1ff8 --- /dev/null +++ b/main/local_relay.c | |||
| @@ -0,0 +1,140 @@ | |||
| 1 | #include "local_relay.h" | ||
| 2 | #include "storage_engine.h" | ||
| 3 | #include "sub_manager.h" | ||
| 4 | #include "rate_limiter.h" | ||
| 5 | #include "ws_server.h" | ||
| 6 | #include "relay_core.h" | ||
| 7 | #include "router.h" | ||
| 8 | #include "handlers.h" | ||
| 9 | #include "broadcaster.h" | ||
| 10 | #include "flash_monitor.h" | ||
| 11 | #include "cJSON.h" | ||
| 12 | #include "esp_log.h" | ||
| 13 | #include "freertos/FreeRTOS.h" | ||
| 14 | #include "freertos/task.h" | ||
| 15 | #include <string.h> | ||
| 16 | |||
| 17 | static const char *TAG = "local_relay"; | ||
| 18 | |||
| 19 | #define LOCAL_RELAY_PORT 4869 | ||
| 20 | #define LOCAL_RELAY_TTL_SEC (21 * 24 * 3600) | ||
| 21 | |||
| 22 | static relay_ctx_t s_relay_ctx; | ||
| 23 | static storage_engine_t s_storage; | ||
| 24 | static sub_manager_t s_sub_mgr; | ||
| 25 | static rate_limiter_t s_rate_limiter; | ||
| 26 | static bool s_initialized = false; | ||
| 27 | |||
| 28 | relay_ctx_t g_relay_ctx; | ||
| 29 | |||
| 30 | static void on_ws_message(int fd, const char *data, size_t len) | ||
| 31 | { | ||
| 32 | router_dispatch(&g_relay_ctx, fd, data, len); | ||
| 33 | } | ||
| 34 | |||
| 35 | static void on_ws_disconnect(int fd) | ||
| 36 | { | ||
| 37 | if (g_relay_ctx.sub_manager) { | ||
| 38 | sub_manager_remove_all(g_relay_ctx.sub_manager, fd); | ||
| 39 | } | ||
| 40 | } | ||
| 41 | |||
| 42 | esp_err_t local_relay_init(void) | ||
| 43 | { | ||
| 44 | memset(&s_relay_ctx, 0, sizeof(s_relay_ctx)); | ||
| 45 | memset(&s_storage, 0, sizeof(s_storage)); | ||
| 46 | memset(&s_sub_mgr, 0, sizeof(s_sub_mgr)); | ||
| 47 | memset(&s_rate_limiter, 0, sizeof(s_rate_limiter)); | ||
| 48 | |||
| 49 | esp_err_t ret = storage_init(&s_storage, LOCAL_RELAY_TTL_SEC); | ||
| 50 | if (ret != ESP_OK) { | ||
| 51 | ESP_LOGE(TAG, "Failed to init storage: %s", esp_err_to_name(ret)); | ||
| 52 | return ret; | ||
| 53 | } | ||
| 54 | |||
| 55 | ret = sub_manager_init(&s_sub_mgr); | ||
| 56 | if (ret != ESP_OK) { | ||
| 57 | storage_destroy(&s_storage); | ||
| 58 | return ret; | ||
| 59 | } | ||
| 60 | |||
| 61 | rate_config_t rl_cfg = { | ||
| 62 | .events_per_minute = 60, | ||
| 63 | .reqs_per_minute = 30, | ||
| 64 | }; | ||
| 65 | rate_limiter_init(&s_rate_limiter, &rl_cfg); | ||
| 66 | |||
| 67 | s_relay_ctx.storage = &s_storage; | ||
| 68 | s_relay_ctx.sub_manager = &s_sub_mgr; | ||
| 69 | s_relay_ctx.rate_limiter = &s_rate_limiter; | ||
| 70 | s_relay_ctx.config.port = LOCAL_RELAY_PORT; | ||
| 71 | s_relay_ctx.config.max_event_age_sec = LOCAL_RELAY_TTL_SEC; | ||
| 72 | s_relay_ctx.config.max_subs_per_conn = 8; | ||
| 73 | s_relay_ctx.config.max_filters_per_sub = 4; | ||
| 74 | s_relay_ctx.config.max_future_sec = 600; | ||
| 75 | |||
| 76 | memcpy(&g_relay_ctx, &s_relay_ctx, sizeof(relay_ctx_t)); | ||
| 77 | |||
| 78 | storage_start_cleanup_task(&s_storage); | ||
| 79 | |||
| 80 | s_initialized = true; | ||
| 81 | ESP_LOGI(TAG, "Local relay initialized (port=%d, TTL=%ds)", LOCAL_RELAY_PORT, LOCAL_RELAY_TTL_SEC); | ||
| 82 | return ESP_OK; | ||
| 83 | } | ||
| 84 | |||
| 85 | void local_relay_start(void) | ||
| 86 | { | ||
| 87 | if (!s_initialized) { | ||
| 88 | ESP_LOGE(TAG, "Not initialized"); | ||
| 89 | return; | ||
| 90 | } | ||
| 91 | |||
| 92 | esp_err_t ret = ws_server_init(&s_relay_ctx.ws_server, LOCAL_RELAY_PORT, on_ws_message); | ||
| 93 | if (ret != ESP_OK) { | ||
| 94 | ESP_LOGE(TAG, "Failed to start WS server: %s", esp_err_to_name(ret)); | ||
| 95 | return; | ||
| 96 | } | ||
| 97 | |||
| 98 | ws_server_set_disconnect_cb(on_ws_disconnect); | ||
| 99 | memcpy(&g_relay_ctx, &s_relay_ctx, sizeof(relay_ctx_t)); | ||
| 100 | |||
| 101 | ESP_LOGI(TAG, "Local relay listening on port %d", LOCAL_RELAY_PORT); | ||
| 102 | } | ||
| 103 | |||
| 104 | void local_relay_stop(void) | ||
| 105 | { | ||
| 106 | if (!s_initialized) return; | ||
| 107 | ws_server_stop(&s_relay_ctx.ws_server); | ||
| 108 | ESP_LOGI(TAG, "Local relay stopped"); | ||
| 109 | } | ||
| 110 | |||
| 111 | esp_err_t local_relay_publish(const char *event_json, size_t event_len) | ||
| 112 | { | ||
| 113 | if (!s_initialized || !event_json) return ESP_ERR_INVALID_STATE; | ||
| 114 | |||
| 115 | storage_error_t err = storage_save_event_json(s_relay_ctx.storage, event_json, event_len); | ||
| 116 | if (err == STORAGE_ERR_DUPLICATE) { | ||
| 117 | ESP_LOGD(TAG, "Duplicate event, skipping broadcast"); | ||
| 118 | return ESP_OK; | ||
| 119 | } | ||
| 120 | if (err != STORAGE_OK) { | ||
| 121 | ESP_LOGW(TAG, "Failed to save event: %d", err); | ||
| 122 | return ESP_FAIL; | ||
| 123 | } | ||
| 124 | |||
| 125 | cJSON *obj = cJSON_ParseWithLength(event_json, event_len); | ||
| 126 | if (!obj) return ESP_OK; | ||
| 127 | |||
| 128 | cJSON *pk = cJSON_GetObjectItem(obj, "pubkey"); | ||
| 129 | cJSON *kind = cJSON_GetObjectItem(obj, "kind"); | ||
| 130 | cJSON *ca = cJSON_GetObjectItem(obj, "created_at"); | ||
| 131 | |||
| 132 | if (pk && kind && ca) { | ||
| 133 | broadcaster_fanout_json(&s_relay_ctx, event_json, event_len, | ||
| 134 | kind->valueint, pk->valuestring, | ||
| 135 | (uint64_t)ca->valuedouble); | ||
| 136 | } | ||
| 137 | cJSON_Delete(obj); | ||
| 138 | |||
| 139 | return ESP_OK; | ||
| 140 | } | ||
diff --git a/main/local_relay.h b/main/local_relay.h new file mode 100644 index 0000000..8ae1653 --- /dev/null +++ b/main/local_relay.h | |||
| @@ -0,0 +1,13 @@ | |||
| 1 | #ifndef LOCAL_RELAY_H | ||
| 2 | #define LOCAL_RELAY_H | ||
| 3 | |||
| 4 | #include "esp_err.h" | ||
| 5 | #include <stddef.h> | ||
| 6 | |||
| 7 | esp_err_t local_relay_init(void); | ||
| 8 | void local_relay_start(void); | ||
| 9 | void local_relay_stop(void); | ||
| 10 | |||
| 11 | esp_err_t local_relay_publish(const char *event_json, size_t event_len); | ||
| 12 | |||
| 13 | #endif | ||
diff --git a/main/relay_selector.c b/main/relay_selector.c new file mode 100644 index 0000000..7c443fe --- /dev/null +++ b/main/relay_selector.c | |||
| @@ -0,0 +1,270 @@ | |||
| 1 | #include "relay_selector.h" | ||
| 2 | #include "config.h" | ||
| 3 | #include "esp_log.h" | ||
| 4 | #include "esp_http_client.h" | ||
| 5 | #include "esp_tls.h" | ||
| 6 | #include "esp_crt_bundle.h" | ||
| 7 | #include "esp_timer.h" | ||
| 8 | #include "cJSON.h" | ||
| 9 | #include "freertos/FreeRTOS.h" | ||
| 10 | #include "freertos/semphr.h" | ||
| 11 | #include <string.h> | ||
| 12 | #include <stdlib.h> | ||
| 13 | |||
| 14 | static const char *TAG = "relay_sel"; | ||
| 15 | static const int MAX_REDIRECTS = 3; | ||
| 16 | static const int PROBE_TIMEOUT_MS = 5000; | ||
| 17 | static const int MAX_FAILURES = 3; | ||
| 18 | |||
| 19 | static int compare_relays(const void *a, const void *b) | ||
| 20 | { | ||
| 21 | const relay_info_t *ra = (const relay_info_t *)a; | ||
| 22 | const relay_info_t *rb = (const relay_info_t *)b; | ||
| 23 | |||
| 24 | if (ra->alive && !rb->alive) return -1; | ||
| 25 | if (!ra->alive && rb->alive) return 1; | ||
| 26 | |||
| 27 | int score_a = (ra->supports_nip77 ? 1000 : 0) - ra->consecutive_failures * 100; | ||
| 28 | int score_b = (rb->supports_nip77 ? 1000 : 0) - rb->consecutive_failures * 100; | ||
| 29 | if (score_a != score_b) return score_b - score_a; | ||
| 30 | |||
| 31 | return (int)ra->latency_ms - (int)rb->latency_ms; | ||
| 32 | } | ||
| 33 | |||
| 34 | esp_err_t relay_selector_init(relay_selector_t *sel) | ||
| 35 | { | ||
| 36 | memset(sel, 0, sizeof(relay_selector_t)); | ||
| 37 | sel->primary_idx = -1; | ||
| 38 | sel->fallback_idx = -1; | ||
| 39 | sel->lock = xSemaphoreCreateMutex(); | ||
| 40 | if (!sel->lock) return ESP_ERR_NO_MEM; | ||
| 41 | return ESP_OK; | ||
| 42 | } | ||
| 43 | |||
| 44 | void relay_selector_destroy(relay_selector_t *sel) | ||
| 45 | { | ||
| 46 | if (sel->lock) { vSemaphoreDelete(sel->lock); sel->lock = NULL; } | ||
| 47 | } | ||
| 48 | |||
| 49 | static esp_err_t probe_nip11(const char *wss_url, relay_info_t *info) | ||
| 50 | { | ||
| 51 | char http_url[192]; | ||
| 52 | const char *host_start = wss_url; | ||
| 53 | if (strncmp(wss_url, "wss://", 6) == 0) host_start = wss_url + 6; | ||
| 54 | else if (strncmp(wss_url, "ws://", 5) == 0) host_start = wss_url + 5; | ||
| 55 | |||
| 56 | snprintf(http_url, sizeof(http_url), "https://%s/", host_start); | ||
| 57 | |||
| 58 | char response[4096]; | ||
| 59 | int total_len = 0; | ||
| 60 | |||
| 61 | esp_http_client_config_t http_cfg = { | ||
| 62 | .url = http_url, | ||
| 63 | .method = HTTP_METHOD_GET, | ||
| 64 | .timeout_ms = PROBE_TIMEOUT_MS, | ||
| 65 | .crt_bundle_attach = esp_crt_bundle_attach, | ||
| 66 | .max_redirection_count = MAX_REDIRECTS, | ||
| 67 | .disable_auto_redirect = false, | ||
| 68 | }; | ||
| 69 | |||
| 70 | esp_http_client_handle_t client = esp_http_client_init(&http_cfg); | ||
| 71 | if (!client) return ESP_FAIL; | ||
| 72 | |||
| 73 | esp_http_client_set_header(client, "Accept", "application/nostr+json"); | ||
| 74 | |||
| 75 | int64_t start_time = esp_timer_get_time(); | ||
| 76 | esp_err_t err = esp_http_client_open(client, 0); | ||
| 77 | if (err != ESP_OK) { | ||
| 78 | esp_http_client_cleanup(client); | ||
| 79 | info->alive = false; | ||
| 80 | return err; | ||
| 81 | } | ||
| 82 | |||
| 83 | int content_length = esp_http_client_fetch_headers(client); | ||
| 84 | int status = esp_http_client_get_status_code(client); | ||
| 85 | |||
| 86 | if (status != 200) { | ||
| 87 | esp_http_client_close(client); | ||
| 88 | esp_http_client_cleanup(client); | ||
| 89 | info->alive = (status > 0); | ||
| 90 | return ESP_FAIL; | ||
| 91 | } | ||
| 92 | |||
| 93 | int max_read = content_length > 0 ? content_length : (int)sizeof(response) - 1; | ||
| 94 | if (max_read > (int)sizeof(response) - 1) max_read = (int)sizeof(response) - 1; | ||
| 95 | |||
| 96 | while (total_len < max_read) { | ||
| 97 | int read_len = esp_http_client_read(client, response + total_len, | ||
| 98 | max_read - total_len); | ||
| 99 | if (read_len <= 0) break; | ||
| 100 | total_len += read_len; | ||
| 101 | } | ||
| 102 | response[total_len] = '\0'; | ||
| 103 | |||
| 104 | int64_t end_time = esp_timer_get_time(); | ||
| 105 | info->latency_ms = (uint32_t)((end_time - start_time) / 1000); | ||
| 106 | |||
| 107 | esp_http_client_close(client); | ||
| 108 | esp_http_client_cleanup(client); | ||
| 109 | |||
| 110 | info->alive = true; | ||
| 111 | info->consecutive_failures = 0; | ||
| 112 | |||
| 113 | cJSON *root = cJSON_Parse(response); | ||
| 114 | if (!root) return ESP_OK; | ||
| 115 | |||
| 116 | cJSON *name = cJSON_GetObjectItem(root, "name"); | ||
| 117 | if (name && cJSON_IsString(name)) | ||
| 118 | strncpy(info->name, name->valuestring, sizeof(info->name) - 1); | ||
| 119 | |||
| 120 | cJSON *nips = cJSON_GetObjectItem(root, "supported_nips"); | ||
| 121 | if (nips && cJSON_IsArray(nips)) { | ||
| 122 | info->nips_count = cJSON_GetArraySize(nips); | ||
| 123 | if (info->nips_count > 32) info->nips_count = 32; | ||
| 124 | info->supports_nip77 = false; | ||
| 125 | for (size_t i = 0; i < info->nips_count; i++) { | ||
| 126 | cJSON *nip = cJSON_GetArrayItem(nips, i); | ||
| 127 | if (nip) { | ||
| 128 | info->supported_nips[i] = (uint8_t)nip->valueint; | ||
| 129 | if (nip->valueint == 77) info->supports_nip77 = true; | ||
| 130 | } | ||
| 131 | } | ||
| 132 | } | ||
| 133 | |||
| 134 | cJSON_Delete(root); | ||
| 135 | return ESP_OK; | ||
| 136 | } | ||
| 137 | |||
| 138 | static void select_primary_fallback(relay_selector_t *sel) | ||
| 139 | { | ||
| 140 | relay_info_t sorted[RELAY_SELECTOR_MAX_RELAYS]; | ||
| 141 | size_t sorted_count = 0; | ||
| 142 | |||
| 143 | for (size_t i = 0; i < sel->count; i++) { | ||
| 144 | if (sel->relays[i].alive) { | ||
| 145 | sorted[sorted_count++] = sel->relays[i]; | ||
| 146 | } | ||
| 147 | } | ||
| 148 | |||
| 149 | if (sorted_count == 0) { | ||
| 150 | sel->primary_idx = -1; | ||
| 151 | sel->fallback_idx = -1; | ||
| 152 | return; | ||
| 153 | } | ||
| 154 | |||
| 155 | qsort(sorted, sorted_count, sizeof(relay_info_t), compare_relays); | ||
| 156 | |||
| 157 | for (size_t i = 0; i < sel->count; i++) { | ||
| 158 | if (strcmp(sel->relays[i].url, sorted[0].url) == 0) { | ||
| 159 | sel->primary_idx = (int)i; | ||
| 160 | break; | ||
| 161 | } | ||
| 162 | } | ||
| 163 | |||
| 164 | if (sorted_count > 1) { | ||
| 165 | for (size_t i = 0; i < sel->count; i++) { | ||
| 166 | if (strcmp(sel->relays[i].url, sorted[1].url) == 0) { | ||
| 167 | sel->fallback_idx = (int)i; | ||
| 168 | break; | ||
| 169 | } | ||
| 170 | } | ||
| 171 | } else { | ||
| 172 | sel->fallback_idx = -1; | ||
| 173 | } | ||
| 174 | |||
| 175 | ESP_LOGI(TAG, "Primary: %s (latency=%lums, NIP-77=%s)", | ||
| 176 | sel->primary_idx >= 0 ? sel->relays[sel->primary_idx].url : "none", | ||
| 177 | sel->primary_idx >= 0 ? (unsigned long)sel->relays[sel->primary_idx].latency_ms : 0, | ||
| 178 | sel->primary_idx >= 0 && sel->relays[sel->primary_idx].supports_nip77 ? "yes" : "no"); | ||
| 179 | } | ||
| 180 | |||
| 181 | esp_err_t relay_selector_probe_all(relay_selector_t *sel) | ||
| 182 | { | ||
| 183 | xSemaphoreTake(sel->lock, portMAX_DELAY); | ||
| 184 | |||
| 185 | ESP_LOGI(TAG, "Probing %zu relays via NIP-11...", sel->count); | ||
| 186 | |||
| 187 | for (size_t i = 0; i < sel->count; i++) { | ||
| 188 | ESP_LOGI(TAG, "Probing %s...", sel->relays[i].url); | ||
| 189 | esp_err_t err = probe_nip11(sel->relays[i].url, &sel->relays[i]); | ||
| 190 | if (err != ESP_OK) { | ||
| 191 | sel->relays[i].consecutive_failures++; | ||
| 192 | ESP_LOGW(TAG, "Probe failed for %s (failures=%d)", | ||
| 193 | sel->relays[i].url, sel->relays[i].consecutive_failures); | ||
| 194 | if (sel->relays[i].consecutive_failures >= MAX_FAILURES) { | ||
| 195 | sel->relays[i].alive = false; | ||
| 196 | } | ||
| 197 | } | ||
| 198 | vTaskDelay(pdMS_TO_TICKS(100)); | ||
| 199 | } | ||
| 200 | |||
| 201 | select_primary_fallback(sel); | ||
| 202 | |||
| 203 | int64_t now = (int64_t)(xTaskGetTickCount() / configTICK_RATE_HZ); | ||
| 204 | sel->last_full_probe = (uint32_t)now; | ||
| 205 | |||
| 206 | xSemaphoreGive(sel->lock); | ||
| 207 | return ESP_OK; | ||
| 208 | } | ||
| 209 | |||
| 210 | const relay_info_t *relay_selector_get_primary(relay_selector_t *sel) | ||
| 211 | { | ||
| 212 | if (sel->primary_idx < 0 || sel->primary_idx >= (int)sel->count) return NULL; | ||
| 213 | return &sel->relays[sel->primary_idx]; | ||
| 214 | } | ||
| 215 | |||
| 216 | const relay_info_t *relay_selector_get_fallback(relay_selector_t *sel, int idx) | ||
| 217 | { | ||
| 218 | if (idx == 0) { | ||
| 219 | if (sel->fallback_idx < 0) return NULL; | ||
| 220 | return &sel->relays[sel->fallback_idx]; | ||
| 221 | } | ||
| 222 | for (size_t i = 0; i < sel->count; i++) { | ||
| 223 | if ((int)i != sel->primary_idx && (int)i != sel->fallback_idx) { | ||
| 224 | if (sel->relays[i].alive) { | ||
| 225 | if (idx <= 0) return &sel->relays[i]; | ||
| 226 | idx--; | ||
| 227 | } | ||
| 228 | } | ||
| 229 | } | ||
| 230 | return NULL; | ||
| 231 | } | ||
| 232 | |||
| 233 | void relay_selector_report_disconnect(relay_selector_t *sel, const char *url) | ||
| 234 | { | ||
| 235 | xSemaphoreTake(sel->lock, portMAX_DELAY); | ||
| 236 | for (size_t i = 0; i < sel->count; i++) { | ||
| 237 | if (strcmp(sel->relays[i].url, url) == 0) { | ||
| 238 | sel->relays[i].consecutive_failures++; | ||
| 239 | ESP_LOGW(TAG, "Disconnect reported for %s (failures=%d)", | ||
| 240 | url, sel->relays[i].consecutive_failures); | ||
| 241 | if (sel->relays[i].consecutive_failures >= MAX_FAILURES) { | ||
| 242 | sel->relays[i].alive = false; | ||
| 243 | ESP_LOGW(TAG, "Relay %s marked dead, triggering re-probe", url); | ||
| 244 | select_primary_fallback(sel); | ||
| 245 | } | ||
| 246 | break; | ||
| 247 | } | ||
| 248 | } | ||
| 249 | xSemaphoreGive(sel->lock); | ||
| 250 | } | ||
| 251 | |||
| 252 | esp_err_t relay_selector_seed_from_config(relay_selector_t *sel) | ||
| 253 | { | ||
| 254 | const tollgate_config_t *cfg = tollgate_config_get(); | ||
| 255 | xSemaphoreTake(sel->lock, portMAX_DELAY); | ||
| 256 | |||
| 257 | sel->count = 0; | ||
| 258 | for (int i = 0; i < cfg->nostr_seed_relay_count && sel->count < RELAY_SELECTOR_MAX_RELAYS; i++) { | ||
| 259 | if (cfg->nostr_seed_relays[i][0] != '\0') { | ||
| 260 | strncpy(sel->relays[sel->count].url, cfg->nostr_seed_relays[i], | ||
| 261 | RELAY_SELECTOR_URL_LEN - 1); | ||
| 262 | sel->relays[sel->count].alive = true; | ||
| 263 | sel->count++; | ||
| 264 | } | ||
| 265 | } | ||
| 266 | |||
| 267 | xSemaphoreGive(sel->lock); | ||
| 268 | ESP_LOGI(TAG, "Seeded %zu relays from config", sel->count); | ||
| 269 | return ESP_OK; | ||
| 270 | } | ||
diff --git a/main/relay_selector.h b/main/relay_selector.h new file mode 100644 index 0000000..4403944 --- /dev/null +++ b/main/relay_selector.h | |||
| @@ -0,0 +1,46 @@ | |||
| 1 | #ifndef RELAY_SELECTOR_H | ||
| 2 | #define RELAY_SELECTOR_H | ||
| 3 | |||
| 4 | #include "esp_err.h" | ||
| 5 | #include "freertos/FreeRTOS.h" | ||
| 6 | #include "freertos/semphr.h" | ||
| 7 | #include <stdbool.h> | ||
| 8 | #include <stdint.h> | ||
| 9 | |||
| 10 | #define RELAY_SELECTOR_MAX_RELAYS 8 | ||
| 11 | #define RELAY_SELECTOR_URL_LEN 128 | ||
| 12 | |||
| 13 | typedef struct { | ||
| 14 | char url[RELAY_SELECTOR_URL_LEN]; | ||
| 15 | char name[64]; | ||
| 16 | uint32_t latency_ms; | ||
| 17 | bool supports_nip77; | ||
| 18 | bool alive; | ||
| 19 | int consecutive_failures; | ||
| 20 | uint32_t last_probe_time; | ||
| 21 | uint8_t supported_nips[32]; | ||
| 22 | size_t nips_count; | ||
| 23 | } relay_info_t; | ||
| 24 | |||
| 25 | typedef struct { | ||
| 26 | relay_info_t relays[RELAY_SELECTOR_MAX_RELAYS]; | ||
| 27 | size_t count; | ||
| 28 | int primary_idx; | ||
| 29 | int fallback_idx; | ||
| 30 | uint32_t last_full_probe; | ||
| 31 | SemaphoreHandle_t lock; | ||
| 32 | } relay_selector_t; | ||
| 33 | |||
| 34 | esp_err_t relay_selector_init(relay_selector_t *sel); | ||
| 35 | void relay_selector_destroy(relay_selector_t *sel); | ||
| 36 | |||
| 37 | esp_err_t relay_selector_probe_all(relay_selector_t *sel); | ||
| 38 | |||
| 39 | const relay_info_t *relay_selector_get_primary(relay_selector_t *sel); | ||
| 40 | const relay_info_t *relay_selector_get_fallback(relay_selector_t *sel, int idx); | ||
| 41 | |||
| 42 | void relay_selector_report_disconnect(relay_selector_t *sel, const char *url); | ||
| 43 | |||
| 44 | esp_err_t relay_selector_seed_from_config(relay_selector_t *sel); | ||
| 45 | |||
| 46 | #endif | ||
diff --git a/main/sync_manager.c b/main/sync_manager.c new file mode 100644 index 0000000..1766b2b --- /dev/null +++ b/main/sync_manager.c | |||
| @@ -0,0 +1,399 @@ | |||
| 1 | #include "sync_manager.h" | ||
| 2 | #include "local_relay.h" | ||
| 3 | #include "storage_engine.h" | ||
| 4 | #include "relay_core.h" | ||
| 5 | #include "config.h" | ||
| 6 | #include "nostr_event.h" | ||
| 7 | #include "esp_log.h" | ||
| 8 | #include "esp_tls.h" | ||
| 9 | #include "esp_crt_bundle.h" | ||
| 10 | #include "cJSON.h" | ||
| 11 | #include "freertos/FreeRTOS.h" | ||
| 12 | #include "freertos/task.h" | ||
| 13 | #include "freertos/timers.h" | ||
| 14 | #include <string.h> | ||
| 15 | #include <stdlib.h> | ||
| 16 | |||
| 17 | static const char *TAG = "sync_mgr"; | ||
| 18 | |||
| 19 | static const uint8_t WS_FIN_TEXT = 0x81; | ||
| 20 | static const uint8_t WS_FIN_CLOSE = 0x88; | ||
| 21 | |||
| 22 | static esp_err_t ws_connect(const char *wss_url, esp_tls_t **out_tls) | ||
| 23 | { | ||
| 24 | char host[128] = {0}; | ||
| 25 | int port = 443; | ||
| 26 | char path[128] = "/"; | ||
| 27 | |||
| 28 | const char *url_start = wss_url; | ||
| 29 | if (strncmp(wss_url, "wss://", 6) == 0) url_start = wss_url + 6; | ||
| 30 | |||
| 31 | const char *path_ptr = strchr(url_start, '/'); | ||
| 32 | if (path_ptr) { | ||
| 33 | size_t host_len = path_ptr - url_start; | ||
| 34 | if (host_len >= sizeof(host)) host_len = sizeof(host) - 1; | ||
| 35 | memcpy(host, url_start, host_len); | ||
| 36 | host[host_len] = '\0'; | ||
| 37 | strncpy(path, path_ptr, sizeof(path) - 1); | ||
| 38 | } else { | ||
| 39 | strncpy(host, url_start, sizeof(host) - 1); | ||
| 40 | } | ||
| 41 | |||
| 42 | char *colon = strchr(host, ':'); | ||
| 43 | if (colon) { *colon = '\0'; port = atoi(colon + 1); } | ||
| 44 | |||
| 45 | esp_tls_cfg_t tls_cfg = { .crt_bundle_attach = esp_crt_bundle_attach }; | ||
| 46 | esp_tls_t *tls = esp_tls_init(); | ||
| 47 | if (!tls) return ESP_ERR_NO_MEM; | ||
| 48 | |||
| 49 | int ret = esp_tls_conn_new_sync(host, strlen(host), port, &tls_cfg, tls); | ||
| 50 | if (ret < 0) { | ||
| 51 | esp_tls_conn_destroy(tls); | ||
| 52 | ESP_LOGW(TAG, "TLS connect failed to %s", host); | ||
| 53 | return ESP_FAIL; | ||
| 54 | } | ||
| 55 | |||
| 56 | char upgrade[512]; | ||
| 57 | snprintf(upgrade, sizeof(upgrade), | ||
| 58 | "GET %s HTTP/1.1\r\n" | ||
| 59 | "Host: %s\r\n" | ||
| 60 | "Upgrade: websocket\r\n" | ||
| 61 | "Connection: Upgrade\r\n" | ||
| 62 | "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" | ||
| 63 | "Sec-WebSocket-Version: 13\r\n" | ||
| 64 | "\r\n", path, host); | ||
| 65 | |||
| 66 | esp_tls_conn_write(tls, (const unsigned char *)upgrade, strlen(upgrade)); | ||
| 67 | |||
| 68 | char resp[1024]; | ||
| 69 | int rlen = esp_tls_conn_read(tls, (unsigned char *)resp, sizeof(resp) - 1); | ||
| 70 | if (rlen <= 0 || !strstr(resp, "101")) { | ||
| 71 | esp_tls_conn_destroy(tls); | ||
| 72 | return ESP_FAIL; | ||
| 73 | } | ||
| 74 | |||
| 75 | *out_tls = tls; | ||
| 76 | return ESP_OK; | ||
| 77 | } | ||
| 78 | |||
| 79 | static void ws_send_text(esp_tls_t *tls, const char *data, size_t len) | ||
| 80 | { | ||
| 81 | uint8_t header[10]; | ||
| 82 | int hlen = 0; | ||
| 83 | header[0] = WS_FIN_TEXT; | ||
| 84 | if (len <= 125) { header[1] = (uint8_t)len; hlen = 2; } | ||
| 85 | else if (len <= 65535) { | ||
| 86 | header[1] = 126; | ||
| 87 | header[2] = (uint8_t)((len >> 8) & 0xff); | ||
| 88 | header[3] = (uint8_t)(len & 0xff); | ||
| 89 | hlen = 4; | ||
| 90 | } else { | ||
| 91 | header[1] = 127; | ||
| 92 | for (int i = 0; i < 8; i++) | ||
| 93 | header[2 + i] = (uint8_t)((len >> (56 - i * 8)) & 0xff); | ||
| 94 | hlen = 10; | ||
| 95 | } | ||
| 96 | esp_tls_conn_write(tls, header, hlen); | ||
| 97 | esp_tls_conn_write(tls, (const unsigned char *)data, len); | ||
| 98 | } | ||
| 99 | |||
| 100 | static void ws_send_close(esp_tls_t *tls) | ||
| 101 | { | ||
| 102 | uint8_t close_frame[2] = {WS_FIN_CLOSE, 0x00}; | ||
| 103 | esp_tls_conn_write(tls, close_frame, 2); | ||
| 104 | } | ||
| 105 | |||
| 106 | static int ws_read_text(esp_tls_t *tls, char *buf, size_t buf_len) | ||
| 107 | { | ||
| 108 | uint8_t header[2]; | ||
| 109 | int rlen = esp_tls_conn_read(tls, header, 2); | ||
| 110 | if (rlen < 2) return -1; | ||
| 111 | |||
| 112 | if ((header[0] & 0x0f) == 0x08) return -1; | ||
| 113 | |||
| 114 | int payload_len = header[1] & 0x7f; | ||
| 115 | if (payload_len == 126) { | ||
| 116 | uint8_t ext[2]; | ||
| 117 | esp_tls_conn_read(tls, ext, 2); | ||
| 118 | payload_len = (ext[0] << 8) | ext[1]; | ||
| 119 | } else if (payload_len == 127) { | ||
| 120 | uint8_t ext[8]; | ||
| 121 | esp_tls_conn_read(tls, ext, 8); | ||
| 122 | payload_len = 0; | ||
| 123 | for (int i = 0; i < 8; i++) payload_len = (payload_len << 8) | ext[i]; | ||
| 124 | } | ||
| 125 | |||
| 126 | int mask_len = (header[1] & 0x80) ? 4 : 0; | ||
| 127 | uint8_t mask[4] = {0}; | ||
| 128 | if (mask_len) esp_tls_conn_read(tls, mask, 4); | ||
| 129 | |||
| 130 | if (payload_len > (int)buf_len - 1) payload_len = (int)buf_len - 1; | ||
| 131 | esp_tls_conn_read(tls, (unsigned char *)buf, payload_len); | ||
| 132 | for (int i = 0; i < payload_len; i++) buf[i] ^= mask[i % 4]; | ||
| 133 | buf[payload_len] = '\0'; | ||
| 134 | return payload_len; | ||
| 135 | } | ||
| 136 | |||
| 137 | static void get_event_ids_from_storage(char ***ids_out, uint16_t *count_out) | ||
| 138 | { | ||
| 139 | extern relay_ctx_t g_relay_ctx; | ||
| 140 | if (!g_relay_ctx.storage) { *ids_out = NULL; *count_out = 0; return; } | ||
| 141 | |||
| 142 | char **results = NULL; | ||
| 143 | uint16_t count = 0; | ||
| 144 | storage_query_events_json(g_relay_ctx.storage, -1, NULL, 5000, &results, &count); | ||
| 145 | |||
| 146 | char **ids = calloc(count, sizeof(char *)); | ||
| 147 | uint16_t id_count = 0; | ||
| 148 | |||
| 149 | for (uint16_t i = 0; i < count; i++) { | ||
| 150 | cJSON *obj = cJSON_Parse(results[i]); | ||
| 151 | if (!obj) continue; | ||
| 152 | cJSON *id = cJSON_GetObjectItem(obj, "id"); | ||
| 153 | if (id && cJSON_IsString(id)) { | ||
| 154 | ids[id_count++] = strdup(id->valuestring); | ||
| 155 | } | ||
| 156 | cJSON_Delete(obj); | ||
| 157 | } | ||
| 158 | |||
| 159 | storage_free_query_results(results, count); | ||
| 160 | *ids_out = ids; | ||
| 161 | *count_out = id_count; | ||
| 162 | } | ||
| 163 | |||
| 164 | static void free_event_ids(char **ids, uint16_t count) | ||
| 165 | { | ||
| 166 | for (uint16_t i = 0; i < count; i++) free(ids[i]); | ||
| 167 | free(ids); | ||
| 168 | } | ||
| 169 | |||
| 170 | esp_err_t sync_manager_init(sync_manager_t *mgr, relay_selector_t *selector) | ||
| 171 | { | ||
| 172 | memset(mgr, 0, sizeof(sync_manager_t)); | ||
| 173 | mgr->selector = selector; | ||
| 174 | mgr->lock = xSemaphoreCreateMutex(); | ||
| 175 | if (!mgr->lock) return ESP_ERR_NO_MEM; | ||
| 176 | return ESP_OK; | ||
| 177 | } | ||
| 178 | |||
| 179 | static void sync_task(void *arg); | ||
| 180 | |||
| 181 | void sync_manager_start(sync_manager_t *mgr) | ||
| 182 | { | ||
| 183 | mgr->running = true; | ||
| 184 | xTaskCreate(sync_task, "sync_mgr", 16384, mgr, 3, NULL); | ||
| 185 | ESP_LOGI(TAG, "Sync manager started"); | ||
| 186 | } | ||
| 187 | |||
| 188 | void sync_manager_stop(sync_manager_t *mgr) | ||
| 189 | { | ||
| 190 | mgr->running = false; | ||
| 191 | } | ||
| 192 | |||
| 193 | esp_err_t sync_manager_do_negentropy_sync(sync_manager_t *mgr) | ||
| 194 | { | ||
| 195 | if (!mgr->selector) return ESP_ERR_INVALID_STATE; | ||
| 196 | |||
| 197 | xSemaphoreTake(mgr->lock, portMAX_DELAY); | ||
| 198 | mgr->sync_in_progress = true; | ||
| 199 | xSemaphoreGive(mgr->lock); | ||
| 200 | |||
| 201 | const relay_info_t *primary = relay_selector_get_primary(mgr->selector); | ||
| 202 | if (!primary || !primary->alive) { | ||
| 203 | ESP_LOGW(TAG, "No primary relay for negentropy sync"); | ||
| 204 | xSemaphoreTake(mgr->lock, portMAX_DELAY); | ||
| 205 | mgr->sync_in_progress = false; | ||
| 206 | xSemaphoreGive(mgr->lock); | ||
| 207 | return ESP_ERR_NOT_FOUND; | ||
| 208 | } | ||
| 209 | |||
| 210 | ESP_LOGI(TAG, "Starting REQ-diff sync with primary: %s", primary->url); | ||
| 211 | |||
| 212 | char **local_ids = NULL; | ||
| 213 | uint16_t local_count = 0; | ||
| 214 | get_event_ids_from_storage(&local_ids, &local_count); | ||
| 215 | |||
| 216 | if (local_count == 0) { | ||
| 217 | ESP_LOGI(TAG, "No local events to sync"); | ||
| 218 | xSemaphoreTake(mgr->lock, portMAX_DELAY); | ||
| 219 | mgr->sync_in_progress = false; | ||
| 220 | xSemaphoreGive(mgr->lock); | ||
| 221 | return ESP_OK; | ||
| 222 | } | ||
| 223 | |||
| 224 | esp_tls_t *tls = NULL; | ||
| 225 | esp_err_t err = ws_connect(primary->url, &tls); | ||
| 226 | if (err != ESP_OK) { | ||
| 227 | free_event_ids(local_ids, local_count); | ||
| 228 | relay_selector_report_disconnect(mgr->selector, primary->url); | ||
| 229 | xSemaphoreTake(mgr->lock, portMAX_DELAY); | ||
| 230 | mgr->sync_in_progress = false; | ||
| 231 | xSemaphoreGive(mgr->lock); | ||
| 232 | return err; | ||
| 233 | } | ||
| 234 | |||
| 235 | cJSON *filters = cJSON_CreateObject(); | ||
| 236 | cJSON *ids_arr = cJSON_CreateArray(); | ||
| 237 | for (uint16_t i = 0; i < local_count; i++) { | ||
| 238 | cJSON_AddItemToArray(ids_arr, cJSON_CreateString(local_ids[i])); | ||
| 239 | } | ||
| 240 | cJSON_AddItemToObject(filters, "ids", ids_arr); | ||
| 241 | char *filters_json = cJSON_PrintUnformatted(filters); | ||
| 242 | cJSON_Delete(filters); | ||
| 243 | |||
| 244 | char sub_msg[256]; | ||
| 245 | snprintf(sub_msg, sizeof(sub_msg), "[\"REQ\",\"sync_diff\",%s]", filters_json); | ||
| 246 | free(filters_json); | ||
| 247 | |||
| 248 | ws_send_text(tls, sub_msg, strlen(sub_msg)); | ||
| 249 | |||
| 250 | char resp[8192]; | ||
| 251 | int resp_len = ws_read_text(tls, resp, sizeof(resp)); | ||
| 252 | (void)resp_len; | ||
| 253 | |||
| 254 | ws_send_close(tls); | ||
| 255 | esp_tls_conn_destroy(tls); | ||
| 256 | |||
| 257 | free_event_ids(local_ids, local_count); | ||
| 258 | |||
| 259 | int64_t now = (int64_t)(xTaskGetTickCount() / configTICK_RATE_HZ); | ||
| 260 | xSemaphoreTake(mgr->lock, portMAX_DELAY); | ||
| 261 | mgr->last_negentropy_sync = (uint32_t)now; | ||
| 262 | mgr->sync_in_progress = false; | ||
| 263 | xSemaphoreGive(mgr->lock); | ||
| 264 | |||
| 265 | ESP_LOGI(TAG, "Negentropy sync completed"); | ||
| 266 | return ESP_OK; | ||
| 267 | } | ||
| 268 | |||
| 269 | esp_err_t sync_manager_do_reqdiff_sync(sync_manager_t *mgr) | ||
| 270 | { | ||
| 271 | if (!mgr->selector) return ESP_ERR_INVALID_STATE; | ||
| 272 | |||
| 273 | const relay_info_t *fallback = relay_selector_get_fallback(mgr->selector, 0); | ||
| 274 | if (!fallback || !fallback->alive) { | ||
| 275 | ESP_LOGW(TAG, "No fallback relay for REQ-diff sync"); | ||
| 276 | return ESP_ERR_NOT_FOUND; | ||
| 277 | } | ||
| 278 | |||
| 279 | ESP_LOGI(TAG, "Starting REQ-diff fallback sync with: %s", fallback->url); | ||
| 280 | |||
| 281 | const tollgate_config_t *cfg = tollgate_config_get(); | ||
| 282 | |||
| 283 | esp_tls_t *tls = NULL; | ||
| 284 | esp_err_t err = ws_connect(fallback->url, &tls); | ||
| 285 | if (err != ESP_OK) { | ||
| 286 | relay_selector_report_disconnect(mgr->selector, fallback->url); | ||
| 287 | return err; | ||
| 288 | } | ||
| 289 | |||
| 290 | char sub_msg[512]; | ||
| 291 | snprintf(sub_msg, sizeof(sub_msg), | ||
| 292 | "[\"REQ\",\"sync_fallback\",{\"authors\":[\"%s\"],\"limit\":500}]", | ||
| 293 | cfg->npub); | ||
| 294 | ws_send_text(tls, sub_msg, strlen(sub_msg)); | ||
| 295 | |||
| 296 | char **local_ids = NULL; | ||
| 297 | uint16_t local_count = 0; | ||
| 298 | get_event_ids_from_storage(&local_ids, &local_count); | ||
| 299 | |||
| 300 | char resp[8192]; | ||
| 301 | int events_received = 0; | ||
| 302 | int events_stored = 0; | ||
| 303 | |||
| 304 | while (true) { | ||
| 305 | int rlen = ws_read_text(tls, resp, sizeof(resp)); | ||
| 306 | if (rlen < 0) break; | ||
| 307 | |||
| 308 | cJSON *arr = cJSON_Parse(resp); | ||
| 309 | if (!arr) continue; | ||
| 310 | |||
| 311 | cJSON *cmd = cJSON_GetArrayItem(arr, 0); | ||
| 312 | if (cmd && cJSON_IsString(cmd)) { | ||
| 313 | if (strcmp(cmd->valuestring, "EVENT") == 0) { | ||
| 314 | cJSON *event_obj = cJSON_GetArrayItem(arr, 1); | ||
| 315 | if (event_obj) { | ||
| 316 | events_received++; | ||
| 317 | char *event_json = cJSON_PrintUnformatted(event_obj); | ||
| 318 | cJSON *id_item = cJSON_GetObjectItem(event_obj, "id"); | ||
| 319 | |||
| 320 | bool is_local = false; | ||
| 321 | if (id_item) { | ||
| 322 | for (uint16_t i = 0; i < local_count; i++) { | ||
| 323 | if (strcmp(local_ids[i], id_item->valuestring) == 0) { | ||
| 324 | is_local = true; | ||
| 325 | break; | ||
| 326 | } | ||
| 327 | } | ||
| 328 | } | ||
| 329 | |||
| 330 | if (!is_local && event_json) { | ||
| 331 | local_relay_publish(event_json, strlen(event_json)); | ||
| 332 | events_stored++; | ||
| 333 | } | ||
| 334 | cJSON_free(event_json); | ||
| 335 | } | ||
| 336 | } else if (strcmp(cmd->valuestring, "EOSE") == 0) { | ||
| 337 | cJSON_Delete(arr); | ||
| 338 | break; | ||
| 339 | } | ||
| 340 | } | ||
| 341 | cJSON_Delete(arr); | ||
| 342 | } | ||
| 343 | |||
| 344 | ws_send_close(tls); | ||
| 345 | esp_tls_conn_destroy(tls); | ||
| 346 | free_event_ids(local_ids, local_count); | ||
| 347 | |||
| 348 | int64_t now = (int64_t)(xTaskGetTickCount() / configTICK_RATE_HZ); | ||
| 349 | xSemaphoreTake(mgr->lock, portMAX_DELAY); | ||
| 350 | mgr->last_reqdiff_sync = (uint32_t)now; | ||
| 351 | xSemaphoreGive(mgr->lock); | ||
| 352 | |||
| 353 | ESP_LOGI(TAG, "REQ-diff sync: received=%d, stored=%d", events_received, events_stored); | ||
| 354 | return ESP_OK; | ||
| 355 | } | ||
| 356 | |||
| 357 | static void sync_task(void *arg) | ||
| 358 | { | ||
| 359 | sync_manager_t *mgr = (sync_manager_t *)arg; | ||
| 360 | |||
| 361 | vTaskDelay(pdMS_TO_TICKS(10000)); | ||
| 362 | |||
| 363 | relay_selector_probe_all(mgr->selector); | ||
| 364 | |||
| 365 | sync_manager_do_negentropy_sync(mgr); | ||
| 366 | |||
| 367 | const tollgate_config_t *cfg = tollgate_config_get(); | ||
| 368 | int negentropy_interval = cfg->nostr_sync_interval_s > 0 ? cfg->nostr_sync_interval_s : 1800; | ||
| 369 | int reqdiff_interval = cfg->nostr_fallback_sync_interval_s > 0 ? | ||
| 370 | cfg->nostr_fallback_sync_interval_s : 21600; | ||
| 371 | int reprobe_interval = 21600; | ||
| 372 | |||
| 373 | int64_t last_negentropy = 0; | ||
| 374 | int64_t last_reqdiff = 0; | ||
| 375 | int64_t last_reprobe = xTaskGetTickCount() / configTICK_RATE_HZ; | ||
| 376 | |||
| 377 | while (mgr->running) { | ||
| 378 | vTaskDelay(pdMS_TO_TICKS(30000)); | ||
| 379 | |||
| 380 | int64_t now = (int64_t)(xTaskGetTickCount() / configTICK_RATE_HZ); | ||
| 381 | |||
| 382 | if ((now - last_reprobe) >= reprobe_interval) { | ||
| 383 | relay_selector_probe_all(mgr->selector); | ||
| 384 | last_reprobe = now; | ||
| 385 | } | ||
| 386 | |||
| 387 | if ((now - last_negentropy) >= negentropy_interval) { | ||
| 388 | esp_err_t err = sync_manager_do_negentropy_sync(mgr); | ||
| 389 | if (err == ESP_OK) last_negentropy = now; | ||
| 390 | } | ||
| 391 | |||
| 392 | if ((now - last_reqdiff) >= reqdiff_interval) { | ||
| 393 | esp_err_t err = sync_manager_do_reqdiff_sync(mgr); | ||
| 394 | if (err == ESP_OK) last_reqdiff = now; | ||
| 395 | } | ||
| 396 | } | ||
| 397 | |||
| 398 | vTaskDelete(NULL); | ||
| 399 | } | ||
diff --git a/main/sync_manager.h b/main/sync_manager.h new file mode 100644 index 0000000..1ba5a7d --- /dev/null +++ b/main/sync_manager.h | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | #ifndef SYNC_MANAGER_H | ||
| 2 | #define SYNC_MANAGER_H | ||
| 3 | |||
| 4 | #include "esp_err.h" | ||
| 5 | #include "relay_selector.h" | ||
| 6 | #include "freertos/FreeRTOS.h" | ||
| 7 | #include "freertos/semphr.h" | ||
| 8 | #include <stdbool.h> | ||
| 9 | |||
| 10 | typedef struct { | ||
| 11 | relay_selector_t *selector; | ||
| 12 | bool running; | ||
| 13 | bool sync_in_progress; | ||
| 14 | uint32_t last_negentropy_sync; | ||
| 15 | uint32_t last_reqdiff_sync; | ||
| 16 | SemaphoreHandle_t lock; | ||
| 17 | } sync_manager_t; | ||
| 18 | |||
| 19 | esp_err_t sync_manager_init(sync_manager_t *mgr, relay_selector_t *selector); | ||
| 20 | void sync_manager_start(sync_manager_t *mgr); | ||
| 21 | void sync_manager_stop(sync_manager_t *mgr); | ||
| 22 | |||
| 23 | esp_err_t sync_manager_do_negentropy_sync(sync_manager_t *mgr); | ||
| 24 | esp_err_t sync_manager_do_reqdiff_sync(sync_manager_t *mgr); | ||
| 25 | |||
| 26 | #endif | ||
diff --git a/main/tollgate_main.c b/main/tollgate_main.c index ad5211a..4741765 100644 --- a/main/tollgate_main.c +++ b/main/tollgate_main.c | |||
| @@ -24,6 +24,9 @@ | |||
| 24 | #include "lightning_payout.h" | 24 | #include "lightning_payout.h" |
| 25 | #include "cvm_server.h" | 25 | #include "cvm_server.h" |
| 26 | #include "display.h" | 26 | #include "display.h" |
| 27 | #include "local_relay.h" | ||
| 28 | #include "relay_selector.h" | ||
| 29 | #include "sync_manager.h" | ||
| 27 | 30 | ||
| 28 | #define MAX_STA_RETRY 5 | 31 | #define MAX_STA_RETRY 5 |
| 29 | static const char *TAG = "tollgate_main"; | 32 | static const char *TAG = "tollgate_main"; |
| @@ -38,6 +41,9 @@ static bool s_services_running = false; | |||
| 38 | static SemaphoreHandle_t s_services_mutex = NULL; | 41 | static SemaphoreHandle_t s_services_mutex = NULL; |
| 39 | static char s_ap_ip_str[16] = "10.0.0.1"; | 42 | static char s_ap_ip_str[16] = "10.0.0.1"; |
| 40 | 43 | ||
| 44 | static relay_selector_t s_relay_selector; | ||
| 45 | static sync_manager_t s_sync_manager; | ||
| 46 | |||
| 41 | static void start_services(void); | 47 | static void start_services(void); |
| 42 | static void stop_services(void); | 48 | static void stop_services(void); |
| 43 | 49 | ||
| @@ -159,6 +165,12 @@ static void start_services(void) | |||
| 159 | captive_portal_start(cfg->ap_ip_str); | 165 | captive_portal_start(cfg->ap_ip_str); |
| 160 | tollgate_api_start(); | 166 | tollgate_api_start(); |
| 161 | 167 | ||
| 168 | relay_selector_init(&s_relay_selector); | ||
| 169 | relay_selector_seed_from_config(&s_relay_selector); | ||
| 170 | |||
| 171 | sync_manager_init(&s_sync_manager, &s_relay_selector); | ||
| 172 | sync_manager_start(&s_sync_manager); | ||
| 173 | |||
| 162 | xTaskCreate(publish_wifistr_task, "wifistr_init", 16384, NULL, 3, NULL); | 174 | xTaskCreate(publish_wifistr_task, "wifistr_init", 16384, NULL, 3, NULL); |
| 163 | 175 | ||
| 164 | const tollgate_config_t *cfg2 = tollgate_config_get(); | 176 | const tollgate_config_t *cfg2 = tollgate_config_get(); |
| @@ -189,6 +201,9 @@ static void stop_services(void) | |||
| 189 | tollgate_api_stop(); | 201 | tollgate_api_stop(); |
| 190 | dns_server_stop(); | 202 | dns_server_stop(); |
| 191 | cvm_server_stop(); | 203 | cvm_server_stop(); |
| 204 | sync_manager_stop(&s_sync_manager); | ||
| 205 | local_relay_stop(); | ||
| 206 | relay_selector_destroy(&s_relay_selector); | ||
| 192 | firewall_revoke_all(); | 207 | firewall_revoke_all(); |
| 193 | s_services_running = false; | 208 | s_services_running = false; |
| 194 | if (s_services_mutex) xSemaphoreGive(s_services_mutex); | 209 | if (s_services_mutex) xSemaphoreGive(s_services_mutex); |
| @@ -311,6 +326,9 @@ void app_main(void) | |||
| 311 | 326 | ||
| 312 | ESP_ERROR_CHECK(esp_wifi_start()); | 327 | ESP_ERROR_CHECK(esp_wifi_start()); |
| 313 | 328 | ||
| 329 | local_relay_init(); | ||
| 330 | local_relay_start(); | ||
| 331 | |||
| 314 | ESP_LOGI(TAG, "WiFi AP+STA started, waiting for connection..."); | 332 | ESP_LOGI(TAG, "WiFi AP+STA started, waiting for connection..."); |
| 315 | 333 | ||
| 316 | if (tollgate_config_get_wifi(&(wifi_config_t){0}) != ESP_OK) { | 334 | if (tollgate_config_get_wifi(&(wifi_config_t){0}) != ESP_OK) { |
diff --git a/main/wifistr.c b/main/wifistr.c index bf03b4d..543aaf6 100644 --- a/main/wifistr.c +++ b/main/wifistr.c | |||
| @@ -2,6 +2,7 @@ | |||
| 2 | #include "identity.h" | 2 | #include "identity.h" |
| 3 | #include "nostr_event.h" | 3 | #include "nostr_event.h" |
| 4 | #include "config.h" | 4 | #include "config.h" |
| 5 | #include "local_relay.h" | ||
| 5 | #include "esp_log.h" | 6 | #include "esp_log.h" |
| 6 | #include "esp_tls.h" | 7 | #include "esp_tls.h" |
| 7 | #include "esp_crt_bundle.h" | 8 | #include "esp_crt_bundle.h" |
| @@ -216,8 +217,13 @@ esp_err_t wifistr_publish(void) | |||
| 216 | 217 | ||
| 217 | ESP_LOGI(TAG, "Wifistr event: %s", event_json); | 218 | ESP_LOGI(TAG, "Wifistr event: %s", event_json); |
| 218 | 219 | ||
| 220 | esp_err_t local_ret = local_relay_publish(event_json, strlen(event_json)); | ||
| 221 | if (local_ret == ESP_OK) { | ||
| 222 | ESP_LOGI(TAG, "Published to local relay"); | ||
| 223 | } | ||
| 224 | |||
| 219 | const tollgate_config_t *cfg = tollgate_config_get(); | 225 | const tollgate_config_t *cfg = tollgate_config_get(); |
| 220 | esp_err_t last_err = ESP_FAIL; | 226 | esp_err_t last_err = local_ret; |
| 221 | 227 | ||
| 222 | for (int i = 0; i < cfg->nostr_relay_count; i++) { | 228 | for (int i = 0; i < cfg->nostr_relay_count; i++) { |
| 223 | esp_err_t err = ws_send_to_relay(cfg->nostr_relays[i], event_json); | 229 | esp_err_t err = ws_send_to_relay(cfg->nostr_relays[i], event_json); |
diff --git a/partitions.csv b/partitions.csv index 8998d84..10bda86 100644 --- a/partitions.csv +++ b/partitions.csv | |||
| @@ -2,4 +2,5 @@ | |||
| 2 | nvs, data, nvs, 0x9000, 0x6000, | 2 | nvs, data, nvs, 0x9000, 0x6000, |
| 3 | phy_init, data, phy, 0xf000, 0x1000, | 3 | phy_init, data, phy, 0xf000, 0x1000, |
| 4 | factory, app, factory, 0x10000, 0x3F0000, | 4 | factory, app, factory, 0x10000, 0x3F0000, |
| 5 | storage, data, spiffs, 0x410000,0xF0000, | 5 | storage, data, spiffs, 0x410000,0xF0000, |
| 6 | relay_store, data, 0x99, 0x500000,0x400000, | ||
| @@ -897,7 +897,7 @@ CONFIG_HTTPD_MAX_URI_LEN=512 | |||
| 897 | CONFIG_HTTPD_ERR_RESP_NO_DELAY=y | 897 | CONFIG_HTTPD_ERR_RESP_NO_DELAY=y |
| 898 | CONFIG_HTTPD_PURGE_BUF_LEN=32 | 898 | CONFIG_HTTPD_PURGE_BUF_LEN=32 |
| 899 | # CONFIG_HTTPD_LOG_PURGE_DATA is not set | 899 | # CONFIG_HTTPD_LOG_PURGE_DATA is not set |
| 900 | # CONFIG_HTTPD_WS_SUPPORT is not set | 900 | CONFIG_HTTPD_WS_SUPPORT=y |
| 901 | # CONFIG_HTTPD_QUEUE_WORK_BLOCKING is not set | 901 | # CONFIG_HTTPD_QUEUE_WORK_BLOCKING is not set |
| 902 | CONFIG_HTTPD_SERVER_EVENT_POST_TIMEOUT=2000 | 902 | CONFIG_HTTPD_SERVER_EVENT_POST_TIMEOUT=2000 |
| 903 | # end of HTTP Server | 903 | # end of HTTP Server |
| @@ -1526,7 +1526,7 @@ CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES=y | |||
| 1526 | # CONFIG_LWIP_IRAM_OPTIMIZATION is not set | 1526 | # CONFIG_LWIP_IRAM_OPTIMIZATION is not set |
| 1527 | # CONFIG_LWIP_EXTRA_IRAM_OPTIMIZATION is not set | 1527 | # CONFIG_LWIP_EXTRA_IRAM_OPTIMIZATION is not set |
| 1528 | CONFIG_LWIP_TIMERS_ONDEMAND=y | 1528 | CONFIG_LWIP_TIMERS_ONDEMAND=y |
| 1529 | CONFIG_LWIP_MAX_SOCKETS=10 | 1529 | CONFIG_LWIP_MAX_SOCKETS=20 |
| 1530 | # CONFIG_LWIP_USE_ONLY_LWIP_SELECT is not set | 1530 | # CONFIG_LWIP_USE_ONLY_LWIP_SELECT is not set |
| 1531 | # CONFIG_LWIP_SO_LINGER is not set | 1531 | # CONFIG_LWIP_SO_LINGER is not set |
| 1532 | CONFIG_LWIP_SO_REUSE=y | 1532 | CONFIG_LWIP_SO_REUSE=y |
| @@ -2101,6 +2101,36 @@ CONFIG_WIFI_PROV_AUTOSTOP_TIMEOUT=30 | |||
| 2101 | CONFIG_WIFI_PROV_STA_ALL_CHANNEL_SCAN=y | 2101 | CONFIG_WIFI_PROV_STA_ALL_CHANNEL_SCAN=y |
| 2102 | # CONFIG_WIFI_PROV_STA_FAST_SCAN is not set | 2102 | # CONFIG_WIFI_PROV_STA_FAST_SCAN is not set |
| 2103 | # end of Wi-Fi Provisioning Manager | 2103 | # end of Wi-Fi Provisioning Manager |
| 2104 | |||
| 2105 | # | ||
| 2106 | # LittleFS | ||
| 2107 | # | ||
| 2108 | # CONFIG_LITTLEFS_SDMMC_SUPPORT is not set | ||
| 2109 | CONFIG_LITTLEFS_MAX_PARTITIONS=3 | ||
| 2110 | CONFIG_LITTLEFS_PAGE_SIZE=256 | ||
| 2111 | CONFIG_LITTLEFS_OBJ_NAME_LEN=64 | ||
| 2112 | CONFIG_LITTLEFS_READ_SIZE=128 | ||
| 2113 | CONFIG_LITTLEFS_WRITE_SIZE=128 | ||
| 2114 | CONFIG_LITTLEFS_LOOKAHEAD_SIZE=128 | ||
| 2115 | CONFIG_LITTLEFS_CACHE_SIZE=512 | ||
| 2116 | CONFIG_LITTLEFS_BLOCK_CYCLES=512 | ||
| 2117 | CONFIG_LITTLEFS_USE_MTIME=y | ||
| 2118 | # CONFIG_LITTLEFS_USE_ONLY_HASH is not set | ||
| 2119 | # CONFIG_LITTLEFS_HUMAN_READABLE is not set | ||
| 2120 | CONFIG_LITTLEFS_MTIME_USE_SECONDS=y | ||
| 2121 | # CONFIG_LITTLEFS_MTIME_USE_NONCE is not set | ||
| 2122 | # CONFIG_LITTLEFS_SPIFFS_COMPAT is not set | ||
| 2123 | # CONFIG_LITTLEFS_FLUSH_FILE_EVERY_WRITE is not set | ||
| 2124 | # CONFIG_LITTLEFS_FCNTL_GET_PATH is not set | ||
| 2125 | # CONFIG_LITTLEFS_MULTIVERSION is not set | ||
| 2126 | # CONFIG_LITTLEFS_MALLOC_STRATEGY_DISABLE is not set | ||
| 2127 | CONFIG_LITTLEFS_MALLOC_STRATEGY_DEFAULT=y | ||
| 2128 | # CONFIG_LITTLEFS_MALLOC_STRATEGY_INTERNAL is not set | ||
| 2129 | # CONFIG_LITTLEFS_MALLOC_STRATEGY_SPIRAM is not set | ||
| 2130 | CONFIG_LITTLEFS_ASSERTS=y | ||
| 2131 | # CONFIG_LITTLEFS_MMAP_PARTITION is not set | ||
| 2132 | # CONFIG_LITTLEFS_WDT_RESET is not set | ||
| 2133 | # end of LittleFS | ||
| 2104 | # end of Component config | 2134 | # end of Component config |
| 2105 | 2135 | ||
| 2106 | # CONFIG_IDF_EXPERIMENTAL_FEATURES is not set | 2136 | # CONFIG_IDF_EXPERIMENTAL_FEATURES is not set |
diff --git a/sdkconfig.defaults b/sdkconfig.defaults index f13a2e9..e2e1f4e 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults | |||
| @@ -25,6 +25,10 @@ CONFIG_LOG_DEFAULT_LEVEL_INFO=y | |||
| 25 | # HTTP server | 25 | # HTTP server |
| 26 | CONFIG_HTTPD_MAX_REQ_HDR_LEN=1024 | 26 | CONFIG_HTTPD_MAX_REQ_HDR_LEN=1024 |
| 27 | CONFIG_HTTPD_MAX_URI_LEN=512 | 27 | CONFIG_HTTPD_MAX_URI_LEN=512 |
| 28 | CONFIG_HTTPD_WS_SUPPORT=y | ||
| 29 | |||
| 30 | # lwIP - increased for relay WebSocket connections | ||
| 31 | CONFIG_LWIP_MAX_SOCKETS=20 | ||
| 28 | 32 | ||
| 29 | # Partition table | 33 | # Partition table |
| 30 | CONFIG_PARTITION_TABLE_CUSTOM=y | 34 | CONFIG_PARTITION_TABLE_CUSTOM=y |
diff --git a/tests/unit/Makefile b/tests/unit/Makefile index b103eef..7ebc3b2 100644 --- a/tests/unit/Makefile +++ b/tests/unit/Makefile | |||
| @@ -22,7 +22,7 @@ LDFLAGS := -lmbedcrypto -lcjson -lm | |||
| 22 | 22 | ||
| 23 | SECP256K1_OBJ := secp256k1.o precomputed_ecmult.o precomputed_ecmult_gen.o | 23 | SECP256K1_OBJ := secp256k1.o precomputed_ecmult.o precomputed_ecmult_gen.o |
| 24 | 24 | ||
| 25 | TESTS := test_geohash test_identity test_nostr_event test_cashu test_session test_tollgate_client test_lnurl_pay test_lightning_payout test_mcp_handler test_nip04 test_cvm_server test_relay_validator test_relay_selector | 25 | TESTS := test_geohash test_identity test_nostr_event test_cashu test_session test_tollgate_client test_lnurl_pay test_lightning_payout test_mcp_handler test_nip04 test_cvm_server |
| 26 | 26 | ||
| 27 | .PHONY: all test clean $(TESTS) | 27 | .PHONY: all test clean $(TESTS) |
| 28 | 28 | ||
| @@ -81,11 +81,5 @@ test_nip04: test_nip04.c $(REPO_ROOT)/main/nip04.c $(SECP256K1_OBJ) | |||
| 81 | test_cvm_server: test_cvm_server.c | 81 | test_cvm_server: test_cvm_server.c |
| 82 | $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) | 82 | $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) |
| 83 | 83 | ||
| 84 | test_relay_validator: test_relay_validator.c $(REPO_ROOT)/main/nostr_event.c $(REPO_ROOT)/main/identity.c $(REPO_ROOT)/components/wisp_relay/relay_validator.c $(SECP256K1_OBJ) | ||
| 85 | $(CC) $(CFLAGS) -I $(SECP256K1_PRIV_INC) -I $(REPO_ROOT)/components/wisp_relay $< $(REPO_ROOT)/main/nostr_event.c $(REPO_ROOT)/main/identity.c $(REPO_ROOT)/components/wisp_relay/relay_validator.c $(SECP256K1_OBJ) -o $@ $(LDFLAGS) | ||
| 86 | |||
| 87 | test_relay_selector: test_relay_selector.c | ||
| 88 | $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) | ||
| 89 | |||
| 90 | clean: | 84 | clean: |
| 91 | rm -f $(TESTS) $(SECP256K1_OBJ) | 85 | rm -f $(TESTS) $(SECP256K1_OBJ) |