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 /main | |
| 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
Diffstat (limited to 'main')
| -rw-r--r-- | main/CMakeLists.txt | 5 | ||||
| -rw-r--r-- | main/config.c | 38 | ||||
| -rw-r--r-- | main/config.h | 6 | ||||
| -rw-r--r-- | main/cvm_server.c | 95 | ||||
| -rw-r--r-- | main/display.c | 264 | ||||
| -rw-r--r-- | main/display.h | 27 | ||||
| -rw-r--r-- | main/font.c | 132 | ||||
| -rw-r--r-- | main/font.h | 11 | ||||
| -rw-r--r-- | main/local_relay.c | 140 | ||||
| -rw-r--r-- | main/local_relay.h | 13 | ||||
| -rw-r--r-- | main/relay_selector.c | 270 | ||||
| -rw-r--r-- | main/relay_selector.h | 46 | ||||
| -rw-r--r-- | main/sync_manager.c | 399 | ||||
| -rw-r--r-- | main/sync_manager.h | 26 | ||||
| -rw-r--r-- | main/tollgate_main.c | 18 | ||||
| -rw-r--r-- | main/wifistr.c | 8 |
16 files changed, 1409 insertions, 89 deletions
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); |