upleb.uk

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

summaryrefslogtreecommitdiff
path: root/main
diff options
context:
space:
mode:
Diffstat (limited to 'main')
-rw-r--r--main/CMakeLists.txt15
-rw-r--r--main/captive_portal.c386
-rw-r--r--main/captive_portal.h3
-rw-r--r--main/cashu.c1
-rw-r--r--main/config.c90
-rw-r--r--main/config.h1
-rw-r--r--main/cvm_server.c64
-rw-r--r--main/display.c349
-rw-r--r--main/display.h10
-rw-r--r--main/dns_server.c2
-rw-r--r--main/keyboard.c186
-rw-r--r--main/keyboard.h53
-rw-r--r--main/mint_health.c64
-rw-r--r--main/mint_health.h1
-rw-r--r--main/tollgate_api.c113
-rw-r--r--main/tollgate_api.h1
-rw-r--r--main/tollgate_main.c92
-rw-r--r--main/tollgate_platform.c63
-rw-r--r--main/touch.c156
-rw-r--r--main/touch.h29
-rw-r--r--main/wifi_setup.c89
-rw-r--r--main/wifi_setup.h51
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
17static const char *TAG = "captive_portal"; 19static const char *TAG = "captive_portal";
20static bool s_start_called = false;
21static esp_err_t s_start_result = ESP_OK;
18static httpd_handle_t s_server = NULL; 22static httpd_handle_t s_server = NULL;
19static char s_ap_ip_str[16] = "10.0.0.1"; 23static 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
345static esp_err_t catchall_handler(httpd_req_t *req) 349static 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
358static const httpd_uri_t uri_portal = { .uri = "/", .method = HTTP_GET, .handler = portal_handler }; 364static 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
368static const httpd_uri_t uri_ncsi = { .uri = "/ncsi.txt", .method = HTTP_GET, .handler = redirect_to_portal_handler }; 374static const httpd_uri_t uri_ncsi = { .uri = "/ncsi.txt", .method = HTTP_GET, .handler = redirect_to_portal_handler };
369static const httpd_uri_t uri_connecttest = { .uri = "/connecttest.txt", .method = HTTP_GET, .handler = redirect_to_portal_handler }; 375static const httpd_uri_t uri_connecttest = { .uri = "/connecttest.txt", .method = HTTP_GET, .handler = redirect_to_portal_handler };
370static const httpd_uri_t uri_wpad = { .uri = "/wpad.dat", .method = HTTP_GET, .handler = redirect_to_portal_handler }; 376static const httpd_uri_t uri_wpad = { .uri = "/wpad.dat", .method = HTTP_GET, .handler = redirect_to_portal_handler };
371static const httpd_uri_t uri_catchall = { .uri = "/*", .method = HTTP_GET, .handler = catchall_handler }; 377
378static 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>&#128274;</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
495static 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
529static bool is_setup_available(void) {
530 const tollgate_config_t *cfg = tollgate_config_get();
531 return cfg->network_count == 0;
532}
533
534static 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
555static 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
614static 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
677static 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
705static const httpd_uri_t uri_setup = { .uri = "/setup", .method = HTTP_GET, .handler = setup_page_handler };
706static const httpd_uri_t uri_wifi_scan = { .uri = "/wifi/scan", .method = HTTP_GET, .handler = wifi_scan_handler };
707static const httpd_uri_t uri_wifi_connect = { .uri = "/wifi/connect", .method = HTTP_POST, .handler = wifi_connect_handler };
708static const httpd_uri_t uri_wifi_status = { .uri = "/wifi/status", .method = HTTP_GET, .handler = wifi_status_handler };
372 709
373esp_err_t captive_portal_start(const char *ap_ip_str) 710esp_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
754bool captive_portal_was_start_called(void) { return s_start_called; }
755esp_err_t captive_portal_get_start_result(void) { return s_start_result; }
756
407void captive_portal_stop(void) 757void 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 @@
7esp_err_t captive_portal_start(const char *ap_ip_str); 7esp_err_t captive_portal_start(const char *ap_ip_str);
8void captive_portal_stop(void); 8void captive_portal_stop(void);
9httpd_handle_t captive_portal_get_server(void); 9httpd_handle_t captive_portal_get_server(void);
10bool captive_portal_is_setup_available(void);
11bool captive_portal_was_start_called(void);
12esp_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
479esp_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);
105const tollgate_config_t *tollgate_config_get(void); 105const tollgate_config_t *tollgate_config_get(void);
106esp_err_t tollgate_config_get_wifi(wifi_config_t *wifi_config); 106esp_err_t tollgate_config_get_wifi(wifi_config_t *wifi_config);
107esp_err_t tollgate_config_get_next_wifi(wifi_config_t *wifi_config); 107esp_err_t tollgate_config_get_next_wifi(wifi_config_t *wifi_config);
108esp_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
733void cvm_server_stop(void) 747void 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 @@
12static const char *TAG = "display"; 15static 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
16static volatile display_state_t s_state = DISPLAY_BOOT; 29static volatile display_state_t s_state = DISPLAY_BOOT;
17static char s_ap_ssid[32] = ""; 30static char s_ap_ssid[32] = "";
18static char s_portal_url[256] = ""; 31static char s_portal_url[256] = "";
32static char s_mint_url[256] = "";
33static char s_wifi_status[32] = "starting...";
19static int s_active_clients = 0; 34static int s_active_clients = 0;
20static uint64_t s_wallet_balance = 0; 35static uint64_t s_wallet_balance = 0;
36static int s_price_per_step = 0;
21static bool s_initialized = false; 37static bool s_initialized = false;
22static int64_t s_last_qr_switch = 0; 38static int64_t s_last_qr_switch = 0;
23static display_qr_mode_t s_qr_mode = DISPLAY_QR_WIFI; 39static display_qr_mode_t s_qr_mode = DISPLAY_QR_WIFI;
40static int s_last_payment_sats = 0;
41static int64_t s_last_allotment_ms = 0;
42
43static 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
25static int qr_version_from_strlen(int len) { 49static 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
86static 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
62static void build_wifi_qr_string(char *out, int out_size) { 96static 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
109static 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
68void display_render_text(int x, int y, const char *text, uint16_t fg, uint16_t bg, int scale) { 142void 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
101static 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
138void display_render_qr(const char *text) { 175void 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
146static void render_boot_screen(void) { 183static 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
153static void render_ready_screen(void) { 209static 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
263static 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
183static void render_payment_screen(void) { 302static 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
190static void render_error_screen(void) { 333static 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
252void display_update(const char *ap_ssid, int active_clients, 438void 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
463void 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
470void display_notify_wifi_connected(const char *ip) {
471 (void)ip;
472}
473
474void 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
15typedef enum { 16typedef enum {
@@ -20,7 +21,12 @@ typedef enum {
20esp_err_t display_init(void); 21esp_err_t display_init(void);
21void display_set_state(display_state_t state); 22void display_set_state(display_state_t state);
22void display_update(const char *ap_ssid, int active_clients, 23void 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);
27void display_notify_payment(int amount_sats, int64_t allotment_ms);
28void display_notify_wifi_connected(const char *ip);
29void display_notify_wifi_disconnected(void);
24void display_render_text(int x, int y, const char *text, uint16_t fg, uint16_t bg, int scale); 30void display_render_text(int x, int y, const char *text, uint16_t fg, uint16_t bg, int scale);
25void display_render_qr(const char *text); 31void 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
4static const char *s_alpha_lower[] = {
5 "qwertyuiop",
6 "asdfghjkl",
7 "\001zxcvbnm\b",
8 "\002\003\004"
9};
10
11static const char *s_alpha_upper[] = {
12 "QWERTYUIOP",
13 "ASDFGHJKL",
14 "\001ZXCVBNM\b",
15 "\002\003\004"
16};
17
18static 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
31static 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
40void 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
47void kb_set_layout(const kb_layout_t *layout) {
48 if (layout) s_layout = *layout;
49}
50
51const kb_layout_t *kb_get_layout(void) {
52 return &s_layout;
53}
54
55static 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
63int 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
74static 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
89static 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
103kb_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
151void 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
9typedef enum {
10 KB_ALPHA_LOWER,
11 KB_ALPHA_UPPER,
12 KB_NUMSYM
13} kb_layer_t;
14
15typedef 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
25typedef struct {
26 char input[KB_INPUT_MAX + 1];
27 int cursor;
28 bool reveal;
29 kb_layer_t layer;
30} kb_state_t;
31
32typedef struct {
33 kb_action_t action;
34 char ch;
35} kb_result_t;
36
37typedef 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
46void kb_state_init(kb_state_t *st);
47void kb_set_layout(const kb_layout_t *layout);
48const kb_layout_t *kb_get_layout(void);
49int kb_get_row_keys(int row, kb_layer_t layer, const char **keys_out);
50kb_result_t kb_hit_test(int tx, int ty, kb_layer_t layer);
51void 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
11static const char *TAG = "mint_health"; 14static const char *TAG = "mint_health";
12 15
16#define WALLET_QUEUE_LEN 8
17static QueueHandle_t s_wallet_queue = NULL;
18
19static int s_last_probe_err = 0;
20
13static mint_status_t s_mints[MINT_HEALTH_MAX]; 21static mint_status_t s_mints[MINT_HEALTH_MAX];
14static int s_mint_count = 0; 22static int s_mint_count = 0;
15static bool s_running = false; 23static 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
172static 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
156static void health_task(void *pvParameters) 189static 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
21typedef void (*mint_health_changed_cb)(void); 22typedef 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
21static const char *TAG = "tollgate_api"; 25static const char *TAG = "tollgate_api";
22static httpd_handle_t s_api_server = NULL; 26static httpd_handle_t s_api_server = NULL;
23 27
28static QueueHandle_t s_wallet_queue = NULL;
29
30void tls_worker_set_queue(QueueHandle_t q)
31{
32 s_wallet_queue = q;
33}
34
35static 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, &copy, 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
24static esp_err_t get_client_ip(httpd_req_t *req, uint32_t *ip_out) 53static 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
711extern bool s_start_services_called;
712extern bool s_start_ap_services_called;
713extern bool s_sta_got_ip;
714extern bool s_ap_started;
715extern esp_ip4_addr_t s_sta_ip;
716extern esp_ip4_addr_t s_sta_gw;
717
718static 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
734static const httpd_uri_t uri_market = { .uri = "/market", .method = HTTP_GET, .handler = api_get_market }; 809static const httpd_uri_t uri_market = { .uri = "/market", .method = HTTP_GET, .handler = api_get_market };
810static const httpd_uri_t uri_debug = { .uri = "/debug", .method = HTTP_GET, .handler = api_get_debug };
735 811
736esp_err_t tollgate_api_start(void) 812esp_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
7esp_err_t tollgate_api_start(void); 7esp_err_t tollgate_api_start(void);
8void tollgate_api_stop(void); 8void tollgate_api_stop(void);
9void 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";
55static relay_selector_t s_relay_selector; 55static relay_selector_t s_relay_selector;
56static sync_manager_t s_sync_manager; 56static sync_manager_t s_sync_manager;
57 57
58volatile bool s_start_services_called = false;
59volatile bool s_start_ap_services_called = false;
60volatile bool s_sta_got_ip = false;
61volatile bool s_ap_started = false;
62volatile esp_ip4_addr_t s_sta_ip = {0};
63volatile esp_ip4_addr_t s_sta_gw = {0};
64
58static void start_services(void); 65static void start_services(void);
59static void stop_services(void); 66static void stop_services(void);
60static void start_ap_services(void); 67static void start_ap_services(void);
61 68
62static void start_ap_services(void) 69static 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
127static void services_start_task(void *pvParameters) 138static 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
162static 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
169static void publish_wifistr_task(void *pvParameters) 179static 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
178static void start_services(void) 188static 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
8static const char *TAG = "tollgate_platform";
9
10static 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
16static 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
22static 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
28static const char *platform_get_metric(void)
29{
30 const tollgate_config_t *cfg = tollgate_config_get();
31 return cfg ? cfg->metric : "milliseconds";
32}
33
34static 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
40static int64_t platform_get_time_ms(void)
41{
42 return (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS;
43}
44
45static bool platform_spend_proofs(const char *raw_token_json)
46{
47 (void)raw_token_json;
48 return true;
49}
50
51const 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
9static const char *TAG = "touch";
10
11static i2c_master_bus_handle_t s_bus = NULL;
12static i2c_master_dev_handle_t s_dev = NULL;
13static bool s_initialized = false;
14static int s_rotation = 0;
15
16static const uint8_t s_read_cmd[11] = {
17 0xb5, 0xab, 0xa5, 0x5a, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00
18};
19
20void 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
39esp_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
99bool 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
142void touch_set_rotation(int rotation) {
143 s_rotation = rotation;
144}
145
146void 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
16typedef struct {
17 uint16_t x;
18 uint16_t y;
19 bool touched;
20} touch_point_t;
21
22esp_err_t touch_init(void);
23bool touch_read(touch_point_t *pt);
24void touch_deinit(void);
25void touch_set_rotation(int rotation);
26
27void 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
4void 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
11void 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
20int 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
27const 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
32setup_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
44setup_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
50setup_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
69setup_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
75setup_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
83setup_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
13typedef 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
23typedef struct {
24 char ssid[WIFI_SETUP_SSID_LEN];
25 int rssi;
26 bool secured;
27} wifi_ap_info_t;
28
29typedef 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
40void wifi_setup_init(wifi_setup_t *setup);
41void wifi_setup_set_aps(wifi_setup_t *setup, const wifi_ap_info_t *aps, int count);
42int wifi_setup_visible_count(const wifi_setup_t *setup);
43const wifi_ap_info_t *wifi_setup_get_visible(const wifi_setup_t *setup, int idx);
44setup_state_t wifi_setup_handle_select(wifi_setup_t *setup, int list_idx);
45setup_state_t wifi_setup_handle_connect(wifi_setup_t *setup);
46setup_state_t wifi_setup_handle_connect_result(wifi_setup_t *setup, bool success, const char *ip);
47setup_state_t wifi_setup_handle_cancel(wifi_setup_t *setup);
48setup_state_t wifi_setup_handle_retry(wifi_setup_t *setup);
49setup_state_t wifi_setup_handle_change_network(wifi_setup_t *setup);
50
51#endif