From aa58b47996083f36e3587b8e10f9bbb681610491 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 19 May 2026 04:21:14 +0530 Subject: feat: web-based WiFi setup via captive portal, portrait-only display - Remove touchscreen WiFi setup (touch.c, keyboard.c, wifi_setup.c from build) - Remove offscreen buffer and landscape rotation from axs15231b driver - Add /setup HTML page with WiFi scan/connect via captive portal - Add /wifi/scan, /wifi/connect, /wifi/status HTTP endpoints - Display shows SETUP_PENDING (QR + SSID + setup URL) when unconfigured - Display shows ERROR with setup URL when upstream is down - All 101 unit tests pass, builds and flashes to Board C --- WEB_WIFI_SETUP_PLAN.md | 72 ++++ components/axs15231b/axs15231b.c | 37 +- components/axs15231b/include/axs15231b.h | 2 - main/CMakeLists.txt | 3 - main/captive_portal.c | 338 ++++++++++++++++ main/captive_portal.h | 1 + main/display.c | 654 +++++-------------------------- main/display.h | 3 +- main/tollgate_main.c | 7 +- tests/integration/wifi_setup.mjs | 74 ++++ 10 files changed, 597 insertions(+), 594 deletions(-) create mode 100644 WEB_WIFI_SETUP_PLAN.md create mode 100644 tests/integration/wifi_setup.mjs diff --git a/WEB_WIFI_SETUP_PLAN.md b/WEB_WIFI_SETUP_PLAN.md new file mode 100644 index 0000000..1a3f742 --- /dev/null +++ b/WEB_WIFI_SETUP_PLAN.md @@ -0,0 +1,72 @@ +# Web WiFi Setup Plan + +## Overview + +Move WiFi configuration from on-display touchscreen UI to a web-based setup page +served by the captive portal. The display becomes portrait-only, showing QR codes +and status info. No more landscape rotation, on-screen keyboard, or touch-driven +WiFi setup. + +## Architecture + +### Display (portrait 320x480 only) + +| State | When | Content | +|-------|------|---------| +| BOOT | Startup | "TollGate" title + "starting..." | +| READY (unconfigured) | No STA network | AP WiFi QR + SSID + "http://AP_IP/setup" | +| READY (configured) | STA connected | QR cycling (WiFi↔Portal) + balance/clients/price | +| PAYMENT_RECEIVED | After payment | "ACCESS GRANTED" + amount + time (3s then→READY) | +| ERROR | No upstream | "NO UPSTREAM" + "http://AP_IP/setup" | + +### Captive Portal (new endpoints) + +| Endpoint | Method | Purpose | +|----------|--------|---------| +| `/setup` | GET | WiFi setup HTML page (only when unconfigured) | +| `/wifi/scan` | GET | Trigger scan, return `[{ssid,rssi,secured}]` JSON | +| `/wifi/connect` | POST | Take `{ssid,password}`, save config, connect | +| `/wifi/status` | GET | Return `{connected,ip,ssid}` | + +### Files Removed from Build (kept on disk) + +- `main/touch.c` / `touch.h` +- `main/keyboard.c` / `keyboard.h` +- `main/wifi_setup.c` / `wifi_setup.h` + +### Files Modified + +- `main/display.c` — Strip WiFi setup state, rotation, offscreen; add setup URL text +- `main/display.h` — Remove `DISPLAY_WIFI_SETUP`, `display_enter_wifi_setup()` +- `main/tollgate_main.c` — Remove WiFi setup auto-enter, add display state for unconfigured +- `main/captive_portal.c` — Add WiFi scan/connect/status endpoints + `/setup` HTML +- `main/captive_portal.h` — Expose captive_portal_is_setup_available() +- `components/axs15231b/axs15231b.c` — Remove offscreen buffer +- `components/axs15231b/include/axs15231b.h` — Remove `axs15231b_set_offscreen()` +- `main/CMakeLists.txt` — Remove touch/keyboard/wifi_setup sources + +## Checklist + +### Phase 1: Strip display and driver +- [x] Remove offscreen buffer from `axs15231b.c` and `axs15231b.h` +- [x] Strip `display.c` — remove WiFi setup state, rotation, keyboard/touch imports +- [x] Update `display.h` — remove `DISPLAY_WIFI_SETUP`, `display_enter_wifi_setup()` +- [x] Add setup URL text to READY (unconfigured) and ERROR screens +- [x] Remove WiFi setup auto-enter from `tollgate_main.c` + +### Phase 2: Add web WiFi setup +- [x] Add `/wifi/scan` endpoint to `captive_portal.c` +- [x] Add `/wifi/connect` endpoint to `captive_portal.c` +- [x] Add `/wifi/status` endpoint to `captive_portal.c` +- [x] Add `/setup` HTML page with scan list + connect form +- [x] Gate `/setup` behind `network_count == 0` + +### Phase 3: Build configuration +- [x] Remove touch.c, keyboard.c, wifi_setup.c from `main/CMakeLists.txt` + +### Phase 4: Testing +- [x] `make test-unit` passes +- [x] Build succeeds (`idf.py build`) +- [x] Flash to Board C, verify portrait display shows setup URL +- [ ] Test `/setup` page from phone browser +- [x] Write integration test `tests/integration/wifi_setup.mjs` diff --git a/components/axs15231b/axs15231b.c b/components/axs15231b/axs15231b.c index ac05ba7..00e8467 100644 --- a/components/axs15231b/axs15231b.c +++ b/components/axs15231b/axs15231b.c @@ -37,8 +37,7 @@ static spi_device_handle_t s_spi = NULL; static uint16_t *s_fb = NULL; static int s_width = AXS15231B_WIDTH; static int s_height = AXS15231B_HEIGHT; -static int s_rotation = 0; -static int s_stride = 480; +static int s_stride = AXS15231B_WIDTH; static uint8_t *s_swap_buf = NULL; #define SWAP_BUF_PIXELS 2048 @@ -243,7 +242,7 @@ esp_err_t axs15231b_init(void) { cs_init(); - size_t fb_size = (size_t)480 * 480 * 2; + size_t fb_size = (size_t)s_width * s_height * 2; s_fb = heap_caps_malloc(fb_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); if (!s_fb) { ESP_LOGE(TAG, "Failed to allocate framebuffer (%zu bytes)", fb_size); @@ -322,7 +321,6 @@ void axs15231b_flush(void) { qspi_write_command(RAMWR); bool first = true; - cs_low(); for (int row = 0; row < s_height; row++) { int chunk_remaining = s_width; @@ -359,34 +357,3 @@ void axs15231b_flush(void) { int axs15231b_get_width(void) { return s_width; } int axs15231b_get_height(void) { return s_height; } - -void axs15231b_set_rotation(int rotation) { - uint8_t madctl = MADCTL_RGB; - switch (rotation) { - case 0: - madctl = MADCTL_RGB; - s_width = AXS15231B_WIDTH; - s_height = AXS15231B_HEIGHT; - break; - case 1: - madctl = MADCTL_MX | MADCTL_MV; - s_width = AXS15231B_HEIGHT; - s_height = AXS15231B_WIDTH; - break; - case 2: - madctl = MADCTL_MX | MADCTL_MY; - s_width = AXS15231B_WIDTH; - s_height = AXS15231B_HEIGHT; - break; - case 3: - madctl = MADCTL_MY | MADCTL_MV; - s_width = AXS15231B_HEIGHT; - s_height = AXS15231B_WIDTH; - break; - default: - return; - } - s_rotation = rotation; - qspi_write_cmd_data8(MADCTL, madctl); - ESP_LOGI(TAG, "Rotation set to %d (%dx%d)", rotation, s_width, s_height); -} diff --git a/components/axs15231b/include/axs15231b.h b/components/axs15231b/include/axs15231b.h index a8c1a37..32c489f 100644 --- a/components/axs15231b/include/axs15231b.h +++ b/components/axs15231b/include/axs15231b.h @@ -23,6 +23,4 @@ void axs15231b_fill_rect(int x, int y, int w, int h, uint16_t color); void axs15231b_flush(void); int axs15231b_get_width(void); int axs15231b_get_height(void); -void axs15231b_set_rotation(int rotation); - #endif diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index c87b2b8..7e20128 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -18,9 +18,6 @@ idf_component_register(SRCS "tollgate_main.c" "cvm_server.c" "display.c" "font.c" - "touch.c" - "keyboard.c" - "wifi_setup.c" INCLUDE_DIRS "." REQUIRES esp_wifi esp_event esp_netif nvs_flash esp_http_server lwip json esp_http_client mbedtls esp-tls log spiffs diff --git a/main/captive_portal.c b/main/captive_portal.c index 1a3d5ce..0c1d33b 100644 --- a/main/captive_portal.c +++ b/main/captive_portal.c @@ -4,6 +4,7 @@ #include "config.h" #include "esp_log.h" #include "esp_wifi.h" +#include "esp_netif.h" #include "cJSON.h" #include "lwip/sockets.h" #include "lwip/netdb.h" @@ -11,6 +12,7 @@ #include "freertos/task.h" #include #include +#include static const char *TAG = "captive_portal"; static httpd_handle_t s_server = NULL; @@ -303,6 +305,338 @@ static const httpd_uri_t uri_connecttest = { .uri = "/connecttest.txt", .method static const httpd_uri_t uri_wpad = { .uri = "/wpad.dat", .method = HTTP_GET, .handler = redirect_to_portal_handler }; static const httpd_uri_t uri_catchall = { .uri = "/*", .method = HTTP_GET, .handler = catchall_handler }; +static const char SETUP_HTML_TEMPLATE[] = \ +"" +"" +"" +"" +"TollGate Setup" +"" +"" +"
" +"

TollGate Setup

" +"

Configure upstream WiFi

" +"
Scanning...
" +"
" +"" +"" +"
" +"" +"" +"" +"
" +"" +"
" +"
" +"" +""; + +static char *template_replace(const char *tpl, const char *key, const char *val) { + const char *p; + size_t klen = strlen(key); + size_t vlen = strlen(val); + size_t tlen = strlen(tpl); + size_t extra = 0; + p = tpl; + while ((p = strstr(p, key)) != NULL) { + extra += vlen - klen; + p += klen; + } + size_t out_size = tlen + extra + 1; + char *out = malloc(out_size); + if (!out) return NULL; + char *dst = out; + p = tpl; + while (*p) { + const char *found = strstr(p, key); + if (found) { + memcpy(dst, p, found - p); + dst += found - p; + memcpy(dst, val, vlen); + dst += vlen; + p = found + klen; + } else { + strcpy(dst, p); + dst += strlen(p); + break; + } + } + *dst = '\0'; + return out; +} + +static bool is_setup_available(void) { + const tollgate_config_t *cfg = tollgate_config_get(); + return cfg->network_count == 0; +} + +static esp_err_t setup_page_handler(httpd_req_t *req) { + if (!is_setup_available()) { + httpd_resp_set_status(req, "302 Found"); + char location[64]; + snprintf(location, sizeof(location), "http://%s/", s_ap_ip_str); + httpd_resp_set_hdr(req, "Location", location); + httpd_resp_send(req, NULL, 0); + return ESP_OK; + } + + httpd_resp_set_type(req, "text/html"); + char *html = template_replace(SETUP_HTML_TEMPLATE, "__AP_IP__", s_ap_ip_str); + if (!html) { + httpd_resp_send_500(req); + return ESP_OK; + } + httpd_resp_send(req, html, strlen(html)); + free(html); + return ESP_OK; +} + +static esp_err_t wifi_scan_handler(httpd_req_t *req) { + esp_wifi_disconnect(); + vTaskDelay(pdMS_TO_TICKS(300)); + + wifi_scan_config_t scan_cfg = {0}; + scan_cfg.scan_type = WIFI_SCAN_TYPE_ACTIVE; + scan_cfg.scan_time.active.min = 100; + scan_cfg.scan_time.active.max = 300; + esp_err_t ret = esp_wifi_scan_start(&scan_cfg, true); + if (ret != ESP_OK) { + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, "[]", 2); + return ESP_OK; + } + + uint16_t ap_count = 0; + esp_wifi_scan_get_ap_num(&ap_count); + if (ap_count > 20) ap_count = 20; + wifi_ap_record_t aps[20]; + esp_wifi_scan_get_ap_records(&ap_count, aps); + + for (int i = 0; i < (int)ap_count - 1; i++) { + for (int j = i + 1; j < (int)ap_count; j++) { + if (aps[j].rssi > aps[i].rssi) { + wifi_ap_record_t tmp = aps[i]; + aps[i] = aps[j]; + aps[j] = tmp; + } + } + } + + cJSON *root = cJSON_CreateArray(); + for (int i = 0; i < (int)ap_count; i++) { + if (aps[i].ssid[0] == '\0') continue; + cJSON *ap = cJSON_CreateObject(); + cJSON_AddStringToObject(ap, "ssid", (const char *)aps[i].ssid); + cJSON_AddNumberToObject(ap, "rssi", aps[i].rssi); + cJSON_AddBoolToObject(ap, "secured", aps[i].authmode != WIFI_AUTH_OPEN); + cJSON_AddItemToArray(root, ap); + } + + char *json = cJSON_PrintUnformatted(root); + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, json, strlen(json)); + cJSON_free(json); + cJSON_Delete(root); + + const tollgate_config_t *cfg = tollgate_config_get(); + if (cfg->network_count > 0) { + wifi_config_t wifi_cfg; + if (tollgate_config_get_wifi(&wifi_cfg) == ESP_OK) { + esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg); + esp_wifi_connect(); + } + } + + return ESP_OK; +} + +static esp_err_t wifi_connect_handler(httpd_req_t *req) { + int content_len = req->content_len; + if (content_len <= 0 || content_len > 1024) { + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, "{\"ok\":false,\"error\":\"invalid request\"}", 33); + return ESP_OK; + } + + char *body = malloc(content_len + 1); + if (!body) { + httpd_resp_send_500(req); + return ESP_OK; + } + int total = 0; + while (total < content_len) { + int r = httpd_req_recv(req, body + total, content_len - total); + if (r <= 0) { free(body); httpd_resp_send_500(req); return ESP_OK; } + total += r; + } + body[total] = '\0'; + + cJSON *json = cJSON_Parse(body); + free(body); + if (!json) { + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, "{\"ok\":false,\"error\":\"invalid JSON\"}", 33); + return ESP_OK; + } + + cJSON *ssid_item = cJSON_GetObjectItem(json, "ssid"); + cJSON *pass_item = cJSON_GetObjectItem(json, "password"); + if (!ssid_item || !cJSON_IsString(ssid_item)) { + cJSON_Delete(json); + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, "{\"ok\":false,\"error\":\"missing ssid\"}", 32); + return ESP_OK; + } + + const char *ssid = ssid_item->valuestring; + const char *password = (pass_item && cJSON_IsString(pass_item)) ? pass_item->valuestring : ""; + + esp_err_t err = tollgate_config_add_wifi(ssid, password); + if (err != ESP_OK) { + cJSON_Delete(json); + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, "{\"ok\":false,\"error\":\"save failed\"}", 33); + return ESP_OK; + } + + wifi_config_t wifi_cfg = {0}; + strncpy((char *)wifi_cfg.sta.ssid, ssid, sizeof(wifi_cfg.sta.ssid) - 1); + strncpy((char *)wifi_cfg.sta.password, password, sizeof(wifi_cfg.sta.password) - 1); + wifi_cfg.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK; + esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg); + esp_wifi_connect(); + + cJSON_Delete(json); + + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, "{\"ok\":true}", 10); + return ESP_OK; +} + +static esp_err_t wifi_status_handler(httpd_req_t *req) { + wifi_ap_record_t ap_info; + bool connected = (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK); + + cJSON *root = cJSON_CreateObject(); + cJSON_AddBoolToObject(root, "connected", connected); + + if (connected) { + esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); + if (netif) { + esp_netif_ip_info_t ip_info; + if (esp_netif_get_ip_info(netif, &ip_info) == ESP_OK) { + char ip_str[16]; + snprintf(ip_str, sizeof(ip_str), IPSTR, IP2STR(&ip_info.ip)); + cJSON_AddStringToObject(root, "ip", ip_str); + } + } + cJSON_AddStringToObject(root, "ssid", (const char *)ap_info.ssid); + } + + char *json = cJSON_PrintUnformatted(root); + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, json, strlen(json)); + cJSON_free(json); + cJSON_Delete(root); + return ESP_OK; +} + +static const httpd_uri_t uri_setup = { .uri = "/setup", .method = HTTP_GET, .handler = setup_page_handler }; +static const httpd_uri_t uri_wifi_scan = { .uri = "/wifi/scan", .method = HTTP_GET, .handler = wifi_scan_handler }; +static const httpd_uri_t uri_wifi_connect = { .uri = "/wifi/connect", .method = HTTP_POST, .handler = wifi_connect_handler }; +static const httpd_uri_t uri_wifi_status = { .uri = "/wifi/status", .method = HTTP_GET, .handler = wifi_status_handler }; + esp_err_t captive_portal_start(const char *ap_ip_str) { if (s_server) return ESP_OK; @@ -331,6 +665,10 @@ esp_err_t captive_portal_start(const char *ap_ip_str) httpd_register_uri_handler(s_server, &uri_ncsi); httpd_register_uri_handler(s_server, &uri_connecttest); httpd_register_uri_handler(s_server, &uri_wpad); + httpd_register_uri_handler(s_server, &uri_setup); + httpd_register_uri_handler(s_server, &uri_wifi_scan); + httpd_register_uri_handler(s_server, &uri_wifi_connect); + httpd_register_uri_handler(s_server, &uri_wifi_status); httpd_register_uri_handler(s_server, &uri_catchall); ESP_LOGI(TAG, "Captive portal started on port 80"); diff --git a/main/captive_portal.h b/main/captive_portal.h index 06eb860..e02a4ce 100644 --- a/main/captive_portal.h +++ b/main/captive_portal.h @@ -7,5 +7,6 @@ esp_err_t captive_portal_start(const char *ap_ip_str); void captive_portal_stop(void); httpd_handle_t captive_portal_get_server(void); +bool captive_portal_is_setup_available(void); #endif diff --git a/main/display.c b/main/display.c index 77b911d..53cdd4d 100644 --- a/main/display.c +++ b/main/display.c @@ -3,9 +3,6 @@ #include "qrcoded.h" #include "font.h" #include "nucula_wallet.h" -#include "touch.h" -#include "keyboard.h" -#include "wifi_setup.h" #include "config.h" #include "esp_log.h" #include "esp_wifi.h" @@ -43,74 +40,10 @@ static display_qr_mode_t s_qr_mode = DISPLAY_QR_WIFI; static int s_last_payment_sats = 0; static int64_t s_last_allotment_ms = 0; -#define COLOR_GRAY 0x8410 -#define COLOR_DARKGRAY 0x4208 -#define COLOR_LIGHTBLUE 0xA5FF -#define COLOR_HIGHLIGHT 0x07E0 - -static wifi_setup_t s_wifi_setup; -static kb_state_t s_kb_state; -static bool s_wifi_setup_active = false; -static bool s_touch_initialized = false; -static bool s_wifi_scan_pending = false; -static int s_display_rotation = 0; - -#define SETUP_BTN_X 240 -#define SETUP_BTN_Y 440 -#define SETUP_BTN_W 72 -#define SETUP_BTN_H 30 - -static void enter_wifi_setup_rotation(void) { - if (s_display_rotation != 1) { - s_display_rotation = 1; - axs15231b_set_rotation(1); - touch_set_rotation(1); - kb_layout_t landscape = { - .key_w = 38, - .key_h = 40, - .key_gap = 2, - .start_y = 170, - .screen_w = 480, - .row_count = 4, - }; - kb_set_layout(&landscape); - } -} - -static void exit_wifi_setup_rotation(void) { - if (s_display_rotation != 0) { - s_display_rotation = 0; - axs15231b_set_rotation(0); - touch_set_rotation(0); - kb_layout_t portrait = { - .key_w = 28, - .key_h = 36, - .key_gap = 2, - .start_y = 70, - .screen_w = 320, - .row_count = 4, - }; - kb_set_layout(&portrait); - } -} - -static void render_setup_button(int x, int y, int w, int h) { - axs15231b_fill_rect(x, y, w, h, COLOR_DARKGRAY); - const char *label = "Setup"; - int lw = strlen(label) * 8; - display_render_text(x + (w - lw) / 2, y + (h - 8) / 2, label, COLOR_WHITE, COLOR_DARKGRAY, 1); -} - -static bool touch_in_rect(uint16_t tx, uint16_t ty, int x, int y, int w, int h) { - return tx >= x && tx < x + w && ty >= y && ty < y + h; -} - -static void highlight_rect(int x, int y, int w, int h) { - axs15231b_fill_rect(x, y, w, h, COLOR_HIGHLIGHT); - axs15231b_flush(); - vTaskDelay(pdMS_TO_TICKS(80)); - axs15231b_fill_rect(x, y, w, h, COLOR_DARKGRAY); - axs15231b_flush(); +static uint16_t wallet_color(void) { + if (s_wallet_balance == 0) return COLOR_RED; + if (s_wallet_balance < 100) return COLOR_YELLOW; + return COLOR_GREEN; } static int qr_version_from_strlen(int len) { @@ -150,20 +83,60 @@ static int escape_wifi_field(const char *src, char *dst, int dst_size) { return di; } +static void extract_domain(const char *url, char *out, int out_size) { + const char *start = url; + if (strncmp(url, "https://", 8) == 0) start = url + 8; + else if (strncmp(url, "http://", 7) == 0) start = url + 7; + strncpy(out, start, out_size - 1); + out[out_size - 1] = '\0'; + char *slash = strchr(out, '/'); + if (slash) *slash = '\0'; +} + static void build_wifi_qr_string(char *out, int out_size) { char escaped_ssid[64]; escape_wifi_field(s_ap_ssid, escaped_ssid, sizeof(escaped_ssid)); - snprintf(out, out_size, "WIFI:S:%s;T:nopass;;", escaped_ssid); + const tollgate_config_t *cfg = tollgate_config_get(); + if (strlen(cfg->ap_password) > 0) { + char escaped_pass[128]; + escape_wifi_field(cfg->ap_password, escaped_pass, sizeof(escaped_pass)); + snprintf(out, out_size, "WIFI:S:%s;T:WPA;P:%s;;", escaped_ssid, escaped_pass); + } else { + snprintf(out, out_size, "WIFI:S:%s;T:nopass;;", escaped_ssid); + } } -static void extract_domain(const char *url, char *domain, int domain_size) { - const char *start = url; - if (strncmp(url, "https://", 8) == 0) start = url + 8; - else if (strncmp(url, "http://", 7) == 0) start = url + 7; - strncpy(domain, start, domain_size - 1); - domain[domain_size - 1] = '\0'; - char *slash = strchr(domain, '/'); - if (slash) *slash = '\0'; +static void render_qr_at(const char *text, int x_off, int y_off, int max_w, int max_h) { + int len = strlen(text); + int version = qr_version_from_strlen(len); + int px = qr_pixel_size(len); + + uint16_t buf_size = qrcode_getBufferSize(version); + uint8_t *qr_buf = (uint8_t *)malloc(buf_size); + if (!qr_buf) return; + + QRCode qrcode; + if (qrcode_initText(&qrcode, qr_buf, version, ECC_LOW, text) != 0) { + free(qr_buf); + return; + } + + int qr_px_w = qrcode.size * px; + int qr_px_h = qrcode.size * px; + int cx = x_off + (max_w - qr_px_w) / 2; + int cy = y_off + (max_h - qr_px_h) / 2; + if (cx < 0) cx = 0; + if (cy < 0) cy = 0; + + for (int y = 0; y < qrcode.size; y++) { + for (int x = 0; x < qrcode.size; x++) { + bool mod = qrcode_getModule(&qrcode, x, y); + uint16_t color = mod ? COLOR_WHITE : COLOR_BG; + axs15231b_fill_rect(cx + x * px, cy + y * px, px, px, color); + } + } + + free(qr_buf); } void display_render_text(int x, int y, const char *text, uint16_t fg, uint16_t bg, int scale) { @@ -199,57 +172,14 @@ void display_render_text(int x, int y, const char *text, uint16_t fg, uint16_t b } } -static void render_qr_at(const char *text, int x_off, int y_off, int max_w, int max_h) { - int len = strlen(text); - int version = qr_version_from_strlen(len); - int px = qr_pixel_size(len); - - uint16_t buf_size = qrcode_getBufferSize(version); - uint8_t *qr_buf = (uint8_t *)malloc(buf_size); - if (!qr_buf) { - ESP_LOGE(TAG, "Failed to allocate QR buffer"); - return; - } - - QRCode qr; - if (qrcode_initText(&qr, qr_buf, version, ECC_LOW, text) != 0) { - ESP_LOGE(TAG, "QR generation failed"); - free(qr_buf); - return; - } - - int qr_px_w = qr.size * px; - int qr_px_h = qr.size * px; - int cx = x_off + (max_w - qr_px_w) / 2; - int cy = y_off + (max_h - qr_px_h) / 2; - if (cx < 0) cx = 0; - if (cy < 0) cy = 0; - - for (int y = 0; y < qr.size; y++) { - for (int x = 0; x < qr.size; x++) { - bool mod = qrcode_getModule(&qr, x, y); - uint16_t color = mod ? 0xFFFF : 0x0000; - axs15231b_fill_rect(cx + x * px, cy + y * px, px, px, color); - } - } - - free(qr_buf); -} - void display_render_qr(const char *text) { int screen_w = axs15231b_get_width(); int screen_h = axs15231b_get_height(); - axs15231b_fill_screen(0x0000); + axs15231b_fill_screen(COLOR_BG); render_qr_at(text, 0, 0, screen_w, screen_h); axs15231b_flush(); } -static uint16_t wallet_color(void) { - if (s_wallet_balance == 0) return COLOR_RED; - if (s_wallet_balance < 100) return COLOR_YELLOW; - return COLOR_GREEN; -} - static void render_boot_screen(void) { int screen_w = axs15231b_get_width(); axs15231b_fill_screen(COLOR_BG); @@ -315,7 +245,44 @@ static void render_ready_screen(void) { display_render_text(10, y, line, COLOR_GREEN, COLOR_BG, 1); } - render_setup_button(SETUP_BTN_X, SETUP_BTN_Y, SETUP_BTN_W, SETUP_BTN_H); + axs15231b_flush(); +} + +static void render_setup_pending_screen(void) { + int screen_w = axs15231b_get_width(); + axs15231b_fill_screen(COLOR_BG); + + char qr_text[320]; + build_wifi_qr_string(qr_text, sizeof(qr_text)); + render_qr_at(qr_text, 0, 5, screen_w, 280); + + int y = 290; + char line[64]; + + const char *title = "WiFi Setup"; + int tw = strlen(title) * 8; + display_render_text((screen_w - tw) / 2, y, title, COLOR_CYAN, COLOR_BG, 1); + y += 20; + + snprintf(line, sizeof(line), "SSID: %s", s_ap_ssid); + display_render_text(10, y, line, COLOR_WHITE, COLOR_BG, 1); + y += 18; + + const char *hint1 = "1. Connect to WiFi above"; + display_render_text(10, y, hint1, COLOR_DIM, COLOR_BG, 1); + y += 16; + + const char *hint2 = "2. Open browser, go to:"; + display_render_text(10, y, hint2, COLOR_DIM, COLOR_BG, 1); + y += 18; + + const tollgate_config_t *cfg = tollgate_config_get(); + snprintf(line, sizeof(line), "http://%s/setup", cfg->ap_ip_str); + display_render_text(10, y, line, COLOR_YELLOW, COLOR_BG, 1); + y += 22; + + const char *hint3 = "3. Configure upstream WiFi"; + display_render_text(10, y, hint3, COLOR_DIM, COLOR_BG, 1); axs15231b_flush(); } @@ -360,324 +327,29 @@ static void render_error_screen(void) { int msg_w = strlen(msg) * 8 * 2; display_render_text((screen_w - msg_w) / 2, 202, msg, COLOR_WHITE, COLOR_RED, 2); - char line[48]; + char line[64]; int lw; const char *l1 = "Internet unavailable"; lw = strlen(l1) * 8; display_render_text((screen_w - lw) / 2, 270, l1, COLOR_WHITE, COLOR_BG, 1); - const char *l2 = "Check WiFi config"; - lw = strlen(l2) * 8; - display_render_text((screen_w - lw) / 2, 290, l2, COLOR_YELLOW, COLOR_BG, 1); - - const char *l3 = "AP still active"; - lw = strlen(l3) * 8; - display_render_text((screen_w - lw) / 2, 320, l3, COLOR_GREEN, COLOR_BG, 1); - snprintf(line, sizeof(line), "SSID: %s", s_ap_ssid); lw = strlen(line) * 8; - display_render_text((screen_w - lw) / 2, 340, line, COLOR_DIM, COLOR_BG, 1); - - render_setup_button(SETUP_BTN_X, SETUP_BTN_Y, SETUP_BTN_W, SETUP_BTN_H); - - axs15231b_flush(); -} + display_render_text((screen_w - lw) / 2, 300, line, COLOR_DIM, COLOR_BG, 1); -static void render_wifi_setup_scanning(void) { - int screen_w = axs15231b_get_width(); - axs15231b_fill_screen(COLOR_BG); - - const char *title = "WiFi Setup"; - int tw = strlen(title) * 8; - display_render_text((screen_w - tw) / 2, 180, title, COLOR_CYAN, COLOR_BG, 1); - - const char *msg = "Scanning..."; - int mw = strlen(msg) * 8; - display_render_text((screen_w - mw) / 2, 220, msg, COLOR_WHITE, COLOR_BG, 1); - - axs15231b_flush(); -} - -static void render_wifi_setup_list(void) { - int screen_w = axs15231b_get_width(); - int screen_h = axs15231b_get_height(); - axs15231b_fill_screen(COLOR_BG); - - const char *title = "Select Network"; - int tw = strlen(title) * 8; - display_render_text((screen_w - tw) / 2, 5, title, COLOR_CYAN, COLOR_BG, 1); - - int y = 25; - int visible = wifi_setup_visible_count(&s_wifi_setup); - int item_w = screen_w - 20; - int max_y = screen_h - 40; - - for (int i = 0; i < visible && y < max_y; i++) { - const wifi_ap_info_t *ap = wifi_setup_get_visible(&s_wifi_setup, i); - if (!ap) break; - - int rssi_bars = 0; - if (ap->rssi >= -30) rssi_bars = 4; - else if (ap->rssi >= -50) rssi_bars = 3; - else if (ap->rssi >= -70) rssi_bars = 2; - else rssi_bars = 1; - - axs15231b_fill_rect(10, y, item_w, 26, COLOR_DARKGRAY); - display_render_text(15, y + 4, ap->ssid, COLOR_WHITE, COLOR_DARKGRAY, 1); - - int bar_x = 10 + item_w - 50; - for (int b = 0; b < rssi_bars; b++) { - axs15231b_fill_rect(bar_x + b * 10, y + 16 - (b + 1) * 4, 8, (b + 1) * 4, COLOR_GREEN); - } - - if (ap->secured) { - display_render_text(bar_x - 12, y + 4, "*", COLOR_YELLOW, COLOR_DARKGRAY, 1); - } - - y += 30; - } - - int btn_y = screen_h - 30; - axs15231b_fill_rect(10, btn_y, 50, 26, COLOR_DARKGRAY); - display_render_text(25, btn_y + 5, "X", COLOR_WHITE, COLOR_DARKGRAY, 1); - - axs15231b_flush(); -} - -static void render_wifi_setup_password(void) { - int screen_w = axs15231b_get_width(); - const kb_layout_t *kb = kb_get_layout(); - axs15231b_fill_screen(COLOR_BG); - - display_render_text(10, 5, s_wifi_setup.selected_ssid, COLOR_CYAN, COLOR_BG, 1); - display_render_text(10, 25, "Password:", COLOR_DIM, COLOR_BG, 1); - - int field_w = screen_w - 80; - axs15231b_fill_rect(10, 40, field_w, 20, COLOR_DARKGRAY); - - char masked[KB_INPUT_MAX + 1]; - if (s_kb_state.reveal) { - strncpy(masked, s_kb_state.input, sizeof(masked) - 1); - masked[sizeof(masked) - 1] = '\0'; - } else { - int len = s_kb_state.cursor; - int max_chars = (field_w - 8) / 8; - if (len > max_chars) len = max_chars; - for (int i = 0; i < len; i++) masked[i] = '*'; - masked[len] = '\0'; - } - display_render_text(14, 43, masked, COLOR_WHITE, COLOR_DARKGRAY, 1); - - int eye_x = 10 + field_w + 5; - const char *eye_label = s_kb_state.reveal ? "H" : "S"; - axs15231b_fill_rect(eye_x, 40, 24, 20, COLOR_GRAY); - display_render_text(eye_x + 8, 43, eye_label, COLOR_WHITE, COLOR_GRAY, 1); - - int kb_y = kb->start_y; - for (int row = 0; row < kb->row_count; row++) { - const char *row_str; - int key_count = kb_get_row_keys(row, s_kb_state.layer, &row_str); - const char *tmp; - int total_keys = kb_get_row_keys(row, s_kb_state.layer, &tmp); - int x_off = (row == 0) ? (screen_w - total_keys * kb->key_w - (total_keys - 1) * kb->key_gap) / 2 - : (row == 1) ? (screen_w - total_keys * kb->key_w - (total_keys - 1) * kb->key_gap) / 2 + kb->key_w / 2 - : (row == 2) ? (screen_w - total_keys * kb->key_w - (total_keys - 1) * kb->key_gap) / 2 + kb->key_w - : (screen_w - total_keys * kb->key_w - (total_keys - 1) * kb->key_gap) / 2; - - int cx = x_off; - for (int col = 0; col < key_count; col++) { - char c = row_str[col]; - int kw = kb->key_w; - if (row == 3) { - int margin = (screen_w - total_keys * kb->key_w - (total_keys - 1) * kb->key_gap) / 2; - int available = screen_w - margin * 2; - int side_w = (available - kb->key_gap) / 4; - if (col == 0) kw = side_w; - else if (col == key_count - 1) kw = side_w; - else kw = available - side_w * 2 - kb->key_gap * 2; - } - - uint16_t bg = COLOR_DARKGRAY; - uint16_t fg = COLOR_WHITE; - char label[2] = {0, 0}; - - if (c == '\001') { - label[0] = s_kb_state.layer == KB_ALPHA_UPPER ? 'A' : 'a'; - bg = COLOR_GRAY; - } else if (c == '\002') { - label[0] = s_kb_state.layer == KB_NUMSYM ? 'a' : '#'; - bg = COLOR_GRAY; - } else if (c == '\003') { - label[0] = '_'; - } else if (c == '\004') { - label[0] = '>'; - fg = COLOR_GREEN; - } else if (c == '\b') { - label[0] = '<'; - bg = COLOR_GRAY; - } else { - label[0] = c; - } - - axs15231b_fill_rect(cx, kb_y, kw, kb->key_h, bg); - int lw = 8; - display_render_text(cx + (kw - lw) / 2, kb_y + (kb->key_h - 8) / 2, label, fg, bg, 1); - cx += kw + kb->key_gap; - } - kb_y += kb->key_h + kb->key_gap; - } - - axs15231b_flush(); -} - -static void render_wifi_setup_connecting(void) { - int screen_w = axs15231b_get_width(); - axs15231b_fill_screen(COLOR_BG); - - const char *msg1 = "Connecting to"; - int w1 = strlen(msg1) * 8; - display_render_text((screen_w - w1) / 2, 200, msg1, COLOR_WHITE, COLOR_BG, 1); - - int w2 = strlen(s_wifi_setup.selected_ssid) * 8; - display_render_text((screen_w - w2) / 2, 225, s_wifi_setup.selected_ssid, COLOR_CYAN, COLOR_BG, 1); - - const char *dots = "..."; - int dw = strlen(dots) * 8; - display_render_text((screen_w - dw) / 2, 255, dots, COLOR_DIM, COLOR_BG, 1); - - axs15231b_flush(); -} - -static void render_wifi_setup_result(void) { - int screen_w = axs15231b_get_width(); - int screen_h = axs15231b_get_height(); - axs15231b_fill_screen(COLOR_BG); - - if (s_wifi_setup.state == SETUP_SUCCESS) { - const char *msg = "Connected!"; - int mw = strlen(msg) * 8 * 2; - display_render_text((screen_w - mw) / 2, screen_h / 2 - 40, msg, COLOR_WHITE, COLOR_GREEN, 2); + const tollgate_config_t *cfg = tollgate_config_get(); + snprintf(line, sizeof(line), "Setup: http://%s/setup", cfg->ap_ip_str); + lw = strlen(line) * 8; + display_render_text((screen_w - lw) / 2, 330, line, COLOR_YELLOW, COLOR_BG, 1); - if (s_wifi_setup.connect_ip[0]) { - char ip_line[32]; - snprintf(ip_line, sizeof(ip_line), "IP: %s", s_wifi_setup.connect_ip); - int iw = strlen(ip_line) * 8; - display_render_text((screen_w - iw) / 2, screen_h / 2, ip_line, COLOR_WHITE, COLOR_BG, 1); - } - } else { - const char *msg = "Connection failed"; - int mw = strlen(msg) * 8 * 2; - display_render_text((screen_w - mw) / 2, screen_h / 2 - 40, msg, COLOR_WHITE, COLOR_RED, 2); - - const char *hint = "Wrong password?"; - int hw = strlen(hint) * 8; - display_render_text((screen_w - hw) / 2, screen_h / 2, hint, COLOR_YELLOW, COLOR_BG, 1); - - int btn_y = screen_h / 2 + 30; - int btn_w = (screen_w - 40) / 2; - axs15231b_fill_rect(10, btn_y, btn_w, 30, COLOR_DARKGRAY); - display_render_text(10 + (btn_w - 40) / 2, btn_y + 8, "Retry", COLOR_WHITE, COLOR_DARKGRAY, 1); - axs15231b_fill_rect(20 + btn_w, btn_y, btn_w, 30, COLOR_DARKGRAY); - display_render_text(20 + btn_w + (btn_w - 50) / 2, btn_y + 8, "Change", COLOR_WHITE, COLOR_DARKGRAY, 1); - } + const char *l3 = "AP still active"; + lw = strlen(l3) * 8; + display_render_text((screen_w - lw) / 2, 360, l3, COLOR_GREEN, COLOR_BG, 1); axs15231b_flush(); } -static void handle_wifi_setup_touch(uint16_t tx, uint16_t ty) { - int screen_w = axs15231b_get_width(); - int screen_h = axs15231b_get_height(); - - switch (s_wifi_setup.state) { - case SETUP_SCAN: - break; - - case SETUP_LIST: { - int btn_y = screen_h - 30; - if (touch_in_rect(tx, ty, 10, btn_y, 50, 26)) { - highlight_rect(10, btn_y, 50, 26); - wifi_setup_handle_cancel(&s_wifi_setup); - s_wifi_setup_active = false; - exit_wifi_setup_rotation(); - s_state = DISPLAY_ERROR; - return; - } - int item_w = screen_w - 20; - int y = 25; - int max_y = screen_h - 40; - int visible = wifi_setup_visible_count(&s_wifi_setup); - for (int i = 0; i < visible && y < max_y; i++) { - if (touch_in_rect(tx, ty, 10, y, item_w, 26)) { - highlight_rect(10, y, item_w, 26); - wifi_setup_handle_select(&s_wifi_setup, i); - kb_state_init(&s_kb_state); - return; - } - y += 30; - } - break; - } - - case SETUP_PASSWORD: { - int field_w = screen_w - 80; - int eye_x = 10 + field_w + 5; - if (touch_in_rect(tx, ty, eye_x, 40, 24, 20)) { - s_kb_state.reveal = !s_kb_state.reveal; - return; - } - kb_result_t r = kb_hit_test(tx, ty, s_kb_state.layer); - if (r.action != KB_ACTION_NONE) { - axs15231b_fill_rect(tx - 20, ty - 20, 40, 40, COLOR_HIGHLIGHT); - axs15231b_flush(); - vTaskDelay(pdMS_TO_TICKS(60)); - if (r.action == KB_ACTION_DONE && s_kb_state.cursor > 0) { - wifi_setup_handle_connect(&s_wifi_setup); - wifi_config_t wifi_cfg = {0}; - strncpy((char *)wifi_cfg.sta.ssid, s_wifi_setup.selected_ssid, - sizeof(wifi_cfg.sta.ssid) - 1); - strncpy((char *)wifi_cfg.sta.password, s_kb_state.input, - sizeof(wifi_cfg.sta.password) - 1); - wifi_cfg.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK; - esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg); - esp_wifi_connect(); - return; - } - kb_apply(&s_kb_state, r); - } - break; - } - - case SETUP_CONNECTING: - break; - - case SETUP_SUCCESS: { - wifi_setup_handle_cancel(&s_wifi_setup); - s_wifi_setup_active = false; - exit_wifi_setup_rotation(); - s_state = DISPLAY_READY; - return; - } - - case SETUP_FAILED: { - int btn_y = screen_h / 2 + 30; - int btn_w = (screen_w - 40) / 2; - if (touch_in_rect(tx, ty, 10, btn_y, btn_w, 30)) { - highlight_rect(10, btn_y, btn_w, 30); - wifi_setup_handle_retry(&s_wifi_setup); - kb_state_init(&s_kb_state); - } else if (touch_in_rect(tx, ty, 20 + btn_w, btn_y, btn_w, 30)) { - highlight_rect(20 + btn_w, btn_y, btn_w, 30); - wifi_setup_handle_change_network(&s_wifi_setup); - } - break; - } - - default: - break; - } -} - static void display_task(void *pvParameters) { ESP_LOGI(TAG, "Display task started"); @@ -692,72 +364,6 @@ static void display_task(void *pvParameters) { } } - touch_point_t tp; - if (s_touch_initialized && touch_read(&tp) && tp.touched) { - if (state == DISPLAY_WIFI_SETUP) { - handle_wifi_setup_touch(tp.x, tp.y); - state = s_state; - } else if (state == DISPLAY_ERROR || state == DISPLAY_READY) { - if (touch_in_rect(tp.x, tp.y, SETUP_BTN_X, SETUP_BTN_Y, SETUP_BTN_W, SETUP_BTN_H)) { - enter_wifi_setup_rotation(); - s_wifi_setup_active = true; - wifi_setup_init(&s_wifi_setup); - kb_state_init(&s_kb_state); - s_wifi_scan_pending = true; - s_state = DISPLAY_WIFI_SETUP; - state = DISPLAY_WIFI_SETUP; - } - } - } - - if (s_wifi_scan_pending && s_wifi_setup.state == SETUP_SCAN) { - s_wifi_scan_pending = false; - esp_wifi_disconnect(); - vTaskDelay(pdMS_TO_TICKS(500)); - wifi_scan_config_t scan_cfg = {0}; - scan_cfg.scan_type = WIFI_SCAN_TYPE_ACTIVE; - scan_cfg.scan_time.active.min = 100; - scan_cfg.scan_time.active.max = 300; - esp_err_t scan_ret = esp_wifi_scan_start(&scan_cfg, true); - if (scan_ret != ESP_OK) { - ESP_LOGE(TAG, "WiFi scan failed: %s", esp_err_to_name(scan_ret)); - wifi_setup_set_aps(&s_wifi_setup, NULL, 0); - } else { - uint16_t ap_count = 0; - esp_wifi_scan_get_ap_num(&ap_count); - if (ap_count > WIFI_SETUP_MAX_APS) ap_count = WIFI_SETUP_MAX_APS; - wifi_ap_record_t ap_records[WIFI_SETUP_MAX_APS]; - esp_wifi_scan_get_ap_records(&ap_count, ap_records); - - wifi_ap_info_t aps[WIFI_SETUP_MAX_APS]; - for (int i = 0; i < (int)ap_count; i++) { - strncpy(aps[i].ssid, (const char *)ap_records[i].ssid, WIFI_SETUP_SSID_LEN - 1); - aps[i].ssid[WIFI_SETUP_SSID_LEN - 1] = '\0'; - aps[i].rssi = ap_records[i].rssi; - aps[i].secured = (ap_records[i].authmode != WIFI_AUTH_OPEN); - } - - for (int i = 0; i < (int)ap_count - 1; i++) { - for (int j = i + 1; j < (int)ap_count; j++) { - if (aps[j].rssi > aps[i].rssi) { - wifi_ap_info_t tmp = aps[i]; - aps[i] = aps[j]; - aps[j] = tmp; - } - } - } - - wifi_setup_set_aps(&s_wifi_setup, aps, (int)ap_count); - } - } - - if (s_wifi_setup_active && s_wifi_setup.state == SETUP_CONNECTING) { - wifi_ap_record_t ap_info; - if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) { - wifi_setup_handle_connect_result(&s_wifi_setup, true, NULL); - } - } - switch (state) { case DISPLAY_BOOT: render_boot_screen(); @@ -773,34 +379,8 @@ static void display_task(void *pvParameters) { case DISPLAY_ERROR: render_error_screen(); break; - case DISPLAY_WIFI_SETUP: - switch (s_wifi_setup.state) { - case SETUP_SCAN: - render_wifi_setup_scanning(); - break; - case SETUP_LIST: - render_wifi_setup_list(); - break; - case SETUP_PASSWORD: - render_wifi_setup_password(); - break; - case SETUP_CONNECTING: - render_wifi_setup_connecting(); - break; - case SETUP_SUCCESS: - render_wifi_setup_result(); - vTaskDelay(pdMS_TO_TICKS(3000)); - wifi_setup_handle_cancel(&s_wifi_setup); - s_wifi_setup_active = false; - exit_wifi_setup_rotation(); - s_state = DISPLAY_READY; - break; - case SETUP_FAILED: - render_wifi_setup_result(); - break; - default: - break; - } + case DISPLAY_SETUP_PENDING: + render_setup_pending_screen(); break; } @@ -820,14 +400,6 @@ esp_err_t display_init(void) { s_initialized = true; s_last_qr_switch = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS; - esp_err_t touch_ret = touch_init(); - if (touch_ret == ESP_OK) { - s_touch_initialized = true; - ESP_LOGI(TAG, "Touch controller initialized"); - } else { - ESP_LOGW(TAG, "Touch init failed (non-fatal): %s", esp_err_to_name(touch_ret)); - } - xTaskCreatePinnedToCore(display_task, "display", 24576, NULL, 2, NULL, 1); ESP_LOGI(TAG, "Display initialized"); @@ -871,26 +443,8 @@ void display_notify_payment(int amount_sats, int64_t allotment_ms) { } void display_notify_wifi_connected(const char *ip) { - if (s_wifi_setup_active && s_wifi_setup.state == SETUP_CONNECTING) { - wifi_setup_handle_connect_result(&s_wifi_setup, true, ip); - if (s_kb_state.cursor > 0) { - tollgate_config_add_wifi(s_wifi_setup.selected_ssid, s_kb_state.input); - } - } + (void)ip; } void display_notify_wifi_disconnected(void) { - if (s_wifi_setup_active && s_wifi_setup.state == SETUP_CONNECTING) { - wifi_setup_handle_connect_result(&s_wifi_setup, false, NULL); - } -} - -void display_enter_wifi_setup(void) { - if (!s_initialized) return; - enter_wifi_setup_rotation(); - s_wifi_setup_active = true; - wifi_setup_init(&s_wifi_setup); - kb_state_init(&s_kb_state); - s_wifi_scan_pending = true; - s_state = DISPLAY_WIFI_SETUP; } diff --git a/main/display.h b/main/display.h index e8824b6..ecb76b6 100644 --- a/main/display.h +++ b/main/display.h @@ -10,7 +10,7 @@ typedef enum { DISPLAY_READY, DISPLAY_PAYMENT_RECEIVED, DISPLAY_ERROR, - DISPLAY_WIFI_SETUP + DISPLAY_SETUP_PENDING } display_state_t; typedef enum { @@ -27,7 +27,6 @@ void display_update(const char *ap_ssid, int active_clients, void display_notify_payment(int amount_sats, int64_t allotment_ms); void display_notify_wifi_connected(const char *ip); void display_notify_wifi_disconnected(void); -void display_enter_wifi_setup(void); void display_render_text(int x, int y, const char *text, uint16_t fg, uint16_t bg, int scale); void display_render_qr(const char *text); diff --git a/main/tollgate_main.c b/main/tollgate_main.c index 59d25f5..9421e16 100644 --- a/main/tollgate_main.c +++ b/main/tollgate_main.c @@ -325,8 +325,11 @@ void app_main(void) tcfg->mint_url, tcfg->price_per_step, "connecting..."); if (tollgate_config_get_wifi(&(wifi_config_t){0}) != ESP_OK) { - ESP_LOGI(TAG, "No STA network configured, entering WiFi setup"); - display_enter_wifi_setup(); + ESP_LOGI(TAG, "No STA network configured, starting setup mode"); + char portal_url[128]; + snprintf(portal_url, sizeof(portal_url), "http://%s/", tcfg->ap_ip_str); + display_update(NULL, 0, 0, portal_url, NULL, 0, "setup mode"); + display_set_state(DISPLAY_SETUP_PENDING); xTaskCreate(services_start_task, "svc_start", 32768, NULL, 5, NULL); } diff --git a/tests/integration/wifi_setup.mjs b/tests/integration/wifi_setup.mjs new file mode 100644 index 0000000..a991ba5 --- /dev/null +++ b/tests/integration/wifi_setup.mjs @@ -0,0 +1,74 @@ +import { execSync } from 'child_process'; + +const IP = process.env.TOLLGATE_IP || '10.192.45.1'; + +console.log(`\n=== WiFi Setup Integration Test ===`); +console.log(`Portal IP: ${IP}\n`); + +let passed = 0, failed = 0; +function assert(cond, msg) { + if (cond) { console.log(` PASS: ${msg}`); passed++; } + else { console.log(` FAIL: ${msg}`); failed++; } +} + +function run(cmd) { + try { return execSync(cmd, { encoding: 'utf8', timeout: 15000 }); } + catch { return null; } +} + +function fetchJSON(path) { + const result = run(`curl -s --connect-timeout 5 http://${IP}${path}`); + if (!result) return null; + try { return JSON.parse(result); } + catch { return null; } +} + +// 1. /setup page returns HTML (or redirects if already configured) +const setupPage = run(`curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 http://${IP}/setup`); +assert(setupPage === '200' || setupPage === '302', `/setup returns 200 or 302 (got ${setupPage})`); + +// 2. /wifi/status endpoint works +const status = fetchJSON('/wifi/status'); +assert(status !== null, '/wifi/status returns JSON'); +assert(typeof status.connected === 'boolean', '/wifi/status has connected field'); + +// 3. /wifi/scan endpoint returns array +console.log('\n (wifi/scan may take a few seconds...)'); +const scanResult = run(`curl -s --connect-timeout 15 --max-time 15 http://${IP}/wifi/scan`); +let scanData = null; +if (scanResult) { + try { scanData = JSON.parse(scanResult); } catch {} +} +assert(scanData !== null, '/wifi/scan returns JSON'); +if (scanData && Array.isArray(scanData)) { + assert(scanData.length >= 0, `/wifi/scan returns array (${scanData.length} APs)`); + if (scanData.length > 0) { + const ap = scanData[0]; + assert(ap.ssid !== undefined, 'AP has ssid field'); + assert(ap.rssi !== undefined, 'AP has rssi field'); + assert(ap.secured !== undefined, 'AP has secured field'); + console.log(` First AP: "${ap.ssid}" (${ap.rssi} dBm, ${ap.secured ? 'secured' : 'open'})`); + } +} + +// 4. /wifi/connect rejects invalid JSON +const badConnect = run(`curl -s -X POST -d 'not json' --connect-timeout 5 http://${IP}/wifi/connect`); +assert(badConnect !== null, '/wifi/connect responds to bad request'); +if (badConnect) { + try { + const err = JSON.parse(badConnect); + assert(err.ok === false, '/wifi/connect returns ok:false for bad request'); + } catch {} +} + +// 5. /wifi/connect rejects missing ssid +const noSsid = run(`curl -s -X POST -H 'Content-Type: application/json' -d '{}' --connect-timeout 5 http://${IP}/wifi/connect`); +if (noSsid) { + try { + const err = JSON.parse(noSsid); + assert(err.ok === false && err.error, '/wifi/connect returns error for missing ssid'); + } catch {} +} + +console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`); +process.exit(failed > 0 ? 1 : 0); -- cgit v1.2.3