diff options
| author | Your Name <you@example.com> | 2026-05-18 22:55:37 +0530 |
|---|---|---|
| committer | Your Name <you@example.com> | 2026-05-18 22:55:37 +0530 |
| commit | 46e7dabaf80333ca045061ff480377a4186e5deb (patch) | |
| tree | 06bd62bed43e15c432cc27faf20993a50d7c3cee | |
| parent | eb2ae72d91a4b2ec0333a2e6d18eeeeee6b60464 (diff) | |
feat: integrate touchscreen WiFi setup into display UI
- Add DISPLAY_WIFI_SETUP state with full rendering pipeline
- Touch init on display_init(), touch polling in display task
- WiFi scan triggered on setup entry, sorted AP list
- On-screen keyboard for password entry
- Connect result via WiFi event notifications
- Setup button on READY and ERROR screens
- Auto-enter WiFi setup on first boot with no credentials
- Save new WiFi credentials to SPIFFS config.json
| -rw-r--r-- | TOUCH_WIFI_SETUP_PLAN.md | 32 | ||||
| -rw-r--r-- | main/CMakeLists.txt | 5 | ||||
| -rw-r--r-- | main/display.c | 410 | ||||
| -rw-r--r-- | main/display.h | 6 | ||||
| -rw-r--r-- | main/tollgate_main.c | 8 |
5 files changed, 441 insertions, 20 deletions
diff --git a/TOUCH_WIFI_SETUP_PLAN.md b/TOUCH_WIFI_SETUP_PLAN.md index afbc0b3..f441138 100644 --- a/TOUCH_WIFI_SETUP_PLAN.md +++ b/TOUCH_WIFI_SETUP_PLAN.md | |||
| @@ -67,26 +67,26 @@ Add `DISPLAY_WIFI_SETUP` to `display_state_t`. | |||
| 67 | ## Implementation Checklist | 67 | ## Implementation Checklist |
| 68 | 68 | ||
| 69 | ### Phase 1: Touch Driver | 69 | ### Phase 1: Touch Driver |
| 70 | - [ ] Create `main/touch.h` with API | 70 | - [x] Create `main/touch.h` with API |
| 71 | - [ ] Create `main/touch.c` with ESP-IDF I2C v5 implementation | 71 | - [x] Create `main/touch.c` with ESP-IDF I2C v5 implementation |
| 72 | - [ ] Create `tests/unit/test_touch.c` with coordinate parsing tests | 72 | - [x] Create `tests/unit/test_touch.c` with coordinate parsing tests |
| 73 | - [ ] Run `make test-unit`, verify all pass | 73 | - [x] Run `make test-unit`, verify all pass |
| 74 | 74 | ||
| 75 | ### Phase 2: On-Screen Keyboard | 75 | ### Phase 2: On-Screen Keyboard |
| 76 | - [ ] Create `main/keyboard.h` with API | 76 | - [x] Create `main/keyboard.h` with API |
| 77 | - [ ] Create `main/keyboard.c` with QWERTY layout + hit detection | 77 | - [x] Create `main/keyboard.c` with QWERTY layout + hit detection |
| 78 | - [ ] Create `tests/unit/test_keyboard.c` with layout + key lookup tests | 78 | - [x] Create `tests/unit/test_keyboard.c` with layout + key lookup tests |
| 79 | - [ ] Run `make test-unit`, verify all pass | 79 | - [x] Run `make test-unit`, verify all pass |
| 80 | 80 | ||
| 81 | ### Phase 3: WiFi Setup Flow | 81 | ### Phase 3: WiFi Setup Flow |
| 82 | - [ ] Create `main/wifi_setup.h` with API | 82 | - [x] Create `main/wifi_setup.h` with API |
| 83 | - [ ] Create `main/wifi_setup.c` with scan/list/connect state machine | 83 | - [x] Create `main/wifi_setup.c` with scan/list/connect state machine |
| 84 | - [ ] Add `tollgate_config_add_wifi()` to config.c + config.h | 84 | - [x] Add `tollgate_config_add_wifi()` to config.c + config.h |
| 85 | - [ ] Create `tests/unit/test_wifi_setup.c` with state machine tests | 85 | - [x] Create `tests/unit/test_wifi_setup.c` with state machine tests |
| 86 | - [ ] Run `make test-unit`, verify all pass | 86 | - [x] Run `make test-unit`, verify all pass |
| 87 | 87 | ||
| 88 | ### Phase 4: Integration | 88 | ### Phase 4: Integration |
| 89 | - [ ] Add `DISPLAY_WIFI_SETUP` to display.h | 89 | - [x] Add `DISPLAY_WIFI_SETUP` to display.h |
| 90 | - [ ] Update display.c with WiFi setup rendering + touch input | 90 | - [x] Update display.c with WiFi setup rendering + touch input |
| 91 | - [ ] Update `main/CMakeLists.txt` with new source files | 91 | - [x] Update `main/CMakeLists.txt` with new source files |
| 92 | - [ ] Build, flash to Board C, verify full flow | 92 | - [ ] Build, flash to Board C, verify full flow |
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 9b0fb1c..c87b2b8 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt | |||
| @@ -18,8 +18,11 @@ idf_component_register(SRCS "tollgate_main.c" | |||
| 18 | "cvm_server.c" | 18 | "cvm_server.c" |
| 19 | "display.c" | 19 | "display.c" |
| 20 | "font.c" | 20 | "font.c" |
| 21 | "touch.c" | ||
| 22 | "keyboard.c" | ||
| 23 | "wifi_setup.c" | ||
| 21 | INCLUDE_DIRS "." | 24 | INCLUDE_DIRS "." |
| 22 | REQUIRES esp_wifi esp_event esp_netif nvs_flash esp_http_server | 25 | REQUIRES esp_wifi esp_event esp_netif nvs_flash esp_http_server |
| 23 | lwip json esp_http_client mbedtls esp-tls log spiffs | 26 | lwip json esp_http_client mbedtls esp-tls log spiffs |
| 24 | nucula_lib secp256k1 axs15231b qrcode | 27 | nucula_lib secp256k1 axs15231b qrcode driver |
| 25 | PRIV_REQUIRES esp-tls) | 28 | PRIV_REQUIRES esp-tls) |
diff --git a/main/display.c b/main/display.c index 0935743..bec795d 100644 --- a/main/display.c +++ b/main/display.c | |||
| @@ -3,7 +3,12 @@ | |||
| 3 | #include "qrcoded.h" | 3 | #include "qrcoded.h" |
| 4 | #include "font.h" | 4 | #include "font.h" |
| 5 | #include "nucula_wallet.h" | 5 | #include "nucula_wallet.h" |
| 6 | #include "touch.h" | ||
| 7 | #include "keyboard.h" | ||
| 8 | #include "wifi_setup.h" | ||
| 9 | #include "config.h" | ||
| 6 | #include "esp_log.h" | 10 | #include "esp_log.h" |
| 11 | #include "esp_wifi.h" | ||
| 7 | #include "freertos/FreeRTOS.h" | 12 | #include "freertos/FreeRTOS.h" |
| 8 | #include "freertos/task.h" | 13 | #include "freertos/task.h" |
| 9 | #include <string.h> | 14 | #include <string.h> |
| @@ -38,6 +43,32 @@ static display_qr_mode_t s_qr_mode = DISPLAY_QR_WIFI; | |||
| 38 | static int s_last_payment_sats = 0; | 43 | static int s_last_payment_sats = 0; |
| 39 | static int64_t s_last_allotment_ms = 0; | 44 | static int64_t s_last_allotment_ms = 0; |
| 40 | 45 | ||
| 46 | #define COLOR_GRAY 0x8410 | ||
| 47 | #define COLOR_DARKGRAY 0x4208 | ||
| 48 | #define COLOR_LIGHTBLUE 0xA5FF | ||
| 49 | |||
| 50 | static wifi_setup_t s_wifi_setup; | ||
| 51 | static kb_state_t s_kb_state; | ||
| 52 | static bool s_wifi_setup_active = false; | ||
| 53 | static bool s_touch_initialized = false; | ||
| 54 | static bool s_wifi_scan_pending = false; | ||
| 55 | |||
| 56 | #define SETUP_BTN_X 240 | ||
| 57 | #define SETUP_BTN_Y 440 | ||
| 58 | #define SETUP_BTN_W 72 | ||
| 59 | #define SETUP_BTN_H 30 | ||
| 60 | |||
| 61 | static void render_setup_button(int x, int y, int w, int h) { | ||
| 62 | axs15231b_fill_rect(x, y, w, h, COLOR_DARKGRAY); | ||
| 63 | const char *label = "Setup"; | ||
| 64 | int lw = strlen(label) * 8; | ||
| 65 | display_render_text(x + (w - lw) / 2, y + (h - 8) / 2, label, COLOR_WHITE, COLOR_DARKGRAY, 1); | ||
| 66 | } | ||
| 67 | |||
| 68 | static bool touch_in_rect(uint16_t tx, uint16_t ty, int x, int y, int w, int h) { | ||
| 69 | return tx >= x && tx < x + w && ty >= y && ty < y + h; | ||
| 70 | } | ||
| 71 | |||
| 41 | static int qr_version_from_strlen(int len) { | 72 | static int qr_version_from_strlen(int len) { |
| 42 | if (len <= 17) return 1; | 73 | if (len <= 17) return 1; |
| 43 | if (len <= 32) return 2; | 74 | if (len <= 32) return 2; |
| @@ -240,6 +271,8 @@ static void render_ready_screen(void) { | |||
| 240 | display_render_text(10, y, line, COLOR_GREEN, COLOR_BG, 1); | 271 | display_render_text(10, y, line, COLOR_GREEN, COLOR_BG, 1); |
| 241 | } | 272 | } |
| 242 | 273 | ||
| 274 | render_setup_button(SETUP_BTN_X, SETUP_BTN_Y, SETUP_BTN_W, SETUP_BTN_H); | ||
| 275 | |||
| 243 | axs15231b_flush(); | 276 | axs15231b_flush(); |
| 244 | } | 277 | } |
| 245 | 278 | ||
| @@ -302,9 +335,269 @@ static void render_error_screen(void) { | |||
| 302 | lw = strlen(line) * 8; | 335 | lw = strlen(line) * 8; |
| 303 | display_render_text((screen_w - lw) / 2, 340, line, COLOR_DIM, COLOR_BG, 1); | 336 | display_render_text((screen_w - lw) / 2, 340, line, COLOR_DIM, COLOR_BG, 1); |
| 304 | 337 | ||
| 338 | render_setup_button(SETUP_BTN_X, SETUP_BTN_Y, SETUP_BTN_W, SETUP_BTN_H); | ||
| 339 | |||
| 340 | axs15231b_flush(); | ||
| 341 | } | ||
| 342 | |||
| 343 | static void render_wifi_setup_scanning(void) { | ||
| 344 | int screen_w = axs15231b_get_width(); | ||
| 345 | axs15231b_fill_screen(COLOR_BG); | ||
| 346 | |||
| 347 | const char *title = "WiFi Setup"; | ||
| 348 | int tw = strlen(title) * 8; | ||
| 349 | display_render_text((screen_w - tw) / 2, 180, title, COLOR_CYAN, COLOR_BG, 1); | ||
| 350 | |||
| 351 | const char *msg = "Scanning..."; | ||
| 352 | int mw = strlen(msg) * 8; | ||
| 353 | display_render_text((screen_w - mw) / 2, 220, msg, COLOR_WHITE, COLOR_BG, 1); | ||
| 354 | |||
| 355 | axs15231b_flush(); | ||
| 356 | } | ||
| 357 | |||
| 358 | static void render_wifi_setup_list(void) { | ||
| 359 | int screen_w = axs15231b_get_width(); | ||
| 360 | axs15231b_fill_screen(COLOR_BG); | ||
| 361 | |||
| 362 | const char *title = "Select Network"; | ||
| 363 | int tw = strlen(title) * 8; | ||
| 364 | display_render_text((screen_w - tw) / 2, 5, title, COLOR_CYAN, COLOR_BG, 1); | ||
| 365 | |||
| 366 | int y = 25; | ||
| 367 | int visible = wifi_setup_visible_count(&s_wifi_setup); | ||
| 368 | |||
| 369 | for (int i = 0; i < visible && y < 295; i++) { | ||
| 370 | const wifi_ap_info_t *ap = wifi_setup_get_visible(&s_wifi_setup, i); | ||
| 371 | if (!ap) break; | ||
| 372 | |||
| 373 | char line[40]; | ||
| 374 | int rssi_bars = 0; | ||
| 375 | if (ap->rssi >= -30) rssi_bars = 4; | ||
| 376 | else if (ap->rssi >= -50) rssi_bars = 3; | ||
| 377 | else if (ap->rssi >= -70) rssi_bars = 2; | ||
| 378 | else rssi_bars = 1; | ||
| 379 | |||
| 380 | snprintf(line, sizeof(line), "%s", ap->ssid); | ||
| 381 | axs15231b_fill_rect(5, y, 260, 26, COLOR_DARKGRAY); | ||
| 382 | display_render_text(10, y + 4, line, COLOR_WHITE, COLOR_DARKGRAY, 1); | ||
| 383 | |||
| 384 | for (int b = 0; b < rssi_bars; b++) { | ||
| 385 | axs15231b_fill_rect(270 + b * 8, y + 16 - (b + 1) * 4, 6, (b + 1) * 4, COLOR_GREEN); | ||
| 386 | } | ||
| 387 | |||
| 388 | if (ap->secured) { | ||
| 389 | const char *lock = "*"; | ||
| 390 | display_render_text(254, y + 4, lock, COLOR_YELLOW, COLOR_DARKGRAY, 1); | ||
| 391 | } | ||
| 392 | |||
| 393 | y += 30; | ||
| 394 | } | ||
| 395 | |||
| 396 | render_setup_button(5, 440, 50, 28); | ||
| 397 | display_render_text(10, 444, "X", COLOR_WHITE, COLOR_DARKGRAY, 1); | ||
| 398 | |||
| 305 | axs15231b_flush(); | 399 | axs15231b_flush(); |
| 306 | } | 400 | } |
| 307 | 401 | ||
| 402 | static void render_wifi_setup_password(void) { | ||
| 403 | axs15231b_fill_screen(COLOR_BG); | ||
| 404 | |||
| 405 | display_render_text(10, 5, s_wifi_setup.selected_ssid, COLOR_CYAN, COLOR_BG, 1); | ||
| 406 | display_render_text(10, 25, "Password:", COLOR_DIM, COLOR_BG, 1); | ||
| 407 | |||
| 408 | axs15231b_fill_rect(10, 40, 220, 20, COLOR_DARKGRAY); | ||
| 409 | |||
| 410 | char masked[KB_INPUT_MAX + 1]; | ||
| 411 | if (s_kb_state.reveal) { | ||
| 412 | strncpy(masked, s_kb_state.input, sizeof(masked) - 1); | ||
| 413 | masked[sizeof(masked) - 1] = '\0'; | ||
| 414 | } else { | ||
| 415 | int len = s_kb_state.cursor; | ||
| 416 | if (len > 27) len = 27; | ||
| 417 | for (int i = 0; i < len; i++) masked[i] = '*'; | ||
| 418 | masked[len] = '\0'; | ||
| 419 | } | ||
| 420 | display_render_text(14, 43, masked, COLOR_WHITE, COLOR_DARKGRAY, 1); | ||
| 421 | |||
| 422 | const char *eye_label = s_kb_state.reveal ? "H" : "S"; | ||
| 423 | axs15231b_fill_rect(235, 40, 24, 20, COLOR_GRAY); | ||
| 424 | display_render_text(241, 43, eye_label, COLOR_WHITE, COLOR_GRAY, 1); | ||
| 425 | |||
| 426 | int kb_y = 70; | ||
| 427 | for (int row = 0; row < KB_ROW_COUNT; row++) { | ||
| 428 | const char *row_str; | ||
| 429 | int key_count = kb_get_row_keys(row, s_kb_state.layer, &row_str); | ||
| 430 | int x_off = (row == 0) ? 5 : (row == 1) ? 14 : (row == 2) ? 23 : 5; | ||
| 431 | |||
| 432 | int cx = x_off; | ||
| 433 | for (int col = 0; col < key_count; col++) { | ||
| 434 | char c = row_str[col]; | ||
| 435 | int kw = 28; | ||
| 436 | if (row == 3) { | ||
| 437 | if (col == 0) kw = 42; | ||
| 438 | else if (col == key_count - 1) kw = 50; | ||
| 439 | else kw = 168; | ||
| 440 | } | ||
| 441 | |||
| 442 | uint16_t bg = COLOR_DARKGRAY; | ||
| 443 | uint16_t fg = COLOR_WHITE; | ||
| 444 | char label[2] = {0, 0}; | ||
| 445 | |||
| 446 | if (c == '\001') { | ||
| 447 | label[0] = s_kb_state.layer == KB_ALPHA_UPPER ? 'A' : 'a'; | ||
| 448 | bg = COLOR_GRAY; | ||
| 449 | } else if (c == '\002') { | ||
| 450 | label[0] = s_kb_state.layer == KB_NUMSYM ? 'a' : '#'; | ||
| 451 | bg = COLOR_GRAY; | ||
| 452 | } else if (c == '\003') { | ||
| 453 | label[0] = '_'; | ||
| 454 | } else if (c == '\004') { | ||
| 455 | label[0] = '>'; | ||
| 456 | fg = COLOR_GREEN; | ||
| 457 | bg = COLOR_DARKGRAY; | ||
| 458 | } else if (c == '\b') { | ||
| 459 | label[0] = '<'; | ||
| 460 | bg = COLOR_GRAY; | ||
| 461 | } else { | ||
| 462 | label[0] = c; | ||
| 463 | } | ||
| 464 | |||
| 465 | axs15231b_fill_rect(cx, kb_y, kw, 30, bg); | ||
| 466 | int lw = 8; | ||
| 467 | display_render_text(cx + (kw - lw) / 2, kb_y + 8, label, fg, bg, 1); | ||
| 468 | cx += kw + 2; | ||
| 469 | } | ||
| 470 | kb_y += 34; | ||
| 471 | } | ||
| 472 | |||
| 473 | axs15231b_flush(); | ||
| 474 | } | ||
| 475 | |||
| 476 | static void render_wifi_setup_connecting(void) { | ||
| 477 | int screen_w = axs15231b_get_width(); | ||
| 478 | axs15231b_fill_screen(COLOR_BG); | ||
| 479 | |||
| 480 | const char *msg1 = "Connecting to"; | ||
| 481 | int w1 = strlen(msg1) * 8; | ||
| 482 | display_render_text((screen_w - w1) / 2, 200, msg1, COLOR_WHITE, COLOR_BG, 1); | ||
| 483 | |||
| 484 | int w2 = strlen(s_wifi_setup.selected_ssid) * 8; | ||
| 485 | display_render_text((screen_w - w2) / 2, 225, s_wifi_setup.selected_ssid, COLOR_CYAN, COLOR_BG, 1); | ||
| 486 | |||
| 487 | const char *dots = "..."; | ||
| 488 | int dw = strlen(dots) * 8; | ||
| 489 | display_render_text((screen_w - dw) / 2, 255, dots, COLOR_DIM, COLOR_BG, 1); | ||
| 490 | |||
| 491 | axs15231b_flush(); | ||
| 492 | } | ||
| 493 | |||
| 494 | static void render_wifi_setup_result(void) { | ||
| 495 | int screen_w = axs15231b_get_width(); | ||
| 496 | axs15231b_fill_screen(COLOR_BG); | ||
| 497 | |||
| 498 | if (s_wifi_setup.state == SETUP_SUCCESS) { | ||
| 499 | const char *msg = "Connected!"; | ||
| 500 | int mw = strlen(msg) * 8 * 2; | ||
| 501 | display_render_text((screen_w - mw) / 2, 180, msg, COLOR_WHITE, COLOR_GREEN, 2); | ||
| 502 | |||
| 503 | if (s_wifi_setup.connect_ip[0]) { | ||
| 504 | char ip_line[32]; | ||
| 505 | snprintf(ip_line, sizeof(ip_line), "IP: %s", s_wifi_setup.connect_ip); | ||
| 506 | int iw = strlen(ip_line) * 8; | ||
| 507 | display_render_text((screen_w - iw) / 2, 240, ip_line, COLOR_WHITE, COLOR_BG, 1); | ||
| 508 | } | ||
| 509 | } else { | ||
| 510 | const char *msg = "Connection failed"; | ||
| 511 | int mw = strlen(msg) * 8 * 2; | ||
| 512 | display_render_text((screen_w - mw) / 2, 180, msg, COLOR_WHITE, COLOR_RED, 2); | ||
| 513 | |||
| 514 | const char *hint = "Wrong password?"; | ||
| 515 | int hw = strlen(hint) * 8; | ||
| 516 | display_render_text((screen_w - hw) / 2, 240, hint, COLOR_YELLOW, COLOR_BG, 1); | ||
| 517 | |||
| 518 | axs15231b_fill_rect(30, 280, 120, 30, COLOR_DARKGRAY); | ||
| 519 | display_render_text(50, 288, "Retry", COLOR_WHITE, COLOR_DARKGRAY, 1); | ||
| 520 | axs15231b_fill_rect(170, 280, 120, 30, COLOR_DARKGRAY); | ||
| 521 | display_render_text(180, 288, "Change", COLOR_WHITE, COLOR_DARKGRAY, 1); | ||
| 522 | } | ||
| 523 | |||
| 524 | axs15231b_flush(); | ||
| 525 | } | ||
| 526 | |||
| 527 | static void handle_wifi_setup_touch(uint16_t tx, uint16_t ty) { | ||
| 528 | switch (s_wifi_setup.state) { | ||
| 529 | case SETUP_SCAN: | ||
| 530 | break; | ||
| 531 | |||
| 532 | case SETUP_LIST: { | ||
| 533 | if (touch_in_rect(tx, ty, 5, 440, 50, 28)) { | ||
| 534 | wifi_setup_handle_cancel(&s_wifi_setup); | ||
| 535 | s_wifi_setup_active = false; | ||
| 536 | s_state = DISPLAY_ERROR; | ||
| 537 | return; | ||
| 538 | } | ||
| 539 | int y = 25; | ||
| 540 | int visible = wifi_setup_visible_count(&s_wifi_setup); | ||
| 541 | for (int i = 0; i < visible; i++) { | ||
| 542 | if (touch_in_rect(tx, ty, 5, y, 300, 26)) { | ||
| 543 | wifi_setup_handle_select(&s_wifi_setup, i); | ||
| 544 | kb_state_init(&s_kb_state); | ||
| 545 | return; | ||
| 546 | } | ||
| 547 | y += 30; | ||
| 548 | } | ||
| 549 | break; | ||
| 550 | } | ||
| 551 | |||
| 552 | case SETUP_PASSWORD: { | ||
| 553 | if (touch_in_rect(tx, ty, 235, 40, 24, 20)) { | ||
| 554 | s_kb_state.reveal = !s_kb_state.reveal; | ||
| 555 | return; | ||
| 556 | } | ||
| 557 | kb_result_t r = kb_hit_test(tx, ty, s_kb_state.layer); | ||
| 558 | if (r.action != KB_ACTION_NONE) { | ||
| 559 | if (r.action == KB_ACTION_DONE && s_kb_state.cursor > 0) { | ||
| 560 | wifi_setup_handle_connect(&s_wifi_setup); | ||
| 561 | wifi_config_t wifi_cfg = {0}; | ||
| 562 | strncpy((char *)wifi_cfg.sta.ssid, s_wifi_setup.selected_ssid, | ||
| 563 | sizeof(wifi_cfg.sta.ssid) - 1); | ||
| 564 | strncpy((char *)wifi_cfg.sta.password, s_kb_state.input, | ||
| 565 | sizeof(wifi_cfg.sta.password) - 1); | ||
| 566 | wifi_cfg.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK; | ||
| 567 | esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg); | ||
| 568 | esp_wifi_connect(); | ||
| 569 | return; | ||
| 570 | } | ||
| 571 | kb_apply(&s_kb_state, r); | ||
| 572 | } | ||
| 573 | break; | ||
| 574 | } | ||
| 575 | |||
| 576 | case SETUP_CONNECTING: | ||
| 577 | break; | ||
| 578 | |||
| 579 | case SETUP_SUCCESS: { | ||
| 580 | wifi_setup_handle_cancel(&s_wifi_setup); | ||
| 581 | s_wifi_setup_active = false; | ||
| 582 | s_state = DISPLAY_READY; | ||
| 583 | return; | ||
| 584 | } | ||
| 585 | |||
| 586 | case SETUP_FAILED: { | ||
| 587 | if (touch_in_rect(tx, ty, 30, 280, 120, 30)) { | ||
| 588 | wifi_setup_handle_retry(&s_wifi_setup); | ||
| 589 | kb_state_init(&s_kb_state); | ||
| 590 | } else if (touch_in_rect(tx, ty, 170, 280, 120, 30)) { | ||
| 591 | wifi_setup_handle_change_network(&s_wifi_setup); | ||
| 592 | } | ||
| 593 | break; | ||
| 594 | } | ||
| 595 | |||
| 596 | default: | ||
| 597 | break; | ||
| 598 | } | ||
| 599 | } | ||
| 600 | |||
| 308 | static void display_task(void *pvParameters) { | 601 | static void display_task(void *pvParameters) { |
| 309 | ESP_LOGI(TAG, "Display task started"); | 602 | ESP_LOGI(TAG, "Display task started"); |
| 310 | 603 | ||
| @@ -319,6 +612,61 @@ static void display_task(void *pvParameters) { | |||
| 319 | } | 612 | } |
| 320 | } | 613 | } |
| 321 | 614 | ||
| 615 | touch_point_t tp; | ||
| 616 | if (s_touch_initialized && touch_read(&tp) && tp.touched) { | ||
| 617 | if (state == DISPLAY_WIFI_SETUP) { | ||
| 618 | handle_wifi_setup_touch(tp.x, tp.y); | ||
| 619 | state = s_state; | ||
| 620 | } else if (state == DISPLAY_ERROR || state == DISPLAY_READY) { | ||
| 621 | if (touch_in_rect(tp.x, tp.y, SETUP_BTN_X, SETUP_BTN_Y, SETUP_BTN_W, SETUP_BTN_H)) { | ||
| 622 | s_wifi_setup_active = true; | ||
| 623 | wifi_setup_init(&s_wifi_setup); | ||
| 624 | kb_state_init(&s_kb_state); | ||
| 625 | s_wifi_scan_pending = true; | ||
| 626 | s_state = DISPLAY_WIFI_SETUP; | ||
| 627 | state = DISPLAY_WIFI_SETUP; | ||
| 628 | } | ||
| 629 | } | ||
| 630 | } | ||
| 631 | |||
| 632 | if (s_wifi_scan_pending && s_wifi_setup.state == SETUP_SCAN) { | ||
| 633 | s_wifi_scan_pending = false; | ||
| 634 | wifi_scan_config_t scan_cfg = {0}; | ||
| 635 | esp_wifi_scan_start(&scan_cfg, true); | ||
| 636 | uint16_t ap_count = 0; | ||
| 637 | esp_wifi_scan_get_ap_num(&ap_count); | ||
| 638 | if (ap_count > WIFI_SETUP_MAX_APS) ap_count = WIFI_SETUP_MAX_APS; | ||
| 639 | wifi_ap_record_t ap_records[WIFI_SETUP_MAX_APS]; | ||
| 640 | esp_wifi_scan_get_ap_records(&ap_count, ap_records); | ||
| 641 | |||
| 642 | wifi_ap_info_t aps[WIFI_SETUP_MAX_APS]; | ||
| 643 | for (int i = 0; i < (int)ap_count; i++) { | ||
| 644 | strncpy(aps[i].ssid, (const char *)ap_records[i].ssid, WIFI_SETUP_SSID_LEN - 1); | ||
| 645 | aps[i].ssid[WIFI_SETUP_SSID_LEN - 1] = '\0'; | ||
| 646 | aps[i].rssi = ap_records[i].rssi; | ||
| 647 | aps[i].secured = (ap_records[i].authmode != WIFI_AUTH_OPEN); | ||
| 648 | } | ||
| 649 | |||
| 650 | for (int i = 0; i < (int)ap_count - 1; i++) { | ||
| 651 | for (int j = i + 1; j < (int)ap_count; j++) { | ||
| 652 | if (aps[j].rssi > aps[i].rssi) { | ||
| 653 | wifi_ap_info_t tmp = aps[i]; | ||
| 654 | aps[i] = aps[j]; | ||
| 655 | aps[j] = tmp; | ||
| 656 | } | ||
| 657 | } | ||
| 658 | } | ||
| 659 | |||
| 660 | wifi_setup_set_aps(&s_wifi_setup, aps, (int)ap_count); | ||
| 661 | } | ||
| 662 | |||
| 663 | if (s_wifi_setup_active && s_wifi_setup.state == SETUP_CONNECTING) { | ||
| 664 | wifi_ap_record_t ap_info; | ||
| 665 | if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) { | ||
| 666 | wifi_setup_handle_connect_result(&s_wifi_setup, true, NULL); | ||
| 667 | } | ||
| 668 | } | ||
| 669 | |||
| 322 | switch (state) { | 670 | switch (state) { |
| 323 | case DISPLAY_BOOT: | 671 | case DISPLAY_BOOT: |
| 324 | render_boot_screen(); | 672 | render_boot_screen(); |
| @@ -334,6 +682,34 @@ static void display_task(void *pvParameters) { | |||
| 334 | case DISPLAY_ERROR: | 682 | case DISPLAY_ERROR: |
| 335 | render_error_screen(); | 683 | render_error_screen(); |
| 336 | break; | 684 | break; |
| 685 | case DISPLAY_WIFI_SETUP: | ||
| 686 | switch (s_wifi_setup.state) { | ||
| 687 | case SETUP_SCAN: | ||
| 688 | render_wifi_setup_scanning(); | ||
| 689 | break; | ||
| 690 | case SETUP_LIST: | ||
| 691 | render_wifi_setup_list(); | ||
| 692 | break; | ||
| 693 | case SETUP_PASSWORD: | ||
| 694 | render_wifi_setup_password(); | ||
| 695 | break; | ||
| 696 | case SETUP_CONNECTING: | ||
| 697 | render_wifi_setup_connecting(); | ||
| 698 | break; | ||
| 699 | case SETUP_SUCCESS: | ||
| 700 | render_wifi_setup_result(); | ||
| 701 | vTaskDelay(pdMS_TO_TICKS(3000)); | ||
| 702 | wifi_setup_handle_cancel(&s_wifi_setup); | ||
| 703 | s_wifi_setup_active = false; | ||
| 704 | s_state = DISPLAY_READY; | ||
| 705 | break; | ||
| 706 | case SETUP_FAILED: | ||
| 707 | render_wifi_setup_result(); | ||
| 708 | break; | ||
| 709 | default: | ||
| 710 | break; | ||
| 711 | } | ||
| 712 | break; | ||
| 337 | } | 713 | } |
| 338 | 714 | ||
| 339 | vTaskDelay(pdMS_TO_TICKS(RENDER_INTERVAL_MS)); | 715 | vTaskDelay(pdMS_TO_TICKS(RENDER_INTERVAL_MS)); |
| @@ -352,7 +728,15 @@ esp_err_t display_init(void) { | |||
| 352 | s_initialized = true; | 728 | s_initialized = true; |
| 353 | s_last_qr_switch = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS; | 729 | s_last_qr_switch = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS; |
| 354 | 730 | ||
| 355 | xTaskCreatePinnedToCore(display_task, "display", 16384, NULL, 2, NULL, 1); | 731 | esp_err_t touch_ret = touch_init(); |
| 732 | if (touch_ret == ESP_OK) { | ||
| 733 | s_touch_initialized = true; | ||
| 734 | ESP_LOGI(TAG, "Touch controller initialized"); | ||
| 735 | } else { | ||
| 736 | ESP_LOGW(TAG, "Touch init failed (non-fatal): %s", esp_err_to_name(touch_ret)); | ||
| 737 | } | ||
| 738 | |||
| 739 | xTaskCreatePinnedToCore(display_task, "display", 24576, NULL, 2, NULL, 1); | ||
| 356 | 740 | ||
| 357 | ESP_LOGI(TAG, "Display initialized"); | 741 | ESP_LOGI(TAG, "Display initialized"); |
| 358 | return ESP_OK; | 742 | return ESP_OK; |
| @@ -393,3 +777,27 @@ void display_notify_payment(int amount_sats, int64_t allotment_ms) { | |||
| 393 | s_wallet_balance = nucula_wallet_balance(); | 777 | s_wallet_balance = nucula_wallet_balance(); |
| 394 | display_set_state(DISPLAY_PAYMENT_RECEIVED); | 778 | display_set_state(DISPLAY_PAYMENT_RECEIVED); |
| 395 | } | 779 | } |
| 780 | |||
| 781 | void display_notify_wifi_connected(const char *ip) { | ||
| 782 | if (s_wifi_setup_active && s_wifi_setup.state == SETUP_CONNECTING) { | ||
| 783 | wifi_setup_handle_connect_result(&s_wifi_setup, true, ip); | ||
| 784 | if (s_kb_state.cursor > 0) { | ||
| 785 | tollgate_config_add_wifi(s_wifi_setup.selected_ssid, s_kb_state.input); | ||
| 786 | } | ||
| 787 | } | ||
| 788 | } | ||
| 789 | |||
| 790 | void display_notify_wifi_disconnected(void) { | ||
| 791 | if (s_wifi_setup_active && s_wifi_setup.state == SETUP_CONNECTING) { | ||
| 792 | wifi_setup_handle_connect_result(&s_wifi_setup, false, NULL); | ||
| 793 | } | ||
| 794 | } | ||
| 795 | |||
| 796 | void display_enter_wifi_setup(void) { | ||
| 797 | if (!s_initialized) return; | ||
| 798 | s_wifi_setup_active = true; | ||
| 799 | wifi_setup_init(&s_wifi_setup); | ||
| 800 | kb_state_init(&s_kb_state); | ||
| 801 | s_wifi_scan_pending = true; | ||
| 802 | s_state = DISPLAY_WIFI_SETUP; | ||
| 803 | } | ||
diff --git a/main/display.h b/main/display.h index 49b3ffd..e8824b6 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_WIFI_SETUP | ||
| 13 | } display_state_t; | 14 | } display_state_t; |
| 14 | 15 | ||
| 15 | typedef enum { | 16 | typedef enum { |
| @@ -24,6 +25,9 @@ void display_update(const char *ap_ssid, int active_clients, | |||
| 24 | const char *mint_url, int price_per_step, | 25 | const char *mint_url, int price_per_step, |
| 25 | const char *wifi_status); | 26 | const char *wifi_status); |
| 26 | void display_notify_payment(int amount_sats, int64_t allotment_ms); | 27 | void display_notify_payment(int amount_sats, int64_t allotment_ms); |
| 28 | void display_notify_wifi_connected(const char *ip); | ||
| 29 | void display_notify_wifi_disconnected(void); | ||
| 30 | void display_enter_wifi_setup(void); | ||
| 27 | void display_render_text(int x, int y, const char *text, uint16_t fg, uint16_t bg, int scale); | 31 | void display_render_text(int x, int y, const char *text, uint16_t fg, uint16_t bg, int scale); |
| 28 | void display_render_qr(const char *text); | 32 | void display_render_qr(const char *text); |
| 29 | 33 | ||
diff --git a/main/tollgate_main.c b/main/tollgate_main.c index 345295a..59d25f5 100644 --- a/main/tollgate_main.c +++ b/main/tollgate_main.c | |||
| @@ -54,6 +54,7 @@ static void wifi_event_handler(void *arg, esp_event_base_t event_base, | |||
| 54 | s_retry_count++; | 54 | s_retry_count++; |
| 55 | ESP_LOGW(TAG, "WiFi disconnected, retry %d/%d", s_retry_count, MAX_STA_RETRY); | 55 | ESP_LOGW(TAG, "WiFi disconnected, retry %d/%d", s_retry_count, MAX_STA_RETRY); |
| 56 | tollgate_client_on_sta_disconnected(); | 56 | tollgate_client_on_sta_disconnected(); |
| 57 | display_notify_wifi_disconnected(); | ||
| 57 | if (s_services_running) { | 58 | if (s_services_running) { |
| 58 | stop_services(); | 59 | stop_services(); |
| 59 | display_set_state(DISPLAY_ERROR); | 60 | display_set_state(DISPLAY_ERROR); |
| @@ -102,6 +103,10 @@ static void ip_event_handler(void *arg, esp_event_base_t event_base, | |||
| 102 | s_retry_count = 0; | 103 | s_retry_count = 0; |
| 103 | xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); | 104 | xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); |
| 104 | 105 | ||
| 106 | char ip_str[16]; | ||
| 107 | snprintf(ip_str, sizeof(ip_str), IPSTR, IP2STR(&event->ip_info.ip)); | ||
| 108 | display_notify_wifi_connected(ip_str); | ||
| 109 | |||
| 105 | esp_sntp_stop(); | 110 | esp_sntp_stop(); |
| 106 | esp_sntp_setoperatingmode(SNTP_OPMODE_POLL); | 111 | esp_sntp_setoperatingmode(SNTP_OPMODE_POLL); |
| 107 | esp_sntp_setservername(0, "pool.ntp.org"); | 112 | esp_sntp_setservername(0, "pool.ntp.org"); |
| @@ -320,7 +325,8 @@ void app_main(void) | |||
| 320 | tcfg->mint_url, tcfg->price_per_step, "connecting..."); | 325 | tcfg->mint_url, tcfg->price_per_step, "connecting..."); |
| 321 | 326 | ||
| 322 | if (tollgate_config_get_wifi(&(wifi_config_t){0}) != ESP_OK) { | 327 | if (tollgate_config_get_wifi(&(wifi_config_t){0}) != ESP_OK) { |
| 323 | ESP_LOGI(TAG, "No STA network configured, starting services immediately"); | 328 | ESP_LOGI(TAG, "No STA network configured, entering WiFi setup"); |
| 329 | display_enter_wifi_setup(); | ||
| 324 | xTaskCreate(services_start_task, "svc_start", 32768, NULL, 5, NULL); | 330 | xTaskCreate(services_start_task, "svc_start", 32768, NULL, 5, NULL); |
| 325 | } | 331 | } |
| 326 | 332 | ||