diff options
Diffstat (limited to 'main')
| -rw-r--r-- | main/CMakeLists.txt | 15 | ||||
| -rw-r--r-- | main/captive_portal.c | 386 | ||||
| -rw-r--r-- | main/captive_portal.h | 3 | ||||
| -rw-r--r-- | main/cashu.c | 1 | ||||
| -rw-r--r-- | main/config.c | 90 | ||||
| -rw-r--r-- | main/config.h | 1 | ||||
| -rw-r--r-- | main/cvm_server.c | 64 | ||||
| -rw-r--r-- | main/display.c | 349 | ||||
| -rw-r--r-- | main/display.h | 10 | ||||
| -rw-r--r-- | main/dns_server.c | 2 | ||||
| -rw-r--r-- | main/keyboard.c | 186 | ||||
| -rw-r--r-- | main/keyboard.h | 53 | ||||
| -rw-r--r-- | main/mint_health.c | 64 | ||||
| -rw-r--r-- | main/mint_health.h | 1 | ||||
| -rw-r--r-- | main/tollgate_api.c | 113 | ||||
| -rw-r--r-- | main/tollgate_api.h | 1 | ||||
| -rw-r--r-- | main/tollgate_main.c | 92 | ||||
| -rw-r--r-- | main/tollgate_platform.c | 63 | ||||
| -rw-r--r-- | main/touch.c | 156 | ||||
| -rw-r--r-- | main/touch.h | 29 | ||||
| -rw-r--r-- | main/wifi_setup.c | 89 | ||||
| -rw-r--r-- | main/wifi_setup.h | 51 |
22 files changed, 1630 insertions, 189 deletions
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 0669b70..9e76f89 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt | |||
| @@ -30,10 +30,13 @@ idf_component_register(SRCS "tollgate_main.c" | |||
| 30 | "stratum_proxy.c" | 30 | "stratum_proxy.c" |
| 31 | "sw_miner.c" | 31 | "sw_miner.c" |
| 32 | "asic_miner.c" | 32 | "asic_miner.c" |
| 33 | "tollgate_platform.c" | ||
| 34 | "touch.c" | ||
| 35 | "keyboard.c" | ||
| 36 | "wifi_setup.c" | ||
| 33 | INCLUDE_DIRS "." | 37 | INCLUDE_DIRS "." |
| 34 | REQUIRES esp_wifi esp_event esp_netif nvs_flash esp_http_server | 38 | REQUIRES esp_wifi esp_event esp_netif nvs_flash esp_http_server |
| 35 | lwip json esp_http_client mbedtls esp-tls log spiffs | 39 | lwip json esp_http_client mbedtls esp-tls log spiffs |
| 36 | nucula_lib secp256k1 axs15231b qrcode wisp_relay | 40 | nucula_lib secp256k1 axs15231b qrcode wisp_relay |
| 37 | esp_littlefs negentropy | 41 | negentropy_lib tcp_transport tollgate_core |
| 38 | esp_timer tcp_transport | 42 | PRIV_REQUIRES esp-tls) |
| 39 | PRIV_REQUIRES esp-tls) | ||
diff --git a/main/captive_portal.c b/main/captive_portal.c index ea83906..9a0a5ff 100644 --- a/main/captive_portal.c +++ b/main/captive_portal.c | |||
| @@ -6,6 +6,7 @@ | |||
| 6 | #include "stratum_proxy.h" | 6 | #include "stratum_proxy.h" |
| 7 | #include "esp_log.h" | 7 | #include "esp_log.h" |
| 8 | #include "esp_wifi.h" | 8 | #include "esp_wifi.h" |
| 9 | #include "esp_netif.h" | ||
| 9 | #include "cJSON.h" | 10 | #include "cJSON.h" |
| 10 | #include "lwip/sockets.h" | 11 | #include "lwip/sockets.h" |
| 11 | #include "lwip/netdb.h" | 12 | #include "lwip/netdb.h" |
| @@ -13,8 +14,11 @@ | |||
| 13 | #include "freertos/task.h" | 14 | #include "freertos/task.h" |
| 14 | #include <string.h> | 15 | #include <string.h> |
| 15 | #include <sys/param.h> | 16 | #include <sys/param.h> |
| 17 | #include <stdio.h> | ||
| 16 | 18 | ||
| 17 | static const char *TAG = "captive_portal"; | 19 | static const char *TAG = "captive_portal"; |
| 20 | static bool s_start_called = false; | ||
| 21 | static esp_err_t s_start_result = ESP_OK; | ||
| 18 | static httpd_handle_t s_server = NULL; | 22 | static httpd_handle_t s_server = NULL; |
| 19 | static char s_ap_ip_str[16] = "10.0.0.1"; | 23 | static char s_ap_ip_str[16] = "10.0.0.1"; |
| 20 | 24 | ||
| @@ -342,17 +346,19 @@ static esp_err_t redirect_to_portal_handler(httpd_req_t *req) | |||
| 342 | return portal_handler(req); | 346 | return portal_handler(req); |
| 343 | } | 347 | } |
| 344 | 348 | ||
| 345 | static esp_err_t catchall_handler(httpd_req_t *req) | 349 | static esp_err_t catchall_err_handler(httpd_req_t *req, httpd_err_code_t err) |
| 346 | { | 350 | { |
| 347 | ESP_LOGI(TAG, "Catchall: GET %s → 302 → http://%s/", req->uri, s_ap_ip_str); | 351 | if (err == HTTPD_404_NOT_FOUND) { |
| 348 | httpd_resp_set_status(req, "302 Found"); | 352 | ESP_LOGI(TAG, "Catchall 404: GET %s → 302 → http://%s/", req->uri, s_ap_ip_str); |
| 349 | 353 | httpd_resp_set_status(req, "302 Found"); | |
| 350 | char location[64]; | 354 | char location[64]; |
| 351 | snprintf(location, sizeof(location), "http://%s/", s_ap_ip_str); | 355 | snprintf(location, sizeof(location), "http://%s/", s_ap_ip_str); |
| 352 | httpd_resp_set_hdr(req, "Location", location); | 356 | httpd_resp_set_hdr(req, "Location", location); |
| 353 | httpd_resp_set_hdr(req, "Connection", "close"); | 357 | httpd_resp_set_hdr(req, "Connection", "close"); |
| 354 | httpd_resp_send(req, NULL, 0); | 358 | httpd_resp_send(req, NULL, 0); |
| 355 | return ESP_OK; | 359 | return ESP_OK; |
| 360 | } | ||
| 361 | return ESP_FAIL; | ||
| 356 | } | 362 | } |
| 357 | 363 | ||
| 358 | static const httpd_uri_t uri_portal = { .uri = "/", .method = HTTP_GET, .handler = portal_handler }; | 364 | static const httpd_uri_t uri_portal = { .uri = "/", .method = HTTP_GET, .handler = portal_handler }; |
| @@ -368,21 +374,353 @@ static const httpd_uri_t uri_success = { .uri = "/success.txt", .method = HTTP_G | |||
| 368 | static const httpd_uri_t uri_ncsi = { .uri = "/ncsi.txt", .method = HTTP_GET, .handler = redirect_to_portal_handler }; | 374 | static const httpd_uri_t uri_ncsi = { .uri = "/ncsi.txt", .method = HTTP_GET, .handler = redirect_to_portal_handler }; |
| 369 | static const httpd_uri_t uri_connecttest = { .uri = "/connecttest.txt", .method = HTTP_GET, .handler = redirect_to_portal_handler }; | 375 | static const httpd_uri_t uri_connecttest = { .uri = "/connecttest.txt", .method = HTTP_GET, .handler = redirect_to_portal_handler }; |
| 370 | static const httpd_uri_t uri_wpad = { .uri = "/wpad.dat", .method = HTTP_GET, .handler = redirect_to_portal_handler }; | 376 | static const httpd_uri_t uri_wpad = { .uri = "/wpad.dat", .method = HTTP_GET, .handler = redirect_to_portal_handler }; |
| 371 | static const httpd_uri_t uri_catchall = { .uri = "/*", .method = HTTP_GET, .handler = catchall_handler }; | 377 | |
| 378 | static const char SETUP_HTML_TEMPLATE[] = \ | ||
| 379 | "<!DOCTYPE html>" | ||
| 380 | "<html><head>" | ||
| 381 | "<meta charset='utf-8'>" | ||
| 382 | "<meta name='viewport' content='width=device-width, initial-scale=1'>" | ||
| 383 | "<title>TollGate Setup</title>" | ||
| 384 | "<style>" | ||
| 385 | "*{box-sizing:border-box;margin:0;padding:0}" | ||
| 386 | "body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;" | ||
| 387 | "background:#0a0a0a;color:#fff;display:flex;align-items:center;justify-content:center;" | ||
| 388 | "min-height:100vh;padding:20px}" | ||
| 389 | ".card{background:#1a1a1a;border:1px solid #333;border-radius:16px;padding:32px;" | ||
| 390 | "max-width:400px;width:100%;text-align:center}" | ||
| 391 | "h1{font-size:24px;margin-bottom:8px;color:#f7931a}" | ||
| 392 | ".subtitle{color:#888;margin-bottom:20px;font-size:13px}" | ||
| 393 | ".networks{margin-top:16px;text-align:left}" | ||
| 394 | ".net-item{background:#252525;border:1px solid #333;border-radius:8px;" | ||
| 395 | "padding:12px;margin-bottom:8px;cursor:pointer;display:flex;justify-content:space-between;align-items:center}" | ||
| 396 | ".net-item:hover{border-color:#f7931a}" | ||
| 397 | ".net-item:active{background:#333}" | ||
| 398 | ".net-ssid{font-size:14px}" | ||
| 399 | ".net-rssi{font-size:11px;color:#888}" | ||
| 400 | ".net-lock{color:#f7931a;margin-right:4px}" | ||
| 401 | ".manual{margin-top:12px}" | ||
| 402 | "input{width:100%;background:#252525;border:1px solid #333;border-radius:8px;" | ||
| 403 | "color:#fff;padding:12px;font-size:14px;margin-bottom:8px;outline:none}" | ||
| 404 | "input:focus{border-color:#f7931a}" | ||
| 405 | ".btn{background:#f7931a;color:#000;border:none;border-radius:8px;padding:14px 28px;" | ||
| 406 | "font-size:16px;font-weight:bold;cursor:pointer;width:100%;margin-top:8px}" | ||
| 407 | ".btn:hover{background:#e8850f}" | ||
| 408 | ".btn:disabled{background:#333;color:#666;cursor:not-allowed}" | ||
| 409 | "#status{margin-top:12px;padding:10px;border-radius:8px;display:none;font-size:13px}" | ||
| 410 | "#status.success{display:block;background:#1a472a;color:#4caf50}" | ||
| 411 | "#status.error{display:block;background:#471a1a;color:#f44336}" | ||
| 412 | "#status.processing{display:block;background:#1a3a47;color:#2196f3}" | ||
| 413 | ".refresh{background:none;border:1px solid #444;color:#aaa;border-radius:6px;" | ||
| 414 | "padding:6px 12px;font-size:12px;cursor:pointer;margin-top:4px}" | ||
| 415 | ".refresh:hover{border-color:#f7931a;color:#f7931a}" | ||
| 416 | "#manualForm{display:none;margin-top:12px}" | ||
| 417 | "</style>" | ||
| 418 | "</head><body>" | ||
| 419 | "<div class='card'>" | ||
| 420 | "<h1>TollGate Setup</h1>" | ||
| 421 | "<p class='subtitle'>Configure upstream WiFi</p>" | ||
| 422 | "<div id='scanStatus'>Scanning...</div>" | ||
| 423 | "<div class='networks' id='networkList'></div>" | ||
| 424 | "<button class='refresh' onclick='scanWifi()'>Rescan</button>" | ||
| 425 | "<button class='refresh' onclick='showManual()'>Manual entry</button>" | ||
| 426 | "<div id='manualForm'>" | ||
| 427 | "<input id='manualSsid' placeholder='SSID'>" | ||
| 428 | "<input id='manualPass' type='password' placeholder='Password'>" | ||
| 429 | "<button class='btn' onclick='connectManual()'>Connect</button>" | ||
| 430 | "</div>" | ||
| 431 | "<div id='passwordForm' style='display:none'>" | ||
| 432 | "<p style='margin:12px 0 8px;text-align:left' id='selectedNetwork'></p>" | ||
| 433 | "<input id='wifiPass' type='password' placeholder='WiFi password'>" | ||
| 434 | "<button class='btn' onclick='connectSelected()'>Connect</button>" | ||
| 435 | "</div>" | ||
| 436 | "<div id='status'></div>" | ||
| 437 | "</div>" | ||
| 438 | "<script>" | ||
| 439 | "const apIp='__AP_IP__';" | ||
| 440 | "let selectedSsid='';" | ||
| 441 | "function showStatus(msg,type){const s=document.getElementById('status');" | ||
| 442 | "s.textContent=msg;s.className=type;}" | ||
| 443 | "function scanWifi(){" | ||
| 444 | "document.getElementById('scanStatus').textContent='Scanning...';" | ||
| 445 | "document.getElementById('networkList').innerHTML='';" | ||
| 446 | "fetch('/wifi/scan').then(r=>r.json()).then(aps=>{" | ||
| 447 | "document.getElementById('scanStatus').textContent=aps.length+' networks found';" | ||
| 448 | "const list=document.getElementById('networkList');" | ||
| 449 | "aps.forEach(ap=>{" | ||
| 450 | "const div=document.createElement('div');" | ||
| 451 | "div.className='net-item';" | ||
| 452 | "const lock=ap.secured?'<span class=net-lock>🔒</span>':'';" | ||
| 453 | "div.innerHTML='<span class=net-ssid>'+lock+ap.ssid+'</span>" | ||
| 454 | "<span class=net-rssi>'+ap.rssi+' dBm</span>';" | ||
| 455 | "div.onclick=()=>selectNetwork(ap.ssid,ap.secured);" | ||
| 456 | "list.appendChild(div);" | ||
| 457 | "});" | ||
| 458 | "}).catch(e=>{document.getElementById('scanStatus').textContent='Scan failed';});" | ||
| 459 | "}" | ||
| 460 | "function selectNetwork(ssid,secured){" | ||
| 461 | "selectedSsid=ssid;" | ||
| 462 | "document.getElementById('selectedNetwork').textContent='Connect to: '+ssid;" | ||
| 463 | "document.getElementById('passwordForm').style.display='block';" | ||
| 464 | "document.getElementById('scanStatus').style.display='none';" | ||
| 465 | "document.getElementById('networkList').style.display='none';" | ||
| 466 | "document.querySelector('.refresh').style.display='none';" | ||
| 467 | "if(!secured){connectSelected();}" | ||
| 468 | "}" | ||
| 469 | "function showManual(){" | ||
| 470 | "document.getElementById('manualForm').style.display='block';" | ||
| 471 | "}" | ||
| 472 | "function connectSelected(){" | ||
| 473 | "const pass=document.getElementById('wifiPass').value;" | ||
| 474 | "doConnect(selectedSsid,pass);" | ||
| 475 | "}" | ||
| 476 | "function connectManual(){" | ||
| 477 | "const ssid=document.getElementById('manualSsid').value.trim();" | ||
| 478 | "const pass=document.getElementById('manualPass').value;" | ||
| 479 | "if(!ssid){showStatus('Enter SSID','error');return;}" | ||
| 480 | "doConnect(ssid,pass);" | ||
| 481 | "}" | ||
| 482 | "function doConnect(ssid,pass){" | ||
| 483 | "showStatus('Connecting to '+ssid+'...','processing');" | ||
| 484 | "fetch('/wifi/connect',{method:'POST',headers:{'Content-Type':'application/json'}," | ||
| 485 | "body:JSON.stringify({ssid:ssid,password:pass})})" | ||
| 486 | ".then(r=>r.json()).then(d=>{" | ||
| 487 | "if(d.ok){showStatus('Connected! Device is restarting...','success');}" | ||
| 488 | "else{showStatus('Failed: '+(d.error||'unknown'),'error');}" | ||
| 489 | "}).catch(e=>{showStatus('Connection error','error');});" | ||
| 490 | "}" | ||
| 491 | "scanWifi();" | ||
| 492 | "</script>" | ||
| 493 | "</body></html>"; | ||
| 494 | |||
| 495 | static char *template_replace(const char *tpl, const char *key, const char *val) { | ||
| 496 | const char *p; | ||
| 497 | size_t klen = strlen(key); | ||
| 498 | size_t vlen = strlen(val); | ||
| 499 | size_t tlen = strlen(tpl); | ||
| 500 | size_t extra = 0; | ||
| 501 | p = tpl; | ||
| 502 | while ((p = strstr(p, key)) != NULL) { | ||
| 503 | extra += vlen - klen; | ||
| 504 | p += klen; | ||
| 505 | } | ||
| 506 | size_t out_size = tlen + extra + 1; | ||
| 507 | char *out = malloc(out_size); | ||
| 508 | if (!out) return NULL; | ||
| 509 | char *dst = out; | ||
| 510 | p = tpl; | ||
| 511 | while (*p) { | ||
| 512 | const char *found = strstr(p, key); | ||
| 513 | if (found) { | ||
| 514 | memcpy(dst, p, found - p); | ||
| 515 | dst += found - p; | ||
| 516 | memcpy(dst, val, vlen); | ||
| 517 | dst += vlen; | ||
| 518 | p = found + klen; | ||
| 519 | } else { | ||
| 520 | strcpy(dst, p); | ||
| 521 | dst += strlen(p); | ||
| 522 | break; | ||
| 523 | } | ||
| 524 | } | ||
| 525 | *dst = '\0'; | ||
| 526 | return out; | ||
| 527 | } | ||
| 528 | |||
| 529 | static bool is_setup_available(void) { | ||
| 530 | const tollgate_config_t *cfg = tollgate_config_get(); | ||
| 531 | return cfg->network_count == 0; | ||
| 532 | } | ||
| 533 | |||
| 534 | static esp_err_t setup_page_handler(httpd_req_t *req) { | ||
| 535 | if (!is_setup_available()) { | ||
| 536 | httpd_resp_set_status(req, "302 Found"); | ||
| 537 | char location[64]; | ||
| 538 | snprintf(location, sizeof(location), "http://%s/", s_ap_ip_str); | ||
| 539 | httpd_resp_set_hdr(req, "Location", location); | ||
| 540 | httpd_resp_send(req, NULL, 0); | ||
| 541 | return ESP_OK; | ||
| 542 | } | ||
| 543 | |||
| 544 | httpd_resp_set_type(req, "text/html"); | ||
| 545 | char *html = template_replace(SETUP_HTML_TEMPLATE, "__AP_IP__", s_ap_ip_str); | ||
| 546 | if (!html) { | ||
| 547 | httpd_resp_send_500(req); | ||
| 548 | return ESP_OK; | ||
| 549 | } | ||
| 550 | httpd_resp_send(req, html, strlen(html)); | ||
| 551 | free(html); | ||
| 552 | return ESP_OK; | ||
| 553 | } | ||
| 554 | |||
| 555 | static esp_err_t wifi_scan_handler(httpd_req_t *req) { | ||
| 556 | esp_wifi_disconnect(); | ||
| 557 | vTaskDelay(pdMS_TO_TICKS(300)); | ||
| 558 | |||
| 559 | wifi_scan_config_t scan_cfg = {0}; | ||
| 560 | scan_cfg.scan_type = WIFI_SCAN_TYPE_ACTIVE; | ||
| 561 | scan_cfg.scan_time.active.min = 100; | ||
| 562 | scan_cfg.scan_time.active.max = 300; | ||
| 563 | esp_err_t ret = esp_wifi_scan_start(&scan_cfg, true); | ||
| 564 | if (ret != ESP_OK) { | ||
| 565 | httpd_resp_set_type(req, "application/json"); | ||
| 566 | httpd_resp_send(req, "[]", 2); | ||
| 567 | return ESP_OK; | ||
| 568 | } | ||
| 569 | |||
| 570 | uint16_t ap_count = 0; | ||
| 571 | esp_wifi_scan_get_ap_num(&ap_count); | ||
| 572 | if (ap_count > 20) ap_count = 20; | ||
| 573 | wifi_ap_record_t aps[20]; | ||
| 574 | esp_wifi_scan_get_ap_records(&ap_count, aps); | ||
| 575 | |||
| 576 | for (int i = 0; i < (int)ap_count - 1; i++) { | ||
| 577 | for (int j = i + 1; j < (int)ap_count; j++) { | ||
| 578 | if (aps[j].rssi > aps[i].rssi) { | ||
| 579 | wifi_ap_record_t tmp = aps[i]; | ||
| 580 | aps[i] = aps[j]; | ||
| 581 | aps[j] = tmp; | ||
| 582 | } | ||
| 583 | } | ||
| 584 | } | ||
| 585 | |||
| 586 | cJSON *root = cJSON_CreateArray(); | ||
| 587 | for (int i = 0; i < (int)ap_count; i++) { | ||
| 588 | if (aps[i].ssid[0] == '\0') continue; | ||
| 589 | cJSON *ap = cJSON_CreateObject(); | ||
| 590 | cJSON_AddStringToObject(ap, "ssid", (const char *)aps[i].ssid); | ||
| 591 | cJSON_AddNumberToObject(ap, "rssi", aps[i].rssi); | ||
| 592 | cJSON_AddBoolToObject(ap, "secured", aps[i].authmode != WIFI_AUTH_OPEN); | ||
| 593 | cJSON_AddItemToArray(root, ap); | ||
| 594 | } | ||
| 595 | |||
| 596 | char *json = cJSON_PrintUnformatted(root); | ||
| 597 | httpd_resp_set_type(req, "application/json"); | ||
| 598 | httpd_resp_send(req, json, strlen(json)); | ||
| 599 | cJSON_free(json); | ||
| 600 | cJSON_Delete(root); | ||
| 601 | |||
| 602 | const tollgate_config_t *cfg = tollgate_config_get(); | ||
| 603 | if (cfg->network_count > 0) { | ||
| 604 | wifi_config_t wifi_cfg; | ||
| 605 | if (tollgate_config_get_wifi(&wifi_cfg) == ESP_OK) { | ||
| 606 | esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg); | ||
| 607 | esp_wifi_connect(); | ||
| 608 | } | ||
| 609 | } | ||
| 610 | |||
| 611 | return ESP_OK; | ||
| 612 | } | ||
| 613 | |||
| 614 | static esp_err_t wifi_connect_handler(httpd_req_t *req) { | ||
| 615 | int content_len = req->content_len; | ||
| 616 | if (content_len <= 0 || content_len > 1024) { | ||
| 617 | httpd_resp_set_type(req, "application/json"); | ||
| 618 | httpd_resp_send(req, "{\"ok\":false,\"error\":\"invalid request\"}", HTTPD_RESP_USE_STRLEN); | ||
| 619 | return ESP_OK; | ||
| 620 | } | ||
| 621 | |||
| 622 | char *body = malloc(content_len + 1); | ||
| 623 | if (!body) { | ||
| 624 | httpd_resp_send_500(req); | ||
| 625 | return ESP_OK; | ||
| 626 | } | ||
| 627 | int total = 0; | ||
| 628 | while (total < content_len) { | ||
| 629 | int r = httpd_req_recv(req, body + total, content_len - total); | ||
| 630 | if (r <= 0) { free(body); httpd_resp_send_500(req); return ESP_OK; } | ||
| 631 | total += r; | ||
| 632 | } | ||
| 633 | body[total] = '\0'; | ||
| 634 | |||
| 635 | cJSON *json = cJSON_Parse(body); | ||
| 636 | free(body); | ||
| 637 | if (!json) { | ||
| 638 | httpd_resp_set_type(req, "application/json"); | ||
| 639 | httpd_resp_send(req, "{\"ok\":false,\"error\":\"invalid JSON\"}", HTTPD_RESP_USE_STRLEN); | ||
| 640 | return ESP_OK; | ||
| 641 | } | ||
| 642 | |||
| 643 | cJSON *ssid_item = cJSON_GetObjectItem(json, "ssid"); | ||
| 644 | cJSON *pass_item = cJSON_GetObjectItem(json, "password"); | ||
| 645 | if (!ssid_item || !cJSON_IsString(ssid_item)) { | ||
| 646 | cJSON_Delete(json); | ||
| 647 | httpd_resp_set_type(req, "application/json"); | ||
| 648 | httpd_resp_send(req, "{\"ok\":false,\"error\":\"missing ssid\"}", HTTPD_RESP_USE_STRLEN); | ||
| 649 | return ESP_OK; | ||
| 650 | } | ||
| 651 | |||
| 652 | const char *ssid = ssid_item->valuestring; | ||
| 653 | const char *password = (pass_item && cJSON_IsString(pass_item)) ? pass_item->valuestring : ""; | ||
| 654 | |||
| 655 | esp_err_t err = tollgate_config_add_wifi(ssid, password); | ||
| 656 | if (err != ESP_OK) { | ||
| 657 | cJSON_Delete(json); | ||
| 658 | httpd_resp_set_type(req, "application/json"); | ||
| 659 | httpd_resp_send(req, "{\"ok\":false,\"error\":\"save failed\"}", HTTPD_RESP_USE_STRLEN); | ||
| 660 | return ESP_OK; | ||
| 661 | } | ||
| 662 | |||
| 663 | wifi_config_t wifi_cfg = {0}; | ||
| 664 | strncpy((char *)wifi_cfg.sta.ssid, ssid, sizeof(wifi_cfg.sta.ssid) - 1); | ||
| 665 | strncpy((char *)wifi_cfg.sta.password, password, sizeof(wifi_cfg.sta.password) - 1); | ||
| 666 | wifi_cfg.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK; | ||
| 667 | esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg); | ||
| 668 | esp_wifi_connect(); | ||
| 669 | |||
| 670 | cJSON_Delete(json); | ||
| 671 | |||
| 672 | httpd_resp_set_type(req, "application/json"); | ||
| 673 | httpd_resp_send(req, "{\"ok\":true}", HTTPD_RESP_USE_STRLEN); | ||
| 674 | return ESP_OK; | ||
| 675 | } | ||
| 676 | |||
| 677 | static esp_err_t wifi_status_handler(httpd_req_t *req) { | ||
| 678 | wifi_ap_record_t ap_info; | ||
| 679 | bool connected = (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK); | ||
| 680 | |||
| 681 | cJSON *root = cJSON_CreateObject(); | ||
| 682 | cJSON_AddBoolToObject(root, "connected", connected); | ||
| 683 | |||
| 684 | if (connected) { | ||
| 685 | esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); | ||
| 686 | if (netif) { | ||
| 687 | esp_netif_ip_info_t ip_info; | ||
| 688 | if (esp_netif_get_ip_info(netif, &ip_info) == ESP_OK) { | ||
| 689 | char ip_str[16]; | ||
| 690 | snprintf(ip_str, sizeof(ip_str), IPSTR, IP2STR(&ip_info.ip)); | ||
| 691 | cJSON_AddStringToObject(root, "ip", ip_str); | ||
| 692 | } | ||
| 693 | } | ||
| 694 | cJSON_AddStringToObject(root, "ssid", (const char *)ap_info.ssid); | ||
| 695 | } | ||
| 696 | |||
| 697 | char *json = cJSON_PrintUnformatted(root); | ||
| 698 | httpd_resp_set_type(req, "application/json"); | ||
| 699 | httpd_resp_send(req, json, strlen(json)); | ||
| 700 | cJSON_free(json); | ||
| 701 | cJSON_Delete(root); | ||
| 702 | return ESP_OK; | ||
| 703 | } | ||
| 704 | |||
| 705 | static const httpd_uri_t uri_setup = { .uri = "/setup", .method = HTTP_GET, .handler = setup_page_handler }; | ||
| 706 | static const httpd_uri_t uri_wifi_scan = { .uri = "/wifi/scan", .method = HTTP_GET, .handler = wifi_scan_handler }; | ||
| 707 | static const httpd_uri_t uri_wifi_connect = { .uri = "/wifi/connect", .method = HTTP_POST, .handler = wifi_connect_handler }; | ||
| 708 | static const httpd_uri_t uri_wifi_status = { .uri = "/wifi/status", .method = HTTP_GET, .handler = wifi_status_handler }; | ||
| 372 | 709 | ||
| 373 | esp_err_t captive_portal_start(const char *ap_ip_str) | 710 | esp_err_t captive_portal_start(const char *ap_ip_str) |
| 374 | { | 711 | { |
| 712 | s_start_called = true; | ||
| 375 | if (s_server) return ESP_OK; | 713 | if (s_server) return ESP_OK; |
| 714 | if (!ap_ip_str) return ESP_ERR_INVALID_ARG; | ||
| 376 | strncpy(s_ap_ip_str, ap_ip_str, sizeof(s_ap_ip_str) - 1); | 715 | strncpy(s_ap_ip_str, ap_ip_str, sizeof(s_ap_ip_str) - 1); |
| 377 | 716 | ||
| 378 | httpd_config_t config = HTTPD_DEFAULT_CONFIG(); | 717 | httpd_config_t config = HTTPD_DEFAULT_CONFIG(); |
| 379 | config.max_uri_handlers = 20; | 718 | config.max_uri_handlers = 20; |
| 380 | config.uri_match_fn = httpd_uri_match_wildcard; | ||
| 381 | 719 | ||
| 382 | esp_err_t ret = httpd_start(&s_server, &config); | 720 | s_start_result = httpd_start(&s_server, &config); |
| 383 | if (ret != ESP_OK) { | 721 | if (s_start_result != ESP_OK) { |
| 384 | ESP_LOGE(TAG, "Failed to start HTTP server: %s", esp_err_to_name(ret)); | 722 | ESP_LOGE(TAG, "Failed to start HTTP server on port %d: %s", config.server_port, esp_err_to_name(s_start_result)); |
| 385 | return ret; | 723 | return s_start_result; |
| 386 | } | 724 | } |
| 387 | 725 | ||
| 388 | httpd_register_uri_handler(s_server, &uri_portal); | 726 | httpd_register_uri_handler(s_server, &uri_portal); |
| @@ -398,12 +736,24 @@ esp_err_t captive_portal_start(const char *ap_ip_str) | |||
| 398 | httpd_register_uri_handler(s_server, &uri_ncsi); | 736 | httpd_register_uri_handler(s_server, &uri_ncsi); |
| 399 | httpd_register_uri_handler(s_server, &uri_connecttest); | 737 | httpd_register_uri_handler(s_server, &uri_connecttest); |
| 400 | httpd_register_uri_handler(s_server, &uri_wpad); | 738 | httpd_register_uri_handler(s_server, &uri_wpad); |
| 401 | httpd_register_uri_handler(s_server, &uri_catchall); | 739 | httpd_register_uri_handler(s_server, &uri_setup); |
| 740 | esp_err_t reg_ret; | ||
| 741 | reg_ret = httpd_register_uri_handler(s_server, &uri_wifi_scan); | ||
| 742 | ESP_LOGI(TAG, "Registered /wifi/scan: %s", esp_err_to_name(reg_ret)); | ||
| 743 | reg_ret = httpd_register_uri_handler(s_server, &uri_wifi_connect); | ||
| 744 | ESP_LOGI(TAG, "Registered /wifi/connect: %s", esp_err_to_name(reg_ret)); | ||
| 745 | reg_ret = httpd_register_uri_handler(s_server, &uri_wifi_status); | ||
| 746 | ESP_LOGI(TAG, "Registered /wifi/status: %s", esp_err_to_name(reg_ret)); | ||
| 747 | |||
| 748 | httpd_register_err_handler(s_server, HTTPD_404_NOT_FOUND, catchall_err_handler); | ||
| 402 | 749 | ||
| 403 | ESP_LOGI(TAG, "Captive portal started on port 80"); | 750 | ESP_LOGI(TAG, "Captive portal started on port 80"); |
| 404 | return ESP_OK; | 751 | return s_start_result; |
| 405 | } | 752 | } |
| 406 | 753 | ||
| 754 | bool captive_portal_was_start_called(void) { return s_start_called; } | ||
| 755 | esp_err_t captive_portal_get_start_result(void) { return s_start_result; } | ||
| 756 | |||
| 407 | void captive_portal_stop(void) | 757 | void captive_portal_stop(void) |
| 408 | { | 758 | { |
| 409 | if (s_server) { | 759 | if (s_server) { |
diff --git a/main/captive_portal.h b/main/captive_portal.h index 06eb860..c3aa923 100644 --- a/main/captive_portal.h +++ b/main/captive_portal.h | |||
| @@ -7,5 +7,8 @@ | |||
| 7 | esp_err_t captive_portal_start(const char *ap_ip_str); | 7 | esp_err_t captive_portal_start(const char *ap_ip_str); |
| 8 | void captive_portal_stop(void); | 8 | void captive_portal_stop(void); |
| 9 | httpd_handle_t captive_portal_get_server(void); | 9 | httpd_handle_t captive_portal_get_server(void); |
| 10 | bool captive_portal_is_setup_available(void); | ||
| 11 | bool captive_portal_was_start_called(void); | ||
| 12 | esp_err_t captive_portal_get_start_result(void); | ||
| 10 | 13 | ||
| 11 | #endif | 14 | #endif |
diff --git a/main/cashu.c b/main/cashu.c index da12ff9..4bcda4d 100644 --- a/main/cashu.c +++ b/main/cashu.c | |||
| @@ -199,6 +199,7 @@ esp_err_t cashu_check_proof_states(const char *mint_url, const cashu_token_t *to | |||
| 199 | if (!client) { free(post_body); free(resp_buf); return ESP_FAIL; } | 199 | if (!client) { free(post_body); free(resp_buf); return ESP_FAIL; } |
| 200 | 200 | ||
| 201 | esp_http_client_set_header(client, "Content-Type", "application/json"); | 201 | esp_http_client_set_header(client, "Content-Type", "application/json"); |
| 202 | |||
| 202 | esp_err_t err = esp_http_client_open(client, strlen(post_body)); | 203 | esp_err_t err = esp_http_client_open(client, strlen(post_body)); |
| 203 | if (err != ESP_OK) { | 204 | if (err != ESP_OK) { |
| 204 | ESP_LOGE(TAG, "checkstate open failed: %s", esp_err_to_name(err)); | 205 | ESP_LOGE(TAG, "checkstate open failed: %s", esp_err_to_name(err)); |
diff --git a/main/config.c b/main/config.c index 6644b3a..2edb6da 100644 --- a/main/config.c +++ b/main/config.c | |||
| @@ -36,7 +36,7 @@ esp_err_t tollgate_config_init(void) | |||
| 36 | g_config.payout.recipient_count = 0; | 36 | g_config.payout.recipient_count = 0; |
| 37 | g_config.payout.mint_count = 0; | 37 | g_config.payout.mint_count = 0; |
| 38 | g_config.cvm_enabled = true; | 38 | g_config.cvm_enabled = true; |
| 39 | strncpy(g_config.cvm_relays, "wss://relay.primal.net", sizeof(g_config.cvm_relays) - 1); | 39 | strncpy(g_config.cvm_relays, "wss://nos.lol", sizeof(g_config.cvm_relays) - 1); |
| 40 | strncpy(g_config.wifi_auth_mode, "WPA2", sizeof(g_config.wifi_auth_mode) - 1); | 40 | strncpy(g_config.wifi_auth_mode, "WPA2", sizeof(g_config.wifi_auth_mode) - 1); |
| 41 | g_config.display_enabled = true; | 41 | g_config.display_enabled = true; |
| 42 | g_config.nostr_sync_interval_s = 1800; | 42 | g_config.nostr_sync_interval_s = 1800; |
| @@ -46,6 +46,9 @@ esp_err_t tollgate_config_init(void) | |||
| 46 | g_config.stratum_port = 3333; | 46 | g_config.stratum_port = 3333; |
| 47 | g_config.mining_port = 3334; | 47 | g_config.mining_port = 3334; |
| 48 | g_config.mining_sandbox_mint_access = true; | 48 | g_config.mining_sandbox_mint_access = true; |
| 49 | g_config.market_enabled = true; | ||
| 50 | g_config.market_scan_interval_s = 30; | ||
| 51 | g_config.client_auto_switch = false; | ||
| 49 | 52 | ||
| 50 | esp_vfs_spiffs_conf_t conf = { | 53 | esp_vfs_spiffs_conf_t conf = { |
| 51 | .base_path = "/spiffs", | 54 | .base_path = "/spiffs", |
| @@ -68,8 +71,8 @@ esp_err_t tollgate_config_init(void) | |||
| 68 | "{\"ssid\":\"c03rad0r\",\"password\":\"c03rad0r123\"}" | 71 | "{\"ssid\":\"c03rad0r\",\"password\":\"c03rad0r123\"}" |
| 69 | "]," | 72 | "]," |
| 70 | "\"ap_password\":\"\"," | 73 | "\"ap_password\":\"\"," |
| 71 | "\"mint_url\":\"https://testnut.cashu.space\"," | 74 | "\"mint_url\":\"https://testnut-nutshell.mints.orangesync.tech\"," |
| 72 | "\"accepted_mints\":[\"https://testnut.cashu.space\"]," | 75 | "\"accepted_mints\":[\"https://testnut-nutshell.mints.orangesync.tech\"]," |
| 73 | "\"price_per_step\":21," | 76 | "\"price_per_step\":21," |
| 74 | "\"step_size_ms\":60000," | 77 | "\"step_size_ms\":60000," |
| 75 | "\"nostr_geohash\":\"u281w0dfz\"," | 78 | "\"nostr_geohash\":\"u281w0dfz\"," |
| @@ -321,26 +324,6 @@ esp_err_t tollgate_config_init(void) | |||
| 321 | g_config.payout.mint_count = 1; | 324 | g_config.payout.mint_count = 1; |
| 322 | } | 325 | } |
| 323 | 326 | ||
| 324 | cJSON *seed_relays = cJSON_GetObjectItem(root, "nostr_seed_relays"); | ||
| 325 | if (seed_relays && cJSON_IsArray(seed_relays)) { | ||
| 326 | int srcount = cJSON_GetArraySize(seed_relays); | ||
| 327 | if (srcount > TOLLGATE_MAX_SEED_RELAYS) srcount = TOLLGATE_MAX_SEED_RELAYS; | ||
| 328 | for (int i = 0; i < srcount; i++) { | ||
| 329 | cJSON *r = cJSON_GetArrayItem(seed_relays, i); | ||
| 330 | if (r && cJSON_IsString(r)) { | ||
| 331 | strncpy(g_config.nostr_seed_relays[i], r->valuestring, | ||
| 332 | sizeof(g_config.nostr_seed_relays[i]) - 1); | ||
| 333 | g_config.nostr_seed_relay_count++; | ||
| 334 | } | ||
| 335 | } | ||
| 336 | } | ||
| 337 | |||
| 338 | cJSON *sync_interval = cJSON_GetObjectItem(root, "nostr_sync_interval_s"); | ||
| 339 | if (sync_interval) g_config.nostr_sync_interval_s = sync_interval->valueint; | ||
| 340 | |||
| 341 | cJSON *fallback_interval = cJSON_GetObjectItem(root, "nostr_fallback_sync_interval_s"); | ||
| 342 | if (fallback_interval) g_config.nostr_fallback_sync_interval_s = fallback_interval->valueint; | ||
| 343 | |||
| 344 | cJSON *mining = cJSON_GetObjectItem(root, "mining"); | 327 | cJSON *mining = cJSON_GetObjectItem(root, "mining"); |
| 345 | if (mining && cJSON_IsObject(mining)) { | 328 | if (mining && cJSON_IsObject(mining)) { |
| 346 | cJSON *m_en = cJSON_GetObjectItem(mining, "enabled"); | 329 | cJSON *m_en = cJSON_GetObjectItem(mining, "enabled"); |
| @@ -381,6 +364,15 @@ esp_err_t tollgate_config_init(void) | |||
| 381 | if (m_sandbox && cJSON_IsBool(m_sandbox)) g_config.mining_sandbox_mint_access = cJSON_IsTrue(m_sandbox); | 364 | if (m_sandbox && cJSON_IsBool(m_sandbox)) g_config.mining_sandbox_mint_access = cJSON_IsTrue(m_sandbox); |
| 382 | } | 365 | } |
| 383 | 366 | ||
| 367 | cJSON *market_enabled = cJSON_GetObjectItem(root, "market_enabled"); | ||
| 368 | if (market_enabled && cJSON_IsBool(market_enabled)) g_config.market_enabled = cJSON_IsTrue(market_enabled); | ||
| 369 | |||
| 370 | cJSON *market_scan_interval = cJSON_GetObjectItem(root, "market_scan_interval_s"); | ||
| 371 | if (market_scan_interval) g_config.market_scan_interval_s = market_scan_interval->valueint; | ||
| 372 | |||
| 373 | cJSON *client_auto_switch = cJSON_GetObjectItem(root, "client_auto_switch"); | ||
| 374 | if (client_auto_switch && cJSON_IsBool(client_auto_switch)) g_config.client_auto_switch = cJSON_IsTrue(client_auto_switch); | ||
| 375 | |||
| 384 | cJSON_Delete(root); | 376 | cJSON_Delete(root); |
| 385 | 377 | ||
| 386 | if (g_config.payout.recipient_count == 0) { | 378 | if (g_config.payout.recipient_count == 0) { |
| @@ -483,3 +475,55 @@ void tollgate_config_derive_unique(tollgate_config_t *cfg) | |||
| 483 | ESP_LOGI(TAG, "Unique config derived from nsec: SSID='%s', AP_IP=%s", | 475 | ESP_LOGI(TAG, "Unique config derived from nsec: SSID='%s', AP_IP=%s", |
| 484 | cfg->ap_ssid, cfg->ap_ip_str); | 476 | cfg->ap_ssid, cfg->ap_ip_str); |
| 485 | } | 477 | } |
| 478 | |||
| 479 | esp_err_t tollgate_config_add_wifi(const char *ssid, const char *password) { | ||
| 480 | if (!ssid || !password) return ESP_ERR_INVALID_ARG; | ||
| 481 | if (g_config.network_count >= TOLLGATE_MAX_WIFI_NETWORKS) return ESP_ERR_NO_MEM; | ||
| 482 | |||
| 483 | strncpy(g_config.networks[g_config.network_count].ssid, ssid, | ||
| 484 | sizeof(g_config.networks[g_config.network_count].ssid) - 1); | ||
| 485 | strncpy(g_config.networks[g_config.network_count].password, password, | ||
| 486 | sizeof(g_config.networks[g_config.network_count].password) - 1); | ||
| 487 | g_config.network_count++; | ||
| 488 | |||
| 489 | cJSON *root = cJSON_CreateObject(); | ||
| 490 | cJSON_AddStringToObject(root, "nsec", g_config.nsec); | ||
| 491 | |||
| 492 | cJSON *networks = cJSON_CreateArray(); | ||
| 493 | for (int i = 0; i < g_config.network_count; i++) { | ||
| 494 | cJSON *net = cJSON_CreateObject(); | ||
| 495 | cJSON_AddStringToObject(net, "ssid", g_config.networks[i].ssid); | ||
| 496 | cJSON_AddStringToObject(net, "password", g_config.networks[i].password); | ||
| 497 | cJSON_AddItemToArray(networks, net); | ||
| 498 | } | ||
| 499 | cJSON_AddItemToObject(root, "wifi_networks", networks); | ||
| 500 | |||
| 501 | if (g_config.ap_password[0]) | ||
| 502 | cJSON_AddStringToObject(root, "ap_password", g_config.ap_password); | ||
| 503 | cJSON_AddStringToObject(root, "mint_url", g_config.mint_url); | ||
| 504 | cJSON_AddNumberToObject(root, "price_per_step", g_config.price_per_step); | ||
| 505 | cJSON_AddNumberToObject(root, "step_size_ms", g_config.step_size_ms); | ||
| 506 | |||
| 507 | if (g_config.metric[0]) | ||
| 508 | cJSON_AddStringToObject(root, "metric", g_config.metric); | ||
| 509 | if (g_config.nostr_geohash[0]) | ||
| 510 | cJSON_AddStringToObject(root, "nostr_geohash", g_config.nostr_geohash); | ||
| 511 | |||
| 512 | cJSON *relays = cJSON_CreateArray(); | ||
| 513 | for (int i = 0; i < g_config.nostr_relay_count; i++) { | ||
| 514 | cJSON_AddItemToArray(relays, cJSON_CreateString(g_config.nostr_relays[i])); | ||
| 515 | } | ||
| 516 | cJSON_AddItemToObject(root, "nostr_relays", relays); | ||
| 517 | |||
| 518 | FILE *f = fopen("/spiffs/config.json", "w"); | ||
| 519 | if (f) { | ||
| 520 | char *json = cJSON_PrintUnformatted(root); | ||
| 521 | fputs(json, f); | ||
| 522 | free(json); | ||
| 523 | fclose(f); | ||
| 524 | } | ||
| 525 | cJSON_Delete(root); | ||
| 526 | |||
| 527 | ESP_LOGI(TAG, "WiFi network added: %s (total: %d)", ssid, g_config.network_count); | ||
| 528 | return ESP_OK; | ||
| 529 | } | ||
diff --git a/main/config.h b/main/config.h index 50f7efb..3092306 100644 --- a/main/config.h +++ b/main/config.h | |||
| @@ -105,5 +105,6 @@ esp_err_t tollgate_config_init(void); | |||
| 105 | const tollgate_config_t *tollgate_config_get(void); | 105 | const tollgate_config_t *tollgate_config_get(void); |
| 106 | esp_err_t tollgate_config_get_wifi(wifi_config_t *wifi_config); | 106 | esp_err_t tollgate_config_get_wifi(wifi_config_t *wifi_config); |
| 107 | esp_err_t tollgate_config_get_next_wifi(wifi_config_t *wifi_config); | 107 | esp_err_t tollgate_config_get_next_wifi(wifi_config_t *wifi_config); |
| 108 | esp_err_t tollgate_config_add_wifi(const char *ssid, const char *password); | ||
| 108 | 109 | ||
| 109 | #endif | 110 | #endif |
diff --git a/main/cvm_server.c b/main/cvm_server.c index f3a5ab8..1ac5cb6 100644 --- a/main/cvm_server.c +++ b/main/cvm_server.c | |||
| @@ -12,6 +12,7 @@ | |||
| 12 | #include "esp_tls.h" | 12 | #include "esp_tls.h" |
| 13 | #include "esp_crt_bundle.h" | 13 | #include "esp_crt_bundle.h" |
| 14 | #include "esp_random.h" | 14 | #include "esp_random.h" |
| 15 | #include "esp_heap_caps.h" | ||
| 15 | #include "freertos/FreeRTOS.h" | 16 | #include "freertos/FreeRTOS.h" |
| 16 | #include "freertos/task.h" | 17 | #include "freertos/task.h" |
| 17 | #include <string.h> | 18 | #include <string.h> |
| @@ -150,7 +151,7 @@ static esp_err_t ws_connect(const char *relay_url, esp_tls_t **tls_out) | |||
| 150 | 151 | ||
| 151 | esp_tls_cfg_t tls_cfg = { | 152 | esp_tls_cfg_t tls_cfg = { |
| 152 | .crt_bundle_attach = esp_crt_bundle_attach, | 153 | .crt_bundle_attach = esp_crt_bundle_attach, |
| 153 | .timeout_ms = 15000, | 154 | .timeout_ms = 5000, |
| 154 | }; | 155 | }; |
| 155 | esp_tls_t *tls = esp_tls_init(); | 156 | esp_tls_t *tls = esp_tls_init(); |
| 156 | if (!tls) return ESP_ERR_NO_MEM; | 157 | if (!tls) return ESP_ERR_NO_MEM; |
| @@ -507,7 +508,9 @@ static esp_err_t subscribe_to_relay(esp_tls_t *tls, const char *npub) | |||
| 507 | cJSON *kinds = cJSON_CreateArray(); | 508 | cJSON *kinds = cJSON_CreateArray(); |
| 508 | cJSON_AddItemToArray(kinds, cJSON_CreateNumber(25910)); | 509 | cJSON_AddItemToArray(kinds, cJSON_CreateNumber(25910)); |
| 509 | cJSON_AddItemToObject(filter, "kinds", kinds); | 510 | cJSON_AddItemToObject(filter, "kinds", kinds); |
| 510 | cJSON_AddStringToObject(filter, "#p", npub); | 511 | cJSON *p_tag = cJSON_CreateArray(); |
| 512 | cJSON_AddItemToArray(p_tag, cJSON_CreateString(npub)); | ||
| 513 | cJSON_AddItemToObject(filter, "#p", p_tag); | ||
| 511 | cJSON_AddNumberToObject(filter, "limit", 100); | 514 | cJSON_AddNumberToObject(filter, "limit", 100); |
| 512 | cJSON_AddItemToArray(sub, filter); | 515 | cJSON_AddItemToArray(sub, filter); |
| 513 | 516 | ||
| @@ -556,33 +559,33 @@ static void cvm_relay_task(void *arg) | |||
| 556 | } | 559 | } |
| 557 | 560 | ||
| 558 | int64_t last_ping_time = (int64_t)(xTaskGetTickCount() * portTICK_PERIOD_MS) / 1000; | 561 | int64_t last_ping_time = (int64_t)(xTaskGetTickCount() * portTICK_PERIOD_MS) / 1000; |
| 559 | int consecutive_timeouts = 0; | ||
| 560 | while (g_running) { | 562 | while (g_running) { |
| 561 | int rlen = esp_tls_conn_read(tls, buf, CVM_WS_BUF_SIZE - 1); | 563 | int rlen = esp_tls_conn_read(tls, buf, CVM_WS_BUF_SIZE - 1); |
| 562 | if (rlen < 0) { | ||
| 563 | ESP_LOGW(TAG, "Read error on %s (rlen=%d)", relay_url, rlen); | ||
| 564 | break; | ||
| 565 | } | ||
| 566 | if (rlen == 0) { | 564 | if (rlen == 0) { |
| 565 | ESP_LOGW(TAG, "Connection closed by relay"); | ||
| 567 | break; | 566 | break; |
| 568 | } else { | 567 | } |
| 569 | consecutive_timeouts = 0; | 568 | if (rlen < 0) { |
| 570 | if ((buf[0] & 0x0F) == 0x01) { | 569 | vTaskDelay(pdMS_TO_TICKS(100)); |
| 571 | char *text = parse_ws_text_frame(buf, rlen); | 570 | continue; |
| 572 | if (text) { | 571 | } |
| 573 | if (strlen(text) > 0) { | 572 | |
| 574 | process_relay_message(relay_url, text); | 573 | ESP_LOGI(TAG, "WS frame received: %d bytes, opcode=0x%02x", rlen, buf[0] & 0x0F); |
| 575 | } | 574 | |
| 576 | free(text); | 575 | if ((buf[0] & 0x0F) == 0x01) { |
| 576 | char *text = parse_ws_text_frame(buf, rlen); | ||
| 577 | if (text) { | ||
| 578 | if (strlen(text) > 0) { | ||
| 579 | process_relay_message(relay_url, text); | ||
| 577 | } | 580 | } |
| 578 | } else if ((buf[0] & 0x0F) == 0x09) { | 581 | free(text); |
| 579 | ESP_LOGD(TAG, "Relay ping received, sending pong"); | ||
| 580 | uint8_t pong[2] = {0x8A, 0x00}; | ||
| 581 | esp_tls_conn_write(tls, pong, 2); | ||
| 582 | } else if ((buf[0] & 0x0F) == 0x08) { | ||
| 583 | ESP_LOGW(TAG, "Relay sent close frame"); | ||
| 584 | break; | ||
| 585 | } | 582 | } |
| 583 | } else if ((buf[0] & 0x0F) == 0x09) { | ||
| 584 | uint8_t pong[2] = {0x8A, 0x00}; | ||
| 585 | esp_tls_conn_write(tls, pong, 2); | ||
| 586 | } else if ((buf[0] & 0x0F) == 0x08) { | ||
| 587 | ESP_LOGW(TAG, "Relay sent close frame"); | ||
| 588 | break; | ||
| 586 | } | 589 | } |
| 587 | 590 | ||
| 588 | int64_t now = (int64_t)(xTaskGetTickCount() * portTICK_PERIOD_MS) / 1000; | 591 | int64_t now = (int64_t)(xTaskGetTickCount() * portTICK_PERIOD_MS) / 1000; |
| @@ -590,7 +593,6 @@ static void cvm_relay_task(void *arg) | |||
| 590 | uint8_t ping[2] = {0x89, 0x00}; | 593 | uint8_t ping[2] = {0x89, 0x00}; |
| 591 | esp_tls_conn_write(tls, ping, 2); | 594 | esp_tls_conn_write(tls, ping, 2); |
| 592 | last_ping_time = now; | 595 | last_ping_time = now; |
| 593 | ESP_LOGD(TAG, "Sent WS keepalive ping"); | ||
| 594 | } | 596 | } |
| 595 | } | 597 | } |
| 596 | 598 | ||
| @@ -726,8 +728,20 @@ void cvm_server_start(void) | |||
| 726 | const tollgate_config_t *cfg = tollgate_config_get(); | 728 | const tollgate_config_t *cfg = tollgate_config_get(); |
| 727 | const char *relay = (cfg->cvm_relays[0]) ? cfg->cvm_relays : "wss://relay.primal.net"; | 729 | const char *relay = (cfg->cvm_relays[0]) ? cfg->cvm_relays : "wss://relay.primal.net"; |
| 728 | 730 | ||
| 731 | ESP_LOGI(TAG, "Starting CVM relay task (free internal: %u, largest: %u)", | ||
| 732 | heap_caps_get_free_size(MALLOC_CAP_INTERNAL), | ||
| 733 | heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL)); | ||
| 734 | |||
| 729 | char *relay_copy = strdup(relay); | 735 | char *relay_copy = strdup(relay); |
| 730 | xTaskCreate(cvm_relay_task, "cvm_relay", 16384, relay_copy, 5, &g_task); | 736 | BaseType_t ret = xTaskCreatePinnedToCore(cvm_relay_task, "cvm_relay", 16384, relay_copy, 5, &g_task, 1); |
| 737 | if (ret != pdPASS) { | ||
| 738 | ESP_LOGE(TAG, "Failed to create cvm_relay task (ret=%d, free internal: %u)", | ||
| 739 | ret, heap_caps_get_free_size(MALLOC_CAP_INTERNAL)); | ||
| 740 | g_running = false; | ||
| 741 | free(relay_copy); | ||
| 742 | return; | ||
| 743 | } | ||
| 744 | ESP_LOGI(TAG, "CVM relay task created on core %d", xPortGetCoreID()); | ||
| 731 | } | 745 | } |
| 732 | 746 | ||
| 733 | void cvm_server_stop(void) | 747 | void cvm_server_stop(void) |
diff --git a/main/display.c b/main/display.c index 2b6cc88..ccd08b7 100644 --- a/main/display.c +++ b/main/display.c | |||
| @@ -2,7 +2,10 @@ | |||
| 2 | #include "axs15231b.h" | 2 | #include "axs15231b.h" |
| 3 | #include "qrcoded.h" | 3 | #include "qrcoded.h" |
| 4 | #include "font.h" | 4 | #include "font.h" |
| 5 | #include "nucula_wallet.h" | ||
| 6 | #include "config.h" | ||
| 5 | #include "esp_log.h" | 7 | #include "esp_log.h" |
| 8 | #include "esp_wifi.h" | ||
| 6 | #include "freertos/FreeRTOS.h" | 9 | #include "freertos/FreeRTOS.h" |
| 7 | #include "freertos/task.h" | 10 | #include "freertos/task.h" |
| 8 | #include <string.h> | 11 | #include <string.h> |
| @@ -12,15 +15,36 @@ | |||
| 12 | static const char *TAG = "display"; | 15 | static const char *TAG = "display"; |
| 13 | 16 | ||
| 14 | #define QR_CYCLE_MS 5000 | 17 | #define QR_CYCLE_MS 5000 |
| 18 | #define RENDER_INTERVAL_MS 2000 | ||
| 19 | |||
| 20 | #define COLOR_BG 0x0000 | ||
| 21 | #define COLOR_WHITE 0xFFFF | ||
| 22 | #define COLOR_CYAN 0x07FF | ||
| 23 | #define COLOR_YELLOW 0xFFE0 | ||
| 24 | #define COLOR_GREEN 0x07E0 | ||
| 25 | #define COLOR_ORANGE 0xFD20 | ||
| 26 | #define COLOR_RED 0xF800 | ||
| 27 | #define COLOR_DIM 0x8410 | ||
| 15 | 28 | ||
| 16 | static volatile display_state_t s_state = DISPLAY_BOOT; | 29 | static volatile display_state_t s_state = DISPLAY_BOOT; |
| 17 | static char s_ap_ssid[32] = ""; | 30 | static char s_ap_ssid[32] = ""; |
| 18 | static char s_portal_url[256] = ""; | 31 | static char s_portal_url[256] = ""; |
| 32 | static char s_mint_url[256] = ""; | ||
| 33 | static char s_wifi_status[32] = "starting..."; | ||
| 19 | static int s_active_clients = 0; | 34 | static int s_active_clients = 0; |
| 20 | static uint64_t s_wallet_balance = 0; | 35 | static uint64_t s_wallet_balance = 0; |
| 36 | static int s_price_per_step = 0; | ||
| 21 | static bool s_initialized = false; | 37 | static bool s_initialized = false; |
| 22 | static int64_t s_last_qr_switch = 0; | 38 | static int64_t s_last_qr_switch = 0; |
| 23 | static display_qr_mode_t s_qr_mode = DISPLAY_QR_WIFI; | 39 | static display_qr_mode_t s_qr_mode = DISPLAY_QR_WIFI; |
| 40 | static int s_last_payment_sats = 0; | ||
| 41 | static int64_t s_last_allotment_ms = 0; | ||
| 42 | |||
| 43 | static uint16_t wallet_color(void) { | ||
| 44 | if (s_wallet_balance == 0) return COLOR_RED; | ||
| 45 | if (s_wallet_balance < 100) return COLOR_YELLOW; | ||
| 46 | return COLOR_GREEN; | ||
| 47 | } | ||
| 24 | 48 | ||
| 25 | static int qr_version_from_strlen(int len) { | 49 | static int qr_version_from_strlen(int len) { |
| 26 | if (len <= 17) return 1; | 50 | if (len <= 17) return 1; |
| @@ -59,10 +83,60 @@ static int escape_wifi_field(const char *src, char *dst, int dst_size) { | |||
| 59 | return di; | 83 | return di; |
| 60 | } | 84 | } |
| 61 | 85 | ||
| 86 | static void extract_domain(const char *url, char *out, int out_size) { | ||
| 87 | const char *start = url; | ||
| 88 | if (strncmp(url, "https://", 8) == 0) start = url + 8; | ||
| 89 | else if (strncmp(url, "http://", 7) == 0) start = url + 7; | ||
| 90 | strncpy(out, start, out_size - 1); | ||
| 91 | out[out_size - 1] = '\0'; | ||
| 92 | char *slash = strchr(out, '/'); | ||
| 93 | if (slash) *slash = '\0'; | ||
| 94 | } | ||
| 95 | |||
| 62 | static void build_wifi_qr_string(char *out, int out_size) { | 96 | static void build_wifi_qr_string(char *out, int out_size) { |
| 63 | char escaped_ssid[64]; | 97 | char escaped_ssid[64]; |
| 64 | escape_wifi_field(s_ap_ssid, escaped_ssid, sizeof(escaped_ssid)); | 98 | escape_wifi_field(s_ap_ssid, escaped_ssid, sizeof(escaped_ssid)); |
| 65 | snprintf(out, out_size, "WIFI:S:%s;T:nopass;;", escaped_ssid); | 99 | const tollgate_config_t *cfg = tollgate_config_get(); |
| 100 | if (strlen(cfg->ap_password) > 0) { | ||
| 101 | char escaped_pass[128]; | ||
| 102 | escape_wifi_field(cfg->ap_password, escaped_pass, sizeof(escaped_pass)); | ||
| 103 | snprintf(out, out_size, "WIFI:S:%s;T:WPA;P:%s;;", escaped_ssid, escaped_pass); | ||
| 104 | } else { | ||
| 105 | snprintf(out, out_size, "WIFI:S:%s;T:nopass;;", escaped_ssid); | ||
| 106 | } | ||
| 107 | } | ||
| 108 | |||
| 109 | static void render_qr_at(const char *text, int x_off, int y_off, int max_w, int max_h) { | ||
| 110 | int len = strlen(text); | ||
| 111 | int version = qr_version_from_strlen(len); | ||
| 112 | int px = qr_pixel_size(len); | ||
| 113 | |||
| 114 | uint16_t buf_size = qrcode_getBufferSize(version); | ||
| 115 | uint8_t *qr_buf = (uint8_t *)malloc(buf_size); | ||
| 116 | if (!qr_buf) return; | ||
| 117 | |||
| 118 | QRCode qrcode; | ||
| 119 | if (qrcode_initText(&qrcode, qr_buf, version, ECC_LOW, text) != 0) { | ||
| 120 | free(qr_buf); | ||
| 121 | return; | ||
| 122 | } | ||
| 123 | |||
| 124 | int qr_px_w = qrcode.size * px; | ||
| 125 | int qr_px_h = qrcode.size * px; | ||
| 126 | int cx = x_off + (max_w - qr_px_w) / 2; | ||
| 127 | int cy = y_off + (max_h - qr_px_h) / 2; | ||
| 128 | if (cx < 0) cx = 0; | ||
| 129 | if (cy < 0) cy = 0; | ||
| 130 | |||
| 131 | for (int y = 0; y < qrcode.size; y++) { | ||
| 132 | for (int x = 0; x < qrcode.size; x++) { | ||
| 133 | bool mod = qrcode_getModule(&qrcode, x, y); | ||
| 134 | uint16_t color = mod ? COLOR_WHITE : COLOR_BG; | ||
| 135 | axs15231b_fill_rect(cx + x * px, cy + y * px, px, px, color); | ||
| 136 | } | ||
| 137 | } | ||
| 138 | |||
| 139 | free(qr_buf); | ||
| 66 | } | 140 | } |
| 67 | 141 | ||
| 68 | void display_render_text(int x, int y, const char *text, uint16_t fg, uint16_t bg, int scale) { | 142 | void display_render_text(int x, int y, const char *text, uint16_t fg, uint16_t bg, int scale) { |
| @@ -98,99 +172,206 @@ void display_render_text(int x, int y, const char *text, uint16_t fg, uint16_t b | |||
| 98 | } | 172 | } |
| 99 | } | 173 | } |
| 100 | 174 | ||
| 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) { | 175 | void display_render_qr(const char *text) { |
| 139 | int screen_w = axs15231b_get_width(); | 176 | int screen_w = axs15231b_get_width(); |
| 140 | int screen_h = axs15231b_get_height(); | 177 | int screen_h = axs15231b_get_height(); |
| 141 | axs15231b_fill_screen(0x0000); | 178 | axs15231b_fill_screen(COLOR_BG); |
| 142 | render_qr_at(text, 0, 0, screen_w, screen_h); | 179 | render_qr_at(text, 0, 0, screen_w, screen_h); |
| 143 | axs15231b_flush(); | 180 | axs15231b_flush(); |
| 144 | } | 181 | } |
| 145 | 182 | ||
| 146 | static void render_boot_screen(void) { | 183 | static void render_boot_screen(void) { |
| 147 | axs15231b_fill_screen(0x0000); | 184 | int screen_w = axs15231b_get_width(); |
| 148 | display_render_text(140, 100, "TollGate", 0xF79F, 0x0000, 3); | 185 | axs15231b_fill_screen(COLOR_BG); |
| 149 | display_render_text(140, 140, "starting...", 0xB5B6, 0x0000, 2); | 186 | |
| 187 | char qr_text[320]; | ||
| 188 | build_wifi_qr_string(qr_text, sizeof(qr_text)); | ||
| 189 | render_qr_at(qr_text, 0, 10, screen_w, 220); | ||
| 190 | |||
| 191 | const char *title = "TollGate"; | ||
| 192 | int title_w = strlen(title) * 8 * 2; | ||
| 193 | display_render_text((screen_w - title_w) / 2, 240, title, COLOR_CYAN, COLOR_BG, 2); | ||
| 194 | |||
| 195 | int status_w = strlen(s_wifi_status) * 8; | ||
| 196 | display_render_text((screen_w - status_w) / 2, 268, s_wifi_status, COLOR_YELLOW, COLOR_BG, 1); | ||
| 197 | |||
| 198 | snprintf(qr_text, sizeof(qr_text), "SSID: %s", s_ap_ssid); | ||
| 199 | int ssid_w = strlen(qr_text) * 8; | ||
| 200 | display_render_text((screen_w - ssid_w) / 2, 295, qr_text, COLOR_DIM, COLOR_BG, 1); | ||
| 201 | |||
| 202 | const char *hint = "Scan QR to connect"; | ||
| 203 | int hint_w = strlen(hint) * 8; | ||
| 204 | display_render_text((screen_w - hint_w) / 2, 315, hint, COLOR_DIM, COLOR_BG, 1); | ||
| 205 | |||
| 150 | axs15231b_flush(); | 206 | axs15231b_flush(); |
| 151 | } | 207 | } |
| 152 | 208 | ||
| 153 | static void render_ready_screen(void) { | 209 | static void render_ready_screen(void) { |
| 154 | axs15231b_fill_screen(0x0000); | ||
| 155 | |||
| 156 | int screen_w = axs15231b_get_width(); | 210 | int screen_w = axs15231b_get_width(); |
| 157 | int screen_h = axs15231b_get_height(); | 211 | int text_area_y = 330; |
| 158 | int text_area_y = screen_h - 55; | 212 | axs15231b_fill_screen(COLOR_BG); |
| 159 | 213 | ||
| 160 | char qr_text[320]; | 214 | char qr_text[320]; |
| 161 | const char *label; | ||
| 162 | |||
| 163 | if (s_qr_mode == DISPLAY_QR_WIFI) { | 215 | if (s_qr_mode == DISPLAY_QR_WIFI) { |
| 164 | build_wifi_qr_string(qr_text, sizeof(qr_text)); | 216 | build_wifi_qr_string(qr_text, sizeof(qr_text)); |
| 165 | label = "Scan to connect"; | ||
| 166 | } else { | 217 | } else { |
| 167 | strncpy(qr_text, s_portal_url, sizeof(qr_text) - 1); | 218 | strncpy(qr_text, s_portal_url, sizeof(qr_text) - 1); |
| 168 | qr_text[sizeof(qr_text) - 1] = '\0'; | 219 | qr_text[sizeof(qr_text) - 1] = '\0'; |
| 169 | label = "Portal URL"; | ||
| 170 | } | 220 | } |
| 171 | 221 | ||
| 172 | render_qr_at(qr_text, 0, 0, screen_w, text_area_y - 5); | 222 | render_qr_at(qr_text, 0, 5, screen_w, text_area_y - 10); |
| 223 | |||
| 224 | int y = text_area_y; | ||
| 225 | char line[48]; | ||
| 226 | |||
| 227 | if (s_qr_mode == DISPLAY_QR_WIFI) { | ||
| 228 | snprintf(line, sizeof(line), "Scan to connect"); | ||
| 229 | display_render_text(10, y, line, COLOR_CYAN, COLOR_BG, 1); | ||
| 230 | y += 16; | ||
| 231 | |||
| 232 | snprintf(line, sizeof(line), "SSID: %s", s_ap_ssid); | ||
| 233 | display_render_text(10, y, line, COLOR_WHITE, COLOR_BG, 1); | ||
| 234 | y += 16; | ||
| 235 | } else { | ||
| 236 | snprintf(line, sizeof(line), "Portal URL"); | ||
| 237 | display_render_text(10, y, line, COLOR_CYAN, COLOR_BG, 1); | ||
| 238 | y += 16; | ||
| 239 | |||
| 240 | char domain[48]; | ||
| 241 | extract_domain(s_mint_url, domain, sizeof(domain)); | ||
| 242 | snprintf(line, sizeof(line), "Mint: %.30s", domain); | ||
| 243 | display_render_text(10, y, line, COLOR_ORANGE, COLOR_BG, 1); | ||
| 244 | y += 16; | ||
| 245 | } | ||
| 246 | |||
| 247 | snprintf(line, sizeof(line), "%d sats/min", s_price_per_step); | ||
| 248 | display_render_text(10, y, line, COLOR_ORANGE, COLOR_BG, 1); | ||
| 249 | y += 16; | ||
| 250 | |||
| 251 | snprintf(line, sizeof(line), "Wallet: %llu sats", (unsigned long long)s_wallet_balance); | ||
| 252 | display_render_text(10, y, line, wallet_color(), COLOR_BG, 1); | ||
| 253 | y += 16; | ||
| 254 | |||
| 255 | if (s_active_clients > 0) { | ||
| 256 | snprintf(line, sizeof(line), "Clients: %d", s_active_clients); | ||
| 257 | display_render_text(10, y, line, COLOR_GREEN, COLOR_BG, 1); | ||
| 258 | } | ||
| 259 | |||
| 260 | axs15231b_flush(); | ||
| 261 | } | ||
| 262 | |||
| 263 | static void render_setup_pending_screen(void) { | ||
| 264 | int screen_w = axs15231b_get_width(); | ||
| 265 | axs15231b_fill_screen(COLOR_BG); | ||
| 173 | 266 | ||
| 174 | display_render_text(10, text_area_y, label, 0xB5B6, 0x0000, 2); | 267 | char qr_text[320]; |
| 268 | build_wifi_qr_string(qr_text, sizeof(qr_text)); | ||
| 269 | render_qr_at(qr_text, 0, 5, screen_w, 280); | ||
| 175 | 270 | ||
| 271 | int y = 290; | ||
| 176 | char line[64]; | 272 | char line[64]; |
| 273 | |||
| 274 | const char *title = "WiFi Setup"; | ||
| 275 | int tw = strlen(title) * 8; | ||
| 276 | display_render_text((screen_w - tw) / 2, y, title, COLOR_CYAN, COLOR_BG, 1); | ||
| 277 | y += 20; | ||
| 278 | |||
| 177 | snprintf(line, sizeof(line), "SSID: %s", s_ap_ssid); | 279 | snprintf(line, sizeof(line), "SSID: %s", s_ap_ssid); |
| 178 | display_render_text(10, text_area_y + 20, line, 0xB5B6, 0x0000, 2); | 280 | display_render_text(10, y, line, COLOR_WHITE, COLOR_BG, 1); |
| 281 | y += 18; | ||
| 282 | |||
| 283 | const char *hint1 = "1. Connect to WiFi above"; | ||
| 284 | display_render_text(10, y, hint1, COLOR_DIM, COLOR_BG, 1); | ||
| 285 | y += 16; | ||
| 286 | |||
| 287 | const char *hint2 = "2. Open browser, go to:"; | ||
| 288 | display_render_text(10, y, hint2, COLOR_DIM, COLOR_BG, 1); | ||
| 289 | y += 18; | ||
| 290 | |||
| 291 | const tollgate_config_t *cfg = tollgate_config_get(); | ||
| 292 | snprintf(line, sizeof(line), "http://%s/setup", cfg->ap_ip_str); | ||
| 293 | display_render_text(10, y, line, COLOR_YELLOW, COLOR_BG, 1); | ||
| 294 | y += 22; | ||
| 295 | |||
| 296 | const char *hint3 = "3. Configure upstream WiFi"; | ||
| 297 | display_render_text(10, y, hint3, COLOR_DIM, COLOR_BG, 1); | ||
| 179 | 298 | ||
| 180 | axs15231b_flush(); | 299 | axs15231b_flush(); |
| 181 | } | 300 | } |
| 182 | 301 | ||
| 183 | static void render_payment_screen(void) { | 302 | static void render_payment_screen(void) { |
| 184 | axs15231b_fill_screen(0x07E0); | 303 | int screen_w = axs15231b_get_width(); |
| 185 | display_render_text(140, 100, "Paid!", 0x0000, 0x07E0, 3); | 304 | axs15231b_fill_screen(COLOR_BG); |
| 186 | display_render_text(130, 140, "Access granted", 0x0000, 0x07E0, 2); | 305 | |
| 306 | axs15231b_fill_rect(0, 190, screen_w, 50, COLOR_GREEN); | ||
| 307 | const char *msg = "ACCESS GRANTED"; | ||
| 308 | int msg_w = strlen(msg) * 8 * 2; | ||
| 309 | display_render_text((screen_w - msg_w) / 2, 202, msg, COLOR_WHITE, COLOR_GREEN, 2); | ||
| 310 | |||
| 311 | char line[48]; | ||
| 312 | |||
| 313 | snprintf(line, sizeof(line), "Paid: %d sats", s_last_payment_sats); | ||
| 314 | int lw = strlen(line) * 8; | ||
| 315 | display_render_text((screen_w - lw) / 2, 270, line, COLOR_WHITE, COLOR_BG, 1); | ||
| 316 | |||
| 317 | int64_t secs = s_last_allotment_ms / 1000; | ||
| 318 | if (secs >= 60) { | ||
| 319 | snprintf(line, sizeof(line), "Time: %lld min", (long long)(secs / 60)); | ||
| 320 | } else { | ||
| 321 | snprintf(line, sizeof(line), "Time: %lld sec", (long long)secs); | ||
| 322 | } | ||
| 323 | lw = strlen(line) * 8; | ||
| 324 | display_render_text((screen_w - lw) / 2, 290, line, COLOR_WHITE, COLOR_BG, 1); | ||
| 325 | |||
| 326 | snprintf(line, sizeof(line), "Wallet: %llu sats", (unsigned long long)s_wallet_balance); | ||
| 327 | lw = strlen(line) * 8; | ||
| 328 | display_render_text((screen_w - lw) / 2, 320, line, wallet_color(), COLOR_BG, 1); | ||
| 329 | |||
| 187 | axs15231b_flush(); | 330 | axs15231b_flush(); |
| 188 | } | 331 | } |
| 189 | 332 | ||
| 190 | static void render_error_screen(void) { | 333 | static void render_error_screen(void) { |
| 191 | axs15231b_fill_screen(0xF800); | 334 | int screen_w = axs15231b_get_width(); |
| 192 | display_render_text(120, 100, "No upstream", 0xFFFF, 0xF800, 3); | 335 | axs15231b_fill_screen(COLOR_BG); |
| 193 | display_render_text(130, 140, "Check config", 0xFFFF, 0xF800, 2); | 336 | |
| 337 | char qr_text[320]; | ||
| 338 | build_wifi_qr_string(qr_text, sizeof(qr_text)); | ||
| 339 | render_qr_at(qr_text, 0, 5, screen_w, 150); | ||
| 340 | |||
| 341 | axs15231b_fill_rect(0, 160, screen_w, 36, COLOR_RED); | ||
| 342 | const char *msg = "NO UPSTREAM"; | ||
| 343 | int msg_w = strlen(msg) * 8 * 2; | ||
| 344 | display_render_text((screen_w - msg_w) / 2, 170, msg, COLOR_WHITE, COLOR_RED, 2); | ||
| 345 | |||
| 346 | char line[64]; | ||
| 347 | int lw; | ||
| 348 | int y = 210; | ||
| 349 | |||
| 350 | const char *l1 = "Internet unavailable"; | ||
| 351 | lw = strlen(l1) * 8; | ||
| 352 | display_render_text((screen_w - lw) / 2, y, l1, COLOR_WHITE, COLOR_BG, 1); | ||
| 353 | y += 20; | ||
| 354 | |||
| 355 | const char *l3 = "AP still active"; | ||
| 356 | lw = strlen(l3) * 8; | ||
| 357 | display_render_text((screen_w - lw) / 2, y, l3, COLOR_GREEN, COLOR_BG, 1); | ||
| 358 | y += 20; | ||
| 359 | |||
| 360 | snprintf(line, sizeof(line), "SSID: %s", s_ap_ssid); | ||
| 361 | lw = strlen(line) * 8; | ||
| 362 | display_render_text((screen_w - lw) / 2, y, line, COLOR_DIM, COLOR_BG, 1); | ||
| 363 | y += 20; | ||
| 364 | |||
| 365 | const tollgate_config_t *cfg = tollgate_config_get(); | ||
| 366 | snprintf(line, sizeof(line), "http://%s/setup", cfg->ap_ip_str); | ||
| 367 | lw = strlen(line) * 8; | ||
| 368 | display_render_text((screen_w - lw) / 2, y, line, COLOR_YELLOW, COLOR_BG, 1); | ||
| 369 | y += 16; | ||
| 370 | |||
| 371 | const char *hint = "Scan QR to connect"; | ||
| 372 | lw = strlen(hint) * 8; | ||
| 373 | display_render_text((screen_w - lw) / 2, y, hint, COLOR_DIM, COLOR_BG, 1); | ||
| 374 | |||
| 194 | axs15231b_flush(); | 375 | axs15231b_flush(); |
| 195 | } | 376 | } |
| 196 | 377 | ||
| @@ -200,6 +381,14 @@ static void display_task(void *pvParameters) { | |||
| 200 | while (1) { | 381 | while (1) { |
| 201 | display_state_t state = s_state; | 382 | display_state_t state = s_state; |
| 202 | 383 | ||
| 384 | if (state == DISPLAY_READY) { | ||
| 385 | int64_t now = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS; | ||
| 386 | if ((now - s_last_qr_switch) >= QR_CYCLE_MS) { | ||
| 387 | s_qr_mode = (s_qr_mode == DISPLAY_QR_WIFI) ? DISPLAY_QR_PORTAL : DISPLAY_QR_WIFI; | ||
| 388 | s_last_qr_switch = now; | ||
| 389 | } | ||
| 390 | } | ||
| 391 | |||
| 203 | switch (state) { | 392 | switch (state) { |
| 204 | case DISPLAY_BOOT: | 393 | case DISPLAY_BOOT: |
| 205 | render_boot_screen(); | 394 | render_boot_screen(); |
| @@ -209,21 +398,18 @@ static void display_task(void *pvParameters) { | |||
| 209 | break; | 398 | break; |
| 210 | case DISPLAY_PAYMENT_RECEIVED: | 399 | case DISPLAY_PAYMENT_RECEIVED: |
| 211 | render_payment_screen(); | 400 | render_payment_screen(); |
| 212 | vTaskDelay(pdMS_TO_TICKS(2000)); | 401 | vTaskDelay(pdMS_TO_TICKS(3000)); |
| 213 | s_state = DISPLAY_READY; | 402 | s_state = DISPLAY_READY; |
| 214 | break; | 403 | break; |
| 215 | case DISPLAY_ERROR: | 404 | case DISPLAY_ERROR: |
| 216 | render_error_screen(); | 405 | render_error_screen(); |
| 217 | break; | 406 | break; |
| 407 | case DISPLAY_SETUP_PENDING: | ||
| 408 | render_setup_pending_screen(); | ||
| 409 | break; | ||
| 218 | } | 410 | } |
| 219 | 411 | ||
| 220 | int64_t now = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS; | 412 | vTaskDelay(pdMS_TO_TICKS(RENDER_INTERVAL_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 | } | 413 | } |
| 228 | } | 414 | } |
| 229 | 415 | ||
| @@ -239,7 +425,7 @@ esp_err_t display_init(void) { | |||
| 239 | s_initialized = true; | 425 | s_initialized = true; |
| 240 | s_last_qr_switch = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS; | 426 | s_last_qr_switch = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS; |
| 241 | 427 | ||
| 242 | xTaskCreatePinnedToCore(display_task, "display", 16384, NULL, 2, NULL, 1); | 428 | xTaskCreatePinnedToCore(display_task, "display", 24576, NULL, 2, NULL, 1); |
| 243 | 429 | ||
| 244 | ESP_LOGI(TAG, "Display initialized"); | 430 | ESP_LOGI(TAG, "Display initialized"); |
| 245 | return ESP_OK; | 431 | return ESP_OK; |
| @@ -250,7 +436,9 @@ void display_set_state(display_state_t state) { | |||
| 250 | } | 436 | } |
| 251 | 437 | ||
| 252 | void display_update(const char *ap_ssid, int active_clients, | 438 | void display_update(const char *ap_ssid, int active_clients, |
| 253 | uint64_t wallet_balance, const char *portal_url) { | 439 | uint64_t wallet_balance, const char *portal_url, |
| 440 | const char *mint_url, int price_per_step, | ||
| 441 | const char *wifi_status) { | ||
| 254 | if (ap_ssid) { | 442 | if (ap_ssid) { |
| 255 | strncpy(s_ap_ssid, ap_ssid, sizeof(s_ap_ssid) - 1); | 443 | strncpy(s_ap_ssid, ap_ssid, sizeof(s_ap_ssid) - 1); |
| 256 | s_ap_ssid[sizeof(s_ap_ssid) - 1] = '\0'; | 444 | s_ap_ssid[sizeof(s_ap_ssid) - 1] = '\0'; |
| @@ -259,6 +447,29 @@ void display_update(const char *ap_ssid, int active_clients, | |||
| 259 | strncpy(s_portal_url, portal_url, sizeof(s_portal_url) - 1); | 447 | strncpy(s_portal_url, portal_url, sizeof(s_portal_url) - 1); |
| 260 | s_portal_url[sizeof(s_portal_url) - 1] = '\0'; | 448 | s_portal_url[sizeof(s_portal_url) - 1] = '\0'; |
| 261 | } | 449 | } |
| 450 | if (mint_url) { | ||
| 451 | strncpy(s_mint_url, mint_url, sizeof(s_mint_url) - 1); | ||
| 452 | s_mint_url[sizeof(s_mint_url) - 1] = '\0'; | ||
| 453 | } | ||
| 454 | if (wifi_status) { | ||
| 455 | strncpy(s_wifi_status, wifi_status, sizeof(s_wifi_status) - 1); | ||
| 456 | s_wifi_status[sizeof(s_wifi_status) - 1] = '\0'; | ||
| 457 | } | ||
| 458 | if (price_per_step > 0) s_price_per_step = price_per_step; | ||
| 262 | s_active_clients = active_clients; | 459 | s_active_clients = active_clients; |
| 263 | s_wallet_balance = wallet_balance; | 460 | s_wallet_balance = wallet_balance; |
| 264 | } | 461 | } |
| 462 | |||
| 463 | void display_notify_payment(int amount_sats, int64_t allotment_ms) { | ||
| 464 | s_last_payment_sats = amount_sats; | ||
| 465 | s_last_allotment_ms = allotment_ms; | ||
| 466 | s_wallet_balance = nucula_wallet_balance(); | ||
| 467 | display_set_state(DISPLAY_PAYMENT_RECEIVED); | ||
| 468 | } | ||
| 469 | |||
| 470 | void display_notify_wifi_connected(const char *ip) { | ||
| 471 | (void)ip; | ||
| 472 | } | ||
| 473 | |||
| 474 | void display_notify_wifi_disconnected(void) { | ||
| 475 | } | ||
diff --git a/main/display.h b/main/display.h index 407521b..ecb76b6 100644 --- a/main/display.h +++ b/main/display.h | |||
| @@ -9,7 +9,8 @@ typedef enum { | |||
| 9 | DISPLAY_BOOT, | 9 | DISPLAY_BOOT, |
| 10 | DISPLAY_READY, | 10 | DISPLAY_READY, |
| 11 | DISPLAY_PAYMENT_RECEIVED, | 11 | DISPLAY_PAYMENT_RECEIVED, |
| 12 | DISPLAY_ERROR | 12 | DISPLAY_ERROR, |
| 13 | DISPLAY_SETUP_PENDING | ||
| 13 | } display_state_t; | 14 | } display_state_t; |
| 14 | 15 | ||
| 15 | typedef enum { | 16 | typedef enum { |
| @@ -20,7 +21,12 @@ typedef enum { | |||
| 20 | esp_err_t display_init(void); | 21 | esp_err_t display_init(void); |
| 21 | void display_set_state(display_state_t state); | 22 | void display_set_state(display_state_t state); |
| 22 | void display_update(const char *ap_ssid, int active_clients, | 23 | void display_update(const char *ap_ssid, int active_clients, |
| 23 | uint64_t wallet_balance, const char *portal_url); | 24 | uint64_t wallet_balance, const char *portal_url, |
| 25 | const char *mint_url, int price_per_step, | ||
| 26 | const char *wifi_status); | ||
| 27 | void display_notify_payment(int amount_sats, int64_t allotment_ms); | ||
| 28 | void display_notify_wifi_connected(const char *ip); | ||
| 29 | void display_notify_wifi_disconnected(void); | ||
| 24 | void display_render_text(int x, int y, const char *text, uint16_t fg, uint16_t bg, int scale); | 30 | 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); | 31 | void display_render_qr(const char *text); |
| 26 | 32 | ||
diff --git a/main/dns_server.c b/main/dns_server.c index 15a729f..b84a4cf 100644 --- a/main/dns_server.c +++ b/main/dns_server.c | |||
| @@ -161,7 +161,7 @@ static void dns_server_task(void *arg) | |||
| 161 | struct sockaddr_in bind_addr = { | 161 | struct sockaddr_in bind_addr = { |
| 162 | .sin_family = AF_INET, | 162 | .sin_family = AF_INET, |
| 163 | .sin_port = htons(DNS_PORT), | 163 | .sin_port = htons(DNS_PORT), |
| 164 | .sin_addr.s_addr = INADDR_ANY, | 164 | .sin_addr.s_addr = s_ap_ip.addr, |
| 165 | }; | 165 | }; |
| 166 | if (bind(sock, (struct sockaddr *)&bind_addr, sizeof(bind_addr)) < 0) { | 166 | if (bind(sock, (struct sockaddr *)&bind_addr, sizeof(bind_addr)) < 0) { |
| 167 | ESP_LOGE(TAG, "Failed to bind DNS socket"); | 167 | ESP_LOGE(TAG, "Failed to bind DNS socket"); |
diff --git a/main/keyboard.c b/main/keyboard.c new file mode 100644 index 0000000..d16135f --- /dev/null +++ b/main/keyboard.c | |||
| @@ -0,0 +1,186 @@ | |||
| 1 | #include "keyboard.h" | ||
| 2 | #include <string.h> | ||
| 3 | |||
| 4 | static const char *s_alpha_lower[] = { | ||
| 5 | "qwertyuiop", | ||
| 6 | "asdfghjkl", | ||
| 7 | "\001zxcvbnm\b", | ||
| 8 | "\002\003\004" | ||
| 9 | }; | ||
| 10 | |||
| 11 | static const char *s_alpha_upper[] = { | ||
| 12 | "QWERTYUIOP", | ||
| 13 | "ASDFGHJKL", | ||
| 14 | "\001ZXCVBNM\b", | ||
| 15 | "\002\003\004" | ||
| 16 | }; | ||
| 17 | |||
| 18 | static const char *s_numsym[] = { | ||
| 19 | "1234567890", | ||
| 20 | "-/:;()$&@\"", | ||
| 21 | "\001.,?!'\\b", | ||
| 22 | "\002\003\004" | ||
| 23 | }; | ||
| 24 | |||
| 25 | #define CTRL_SHIFT '\001' | ||
| 26 | #define CTRL_LAYER '\002' | ||
| 27 | #define CTRL_SPACE '\003' | ||
| 28 | #define CTRL_DONE '\004' | ||
| 29 | #define CTRL_BS '\b' | ||
| 30 | |||
| 31 | static kb_layout_t s_layout = { | ||
| 32 | .key_w = 28, | ||
| 33 | .key_h = 36, | ||
| 34 | .key_gap = 2, | ||
| 35 | .start_y = 70, | ||
| 36 | .screen_w = 320, | ||
| 37 | .row_count = 4, | ||
| 38 | }; | ||
| 39 | |||
| 40 | void kb_state_init(kb_state_t *st) { | ||
| 41 | if (!st) return; | ||
| 42 | memset(st, 0, sizeof(*st)); | ||
| 43 | st->layer = KB_ALPHA_LOWER; | ||
| 44 | st->reveal = false; | ||
| 45 | } | ||
| 46 | |||
| 47 | void kb_set_layout(const kb_layout_t *layout) { | ||
| 48 | if (layout) s_layout = *layout; | ||
| 49 | } | ||
| 50 | |||
| 51 | const kb_layout_t *kb_get_layout(void) { | ||
| 52 | return &s_layout; | ||
| 53 | } | ||
| 54 | |||
| 55 | static const char **get_layer(kb_layer_t layer) { | ||
| 56 | switch (layer) { | ||
| 57 | case KB_ALPHA_UPPER: return s_alpha_upper; | ||
| 58 | case KB_NUMSYM: return s_numsym; | ||
| 59 | default: return s_alpha_lower; | ||
| 60 | } | ||
| 61 | } | ||
| 62 | |||
| 63 | int kb_get_row_keys(int row, kb_layer_t layer, const char **keys_out) { | ||
| 64 | if (row < 0 || row >= s_layout.row_count) { | ||
| 65 | *keys_out = NULL; | ||
| 66 | return 0; | ||
| 67 | } | ||
| 68 | const char **layer_rows = get_layer(layer); | ||
| 69 | const char *row_str = layer_rows[row]; | ||
| 70 | *keys_out = row_str; | ||
| 71 | return (int)strlen(row_str); | ||
| 72 | } | ||
| 73 | |||
| 74 | static int row_x_offset(int row, int total_keys) { | ||
| 75 | int kw = s_layout.key_w; | ||
| 76 | int gap = s_layout.key_gap; | ||
| 77 | int total_w = total_keys * kw + (total_keys - 1) * gap; | ||
| 78 | int margin = (s_layout.screen_w - total_w) / 2; | ||
| 79 | if (margin < 2) margin = 2; | ||
| 80 | switch (row) { | ||
| 81 | case 0: return margin; | ||
| 82 | case 1: return margin + kw / 2; | ||
| 83 | case 2: return margin + kw; | ||
| 84 | case 3: return margin; | ||
| 85 | default: return margin; | ||
| 86 | } | ||
| 87 | } | ||
| 88 | |||
| 89 | static int key_width_at(int row, int col, int total_keys) { | ||
| 90 | int kw = s_layout.key_w; | ||
| 91 | if (row == 3) { | ||
| 92 | int gap = s_layout.key_gap; | ||
| 93 | int margin = row_x_offset(3, total_keys); | ||
| 94 | int available = s_layout.screen_w - margin * 2; | ||
| 95 | int side_w = (available - gap) / 4; | ||
| 96 | if (col == 0) return side_w; | ||
| 97 | if (col == total_keys - 1) return side_w; | ||
| 98 | return available - side_w * 2 - gap * 2; | ||
| 99 | } | ||
| 100 | return kw; | ||
| 101 | } | ||
| 102 | |||
| 103 | kb_result_t kb_hit_test(int tx, int ty, kb_layer_t layer) { | ||
| 104 | kb_result_t result = {KB_ACTION_NONE, 0}; | ||
| 105 | int sy = s_layout.start_y; | ||
| 106 | int kw = s_layout.key_w; | ||
| 107 | int kh = s_layout.key_h; | ||
| 108 | int gap = s_layout.key_gap; | ||
| 109 | |||
| 110 | if (ty < sy || ty >= sy + s_layout.row_count * (kh + gap)) { | ||
| 111 | return result; | ||
| 112 | } | ||
| 113 | |||
| 114 | int row = (ty - sy) / (kh + gap); | ||
| 115 | if (row < 0 || row >= s_layout.row_count) return result; | ||
| 116 | |||
| 117 | const char *row_str; | ||
| 118 | int total_keys = kb_get_row_keys(row, layer, &row_str); | ||
| 119 | if (total_keys == 0) return result; | ||
| 120 | |||
| 121 | int x_off = row_x_offset(row, total_keys); | ||
| 122 | int cx = x_off; | ||
| 123 | |||
| 124 | for (int col = 0; col < total_keys; col++) { | ||
| 125 | int key_w = key_width_at(row, col, total_keys); | ||
| 126 | if (tx >= cx && tx < cx + key_w) { | ||
| 127 | char c = row_str[col]; | ||
| 128 | if (c == CTRL_SHIFT) { | ||
| 129 | result.action = KB_ACTION_SHIFT; | ||
| 130 | } else if (c == CTRL_LAYER) { | ||
| 131 | result.action = KB_ACTION_LAYER; | ||
| 132 | } else if (c == CTRL_SPACE) { | ||
| 133 | result.action = KB_ACTION_SPACE; | ||
| 134 | result.ch = ' '; | ||
| 135 | } else if (c == CTRL_DONE) { | ||
| 136 | result.action = KB_ACTION_DONE; | ||
| 137 | } else if (c == CTRL_BS) { | ||
| 138 | result.action = KB_ACTION_BACKSPACE; | ||
| 139 | } else { | ||
| 140 | result.action = KB_ACTION_CHAR; | ||
| 141 | result.ch = c; | ||
| 142 | } | ||
| 143 | return result; | ||
| 144 | } | ||
| 145 | cx += key_w + gap; | ||
| 146 | } | ||
| 147 | |||
| 148 | return result; | ||
| 149 | } | ||
| 150 | |||
| 151 | void kb_apply(kb_state_t *st, kb_result_t result) { | ||
| 152 | if (!st || result.action == KB_ACTION_NONE) return; | ||
| 153 | |||
| 154 | switch (result.action) { | ||
| 155 | case KB_ACTION_CHAR: | ||
| 156 | if (st->cursor < KB_INPUT_MAX) { | ||
| 157 | st->input[st->cursor++] = result.ch; | ||
| 158 | st->input[st->cursor] = '\0'; | ||
| 159 | } | ||
| 160 | break; | ||
| 161 | case KB_ACTION_BACKSPACE: | ||
| 162 | if (st->cursor > 0) { | ||
| 163 | st->cursor--; | ||
| 164 | st->input[st->cursor] = '\0'; | ||
| 165 | } | ||
| 166 | break; | ||
| 167 | case KB_ACTION_SHIFT: | ||
| 168 | if (st->layer == KB_ALPHA_LOWER) st->layer = KB_ALPHA_UPPER; | ||
| 169 | else if (st->layer == KB_ALPHA_UPPER) st->layer = KB_ALPHA_LOWER; | ||
| 170 | break; | ||
| 171 | case KB_ACTION_LAYER: | ||
| 172 | if (st->layer == KB_NUMSYM) st->layer = KB_ALPHA_LOWER; | ||
| 173 | else st->layer = KB_NUMSYM; | ||
| 174 | break; | ||
| 175 | case KB_ACTION_SPACE: | ||
| 176 | if (st->cursor < KB_INPUT_MAX) { | ||
| 177 | st->input[st->cursor++] = ' '; | ||
| 178 | st->input[st->cursor] = '\0'; | ||
| 179 | } | ||
| 180 | break; | ||
| 181 | case KB_ACTION_DONE: | ||
| 182 | break; | ||
| 183 | default: | ||
| 184 | break; | ||
| 185 | } | ||
| 186 | } | ||
diff --git a/main/keyboard.h b/main/keyboard.h new file mode 100644 index 0000000..9c4118f --- /dev/null +++ b/main/keyboard.h | |||
| @@ -0,0 +1,53 @@ | |||
| 1 | #ifndef KEYBOARD_H | ||
| 2 | #define KEYBOARD_H | ||
| 3 | |||
| 4 | #include <stdint.h> | ||
| 5 | #include <stdbool.h> | ||
| 6 | |||
| 7 | #define KB_INPUT_MAX 64 | ||
| 8 | |||
| 9 | typedef enum { | ||
| 10 | KB_ALPHA_LOWER, | ||
| 11 | KB_ALPHA_UPPER, | ||
| 12 | KB_NUMSYM | ||
| 13 | } kb_layer_t; | ||
| 14 | |||
| 15 | typedef enum { | ||
| 16 | KB_ACTION_NONE = 0, | ||
| 17 | KB_ACTION_CHAR, | ||
| 18 | KB_ACTION_SHIFT, | ||
| 19 | KB_ACTION_BACKSPACE, | ||
| 20 | KB_ACTION_DONE, | ||
| 21 | KB_ACTION_LAYER, | ||
| 22 | KB_ACTION_SPACE | ||
| 23 | } kb_action_t; | ||
| 24 | |||
| 25 | typedef struct { | ||
| 26 | char input[KB_INPUT_MAX + 1]; | ||
| 27 | int cursor; | ||
| 28 | bool reveal; | ||
| 29 | kb_layer_t layer; | ||
| 30 | } kb_state_t; | ||
| 31 | |||
| 32 | typedef struct { | ||
| 33 | kb_action_t action; | ||
| 34 | char ch; | ||
| 35 | } kb_result_t; | ||
| 36 | |||
| 37 | typedef struct { | ||
| 38 | int key_w; | ||
| 39 | int key_h; | ||
| 40 | int key_gap; | ||
| 41 | int start_y; | ||
| 42 | int screen_w; | ||
| 43 | int row_count; | ||
| 44 | } kb_layout_t; | ||
| 45 | |||
| 46 | void kb_state_init(kb_state_t *st); | ||
| 47 | void kb_set_layout(const kb_layout_t *layout); | ||
| 48 | const kb_layout_t *kb_get_layout(void); | ||
| 49 | int kb_get_row_keys(int row, kb_layer_t layer, const char **keys_out); | ||
| 50 | kb_result_t kb_hit_test(int tx, int ty, kb_layer_t layer); | ||
| 51 | void kb_apply(kb_state_t *st, kb_result_t result); | ||
| 52 | |||
| 53 | #endif | ||
diff --git a/main/mint_health.c b/main/mint_health.c index 5853a39..4ed8a19 100644 --- a/main/mint_health.c +++ b/main/mint_health.c | |||
| @@ -5,11 +5,19 @@ | |||
| 5 | #include "freertos/FreeRTOS.h" | 5 | #include "freertos/FreeRTOS.h" |
| 6 | #include "freertos/task.h" | 6 | #include "freertos/task.h" |
| 7 | #include "freertos/semphr.h" | 7 | #include "freertos/semphr.h" |
| 8 | #include "freertos/queue.h" | ||
| 9 | #include "nucula_wallet.h" | ||
| 10 | #include "tollgate_api.h" | ||
| 8 | #include <string.h> | 11 | #include <string.h> |
| 9 | #include <stdlib.h> | 12 | #include <stdlib.h> |
| 10 | 13 | ||
| 11 | static const char *TAG = "mint_health"; | 14 | static const char *TAG = "mint_health"; |
| 12 | 15 | ||
| 16 | #define WALLET_QUEUE_LEN 8 | ||
| 17 | static QueueHandle_t s_wallet_queue = NULL; | ||
| 18 | |||
| 19 | static int s_last_probe_err = 0; | ||
| 20 | |||
| 13 | static mint_status_t s_mints[MINT_HEALTH_MAX]; | 21 | static mint_status_t s_mints[MINT_HEALTH_MAX]; |
| 14 | static int s_mint_count = 0; | 22 | static int s_mint_count = 0; |
| 15 | static bool s_running = false; | 23 | static bool s_running = false; |
| @@ -60,16 +68,22 @@ static bool probe_mint(const char *url) | |||
| 60 | .crt_bundle_attach = esp_crt_bundle_attach, | 68 | .crt_bundle_attach = esp_crt_bundle_attach, |
| 61 | }; | 69 | }; |
| 62 | esp_http_client_handle_t client = esp_http_client_init(&config); | 70 | esp_http_client_handle_t client = esp_http_client_init(&config); |
| 63 | if (!client) return false; | 71 | if (!client) { |
| 72 | s_last_probe_err = -1; | ||
| 73 | return false; | ||
| 74 | } | ||
| 64 | 75 | ||
| 65 | esp_err_t err = esp_http_client_open(client, 0); | 76 | esp_err_t err = esp_http_client_open(client, 0); |
| 66 | if (err != ESP_OK) { | 77 | if (err != ESP_OK) { |
| 78 | ESP_LOGD(TAG, "probe open failed: %s err=0x%x", probe_url, err); | ||
| 79 | s_last_probe_err = err; | ||
| 67 | esp_http_client_cleanup(client); | 80 | esp_http_client_cleanup(client); |
| 68 | return false; | 81 | return false; |
| 69 | } | 82 | } |
| 70 | 83 | ||
| 71 | int content_length = esp_http_client_fetch_headers(client); | 84 | int content_length = esp_http_client_fetch_headers(client); |
| 72 | int status = esp_http_client_get_status_code(client); | 85 | int status = esp_http_client_get_status_code(client); |
| 86 | s_last_probe_err = 0; | ||
| 73 | 87 | ||
| 74 | char *resp = NULL; | 88 | char *resp = NULL; |
| 75 | if (content_length > 0 && content_length < 8192) { | 89 | if (content_length > 0 && content_length < 8192) { |
| @@ -100,6 +114,7 @@ static void run_probes(void) | |||
| 100 | bool ok = probe_mint(s_mints[i].url); | 114 | bool ok = probe_mint(s_mints[i].url); |
| 101 | s_mints[i].last_probe_ms = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS; | 115 | s_mints[i].last_probe_ms = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS; |
| 102 | s_mints[i].last_http_status = ok ? 200 : 0; | 116 | s_mints[i].last_http_status = ok ? 200 : 0; |
| 117 | s_mints[i].last_err = ok ? 0 : s_last_probe_err; | ||
| 103 | 118 | ||
| 104 | if (ok) { | 119 | if (ok) { |
| 105 | s_mints[i].consecutive_successes++; | 120 | s_mints[i].consecutive_successes++; |
| @@ -111,7 +126,7 @@ static void run_probes(void) | |||
| 111 | } | 126 | } |
| 112 | } else { | 127 | } else { |
| 113 | if (s_mints[i].reachable) { | 128 | if (s_mints[i].reachable) { |
| 114 | ESP_LOGW(TAG, "Mint UNREACHABLE: %s", s_mints[i].url); | 129 | ESP_LOGW(TAG, "Mint UNREACHABLE: %s err=0x%x", s_mints[i].url, s_last_probe_err); |
| 115 | } | 130 | } |
| 116 | s_mints[i].reachable = false; | 131 | s_mints[i].reachable = false; |
| 117 | s_mints[i].consecutive_successes = 0; | 132 | s_mints[i].consecutive_successes = 0; |
| @@ -137,6 +152,7 @@ static void run_initial_probes(void) | |||
| 137 | bool ok = probe_mint(s_mints[i].url); | 152 | bool ok = probe_mint(s_mints[i].url); |
| 138 | s_mints[i].last_probe_ms = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS; | 153 | s_mints[i].last_probe_ms = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS; |
| 139 | s_mints[i].last_http_status = ok ? 200 : 0; | 154 | s_mints[i].last_http_status = ok ? 200 : 0; |
| 155 | s_mints[i].last_err = ok ? 0 : s_last_probe_err; | ||
| 140 | 156 | ||
| 141 | if (ok) { | 157 | if (ok) { |
| 142 | s_mints[i].consecutive_successes = MINT_HEALTH_RECOVERY_THRESHOLD; | 158 | s_mints[i].consecutive_successes = MINT_HEALTH_RECOVERY_THRESHOLD; |
| @@ -153,16 +169,54 @@ static void run_initial_probes(void) | |||
| 153 | fire_callbacks(); | 169 | fire_callbacks(); |
| 154 | } | 170 | } |
| 155 | 171 | ||
| 172 | static void process_wallet_queue(void) | ||
| 173 | { | ||
| 174 | char *token; | ||
| 175 | while (s_wallet_queue && xQueueReceive(s_wallet_queue, &token, 0) == pdTRUE) { | ||
| 176 | if (!token) continue; | ||
| 177 | ESP_LOGI(TAG, "Processing wallet receive (%zu bytes)", strlen(token)); | ||
| 178 | esp_err_t err = nucula_wallet_receive(token); | ||
| 179 | if (err == ESP_OK) { | ||
| 180 | ESP_LOGI(TAG, "Wallet receive OK, balance=%llu", | ||
| 181 | (unsigned long long)nucula_wallet_balance()); | ||
| 182 | } else { | ||
| 183 | ESP_LOGW(TAG, "Wallet receive failed"); | ||
| 184 | } | ||
| 185 | free(token); | ||
| 186 | } | ||
| 187 | } | ||
| 188 | |||
| 156 | static void health_task(void *pvParameters) | 189 | static void health_task(void *pvParameters) |
| 157 | { | 190 | { |
| 158 | ESP_LOGI(TAG, "Health probe task started, waiting for DNS to stabilize..."); | 191 | ESP_LOGI(TAG, "Health probe task started, waiting for DNS to stabilize..."); |
| 159 | vTaskDelay(pdMS_TO_TICKS(5000)); | 192 | vTaskDelay(pdMS_TO_TICKS(5000)); |
| 160 | run_initial_probes(); | 193 | run_initial_probes(); |
| 194 | process_wallet_queue(); | ||
| 161 | 195 | ||
| 162 | while (s_running) { | 196 | while (s_running) { |
| 163 | vTaskDelay(pdMS_TO_TICKS(MINT_HEALTH_PROBE_INTERVAL_S * 1000)); | 197 | TickType_t start = xTaskGetTickCount(); |
| 198 | while (s_running) { | ||
| 199 | TickType_t elapsed = (xTaskGetTickCount() - start) * portTICK_PERIOD_MS; | ||
| 200 | if (elapsed >= MINT_HEALTH_PROBE_INTERVAL_S * 1000) break; | ||
| 201 | |||
| 202 | char *token = NULL; | ||
| 203 | if (s_wallet_queue && xQueueReceive(s_wallet_queue, &token, pdMS_TO_TICKS(1000)) == pdTRUE) { | ||
| 204 | if (token) { | ||
| 205 | ESP_LOGI(TAG, "Processing wallet receive (%zu bytes)", strlen(token)); | ||
| 206 | esp_err_t err = nucula_wallet_receive(token); | ||
| 207 | if (err == ESP_OK) { | ||
| 208 | ESP_LOGI(TAG, "Wallet receive OK, balance=%llu", | ||
| 209 | (unsigned long long)nucula_wallet_balance()); | ||
| 210 | } else { | ||
| 211 | ESP_LOGW(TAG, "Wallet receive failed"); | ||
| 212 | } | ||
| 213 | free(token); | ||
| 214 | } | ||
| 215 | } | ||
| 216 | } | ||
| 164 | if (!s_running) break; | 217 | if (!s_running) break; |
| 165 | run_probes(); | 218 | run_probes(); |
| 219 | process_wallet_queue(); | ||
| 166 | } | 220 | } |
| 167 | 221 | ||
| 168 | s_task_handle = NULL; | 222 | s_task_handle = NULL; |
| @@ -173,6 +227,10 @@ void mint_health_start(void) | |||
| 173 | { | 227 | { |
| 174 | if (s_running) return; | 228 | if (s_running) return; |
| 175 | s_running = true; | 229 | s_running = true; |
| 230 | |||
| 231 | s_wallet_queue = xQueueCreate(WALLET_QUEUE_LEN, sizeof(char *)); | ||
| 232 | tls_worker_set_queue(s_wallet_queue); | ||
| 233 | |||
| 176 | xTaskCreate(health_task, "mint_health", 16384, NULL, 3, &s_task_handle); | 234 | xTaskCreate(health_task, "mint_health", 16384, NULL, 3, &s_task_handle); |
| 177 | } | 235 | } |
| 178 | 236 | ||
diff --git a/main/mint_health.h b/main/mint_health.h index f047d6a..33413db 100644 --- a/main/mint_health.h +++ b/main/mint_health.h | |||
| @@ -16,6 +16,7 @@ typedef struct { | |||
| 16 | uint8_t consecutive_successes; | 16 | uint8_t consecutive_successes; |
| 17 | int64_t last_probe_ms; | 17 | int64_t last_probe_ms; |
| 18 | int last_http_status; | 18 | int last_http_status; |
| 19 | int last_err; | ||
| 19 | } mint_status_t; | 20 | } mint_status_t; |
| 20 | 21 | ||
| 21 | typedef void (*mint_health_changed_cb)(void); | 22 | typedef void (*mint_health_changed_cb)(void); |
diff --git a/main/tollgate_api.c b/main/tollgate_api.c index b775f55..be753ea 100644 --- a/main/tollgate_api.c +++ b/main/tollgate_api.c | |||
| @@ -3,7 +3,10 @@ | |||
| 3 | #include "config.h" | 3 | #include "config.h" |
| 4 | #include "identity.h" | 4 | #include "identity.h" |
| 5 | #include "session.h" | 5 | #include "session.h" |
| 6 | #include "captive_portal.h" | ||
| 6 | #include "firewall.h" | 7 | #include "firewall.h" |
| 8 | #include "lwip/dns.h" | ||
| 9 | #include "esp_heap_caps.h" | ||
| 7 | #include "nucula_wallet.h" | 10 | #include "nucula_wallet.h" |
| 8 | #include "mint_health.h" | 11 | #include "mint_health.h" |
| 9 | #include "market.h" | 12 | #include "market.h" |
| @@ -16,11 +19,37 @@ | |||
| 16 | #include "lwip/sockets.h" | 19 | #include "lwip/sockets.h" |
| 17 | #include "lwip/netdb.h" | 20 | #include "lwip/netdb.h" |
| 18 | #include "freertos/task.h" | 21 | #include "freertos/task.h" |
| 22 | #include "freertos/queue.h" | ||
| 19 | #include <string.h> | 23 | #include <string.h> |
| 20 | 24 | ||
| 21 | static const char *TAG = "tollgate_api"; | 25 | static const char *TAG = "tollgate_api"; |
| 22 | static httpd_handle_t s_api_server = NULL; | 26 | static httpd_handle_t s_api_server = NULL; |
| 23 | 27 | ||
| 28 | static QueueHandle_t s_wallet_queue = NULL; | ||
| 29 | |||
| 30 | void tls_worker_set_queue(QueueHandle_t q) | ||
| 31 | { | ||
| 32 | s_wallet_queue = q; | ||
| 33 | } | ||
| 34 | |||
| 35 | static void tls_worker_submit(const char *token) | ||
| 36 | { | ||
| 37 | if (!s_wallet_queue) { | ||
| 38 | ESP_LOGW(TAG, "No wallet queue, receiving synchronously"); | ||
| 39 | nucula_wallet_receive(token); | ||
| 40 | return; | ||
| 41 | } | ||
| 42 | |||
| 43 | char *copy = strdup(token); | ||
| 44 | if (!copy) return; | ||
| 45 | |||
| 46 | if (xQueueSend(s_wallet_queue, ©, pdMS_TO_TICKS(1000)) != pdTRUE) { | ||
| 47 | ESP_LOGW(TAG, "Wallet queue full, receiving synchronously"); | ||
| 48 | nucula_wallet_receive(copy); | ||
| 49 | free(copy); | ||
| 50 | } | ||
| 51 | } | ||
| 52 | |||
| 24 | static esp_err_t get_client_ip(httpd_req_t *req, uint32_t *ip_out) | 53 | static esp_err_t get_client_ip(httpd_req_t *req, uint32_t *ip_out) |
| 25 | { | 54 | { |
| 26 | int sockfd = httpd_req_to_sockfd(req); | 55 | int sockfd = httpd_req_to_sockfd(req); |
| @@ -277,16 +306,8 @@ static esp_err_t api_post_payment(httpd_req_t *req) | |||
| 277 | err = cashu_check_proof_states(mint_url, token, states, &state_count); | 306 | err = cashu_check_proof_states(mint_url, token, states, &state_count); |
| 278 | ESP_LOGI(TAG, "Stack HWM after checkstate: %u", uxTaskGetStackHighWaterMark(NULL)); | 307 | ESP_LOGI(TAG, "Stack HWM after checkstate: %u", uxTaskGetStackHighWaterMark(NULL)); |
| 279 | if (err != ESP_OK) { | 308 | if (err != ESP_OK) { |
| 280 | free(states); | 309 | ESP_LOGW(TAG, "Checkstate failed, proceeding without spend check (wallet swap will verify)"); |
| 281 | free(token); | 310 | state_count = 0; |
| 282 | cJSON *notice = create_notice("error", "payment-error-verification", "Failed to verify token with mint"); | ||
| 283 | char *json = cJSON_PrintUnformatted(notice); | ||
| 284 | httpd_resp_set_status(req, "502 Bad Gateway"); | ||
| 285 | httpd_resp_set_type(req, "application/json"); | ||
| 286 | httpd_resp_send(req, json, strlen(json)); | ||
| 287 | cJSON_free(json); | ||
| 288 | cJSON_Delete(notice); | ||
| 289 | return ESP_OK; | ||
| 290 | } | 311 | } |
| 291 | 312 | ||
| 292 | for (int i = 0; i < state_count; i++) { | 313 | for (int i = 0; i < state_count; i++) { |
| @@ -348,7 +369,7 @@ static esp_err_t api_post_payment(httpd_req_t *req) | |||
| 348 | cJSON_free(json); | 369 | cJSON_free(json); |
| 349 | cJSON_Delete(session_event); | 370 | cJSON_Delete(session_event); |
| 350 | 371 | ||
| 351 | nucula_wallet_receive(body_copy); | 372 | tls_worker_submit(body_copy); |
| 352 | 373 | ||
| 353 | free(states); | 374 | free(states); |
| 354 | free(token); | 375 | free(token); |
| @@ -509,6 +530,12 @@ static esp_err_t api_get_mints(httpd_req_t *req) | |||
| 509 | cJSON *obj = cJSON_CreateObject(); | 530 | cJSON *obj = cJSON_CreateObject(); |
| 510 | cJSON_AddStringToObject(obj, "url", mints[i].url); | 531 | cJSON_AddStringToObject(obj, "url", mints[i].url); |
| 511 | cJSON_AddBoolToObject(obj, "reachable", mints[i].reachable); | 532 | cJSON_AddBoolToObject(obj, "reachable", mints[i].reachable); |
| 533 | cJSON_AddNumberToObject(obj, "status", mints[i].last_http_status); | ||
| 534 | if (mints[i].last_err) { | ||
| 535 | char errbuf[16]; | ||
| 536 | snprintf(errbuf, sizeof(errbuf), "0x%x", mints[i].last_err); | ||
| 537 | cJSON_AddStringToObject(obj, "last_err", errbuf); | ||
| 538 | } | ||
| 512 | cJSON_AddItemToArray(arr, obj); | 539 | cJSON_AddItemToArray(arr, obj); |
| 513 | } | 540 | } |
| 514 | char *json = cJSON_PrintUnformatted(arr); | 541 | char *json = cJSON_PrintUnformatted(arr); |
| @@ -678,7 +705,55 @@ static esp_err_t api_get_mining_stats(httpd_req_t *req) | |||
| 678 | httpd_resp_send(req, json, strlen(json)); | 705 | httpd_resp_send(req, json, strlen(json)); |
| 679 | cJSON_free(json); | 706 | cJSON_free(json); |
| 680 | cJSON_Delete(root); | 707 | cJSON_Delete(root); |
| 681 | >>>>>>> feature/mining-payment | 708 | return ESP_OK; |
| 709 | } | ||
| 710 | |||
| 711 | extern bool s_start_services_called; | ||
| 712 | extern bool s_start_ap_services_called; | ||
| 713 | extern bool s_sta_got_ip; | ||
| 714 | extern bool s_ap_started; | ||
| 715 | extern esp_ip4_addr_t s_sta_ip; | ||
| 716 | extern esp_ip4_addr_t s_sta_gw; | ||
| 717 | |||
| 718 | static esp_err_t api_get_debug(httpd_req_t *req) | ||
| 719 | { | ||
| 720 | httpd_handle_t portal = captive_portal_get_server(); | ||
| 721 | cJSON *root = cJSON_CreateObject(); | ||
| 722 | cJSON_AddBoolToObject(root, "portal_running", portal != NULL); | ||
| 723 | cJSON_AddBoolToObject(root, "portal_start_called", captive_portal_was_start_called()); | ||
| 724 | cJSON_AddNumberToObject(root, "portal_start_result", captive_portal_get_start_result()); | ||
| 725 | cJSON_AddBoolToObject(root, "start_services_called", s_start_services_called); | ||
| 726 | cJSON_AddBoolToObject(root, "start_ap_services_called", s_start_ap_services_called); | ||
| 727 | cJSON_AddBoolToObject(root, "sta_got_ip", s_sta_got_ip); | ||
| 728 | cJSON_AddBoolToObject(root, "ap_started", s_ap_started); | ||
| 729 | cJSON_AddNumberToObject(root, "free_heap", (double)esp_get_free_heap_size()); | ||
| 730 | cJSON_AddNumberToObject(root, "min_free_heap", (double)esp_get_minimum_free_heap_size()); | ||
| 731 | cJSON_AddNumberToObject(root, "free_internal", (double)heap_caps_get_free_size(MALLOC_CAP_INTERNAL)); | ||
| 732 | cJSON_AddNumberToObject(root, "largest_internal", (double)heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL)); | ||
| 733 | cJSON_AddNumberToObject(root, "free_spiram", (double)heap_caps_get_free_size(MALLOC_CAP_SPIRAM)); | ||
| 734 | |||
| 735 | char dns0[16], dns1[16], dns2[16]; | ||
| 736 | const ip_addr_t *d0 = dns_getserver(0); | ||
| 737 | const ip_addr_t *d1 = dns_getserver(1); | ||
| 738 | const ip_addr_t *d2 = dns_getserver(2); | ||
| 739 | snprintf(dns0, sizeof(dns0), IPSTR, IP2STR(&(esp_ip4_addr_t){.addr=d0->addr})); | ||
| 740 | snprintf(dns1, sizeof(dns1), IPSTR, IP2STR(&(esp_ip4_addr_t){.addr=d1->addr})); | ||
| 741 | snprintf(dns2, sizeof(dns2), IPSTR, IP2STR(&(esp_ip4_addr_t){.addr=d2->addr})); | ||
| 742 | cJSON_AddStringToObject(root, "dns0", dns0); | ||
| 743 | cJSON_AddStringToObject(root, "dns1", dns1); | ||
| 744 | cJSON_AddStringToObject(root, "dns2", dns2); | ||
| 745 | |||
| 746 | char sta_ip_str[16], sta_gw_str[16]; | ||
| 747 | snprintf(sta_ip_str, sizeof(sta_ip_str), IPSTR, IP2STR(&s_sta_ip)); | ||
| 748 | snprintf(sta_gw_str, sizeof(sta_gw_str), IPSTR, IP2STR(&s_sta_gw)); | ||
| 749 | cJSON_AddStringToObject(root, "sta_ip", sta_ip_str); | ||
| 750 | cJSON_AddStringToObject(root, "sta_gw", sta_gw_str); | ||
| 751 | |||
| 752 | char *json = cJSON_PrintUnformatted(root); | ||
| 753 | httpd_resp_set_type(req, "application/json"); | ||
| 754 | httpd_resp_sendstr(req, json); | ||
| 755 | cJSON_free(json); | ||
| 756 | cJSON_Delete(root); | ||
| 682 | return ESP_OK; | 757 | return ESP_OK; |
| 683 | } | 758 | } |
| 684 | 759 | ||
| @@ -732,16 +807,18 @@ static esp_err_t api_get_market(httpd_req_t *req) | |||
| 732 | } | 807 | } |
| 733 | 808 | ||
| 734 | static const httpd_uri_t uri_market = { .uri = "/market", .method = HTTP_GET, .handler = api_get_market }; | 809 | static const httpd_uri_t uri_market = { .uri = "/market", .method = HTTP_GET, .handler = api_get_market }; |
| 810 | static const httpd_uri_t uri_debug = { .uri = "/debug", .method = HTTP_GET, .handler = api_get_debug }; | ||
| 735 | 811 | ||
| 736 | esp_err_t tollgate_api_start(void) | 812 | esp_err_t tollgate_api_start(void) |
| 737 | { | 813 | { |
| 738 | if (s_api_server) return ESP_OK; | 814 | if (s_api_server) return ESP_OK; |
| 739 | 815 | ||
| 740 | httpd_config_t config = HTTPD_DEFAULT_CONFIG(); | 816 | httpd_config_t config = HTTPD_DEFAULT_CONFIG(); |
| 741 | config.server_port = 2121; | 817 | config.server_port = 2121; |
| 742 | config.ctrl_port = 32769; | 818 | config.ctrl_port = 32769; |
| 743 | config.max_uri_handlers = 16; | 819 | config.max_uri_handlers = 16; |
| 744 | config.stack_size = 16384; | 820 | config.stack_size = 16384; |
| 821 | config.core_id = 0; | ||
| 745 | 822 | ||
| 746 | esp_err_t ret = httpd_start(&s_api_server, &config); | 823 | esp_err_t ret = httpd_start(&s_api_server, &config); |
| 747 | if (ret != ESP_OK) { | 824 | if (ret != ESP_OK) { |
| @@ -751,6 +828,7 @@ esp_err_t tollgate_api_start(void) | |||
| 751 | } | 828 | } |
| 752 | 829 | ||
| 753 | httpd_register_uri_handler(s_api_server, &uri_discovery); | 830 | httpd_register_uri_handler(s_api_server, &uri_discovery); |
| 831 | httpd_register_uri_handler(s_api_server, &uri_debug); | ||
| 754 | httpd_register_uri_handler(s_api_server, &uri_payment); | 832 | httpd_register_uri_handler(s_api_server, &uri_payment); |
| 755 | httpd_register_uri_handler(s_api_server, &uri_mints); | 833 | httpd_register_uri_handler(s_api_server, &uri_mints); |
| 756 | httpd_register_uri_handler(s_api_server, &uri_usage); | 834 | httpd_register_uri_handler(s_api_server, &uri_usage); |
| @@ -768,6 +846,7 @@ esp_err_t tollgate_api_start(void) | |||
| 768 | } | 846 | } |
| 769 | 847 | ||
| 770 | ESP_LOGI(TAG, "TollGate API started on port 2121"); | 848 | ESP_LOGI(TAG, "TollGate API started on port 2121"); |
| 849 | |||
| 771 | return ESP_OK; | 850 | return ESP_OK; |
| 772 | } | 851 | } |
| 773 | 852 | ||
diff --git a/main/tollgate_api.h b/main/tollgate_api.h index 23e0d75..2af4b8c 100644 --- a/main/tollgate_api.h +++ b/main/tollgate_api.h | |||
| @@ -6,5 +6,6 @@ | |||
| 6 | 6 | ||
| 7 | esp_err_t tollgate_api_start(void); | 7 | esp_err_t tollgate_api_start(void); |
| 8 | void tollgate_api_stop(void); | 8 | void tollgate_api_stop(void); |
| 9 | void tls_worker_set_queue(QueueHandle_t q); | ||
| 9 | 10 | ||
| 10 | #endif | 11 | #endif |
diff --git a/main/tollgate_main.c b/main/tollgate_main.c index 561fc3f..2d4fa22 100644 --- a/main/tollgate_main.c +++ b/main/tollgate_main.c | |||
| @@ -55,14 +55,24 @@ static char s_ap_ip_str[16] = "10.0.0.1"; | |||
| 55 | static relay_selector_t s_relay_selector; | 55 | static relay_selector_t s_relay_selector; |
| 56 | static sync_manager_t s_sync_manager; | 56 | static sync_manager_t s_sync_manager; |
| 57 | 57 | ||
| 58 | volatile bool s_start_services_called = false; | ||
| 59 | volatile bool s_start_ap_services_called = false; | ||
| 60 | volatile bool s_sta_got_ip = false; | ||
| 61 | volatile bool s_ap_started = false; | ||
| 62 | volatile esp_ip4_addr_t s_sta_ip = {0}; | ||
| 63 | volatile esp_ip4_addr_t s_sta_gw = {0}; | ||
| 64 | |||
| 58 | static void start_services(void); | 65 | static void start_services(void); |
| 59 | static void stop_services(void); | 66 | static void stop_services(void); |
| 60 | static void start_ap_services(void); | 67 | static void start_ap_services(void); |
| 61 | 68 | ||
| 62 | static void start_ap_services(void) | 69 | static void start_ap_services(void) |
| 63 | { | 70 | { |
| 71 | s_start_ap_services_called = true; | ||
| 64 | if (s_ap_services_running) return; | 72 | if (s_ap_services_running) return; |
| 65 | 73 | ||
| 74 | const tollgate_config_t *cfg = tollgate_config_get(); | ||
| 75 | captive_portal_start(cfg->ap_ip_str); | ||
| 66 | tollgate_api_start(); | 76 | tollgate_api_start(); |
| 67 | beacon_price_start(); | 77 | beacon_price_start(); |
| 68 | market_init(); | 78 | market_init(); |
| @@ -120,13 +130,13 @@ static void wifi_event_handler(void *arg, esp_event_base_t event_base, | |||
| 120 | event->mac[0], event->mac[1], event->mac[2], | 130 | event->mac[0], event->mac[1], event->mac[2], |
| 121 | event->mac[3], event->mac[4], event->mac[5]); | 131 | event->mac[3], event->mac[4], event->mac[5]); |
| 122 | } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_START) { | 132 | } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_START) { |
| 133 | s_ap_started = true; | ||
| 123 | start_ap_services(); | 134 | start_ap_services(); |
| 124 | } | 135 | } |
| 125 | } | 136 | } |
| 126 | 137 | ||
| 127 | static void services_start_task(void *pvParameters) | 138 | static void services_start_task(void *pvParameters) |
| 128 | { | 139 | { |
| 129 | vTaskDelay(pdMS_TO_TICKS(3000)); | ||
| 130 | start_services(); | 140 | start_services(); |
| 131 | vTaskDelete(NULL); | 141 | vTaskDelete(NULL); |
| 132 | } | 142 | } |
| @@ -138,8 +148,17 @@ static void ip_event_handler(void *arg, esp_event_base_t event_base, | |||
| 138 | ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data; | 148 | ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data; |
| 139 | ESP_LOGI(TAG, "Got IP:" IPSTR ", GW:" IPSTR, IP2STR(&event->ip_info.ip), IP2STR(&event->ip_info.gw)); | 149 | ESP_LOGI(TAG, "Got IP:" IPSTR ", GW:" IPSTR, IP2STR(&event->ip_info.ip), IP2STR(&event->ip_info.gw)); |
| 140 | s_retry_count = 0; | 150 | s_retry_count = 0; |
| 151 | s_sta_got_ip = true; | ||
| 152 | s_sta_ip.addr = event->ip_info.ip.addr; | ||
| 153 | s_sta_gw.addr = event->ip_info.gw.addr; | ||
| 141 | xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); | 154 | xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); |
| 142 | 155 | ||
| 156 | static TaskHandle_t s_svc_task = NULL; | ||
| 157 | if (s_svc_task == NULL) { | ||
| 158 | xTaskCreate(services_start_task, "svc_start", 16384, NULL, 5, &s_svc_task); | ||
| 159 | ESP_LOGI(TAG, "services_start_task spawned (3s delay)"); | ||
| 160 | } | ||
| 161 | |||
| 143 | esp_sntp_stop(); | 162 | esp_sntp_stop(); |
| 144 | esp_sntp_setoperatingmode(SNTP_OPMODE_POLL); | 163 | esp_sntp_setoperatingmode(SNTP_OPMODE_POLL); |
| 145 | esp_sntp_setservername(0, "pool.ntp.org"); | 164 | esp_sntp_setservername(0, "pool.ntp.org"); |
| @@ -150,8 +169,6 @@ static void ip_event_handler(void *arg, esp_event_base_t event_base, | |||
| 150 | char gw_ip_str[16]; | 169 | char gw_ip_str[16]; |
| 151 | snprintf(gw_ip_str, sizeof(gw_ip_str), IPSTR, IP2STR(&event->ip_info.gw)); | 170 | snprintf(gw_ip_str, sizeof(gw_ip_str), IPSTR, IP2STR(&event->ip_info.gw)); |
| 152 | tollgate_client_on_sta_connected(gw_ip_str); | 171 | tollgate_client_on_sta_connected(gw_ip_str); |
| 153 | |||
| 154 | xTaskCreate(services_start_task, "svc_start", 32768, NULL, 5, NULL); | ||
| 155 | } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_LOST_IP) { | 172 | } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_LOST_IP) { |
| 156 | ESP_LOGW(TAG, "Lost IP address"); | 173 | ESP_LOGW(TAG, "Lost IP address"); |
| 157 | xEventGroupClearBits(s_wifi_event_group, WIFI_CONNECTED_BIT); | 174 | xEventGroupClearBits(s_wifi_event_group, WIFI_CONNECTED_BIT); |
| @@ -159,13 +176,6 @@ static void ip_event_handler(void *arg, esp_event_base_t event_base, | |||
| 159 | } | 176 | } |
| 160 | } | 177 | } |
| 161 | 178 | ||
| 162 | static void wallet_init_task(void *pvParameters) | ||
| 163 | { | ||
| 164 | const tollgate_config_t *cfg = tollgate_config_get(); | ||
| 165 | nucula_wallet_init(cfg->mint_url); | ||
| 166 | vTaskDelete(NULL); | ||
| 167 | } | ||
| 168 | |||
| 169 | static void publish_wifistr_task(void *pvParameters) | 179 | static void publish_wifistr_task(void *pvParameters) |
| 170 | { | 180 | { |
| 171 | vTaskDelay(pdMS_TO_TICKS(5000)); | 181 | vTaskDelay(pdMS_TO_TICKS(5000)); |
| @@ -177,12 +187,16 @@ static void publish_wifistr_task(void *pvParameters) | |||
| 177 | 187 | ||
| 178 | static void start_services(void) | 188 | static void start_services(void) |
| 179 | { | 189 | { |
| 190 | vTaskDelay(pdMS_TO_TICKS(3000)); | ||
| 191 | ESP_LOGI(TAG, ">>> start_services() called"); | ||
| 180 | if (s_services_mutex) xSemaphoreTake(s_services_mutex, portMAX_DELAY); | 192 | if (s_services_mutex) xSemaphoreTake(s_services_mutex, portMAX_DELAY); |
| 181 | if (s_services_running) { | 193 | if (s_services_running) { |
| 182 | if (s_services_mutex) xSemaphoreGive(s_services_mutex); | 194 | if (s_services_mutex) xSemaphoreGive(s_services_mutex); |
| 183 | return; | 195 | return; |
| 184 | } | 196 | } |
| 185 | 197 | ||
| 198 | s_start_services_called = true; | ||
| 199 | |||
| 186 | esp_netif_get_ip_info(s_ap_netif, &(esp_netif_ip_info_t){0}); | 200 | esp_netif_get_ip_info(s_ap_netif, &(esp_netif_ip_info_t){0}); |
| 187 | esp_netif_ip_info_t ap_ip_info; | 201 | esp_netif_ip_info_t ap_ip_info; |
| 188 | esp_netif_get_ip_info(s_ap_netif, &ap_ip_info); | 202 | esp_netif_get_ip_info(s_ap_netif, &ap_ip_info); |
| @@ -191,10 +205,44 @@ static void start_services(void) | |||
| 191 | const ip_addr_t *dns_addr = dns_getserver(0); | 205 | const ip_addr_t *dns_addr = dns_getserver(0); |
| 192 | upstream_dns.addr = dns_addr->addr; | 206 | upstream_dns.addr = dns_addr->addr; |
| 193 | 207 | ||
| 208 | if (upstream_dns.addr == ap_ip_info.ip.addr) { | ||
| 209 | ESP_LOGW(TAG, "DNS[0] is our own AP IP — trying DNS[1] and STA gateway"); | ||
| 210 | const ip_addr_t *dns1 = dns_getserver(1); | ||
| 211 | if (dns1->addr != 0 && dns1->addr != ap_ip_info.ip.addr) { | ||
| 212 | upstream_dns.addr = dns1->addr; | ||
| 213 | dns_setserver(0, dns1); | ||
| 214 | ESP_LOGI(TAG, "Fixed DNS[0] to " IPSTR " from DNS[1]", IP2STR(&upstream_dns)); | ||
| 215 | } else { | ||
| 216 | esp_netif_dns_info_t sta_dns; | ||
| 217 | if (esp_netif_get_dns_info(s_sta_netif, ESP_NETIF_DNS_MAIN, &sta_dns) == ESP_OK | ||
| 218 | && sta_dns.ip.u_addr.ip4.addr != 0 | ||
| 219 | && sta_dns.ip.u_addr.ip4.addr != ap_ip_info.ip.addr) { | ||
| 220 | upstream_dns.addr = sta_dns.ip.u_addr.ip4.addr; | ||
| 221 | ip_addr_t lwip_dns = {0}; | ||
| 222 | lwip_dns.addr = sta_dns.ip.u_addr.ip4.addr; | ||
| 223 | dns_setserver(0, &lwip_dns); | ||
| 224 | ESP_LOGI(TAG, "Fixed DNS[0] to " IPSTR " from STA netif", IP2STR(&upstream_dns)); | ||
| 225 | } else { | ||
| 226 | ESP_LOGE(TAG, "No valid upstream DNS found! Mint verification will fail."); | ||
| 227 | } | ||
| 228 | } | ||
| 229 | } | ||
| 230 | |||
| 231 | ESP_LOGI(TAG, "DNS config: [0]=" IPSTR " [1]=" IPSTR " [2]=" IPSTR, | ||
| 232 | IP2STR(&(esp_ip4_addr_t){.addr=dns_getserver(0)->addr}), | ||
| 233 | IP2STR(&(esp_ip4_addr_t){.addr=dns_getserver(1)->addr}), | ||
| 234 | IP2STR(&(esp_ip4_addr_t){.addr=dns_getserver(2)->addr})); | ||
| 235 | |||
| 194 | firewall_init(ap_ip_info.ip); | 236 | firewall_init(ap_ip_info.ip); |
| 195 | session_manager_init(); | 237 | session_manager_init(); |
| 196 | 238 | ||
| 197 | const tollgate_config_t *cfg = tollgate_config_get(); | 239 | const tollgate_config_t *cfg = tollgate_config_get(); |
| 240 | |||
| 241 | if (cfg->cvm_enabled) { | ||
| 242 | cvm_server_init(); | ||
| 243 | cvm_server_start(); | ||
| 244 | } | ||
| 245 | |||
| 198 | mint_health_init(cfg->accepted_mints, cfg->accepted_mint_count); | 246 | mint_health_init(cfg->accepted_mints, cfg->accepted_mint_count); |
| 199 | mint_health_start(); | 247 | mint_health_start(); |
| 200 | 248 | ||
| @@ -226,19 +274,13 @@ static void start_services(void) | |||
| 226 | 274 | ||
| 227 | xTaskCreate(publish_wifistr_task, "wifistr_init", 16384, NULL, 3, NULL); | 275 | xTaskCreate(publish_wifistr_task, "wifistr_init", 16384, NULL, 3, NULL); |
| 228 | 276 | ||
| 229 | const tollgate_config_t *cfg2 = tollgate_config_get(); | 277 | if (cfg->mining_enabled) { |
| 230 | if (cfg2->cvm_enabled) { | ||
| 231 | cvm_server_init(); | ||
| 232 | cvm_server_start(); | ||
| 233 | } | ||
| 234 | |||
| 235 | if (cfg2->mining_enabled) { | ||
| 236 | ESP_LOGI(TAG, "Mining subsystem enabled, initializing..."); | 278 | ESP_LOGI(TAG, "Mining subsystem enabled, initializing..."); |
| 237 | mining_payment_init(); | 279 | mining_payment_init(); |
| 238 | stratum_client_init(); | 280 | stratum_client_init(); |
| 239 | stratum_proxy_init(cfg2->mining_port); | 281 | stratum_proxy_init(cfg->mining_port); |
| 240 | 282 | ||
| 241 | if (cfg2->mining_payout_mode != MINING_PAYOUT_UPSTREAM) { | 283 | if (cfg->mining_payout_mode != MINING_PAYOUT_UPSTREAM) { |
| 242 | stratum_client_start(); | 284 | stratum_client_start(); |
| 243 | } | 285 | } |
| 244 | 286 | ||
| @@ -260,7 +302,7 @@ static void start_services(void) | |||
| 260 | display_set_state(DISPLAY_READY); | 302 | display_set_state(DISPLAY_READY); |
| 261 | char portal_url[128]; | 303 | char portal_url[128]; |
| 262 | snprintf(portal_url, sizeof(portal_url), "http://%s/", cfg->ap_ip_str); | 304 | snprintf(portal_url, sizeof(portal_url), "http://%s/", cfg->ap_ip_str); |
| 263 | display_update(cfg->ap_ssid, 0, 0, portal_url); | 305 | display_update(cfg->ap_ssid, 0, 0, portal_url, cfg->mint_url, cfg->price_per_step, "connected"); |
| 264 | } | 306 | } |
| 265 | } | 307 | } |
| 266 | 308 | ||
| @@ -292,10 +334,10 @@ static void wifi_create_ap_netif(void) | |||
| 292 | { | 334 | { |
| 293 | s_ap_netif = esp_netif_create_default_wifi_ap(); | 335 | s_ap_netif = esp_netif_create_default_wifi_ap(); |
| 294 | 336 | ||
| 295 | const tollgate_config_t *cfg = tollgate_config_get(); | 337 | const tollgate_config_t *cfg = tollgate_config_get(); |
| 296 | esp_ip4_addr_t ap_ip = cfg->ap_ip; | 338 | esp_ip4_addr_t ap_ip = cfg->ap_ip; |
| 297 | esp_ip4_addr_t ap_gw = cfg->ap_ip; | 339 | esp_ip4_addr_t ap_gw = ap_ip; |
| 298 | esp_ip4_addr_t ap_mask; | 340 | esp_ip4_addr_t ap_mask; |
| 299 | IP4_ADDR(&ap_mask, 255, 255, 255, 0); | 341 | IP4_ADDR(&ap_mask, 255, 255, 255, 0); |
| 300 | 342 | ||
| 301 | strncpy(s_ap_ip_str, cfg->ap_ip_str, sizeof(s_ap_ip_str) - 1); | 343 | strncpy(s_ap_ip_str, cfg->ap_ip_str, sizeof(s_ap_ip_str) - 1); |
| @@ -412,7 +454,7 @@ void app_main(void) | |||
| 412 | 454 | ||
| 413 | if (tollgate_config_get_wifi(&(wifi_config_t){0}) != ESP_OK) { | 455 | if (tollgate_config_get_wifi(&(wifi_config_t){0}) != ESP_OK) { |
| 414 | ESP_LOGI(TAG, "No STA network configured, starting services immediately"); | 456 | ESP_LOGI(TAG, "No STA network configured, starting services immediately"); |
| 415 | xTaskCreate(services_start_task, "svc_start", 32768, NULL, 5, NULL); | 457 | xTaskCreate(services_start_task, "svc_start_fb", 16384, NULL, 5, NULL); |
| 416 | } | 458 | } |
| 417 | 459 | ||
| 418 | while (1) { | 460 | while (1) { |
diff --git a/main/tollgate_platform.c b/main/tollgate_platform.c new file mode 100644 index 0000000..fe8ac2d --- /dev/null +++ b/main/tollgate_platform.c | |||
| @@ -0,0 +1,63 @@ | |||
| 1 | #include "tollgate_platform.h" | ||
| 2 | #include "tollgate_core.h" | ||
| 3 | #include "config.h" | ||
| 4 | #include "esp_log.h" | ||
| 5 | #include "freertos/FreeRTOS.h" | ||
| 6 | #include "freertos/task.h" | ||
| 7 | |||
| 8 | static const char *TAG = "tollgate_platform"; | ||
| 9 | |||
| 10 | static uint16_t platform_get_price_sats(void) | ||
| 11 | { | ||
| 12 | const tollgate_config_t *cfg = tollgate_config_get(); | ||
| 13 | return cfg ? (uint16_t)cfg->price_per_step : 21; | ||
| 14 | } | ||
| 15 | |||
| 16 | static int32_t platform_get_step_ms(void) | ||
| 17 | { | ||
| 18 | const tollgate_config_t *cfg = tollgate_config_get(); | ||
| 19 | return cfg ? (int32_t)cfg->step_size_ms : 60000; | ||
| 20 | } | ||
| 21 | |||
| 22 | static const char *platform_get_mint_url(void) | ||
| 23 | { | ||
| 24 | const tollgate_config_t *cfg = tollgate_config_get(); | ||
| 25 | return cfg ? cfg->mint_url : "https://testnut-nutshell.mints.orangesync.tech"; | ||
| 26 | } | ||
| 27 | |||
| 28 | static const char *platform_get_metric(void) | ||
| 29 | { | ||
| 30 | const tollgate_config_t *cfg = tollgate_config_get(); | ||
| 31 | return cfg ? cfg->metric : "milliseconds"; | ||
| 32 | } | ||
| 33 | |||
| 34 | static int32_t platform_get_step_bytes(void) | ||
| 35 | { | ||
| 36 | const tollgate_config_t *cfg = tollgate_config_get(); | ||
| 37 | return cfg ? (int32_t)cfg->step_size_bytes : 22020096; | ||
| 38 | } | ||
| 39 | |||
| 40 | static int64_t platform_get_time_ms(void) | ||
| 41 | { | ||
| 42 | return (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS; | ||
| 43 | } | ||
| 44 | |||
| 45 | static bool platform_spend_proofs(const char *raw_token_json) | ||
| 46 | { | ||
| 47 | (void)raw_token_json; | ||
| 48 | return true; | ||
| 49 | } | ||
| 50 | |||
| 51 | const tollgate_platform_t *tollgate_get_platform(void) | ||
| 52 | { | ||
| 53 | static const tollgate_platform_t platform = { | ||
| 54 | .get_price_sats = platform_get_price_sats, | ||
| 55 | .get_step_ms = platform_get_step_ms, | ||
| 56 | .get_mint_url = platform_get_mint_url, | ||
| 57 | .get_metric = platform_get_metric, | ||
| 58 | .get_step_bytes = platform_get_step_bytes, | ||
| 59 | .get_time_ms = platform_get_time_ms, | ||
| 60 | .spend_proofs = platform_spend_proofs, | ||
| 61 | }; | ||
| 62 | return &platform; | ||
| 63 | } | ||
diff --git a/main/touch.c b/main/touch.c new file mode 100644 index 0000000..a28d13e --- /dev/null +++ b/main/touch.c | |||
| @@ -0,0 +1,156 @@ | |||
| 1 | #include "touch.h" | ||
| 2 | #include "esp_log.h" | ||
| 3 | #include "driver/i2c_master.h" | ||
| 4 | #include "driver/gpio.h" | ||
| 5 | #include "freertos/FreeRTOS.h" | ||
| 6 | #include "freertos/task.h" | ||
| 7 | #include <string.h> | ||
| 8 | |||
| 9 | static const char *TAG = "touch"; | ||
| 10 | |||
| 11 | static i2c_master_bus_handle_t s_bus = NULL; | ||
| 12 | static i2c_master_dev_handle_t s_dev = NULL; | ||
| 13 | static bool s_initialized = false; | ||
| 14 | static int s_rotation = 0; | ||
| 15 | |||
| 16 | static const uint8_t s_read_cmd[11] = { | ||
| 17 | 0xb5, 0xab, 0xa5, 0x5a, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00 | ||
| 18 | }; | ||
| 19 | |||
| 20 | void touch_parse_raw(const uint8_t *data, touch_point_t *pt) { | ||
| 21 | memset(pt, 0, sizeof(*pt)); | ||
| 22 | |||
| 23 | if (!data || data[0] != 0 || data[1] == 0 || data[1] > 1) { | ||
| 24 | pt->touched = false; | ||
| 25 | return; | ||
| 26 | } | ||
| 27 | |||
| 28 | uint16_t raw_x = ((data[2] & 0x0F) << 8) | data[3]; | ||
| 29 | uint16_t raw_y = ((data[4] & 0x0F) << 8) | data[5]; | ||
| 30 | |||
| 31 | if (raw_x > TOUCH_MAX_X) raw_x = TOUCH_MAX_X; | ||
| 32 | if (raw_y > TOUCH_MAX_Y) raw_y = TOUCH_MAX_Y; | ||
| 33 | |||
| 34 | pt->x = raw_x; | ||
| 35 | pt->y = raw_y; | ||
| 36 | pt->touched = true; | ||
| 37 | } | ||
| 38 | |||
| 39 | esp_err_t touch_init(void) { | ||
| 40 | if (s_initialized) return ESP_OK; | ||
| 41 | |||
| 42 | gpio_config_t rst_conf = { | ||
| 43 | .pin_bit_mask = (1ULL << TOUCH_RST_PIN), | ||
| 44 | .mode = GPIO_MODE_OUTPUT, | ||
| 45 | .pull_up_en = GPIO_PULLUP_DISABLE, | ||
| 46 | .pull_down_en = GPIO_PULLDOWN_DISABLE, | ||
| 47 | .intr_type = GPIO_INTR_DISABLE, | ||
| 48 | }; | ||
| 49 | gpio_config(&rst_conf); | ||
| 50 | |||
| 51 | gpio_set_level(TOUCH_RST_PIN, 0); | ||
| 52 | vTaskDelay(pdMS_TO_TICKS(200)); | ||
| 53 | gpio_set_level(TOUCH_RST_PIN, 1); | ||
| 54 | vTaskDelay(pdMS_TO_TICKS(200)); | ||
| 55 | |||
| 56 | i2c_master_bus_config_t bus_cfg = { | ||
| 57 | .i2c_port = I2C_NUM_0, | ||
| 58 | .sda_io_num = TOUCH_SDA_PIN, | ||
| 59 | .scl_io_num = TOUCH_SCL_PIN, | ||
| 60 | .clk_source = I2C_CLK_SRC_DEFAULT, | ||
| 61 | .glitch_ignore_cnt = 7, | ||
| 62 | .intr_priority = 0, | ||
| 63 | .trans_queue_depth = 0, | ||
| 64 | .flags = { | ||
| 65 | .enable_internal_pullup = 1, | ||
| 66 | .allow_pd = 0, | ||
| 67 | }, | ||
| 68 | }; | ||
| 69 | |||
| 70 | esp_err_t ret = i2c_new_master_bus(&bus_cfg, &s_bus); | ||
| 71 | if (ret != ESP_OK) { | ||
| 72 | ESP_LOGE(TAG, "Failed to create I2C bus: %s", esp_err_to_name(ret)); | ||
| 73 | return ret; | ||
| 74 | } | ||
| 75 | |||
| 76 | i2c_device_config_t dev_cfg = { | ||
| 77 | .dev_addr_length = I2C_ADDR_BIT_LEN_7, | ||
| 78 | .device_address = TOUCH_I2C_ADDR, | ||
| 79 | .scl_speed_hz = 400000, | ||
| 80 | .scl_wait_us = 0, | ||
| 81 | .flags = { | ||
| 82 | .disable_ack_check = 0, | ||
| 83 | }, | ||
| 84 | }; | ||
| 85 | |||
| 86 | ret = i2c_master_bus_add_device(s_bus, &dev_cfg, &s_dev); | ||
| 87 | if (ret != ESP_OK) { | ||
| 88 | ESP_LOGE(TAG, "Failed to add I2C device: %s", esp_err_to_name(ret)); | ||
| 89 | i2c_del_master_bus(s_bus); | ||
| 90 | s_bus = NULL; | ||
| 91 | return ret; | ||
| 92 | } | ||
| 93 | |||
| 94 | s_initialized = true; | ||
| 95 | ESP_LOGI(TAG, "Touch initialized (I2C addr 0x%02X)", TOUCH_I2C_ADDR); | ||
| 96 | return ESP_OK; | ||
| 97 | } | ||
| 98 | |||
| 99 | bool touch_read(touch_point_t *pt) { | ||
| 100 | if (!s_initialized || !s_dev || !pt) { | ||
| 101 | if (pt) pt->touched = false; | ||
| 102 | return false; | ||
| 103 | } | ||
| 104 | |||
| 105 | esp_err_t ret = i2c_master_transmit(s_dev, s_read_cmd, sizeof(s_read_cmd), 100); | ||
| 106 | if (ret != ESP_OK) { | ||
| 107 | pt->touched = false; | ||
| 108 | return false; | ||
| 109 | } | ||
| 110 | |||
| 111 | uint8_t data[8] = {0}; | ||
| 112 | ret = i2c_master_receive(s_dev, data, sizeof(data), 100); | ||
| 113 | if (ret != ESP_OK) { | ||
| 114 | pt->touched = false; | ||
| 115 | return false; | ||
| 116 | } | ||
| 117 | |||
| 118 | touch_parse_raw(data, pt); | ||
| 119 | |||
| 120 | if (pt->touched && s_rotation != 0) { | ||
| 121 | uint16_t raw_x = pt->x; | ||
| 122 | uint16_t raw_y = pt->y; | ||
| 123 | switch (s_rotation) { | ||
| 124 | case 1: | ||
| 125 | pt->x = raw_y; | ||
| 126 | pt->y = TOUCH_MAX_X - raw_x; | ||
| 127 | break; | ||
| 128 | case 2: | ||
| 129 | pt->x = TOUCH_MAX_X - raw_x; | ||
| 130 | pt->y = TOUCH_MAX_Y - raw_y; | ||
| 131 | break; | ||
| 132 | case 3: | ||
| 133 | pt->x = TOUCH_MAX_Y - raw_y; | ||
| 134 | pt->y = raw_x; | ||
| 135 | break; | ||
| 136 | } | ||
| 137 | } | ||
| 138 | |||
| 139 | return pt->touched; | ||
| 140 | } | ||
| 141 | |||
| 142 | void touch_set_rotation(int rotation) { | ||
| 143 | s_rotation = rotation; | ||
| 144 | } | ||
| 145 | |||
| 146 | void touch_deinit(void) { | ||
| 147 | if (s_dev) { | ||
| 148 | i2c_master_bus_rm_device(s_dev); | ||
| 149 | s_dev = NULL; | ||
| 150 | } | ||
| 151 | if (s_bus) { | ||
| 152 | i2c_del_master_bus(s_bus); | ||
| 153 | s_bus = NULL; | ||
| 154 | } | ||
| 155 | s_initialized = false; | ||
| 156 | } | ||
diff --git a/main/touch.h b/main/touch.h new file mode 100644 index 0000000..b9e3ccd --- /dev/null +++ b/main/touch.h | |||
| @@ -0,0 +1,29 @@ | |||
| 1 | #ifndef TOUCH_H | ||
| 2 | #define TOUCH_H | ||
| 3 | |||
| 4 | #include "esp_err.h" | ||
| 5 | #include <stdint.h> | ||
| 6 | #include <stdbool.h> | ||
| 7 | |||
| 8 | #define TOUCH_SDA_PIN 4 | ||
| 9 | #define TOUCH_SCL_PIN 8 | ||
| 10 | #define TOUCH_RST_PIN 12 | ||
| 11 | #define TOUCH_INT_PIN 11 | ||
| 12 | #define TOUCH_I2C_ADDR 0x3B | ||
| 13 | #define TOUCH_MAX_X 319 | ||
| 14 | #define TOUCH_MAX_Y 479 | ||
| 15 | |||
| 16 | typedef struct { | ||
| 17 | uint16_t x; | ||
| 18 | uint16_t y; | ||
| 19 | bool touched; | ||
| 20 | } touch_point_t; | ||
| 21 | |||
| 22 | esp_err_t touch_init(void); | ||
| 23 | bool touch_read(touch_point_t *pt); | ||
| 24 | void touch_deinit(void); | ||
| 25 | void touch_set_rotation(int rotation); | ||
| 26 | |||
| 27 | void touch_parse_raw(const uint8_t *data, touch_point_t *pt); | ||
| 28 | |||
| 29 | #endif | ||
diff --git a/main/wifi_setup.c b/main/wifi_setup.c new file mode 100644 index 0000000..b2669e9 --- /dev/null +++ b/main/wifi_setup.c | |||
| @@ -0,0 +1,89 @@ | |||
| 1 | #include "wifi_setup.h" | ||
| 2 | #include <string.h> | ||
| 3 | |||
| 4 | void wifi_setup_init(wifi_setup_t *setup) { | ||
| 5 | if (!setup) return; | ||
| 6 | memset(setup, 0, sizeof(*setup)); | ||
| 7 | setup->state = SETUP_SCAN; | ||
| 8 | setup->selected_ap = -1; | ||
| 9 | } | ||
| 10 | |||
| 11 | void wifi_setup_set_aps(wifi_setup_t *setup, const wifi_ap_info_t *aps, int count) { | ||
| 12 | if (!setup || !aps) return; | ||
| 13 | if (count > WIFI_SETUP_MAX_APS) count = WIFI_SETUP_MAX_APS; | ||
| 14 | memcpy(setup->aps, aps, count * sizeof(wifi_ap_info_t)); | ||
| 15 | setup->ap_count = count; | ||
| 16 | setup->list_scroll = 0; | ||
| 17 | setup->state = SETUP_LIST; | ||
| 18 | } | ||
| 19 | |||
| 20 | int wifi_setup_visible_count(const wifi_setup_t *setup) { | ||
| 21 | if (!setup) return 0; | ||
| 22 | int remaining = setup->ap_count - setup->list_scroll; | ||
| 23 | if (remaining > WIFI_SETUP_MAX_VISIBLE) remaining = WIFI_SETUP_MAX_VISIBLE; | ||
| 24 | return remaining < 0 ? 0 : remaining; | ||
| 25 | } | ||
| 26 | |||
| 27 | const wifi_ap_info_t *wifi_setup_get_visible(const wifi_setup_t *setup, int idx) { | ||
| 28 | if (!setup || idx < 0 || idx >= wifi_setup_visible_count(setup)) return NULL; | ||
| 29 | return &setup->aps[setup->list_scroll + idx]; | ||
| 30 | } | ||
| 31 | |||
| 32 | setup_state_t wifi_setup_handle_select(wifi_setup_t *setup, int list_idx) { | ||
| 33 | if (!setup || setup->state != SETUP_LIST) return setup ? setup->state : SETUP_CANCELLED; | ||
| 34 | if (list_idx < 0 || list_idx >= wifi_setup_visible_count(setup)) return setup->state; | ||
| 35 | |||
| 36 | int real_idx = setup->list_scroll + list_idx; | ||
| 37 | setup->selected_ap = real_idx; | ||
| 38 | strncpy(setup->selected_ssid, setup->aps[real_idx].ssid, WIFI_SETUP_SSID_LEN - 1); | ||
| 39 | setup->selected_ssid[WIFI_SETUP_SSID_LEN - 1] = '\0'; | ||
| 40 | setup->state = SETUP_PASSWORD; | ||
| 41 | return setup->state; | ||
| 42 | } | ||
| 43 | |||
| 44 | setup_state_t wifi_setup_handle_connect(wifi_setup_t *setup) { | ||
| 45 | if (!setup || setup->state != SETUP_PASSWORD) return setup ? setup->state : SETUP_CANCELLED; | ||
| 46 | setup->state = SETUP_CONNECTING; | ||
| 47 | return setup->state; | ||
| 48 | } | ||
| 49 | |||
| 50 | setup_state_t wifi_setup_handle_connect_result(wifi_setup_t *setup, bool success, const char *ip) { | ||
| 51 | if (!setup) return SETUP_CANCELLED; | ||
| 52 | if (setup->state != SETUP_CONNECTING) return setup->state; | ||
| 53 | |||
| 54 | if (success) { | ||
| 55 | setup->state = SETUP_SUCCESS; | ||
| 56 | if (ip) { | ||
| 57 | strncpy(setup->connect_ip, ip, sizeof(setup->connect_ip) - 1); | ||
| 58 | setup->connect_ip[sizeof(setup->connect_ip) - 1] = '\0'; | ||
| 59 | } | ||
| 60 | setup->connect_failed_auth = false; | ||
| 61 | } else { | ||
| 62 | setup->state = SETUP_FAILED; | ||
| 63 | setup->connect_failed_auth = true; | ||
| 64 | setup->connect_ip[0] = '\0'; | ||
| 65 | } | ||
| 66 | return setup->state; | ||
| 67 | } | ||
| 68 | |||
| 69 | setup_state_t wifi_setup_handle_cancel(wifi_setup_t *setup) { | ||
| 70 | if (!setup) return SETUP_CANCELLED; | ||
| 71 | setup->state = SETUP_CANCELLED; | ||
| 72 | return setup->state; | ||
| 73 | } | ||
| 74 | |||
| 75 | setup_state_t wifi_setup_handle_retry(wifi_setup_t *setup) { | ||
| 76 | if (!setup) return SETUP_CANCELLED; | ||
| 77 | if (setup->state != SETUP_FAILED) return setup->state; | ||
| 78 | setup->state = SETUP_PASSWORD; | ||
| 79 | setup->connect_failed_auth = false; | ||
| 80 | return setup->state; | ||
| 81 | } | ||
| 82 | |||
| 83 | setup_state_t wifi_setup_handle_change_network(wifi_setup_t *setup) { | ||
| 84 | if (!setup) return SETUP_CANCELLED; | ||
| 85 | if (setup->state != SETUP_FAILED) return setup->state; | ||
| 86 | setup->state = SETUP_LIST; | ||
| 87 | setup->connect_failed_auth = false; | ||
| 88 | return setup->state; | ||
| 89 | } | ||
diff --git a/main/wifi_setup.h b/main/wifi_setup.h new file mode 100644 index 0000000..17712d5 --- /dev/null +++ b/main/wifi_setup.h | |||
| @@ -0,0 +1,51 @@ | |||
| 1 | #ifndef WIFI_SETUP_H | ||
| 2 | #define WIFI_SETUP_H | ||
| 3 | |||
| 4 | #include "esp_err.h" | ||
| 5 | #include <stdint.h> | ||
| 6 | #include <stdbool.h> | ||
| 7 | |||
| 8 | #define WIFI_SETUP_MAX_APS 20 | ||
| 9 | #define WIFI_SETUP_MAX_VISIBLE 8 | ||
| 10 | #define WIFI_SETUP_SSID_LEN 33 | ||
| 11 | #define WIFI_SETUP_PASS_LEN 64 | ||
| 12 | |||
| 13 | typedef enum { | ||
| 14 | SETUP_SCAN, | ||
| 15 | SETUP_LIST, | ||
| 16 | SETUP_PASSWORD, | ||
| 17 | SETUP_CONNECTING, | ||
| 18 | SETUP_SUCCESS, | ||
| 19 | SETUP_FAILED, | ||
| 20 | SETUP_CANCELLED | ||
| 21 | } setup_state_t; | ||
| 22 | |||
| 23 | typedef struct { | ||
| 24 | char ssid[WIFI_SETUP_SSID_LEN]; | ||
| 25 | int rssi; | ||
| 26 | bool secured; | ||
| 27 | } wifi_ap_info_t; | ||
| 28 | |||
| 29 | typedef struct { | ||
| 30 | setup_state_t state; | ||
| 31 | wifi_ap_info_t aps[WIFI_SETUP_MAX_APS]; | ||
| 32 | int ap_count; | ||
| 33 | int list_scroll; | ||
| 34 | int selected_ap; | ||
| 35 | char selected_ssid[WIFI_SETUP_SSID_LEN]; | ||
| 36 | char connect_ip[16]; | ||
| 37 | bool connect_failed_auth; | ||
| 38 | } wifi_setup_t; | ||
| 39 | |||
| 40 | void wifi_setup_init(wifi_setup_t *setup); | ||
| 41 | void wifi_setup_set_aps(wifi_setup_t *setup, const wifi_ap_info_t *aps, int count); | ||
| 42 | int wifi_setup_visible_count(const wifi_setup_t *setup); | ||
| 43 | const wifi_ap_info_t *wifi_setup_get_visible(const wifi_setup_t *setup, int idx); | ||
| 44 | setup_state_t wifi_setup_handle_select(wifi_setup_t *setup, int list_idx); | ||
| 45 | setup_state_t wifi_setup_handle_connect(wifi_setup_t *setup); | ||
| 46 | setup_state_t wifi_setup_handle_connect_result(wifi_setup_t *setup, bool success, const char *ip); | ||
| 47 | setup_state_t wifi_setup_handle_cancel(wifi_setup_t *setup); | ||
| 48 | setup_state_t wifi_setup_handle_retry(wifi_setup_t *setup); | ||
| 49 | setup_state_t wifi_setup_handle_change_network(wifi_setup_t *setup); | ||
| 50 | |||
| 51 | #endif | ||