upleb.uk

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

summaryrefslogtreecommitdiff
path: root/main/captive_portal.c
diff options
context:
space:
mode:
authorYour Name <you@example.com>2026-05-20 02:14:45 +0530
committerYour Name <you@example.com>2026-05-20 02:14:45 +0530
commit899016795c389151e6b486ec470653f5688e5c5f (patch)
tree64b4c732d6f585552fd14df5f47890764c66ac1a /main/captive_portal.c
parent4f713120dbedd37a3512c2ec1af0766025a59d57 (diff)
feat: merge display-fix — touch driver, keyboard, WiFi setup UI
Squash-merge of feature/display-fix (27 commits): - AXS15231B touch driver with coordinate parsing (touch.c/h) - On-screen keyboard with layout/hit detection (keyboard.c/h) - WiFi setup state machine + config_add_wifi (wifi_setup.c/h) - Web-based WiFi setup via captive portal - Display rotation fix (stride=480), color fixes, DMA byte-swap - WiFi QR code on BOOT and ERROR screens - ERROR state transition after WiFi retries exhausted - Unit tests: test_touch, test_keyboard, test_wifi_setup - Integration test: wifi_setup.mjs - E2E test: wifi-setup.spec.mjs - Per-board hardware locks for flash targets Conflicts resolved: took master for config/cvm/api/main (more current), took display-fix for display/axs15231b (newer fixes).
Diffstat (limited to 'main/captive_portal.c')
-rw-r--r--main/captive_portal.c368
1 files changed, 355 insertions, 13 deletions
diff --git a/main/captive_portal.c b/main/captive_portal.c
index ea83906..6a8c716 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,6 +14,7 @@
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";
18static httpd_handle_t s_server = NULL; 20static httpd_handle_t s_server = NULL;
@@ -342,17 +344,19 @@ static esp_err_t redirect_to_portal_handler(httpd_req_t *req)
342 return portal_handler(req); 344 return portal_handler(req);
343} 345}
344 346
345static esp_err_t catchall_handler(httpd_req_t *req) 347static esp_err_t catchall_err_handler(httpd_req_t *req, httpd_err_code_t err)
346{ 348{
347 ESP_LOGI(TAG, "Catchall: GET %s → 302 → http://%s/", req->uri, s_ap_ip_str); 349 if (err == HTTPD_404_NOT_FOUND) {
348 httpd_resp_set_status(req, "302 Found"); 350 ESP_LOGI(TAG, "Catchall 404: GET %s → 302 → http://%s/", req->uri, s_ap_ip_str);
349 351 httpd_resp_set_status(req, "302 Found");
350 char location[64]; 352 char location[64];
351 snprintf(location, sizeof(location), "http://%s/", s_ap_ip_str); 353 snprintf(location, sizeof(location), "http://%s/", s_ap_ip_str);
352 httpd_resp_set_hdr(req, "Location", location); 354 httpd_resp_set_hdr(req, "Location", location);
353 httpd_resp_set_hdr(req, "Connection", "close"); 355 httpd_resp_set_hdr(req, "Connection", "close");
354 httpd_resp_send(req, NULL, 0); 356 httpd_resp_send(req, NULL, 0);
355 return ESP_OK; 357 return ESP_OK;
358 }
359 return ESP_FAIL;
356} 360}
357 361
358static const httpd_uri_t uri_portal = { .uri = "/", .method = HTTP_GET, .handler = portal_handler }; 362static const httpd_uri_t uri_portal = { .uri = "/", .method = HTTP_GET, .handler = portal_handler };
@@ -368,7 +372,338 @@ 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 }; 372static 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 }; 373static 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 }; 374static 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 }; 375
376static const char SETUP_HTML_TEMPLATE[] = \
377"<!DOCTYPE html>"
378"<html><head>"
379"<meta charset='utf-8'>"
380"<meta name='viewport' content='width=device-width, initial-scale=1'>"
381"<title>TollGate Setup</title>"
382"<style>"
383"*{box-sizing:border-box;margin:0;padding:0}"
384"body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;"
385"background:#0a0a0a;color:#fff;display:flex;align-items:center;justify-content:center;"
386"min-height:100vh;padding:20px}"
387".card{background:#1a1a1a;border:1px solid #333;border-radius:16px;padding:32px;"
388"max-width:400px;width:100%;text-align:center}"
389"h1{font-size:24px;margin-bottom:8px;color:#f7931a}"
390".subtitle{color:#888;margin-bottom:20px;font-size:13px}"
391".networks{margin-top:16px;text-align:left}"
392".net-item{background:#252525;border:1px solid #333;border-radius:8px;"
393"padding:12px;margin-bottom:8px;cursor:pointer;display:flex;justify-content:space-between;align-items:center}"
394".net-item:hover{border-color:#f7931a}"
395".net-item:active{background:#333}"
396".net-ssid{font-size:14px}"
397".net-rssi{font-size:11px;color:#888}"
398".net-lock{color:#f7931a;margin-right:4px}"
399".manual{margin-top:12px}"
400"input{width:100%;background:#252525;border:1px solid #333;border-radius:8px;"
401"color:#fff;padding:12px;font-size:14px;margin-bottom:8px;outline:none}"
402"input:focus{border-color:#f7931a}"
403".btn{background:#f7931a;color:#000;border:none;border-radius:8px;padding:14px 28px;"
404"font-size:16px;font-weight:bold;cursor:pointer;width:100%;margin-top:8px}"
405".btn:hover{background:#e8850f}"
406".btn:disabled{background:#333;color:#666;cursor:not-allowed}"
407"#status{margin-top:12px;padding:10px;border-radius:8px;display:none;font-size:13px}"
408"#status.success{display:block;background:#1a472a;color:#4caf50}"
409"#status.error{display:block;background:#471a1a;color:#f44336}"
410"#status.processing{display:block;background:#1a3a47;color:#2196f3}"
411".refresh{background:none;border:1px solid #444;color:#aaa;border-radius:6px;"
412"padding:6px 12px;font-size:12px;cursor:pointer;margin-top:4px}"
413".refresh:hover{border-color:#f7931a;color:#f7931a}"
414"#manualForm{display:none;margin-top:12px}"
415"</style>"
416"</head><body>"
417"<div class='card'>"
418"<h1>TollGate Setup</h1>"
419"<p class='subtitle'>Configure upstream WiFi</p>"
420"<div id='scanStatus'>Scanning...</div>"
421"<div class='networks' id='networkList'></div>"
422"<button class='refresh' onclick='scanWifi()'>Rescan</button>"
423"<button class='refresh' onclick='showManual()'>Manual entry</button>"
424"<div id='manualForm'>"
425"<input id='manualSsid' placeholder='SSID'>"
426"<input id='manualPass' type='password' placeholder='Password'>"
427"<button class='btn' onclick='connectManual()'>Connect</button>"
428"</div>"
429"<div id='passwordForm' style='display:none'>"
430"<p style='margin:12px 0 8px;text-align:left' id='selectedNetwork'></p>"
431"<input id='wifiPass' type='password' placeholder='WiFi password'>"
432"<button class='btn' onclick='connectSelected()'>Connect</button>"
433"</div>"
434"<div id='status'></div>"
435"</div>"
436"<script>"
437"const apIp='__AP_IP__';"
438"let selectedSsid='';"
439"function showStatus(msg,type){const s=document.getElementById('status');"
440"s.textContent=msg;s.className=type;}"
441"function scanWifi(){"
442"document.getElementById('scanStatus').textContent='Scanning...';"
443"document.getElementById('networkList').innerHTML='';"
444"fetch('/wifi/scan').then(r=>r.json()).then(aps=>{"
445"document.getElementById('scanStatus').textContent=aps.length+' networks found';"
446"const list=document.getElementById('networkList');"
447"aps.forEach(ap=>{"
448"const div=document.createElement('div');"
449"div.className='net-item';"
450"const lock=ap.secured?'<span class=net-lock>&#128274;</span>':'';"
451"div.innerHTML='<span class=net-ssid>'+lock+ap.ssid+'</span>"
452"<span class=net-rssi>'+ap.rssi+' dBm</span>';"
453"div.onclick=()=>selectNetwork(ap.ssid,ap.secured);"
454"list.appendChild(div);"
455"});"
456"}).catch(e=>{document.getElementById('scanStatus').textContent='Scan failed';});"
457"}"
458"function selectNetwork(ssid,secured){"
459"selectedSsid=ssid;"
460"document.getElementById('selectedNetwork').textContent='Connect to: '+ssid;"
461"document.getElementById('passwordForm').style.display='block';"
462"document.getElementById('scanStatus').style.display='none';"
463"document.getElementById('networkList').style.display='none';"
464"document.querySelector('.refresh').style.display='none';"
465"if(!secured){connectSelected();}"
466"}"
467"function showManual(){"
468"document.getElementById('manualForm').style.display='block';"
469"}"
470"function connectSelected(){"
471"const pass=document.getElementById('wifiPass').value;"
472"doConnect(selectedSsid,pass);"
473"}"
474"function connectManual(){"
475"const ssid=document.getElementById('manualSsid').value.trim();"
476"const pass=document.getElementById('manualPass').value;"
477"if(!ssid){showStatus('Enter SSID','error');return;}"
478"doConnect(ssid,pass);"
479"}"
480"function doConnect(ssid,pass){"
481"showStatus('Connecting to '+ssid+'...','processing');"
482"fetch('/wifi/connect',{method:'POST',headers:{'Content-Type':'application/json'},"
483"body:JSON.stringify({ssid:ssid,password:pass})})"
484".then(r=>r.json()).then(d=>{"
485"if(d.ok){showStatus('Connected! Device is restarting...','success');}"
486"else{showStatus('Failed: '+(d.error||'unknown'),'error');}"
487"}).catch(e=>{showStatus('Connection error','error');});"
488"}"
489"scanWifi();"
490"</script>"
491"</body></html>";
492
493static char *template_replace(const char *tpl, const char *key, const char *val) {
494 const char *p;
495 size_t klen = strlen(key);
496 size_t vlen = strlen(val);
497 size_t tlen = strlen(tpl);
498 size_t extra = 0;
499 p = tpl;
500 while ((p = strstr(p, key)) != NULL) {
501 extra += vlen - klen;
502 p += klen;
503 }
504 size_t out_size = tlen + extra + 1;
505 char *out = malloc(out_size);
506 if (!out) return NULL;
507 char *dst = out;
508 p = tpl;
509 while (*p) {
510 const char *found = strstr(p, key);
511 if (found) {
512 memcpy(dst, p, found - p);
513 dst += found - p;
514 memcpy(dst, val, vlen);
515 dst += vlen;
516 p = found + klen;
517 } else {
518 strcpy(dst, p);
519 dst += strlen(p);
520 break;
521 }
522 }
523 *dst = '\0';
524 return out;
525}
526
527static bool is_setup_available(void) {
528 const tollgate_config_t *cfg = tollgate_config_get();
529 return cfg->network_count == 0;
530}
531
532static esp_err_t setup_page_handler(httpd_req_t *req) {
533 if (!is_setup_available()) {
534 httpd_resp_set_status(req, "302 Found");
535 char location[64];
536 snprintf(location, sizeof(location), "http://%s/", s_ap_ip_str);
537 httpd_resp_set_hdr(req, "Location", location);
538 httpd_resp_send(req, NULL, 0);
539 return ESP_OK;
540 }
541
542 httpd_resp_set_type(req, "text/html");
543 char *html = template_replace(SETUP_HTML_TEMPLATE, "__AP_IP__", s_ap_ip_str);
544 if (!html) {
545 httpd_resp_send_500(req);
546 return ESP_OK;
547 }
548 httpd_resp_send(req, html, strlen(html));
549 free(html);
550 return ESP_OK;
551}
552
553static esp_err_t wifi_scan_handler(httpd_req_t *req) {
554 esp_wifi_disconnect();
555 vTaskDelay(pdMS_TO_TICKS(300));
556
557 wifi_scan_config_t scan_cfg = {0};
558 scan_cfg.scan_type = WIFI_SCAN_TYPE_ACTIVE;
559 scan_cfg.scan_time.active.min = 100;
560 scan_cfg.scan_time.active.max = 300;
561 esp_err_t ret = esp_wifi_scan_start(&scan_cfg, true);
562 if (ret != ESP_OK) {
563 httpd_resp_set_type(req, "application/json");
564 httpd_resp_send(req, "[]", 2);
565 return ESP_OK;
566 }
567
568 uint16_t ap_count = 0;
569 esp_wifi_scan_get_ap_num(&ap_count);
570 if (ap_count > 20) ap_count = 20;
571 wifi_ap_record_t aps[20];
572 esp_wifi_scan_get_ap_records(&ap_count, aps);
573
574 for (int i = 0; i < (int)ap_count - 1; i++) {
575 for (int j = i + 1; j < (int)ap_count; j++) {
576 if (aps[j].rssi > aps[i].rssi) {
577 wifi_ap_record_t tmp = aps[i];
578 aps[i] = aps[j];
579 aps[j] = tmp;
580 }
581 }
582 }
583
584 cJSON *root = cJSON_CreateArray();
585 for (int i = 0; i < (int)ap_count; i++) {
586 if (aps[i].ssid[0] == '\0') continue;
587 cJSON *ap = cJSON_CreateObject();
588 cJSON_AddStringToObject(ap, "ssid", (const char *)aps[i].ssid);
589 cJSON_AddNumberToObject(ap, "rssi", aps[i].rssi);
590 cJSON_AddBoolToObject(ap, "secured", aps[i].authmode != WIFI_AUTH_OPEN);
591 cJSON_AddItemToArray(root, ap);
592 }
593
594 char *json = cJSON_PrintUnformatted(root);
595 httpd_resp_set_type(req, "application/json");
596 httpd_resp_send(req, json, strlen(json));
597 cJSON_free(json);
598 cJSON_Delete(root);
599
600 const tollgate_config_t *cfg = tollgate_config_get();
601 if (cfg->network_count > 0) {
602 wifi_config_t wifi_cfg;
603 if (tollgate_config_get_wifi(&wifi_cfg) == ESP_OK) {
604 esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg);
605 esp_wifi_connect();
606 }
607 }
608
609 return ESP_OK;
610}
611
612static esp_err_t wifi_connect_handler(httpd_req_t *req) {
613 int content_len = req->content_len;
614 if (content_len <= 0 || content_len > 1024) {
615 httpd_resp_set_type(req, "application/json");
616 httpd_resp_send(req, "{\"ok\":false,\"error\":\"invalid request\"}", HTTPD_RESP_USE_STRLEN);
617 return ESP_OK;
618 }
619
620 char *body = malloc(content_len + 1);
621 if (!body) {
622 httpd_resp_send_500(req);
623 return ESP_OK;
624 }
625 int total = 0;
626 while (total < content_len) {
627 int r = httpd_req_recv(req, body + total, content_len - total);
628 if (r <= 0) { free(body); httpd_resp_send_500(req); return ESP_OK; }
629 total += r;
630 }
631 body[total] = '\0';
632
633 cJSON *json = cJSON_Parse(body);
634 free(body);
635 if (!json) {
636 httpd_resp_set_type(req, "application/json");
637 httpd_resp_send(req, "{\"ok\":false,\"error\":\"invalid JSON\"}", HTTPD_RESP_USE_STRLEN);
638 return ESP_OK;
639 }
640
641 cJSON *ssid_item = cJSON_GetObjectItem(json, "ssid");
642 cJSON *pass_item = cJSON_GetObjectItem(json, "password");
643 if (!ssid_item || !cJSON_IsString(ssid_item)) {
644 cJSON_Delete(json);
645 httpd_resp_set_type(req, "application/json");
646 httpd_resp_send(req, "{\"ok\":false,\"error\":\"missing ssid\"}", HTTPD_RESP_USE_STRLEN);
647 return ESP_OK;
648 }
649
650 const char *ssid = ssid_item->valuestring;
651 const char *password = (pass_item && cJSON_IsString(pass_item)) ? pass_item->valuestring : "";
652
653 esp_err_t err = tollgate_config_add_wifi(ssid, password);
654 if (err != ESP_OK) {
655 cJSON_Delete(json);
656 httpd_resp_set_type(req, "application/json");
657 httpd_resp_send(req, "{\"ok\":false,\"error\":\"save failed\"}", HTTPD_RESP_USE_STRLEN);
658 return ESP_OK;
659 }
660
661 wifi_config_t wifi_cfg = {0};
662 strncpy((char *)wifi_cfg.sta.ssid, ssid, sizeof(wifi_cfg.sta.ssid) - 1);
663 strncpy((char *)wifi_cfg.sta.password, password, sizeof(wifi_cfg.sta.password) - 1);
664 wifi_cfg.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
665 esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg);
666 esp_wifi_connect();
667
668 cJSON_Delete(json);
669
670 httpd_resp_set_type(req, "application/json");
671 httpd_resp_send(req, "{\"ok\":true}", HTTPD_RESP_USE_STRLEN);
672 return ESP_OK;
673}
674
675static esp_err_t wifi_status_handler(httpd_req_t *req) {
676 wifi_ap_record_t ap_info;
677 bool connected = (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK);
678
679 cJSON *root = cJSON_CreateObject();
680 cJSON_AddBoolToObject(root, "connected", connected);
681
682 if (connected) {
683 esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
684 if (netif) {
685 esp_netif_ip_info_t ip_info;
686 if (esp_netif_get_ip_info(netif, &ip_info) == ESP_OK) {
687 char ip_str[16];
688 snprintf(ip_str, sizeof(ip_str), IPSTR, IP2STR(&ip_info.ip));
689 cJSON_AddStringToObject(root, "ip", ip_str);
690 }
691 }
692 cJSON_AddStringToObject(root, "ssid", (const char *)ap_info.ssid);
693 }
694
695 char *json = cJSON_PrintUnformatted(root);
696 httpd_resp_set_type(req, "application/json");
697 httpd_resp_send(req, json, strlen(json));
698 cJSON_free(json);
699 cJSON_Delete(root);
700 return ESP_OK;
701}
702
703static const httpd_uri_t uri_setup = { .uri = "/setup", .method = HTTP_GET, .handler = setup_page_handler };
704static const httpd_uri_t uri_wifi_scan = { .uri = "/wifi/scan", .method = HTTP_GET, .handler = wifi_scan_handler };
705static const httpd_uri_t uri_wifi_connect = { .uri = "/wifi/connect", .method = HTTP_POST, .handler = wifi_connect_handler };
706static const httpd_uri_t uri_wifi_status = { .uri = "/wifi/status", .method = HTTP_GET, .handler = wifi_status_handler };
372 707
373esp_err_t captive_portal_start(const char *ap_ip_str) 708esp_err_t captive_portal_start(const char *ap_ip_str)
374{ 709{
@@ -377,7 +712,6 @@ esp_err_t captive_portal_start(const char *ap_ip_str)
377 712
378 httpd_config_t config = HTTPD_DEFAULT_CONFIG(); 713 httpd_config_t config = HTTPD_DEFAULT_CONFIG();
379 config.max_uri_handlers = 20; 714 config.max_uri_handlers = 20;
380 config.uri_match_fn = httpd_uri_match_wildcard;
381 715
382 esp_err_t ret = httpd_start(&s_server, &config); 716 esp_err_t ret = httpd_start(&s_server, &config);
383 if (ret != ESP_OK) { 717 if (ret != ESP_OK) {
@@ -398,7 +732,15 @@ esp_err_t captive_portal_start(const char *ap_ip_str)
398 httpd_register_uri_handler(s_server, &uri_ncsi); 732 httpd_register_uri_handler(s_server, &uri_ncsi);
399 httpd_register_uri_handler(s_server, &uri_connecttest); 733 httpd_register_uri_handler(s_server, &uri_connecttest);
400 httpd_register_uri_handler(s_server, &uri_wpad); 734 httpd_register_uri_handler(s_server, &uri_wpad);
401 httpd_register_uri_handler(s_server, &uri_catchall); 735 httpd_register_uri_handler(s_server, &uri_setup);
736 ret = httpd_register_uri_handler(s_server, &uri_wifi_scan);
737 ESP_LOGI(TAG, "Registered /wifi/scan: %s", esp_err_to_name(ret));
738 ret = httpd_register_uri_handler(s_server, &uri_wifi_connect);
739 ESP_LOGI(TAG, "Registered /wifi/connect: %s", esp_err_to_name(ret));
740 ret = httpd_register_uri_handler(s_server, &uri_wifi_status);
741 ESP_LOGI(TAG, "Registered /wifi/status: %s", esp_err_to_name(ret));
742
743 httpd_register_err_handler(s_server, HTTPD_404_NOT_FOUND, catchall_err_handler);
402 744
403 ESP_LOGI(TAG, "Captive portal started on port 80"); 745 ESP_LOGI(TAG, "Captive portal started on port 80");
404 return ESP_OK; 746 return ESP_OK;