diff options
| author | Your Name <you@example.com> | 2026-05-19 04:21:14 +0530 |
|---|---|---|
| committer | Your Name <you@example.com> | 2026-05-19 04:21:14 +0530 |
| commit | aa58b47996083f36e3587b8e10f9bbb681610491 (patch) | |
| tree | 4c845da2ee4b217a2c77deea3b10dd8d288d1b09 /main/display.c | |
| parent | 9f7dd94029c8dc12117494548f5f32221a729307 (diff) | |
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
Diffstat (limited to 'main/display.c')
| -rw-r--r-- | main/display.c | 654 |
1 files changed, 104 insertions, 550 deletions
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 @@ | |||
| 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 "config.h" |
| 10 | #include "esp_log.h" | 7 | #include "esp_log.h" |
| 11 | #include "esp_wifi.h" | 8 | #include "esp_wifi.h" |
| @@ -43,74 +40,10 @@ static display_qr_mode_t s_qr_mode = DISPLAY_QR_WIFI; | |||
| 43 | static int s_last_payment_sats = 0; | 40 | static int s_last_payment_sats = 0; |
| 44 | static int64_t s_last_allotment_ms = 0; | 41 | static int64_t s_last_allotment_ms = 0; |
| 45 | 42 | ||
| 46 | #define COLOR_GRAY 0x8410 | 43 | static uint16_t wallet_color(void) { |
| 47 | #define COLOR_DARKGRAY 0x4208 | 44 | if (s_wallet_balance == 0) return COLOR_RED; |
| 48 | #define COLOR_LIGHTBLUE 0xA5FF | 45 | if (s_wallet_balance < 100) return COLOR_YELLOW; |
| 49 | #define COLOR_HIGHLIGHT 0x07E0 | 46 | return COLOR_GREEN; |
| 50 | |||
| 51 | static wifi_setup_t s_wifi_setup; | ||
| 52 | static kb_state_t s_kb_state; | ||
| 53 | static bool s_wifi_setup_active = false; | ||
| 54 | static bool s_touch_initialized = false; | ||
| 55 | static bool s_wifi_scan_pending = false; | ||
| 56 | static int s_display_rotation = 0; | ||
| 57 | |||
| 58 | #define SETUP_BTN_X 240 | ||
| 59 | #define SETUP_BTN_Y 440 | ||
| 60 | #define SETUP_BTN_W 72 | ||
| 61 | #define SETUP_BTN_H 30 | ||
| 62 | |||
| 63 | static void enter_wifi_setup_rotation(void) { | ||
| 64 | if (s_display_rotation != 1) { | ||
| 65 | s_display_rotation = 1; | ||
| 66 | axs15231b_set_rotation(1); | ||
| 67 | touch_set_rotation(1); | ||
| 68 | kb_layout_t landscape = { | ||
| 69 | .key_w = 38, | ||
| 70 | .key_h = 40, | ||
| 71 | .key_gap = 2, | ||
| 72 | .start_y = 170, | ||
| 73 | .screen_w = 480, | ||
| 74 | .row_count = 4, | ||
| 75 | }; | ||
| 76 | kb_set_layout(&landscape); | ||
| 77 | } | ||
| 78 | } | ||
| 79 | |||
| 80 | static void exit_wifi_setup_rotation(void) { | ||
| 81 | if (s_display_rotation != 0) { | ||
| 82 | s_display_rotation = 0; | ||
| 83 | axs15231b_set_rotation(0); | ||
| 84 | touch_set_rotation(0); | ||
| 85 | kb_layout_t portrait = { | ||
| 86 | .key_w = 28, | ||
| 87 | .key_h = 36, | ||
| 88 | .key_gap = 2, | ||
| 89 | .start_y = 70, | ||
| 90 | .screen_w = 320, | ||
| 91 | .row_count = 4, | ||
| 92 | }; | ||
| 93 | kb_set_layout(&portrait); | ||
| 94 | } | ||
| 95 | } | ||
| 96 | |||
| 97 | static void render_setup_button(int x, int y, int w, int h) { | ||
| 98 | axs15231b_fill_rect(x, y, w, h, COLOR_DARKGRAY); | ||
| 99 | const char *label = "Setup"; | ||
| 100 | int lw = strlen(label) * 8; | ||
| 101 | display_render_text(x + (w - lw) / 2, y + (h - 8) / 2, label, COLOR_WHITE, COLOR_DARKGRAY, 1); | ||
| 102 | } | ||
| 103 | |||
| 104 | static bool touch_in_rect(uint16_t tx, uint16_t ty, int x, int y, int w, int h) { | ||
| 105 | return tx >= x && tx < x + w && ty >= y && ty < y + h; | ||
| 106 | } | ||
| 107 | |||
| 108 | static void highlight_rect(int x, int y, int w, int h) { | ||
| 109 | axs15231b_fill_rect(x, y, w, h, COLOR_HIGHLIGHT); | ||
| 110 | axs15231b_flush(); | ||
| 111 | vTaskDelay(pdMS_TO_TICKS(80)); | ||
| 112 | axs15231b_fill_rect(x, y, w, h, COLOR_DARKGRAY); | ||
| 113 | axs15231b_flush(); | ||
| 114 | } | 47 | } |
| 115 | 48 | ||
| 116 | static int qr_version_from_strlen(int len) { | 49 | 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) { | |||
| 150 | return di; | 83 | return di; |
| 151 | } | 84 | } |
| 152 | 85 | ||
| 86 | static void extract_domain(const char *url, char *out, int out_size) { | ||
| 87 | const char *start = url; | ||
| 88 | if (strncmp(url, "https://", 8) == 0) start = url + 8; | ||
| 89 | else if (strncmp(url, "http://", 7) == 0) start = url + 7; | ||
| 90 | strncpy(out, start, out_size - 1); | ||
| 91 | out[out_size - 1] = '\0'; | ||
| 92 | char *slash = strchr(out, '/'); | ||
| 93 | if (slash) *slash = '\0'; | ||
| 94 | } | ||
| 95 | |||
| 153 | static void build_wifi_qr_string(char *out, int out_size) { | 96 | static void build_wifi_qr_string(char *out, int out_size) { |
| 154 | char escaped_ssid[64]; | 97 | char escaped_ssid[64]; |
| 155 | escape_wifi_field(s_ap_ssid, escaped_ssid, sizeof(escaped_ssid)); | 98 | escape_wifi_field(s_ap_ssid, escaped_ssid, sizeof(escaped_ssid)); |
| 156 | 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 | } | ||
| 157 | } | 107 | } |
| 158 | 108 | ||
| 159 | static void extract_domain(const char *url, char *domain, int domain_size) { | 109 | static void render_qr_at(const char *text, int x_off, int y_off, int max_w, int max_h) { |
| 160 | const char *start = url; | 110 | int len = strlen(text); |
| 161 | if (strncmp(url, "https://", 8) == 0) start = url + 8; | 111 | int version = qr_version_from_strlen(len); |
| 162 | else if (strncmp(url, "http://", 7) == 0) start = url + 7; | 112 | int px = qr_pixel_size(len); |
| 163 | strncpy(domain, start, domain_size - 1); | 113 | |
| 164 | domain[domain_size - 1] = '\0'; | 114 | uint16_t buf_size = qrcode_getBufferSize(version); |
| 165 | char *slash = strchr(domain, '/'); | 115 | uint8_t *qr_buf = (uint8_t *)malloc(buf_size); |
| 166 | if (slash) *slash = '\0'; | 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); | ||
| 167 | } | 140 | } |
| 168 | 141 | ||
| 169 | void display_render_text(int x, int y, const char *text, uint16_t fg, uint16_t bg, int scale) { | 142 | void display_render_text(int x, int y, const char *text, uint16_t fg, uint16_t bg, int scale) { |
| @@ -199,57 +172,14 @@ void display_render_text(int x, int y, const char *text, uint16_t fg, uint16_t b | |||
| 199 | } | 172 | } |
| 200 | } | 173 | } |
| 201 | 174 | ||
| 202 | static void render_qr_at(const char *text, int x_off, int y_off, int max_w, int max_h) { | ||
| 203 | int len = strlen(text); | ||
| 204 | int version = qr_version_from_strlen(len); | ||
| 205 | int px = qr_pixel_size(len); | ||
| 206 | |||
| 207 | uint16_t buf_size = qrcode_getBufferSize(version); | ||
| 208 | uint8_t *qr_buf = (uint8_t *)malloc(buf_size); | ||
| 209 | if (!qr_buf) { | ||
| 210 | ESP_LOGE(TAG, "Failed to allocate QR buffer"); | ||
| 211 | return; | ||
| 212 | } | ||
| 213 | |||
| 214 | QRCode qr; | ||
| 215 | if (qrcode_initText(&qr, qr_buf, version, ECC_LOW, text) != 0) { | ||
| 216 | ESP_LOGE(TAG, "QR generation failed"); | ||
| 217 | free(qr_buf); | ||
| 218 | return; | ||
| 219 | } | ||
| 220 | |||
| 221 | int qr_px_w = qr.size * px; | ||
| 222 | int qr_px_h = qr.size * px; | ||
| 223 | int cx = x_off + (max_w - qr_px_w) / 2; | ||
| 224 | int cy = y_off + (max_h - qr_px_h) / 2; | ||
| 225 | if (cx < 0) cx = 0; | ||
| 226 | if (cy < 0) cy = 0; | ||
| 227 | |||
| 228 | for (int y = 0; y < qr.size; y++) { | ||
| 229 | for (int x = 0; x < qr.size; x++) { | ||
| 230 | bool mod = qrcode_getModule(&qr, x, y); | ||
| 231 | uint16_t color = mod ? 0xFFFF : 0x0000; | ||
| 232 | axs15231b_fill_rect(cx + x * px, cy + y * px, px, px, color); | ||
| 233 | } | ||
| 234 | } | ||
| 235 | |||
| 236 | free(qr_buf); | ||
| 237 | } | ||
| 238 | |||
| 239 | void display_render_qr(const char *text) { | 175 | void display_render_qr(const char *text) { |
| 240 | int screen_w = axs15231b_get_width(); | 176 | int screen_w = axs15231b_get_width(); |
| 241 | int screen_h = axs15231b_get_height(); | 177 | int screen_h = axs15231b_get_height(); |
| 242 | axs15231b_fill_screen(0x0000); | 178 | axs15231b_fill_screen(COLOR_BG); |
| 243 | render_qr_at(text, 0, 0, screen_w, screen_h); | 179 | render_qr_at(text, 0, 0, screen_w, screen_h); |
| 244 | axs15231b_flush(); | 180 | axs15231b_flush(); |
| 245 | } | 181 | } |
| 246 | 182 | ||
| 247 | static uint16_t wallet_color(void) { | ||
| 248 | if (s_wallet_balance == 0) return COLOR_RED; | ||
| 249 | if (s_wallet_balance < 100) return COLOR_YELLOW; | ||
| 250 | return COLOR_GREEN; | ||
| 251 | } | ||
| 252 | |||
| 253 | static void render_boot_screen(void) { | 183 | static void render_boot_screen(void) { |
| 254 | int screen_w = axs15231b_get_width(); | 184 | int screen_w = axs15231b_get_width(); |
| 255 | axs15231b_fill_screen(COLOR_BG); | 185 | axs15231b_fill_screen(COLOR_BG); |
| @@ -315,7 +245,44 @@ static void render_ready_screen(void) { | |||
| 315 | display_render_text(10, y, line, COLOR_GREEN, COLOR_BG, 1); | 245 | display_render_text(10, y, line, COLOR_GREEN, COLOR_BG, 1); |
| 316 | } | 246 | } |
| 317 | 247 | ||
| 318 | render_setup_button(SETUP_BTN_X, SETUP_BTN_Y, SETUP_BTN_W, SETUP_BTN_H); | 248 | axs15231b_flush(); |
| 249 | } | ||
| 250 | |||
| 251 | static void render_setup_pending_screen(void) { | ||
| 252 | int screen_w = axs15231b_get_width(); | ||
| 253 | axs15231b_fill_screen(COLOR_BG); | ||
| 254 | |||
| 255 | char qr_text[320]; | ||
| 256 | build_wifi_qr_string(qr_text, sizeof(qr_text)); | ||
| 257 | render_qr_at(qr_text, 0, 5, screen_w, 280); | ||
| 258 | |||
| 259 | int y = 290; | ||
| 260 | char line[64]; | ||
| 261 | |||
| 262 | const char *title = "WiFi Setup"; | ||
| 263 | int tw = strlen(title) * 8; | ||
| 264 | display_render_text((screen_w - tw) / 2, y, title, COLOR_CYAN, COLOR_BG, 1); | ||
| 265 | y += 20; | ||
| 266 | |||
| 267 | snprintf(line, sizeof(line), "SSID: %s", s_ap_ssid); | ||
| 268 | display_render_text(10, y, line, COLOR_WHITE, COLOR_BG, 1); | ||
| 269 | y += 18; | ||
| 270 | |||
| 271 | const char *hint1 = "1. Connect to WiFi above"; | ||
| 272 | display_render_text(10, y, hint1, COLOR_DIM, COLOR_BG, 1); | ||
| 273 | y += 16; | ||
| 274 | |||
| 275 | const char *hint2 = "2. Open browser, go to:"; | ||
| 276 | display_render_text(10, y, hint2, COLOR_DIM, COLOR_BG, 1); | ||
| 277 | y += 18; | ||
| 278 | |||
| 279 | const tollgate_config_t *cfg = tollgate_config_get(); | ||
| 280 | snprintf(line, sizeof(line), "http://%s/setup", cfg->ap_ip_str); | ||
| 281 | display_render_text(10, y, line, COLOR_YELLOW, COLOR_BG, 1); | ||
| 282 | y += 22; | ||
| 283 | |||
| 284 | const char *hint3 = "3. Configure upstream WiFi"; | ||
| 285 | display_render_text(10, y, hint3, COLOR_DIM, COLOR_BG, 1); | ||
| 319 | 286 | ||
| 320 | axs15231b_flush(); | 287 | axs15231b_flush(); |
| 321 | } | 288 | } |
| @@ -360,324 +327,29 @@ static void render_error_screen(void) { | |||
| 360 | int msg_w = strlen(msg) * 8 * 2; | 327 | int msg_w = strlen(msg) * 8 * 2; |
| 361 | display_render_text((screen_w - msg_w) / 2, 202, msg, COLOR_WHITE, COLOR_RED, 2); | 328 | display_render_text((screen_w - msg_w) / 2, 202, msg, COLOR_WHITE, COLOR_RED, 2); |
| 362 | 329 | ||
| 363 | char line[48]; | 330 | char line[64]; |
| 364 | int lw; | 331 | int lw; |
| 365 | 332 | ||
| 366 | const char *l1 = "Internet unavailable"; | 333 | const char *l1 = "Internet unavailable"; |
| 367 | lw = strlen(l1) * 8; | 334 | lw = strlen(l1) * 8; |
| 368 | display_render_text((screen_w - lw) / 2, 270, l1, COLOR_WHITE, COLOR_BG, 1); | 335 | display_render_text((screen_w - lw) / 2, 270, l1, COLOR_WHITE, COLOR_BG, 1); |
| 369 | 336 | ||
| 370 | const char *l2 = "Check WiFi config"; | ||
| 371 | lw = strlen(l2) * 8; | ||
| 372 | display_render_text((screen_w - lw) / 2, 290, l2, COLOR_YELLOW, COLOR_BG, 1); | ||
| 373 | |||
| 374 | const char *l3 = "AP still active"; | ||
| 375 | lw = strlen(l3) * 8; | ||
| 376 | display_render_text((screen_w - lw) / 2, 320, l3, COLOR_GREEN, COLOR_BG, 1); | ||
| 377 | |||
| 378 | snprintf(line, sizeof(line), "SSID: %s", s_ap_ssid); | 337 | snprintf(line, sizeof(line), "SSID: %s", s_ap_ssid); |
| 379 | lw = strlen(line) * 8; | 338 | lw = strlen(line) * 8; |
| 380 | display_render_text((screen_w - lw) / 2, 340, line, COLOR_DIM, COLOR_BG, 1); | 339 | display_render_text((screen_w - lw) / 2, 300, line, COLOR_DIM, COLOR_BG, 1); |
| 381 | |||
| 382 | render_setup_button(SETUP_BTN_X, SETUP_BTN_Y, SETUP_BTN_W, SETUP_BTN_H); | ||
| 383 | |||
| 384 | axs15231b_flush(); | ||
| 385 | } | ||
| 386 | 340 | ||
| 387 | static void render_wifi_setup_scanning(void) { | 341 | const tollgate_config_t *cfg = tollgate_config_get(); |
| 388 | int screen_w = axs15231b_get_width(); | 342 | snprintf(line, sizeof(line), "Setup: http://%s/setup", cfg->ap_ip_str); |
| 389 | axs15231b_fill_screen(COLOR_BG); | 343 | lw = strlen(line) * 8; |
| 390 | 344 | display_render_text((screen_w - lw) / 2, 330, line, COLOR_YELLOW, COLOR_BG, 1); | |
| 391 | const char *title = "WiFi Setup"; | ||
| 392 | int tw = strlen(title) * 8; | ||
| 393 | display_render_text((screen_w - tw) / 2, 180, title, COLOR_CYAN, COLOR_BG, 1); | ||
| 394 | |||
| 395 | const char *msg = "Scanning..."; | ||
| 396 | int mw = strlen(msg) * 8; | ||
| 397 | display_render_text((screen_w - mw) / 2, 220, msg, COLOR_WHITE, COLOR_BG, 1); | ||
| 398 | |||
| 399 | axs15231b_flush(); | ||
| 400 | } | ||
| 401 | |||
| 402 | static void render_wifi_setup_list(void) { | ||
| 403 | int screen_w = axs15231b_get_width(); | ||
| 404 | int screen_h = axs15231b_get_height(); | ||
| 405 | axs15231b_fill_screen(COLOR_BG); | ||
| 406 | |||
| 407 | const char *title = "Select Network"; | ||
| 408 | int tw = strlen(title) * 8; | ||
| 409 | display_render_text((screen_w - tw) / 2, 5, title, COLOR_CYAN, COLOR_BG, 1); | ||
| 410 | |||
| 411 | int y = 25; | ||
| 412 | int visible = wifi_setup_visible_count(&s_wifi_setup); | ||
| 413 | int item_w = screen_w - 20; | ||
| 414 | int max_y = screen_h - 40; | ||
| 415 | |||
| 416 | for (int i = 0; i < visible && y < max_y; i++) { | ||
| 417 | const wifi_ap_info_t *ap = wifi_setup_get_visible(&s_wifi_setup, i); | ||
| 418 | if (!ap) break; | ||
| 419 | |||
| 420 | int rssi_bars = 0; | ||
| 421 | if (ap->rssi >= -30) rssi_bars = 4; | ||
| 422 | else if (ap->rssi >= -50) rssi_bars = 3; | ||
| 423 | else if (ap->rssi >= -70) rssi_bars = 2; | ||
| 424 | else rssi_bars = 1; | ||
| 425 | |||
| 426 | axs15231b_fill_rect(10, y, item_w, 26, COLOR_DARKGRAY); | ||
| 427 | display_render_text(15, y + 4, ap->ssid, COLOR_WHITE, COLOR_DARKGRAY, 1); | ||
| 428 | |||
| 429 | int bar_x = 10 + item_w - 50; | ||
| 430 | for (int b = 0; b < rssi_bars; b++) { | ||
| 431 | axs15231b_fill_rect(bar_x + b * 10, y + 16 - (b + 1) * 4, 8, (b + 1) * 4, COLOR_GREEN); | ||
| 432 | } | ||
| 433 | |||
| 434 | if (ap->secured) { | ||
| 435 | display_render_text(bar_x - 12, y + 4, "*", COLOR_YELLOW, COLOR_DARKGRAY, 1); | ||
| 436 | } | ||
| 437 | |||
| 438 | y += 30; | ||
| 439 | } | ||
| 440 | |||
| 441 | int btn_y = screen_h - 30; | ||
| 442 | axs15231b_fill_rect(10, btn_y, 50, 26, COLOR_DARKGRAY); | ||
| 443 | display_render_text(25, btn_y + 5, "X", COLOR_WHITE, COLOR_DARKGRAY, 1); | ||
| 444 | |||
| 445 | axs15231b_flush(); | ||
| 446 | } | ||
| 447 | |||
| 448 | static void render_wifi_setup_password(void) { | ||
| 449 | int screen_w = axs15231b_get_width(); | ||
| 450 | const kb_layout_t *kb = kb_get_layout(); | ||
| 451 | axs15231b_fill_screen(COLOR_BG); | ||
| 452 | |||
| 453 | display_render_text(10, 5, s_wifi_setup.selected_ssid, COLOR_CYAN, COLOR_BG, 1); | ||
| 454 | display_render_text(10, 25, "Password:", COLOR_DIM, COLOR_BG, 1); | ||
| 455 | |||
| 456 | int field_w = screen_w - 80; | ||
| 457 | axs15231b_fill_rect(10, 40, field_w, 20, COLOR_DARKGRAY); | ||
| 458 | |||
| 459 | char masked[KB_INPUT_MAX + 1]; | ||
| 460 | if (s_kb_state.reveal) { | ||
| 461 | strncpy(masked, s_kb_state.input, sizeof(masked) - 1); | ||
| 462 | masked[sizeof(masked) - 1] = '\0'; | ||
| 463 | } else { | ||
| 464 | int len = s_kb_state.cursor; | ||
| 465 | int max_chars = (field_w - 8) / 8; | ||
| 466 | if (len > max_chars) len = max_chars; | ||
| 467 | for (int i = 0; i < len; i++) masked[i] = '*'; | ||
| 468 | masked[len] = '\0'; | ||
| 469 | } | ||
| 470 | display_render_text(14, 43, masked, COLOR_WHITE, COLOR_DARKGRAY, 1); | ||
| 471 | |||
| 472 | int eye_x = 10 + field_w + 5; | ||
| 473 | const char *eye_label = s_kb_state.reveal ? "H" : "S"; | ||
| 474 | axs15231b_fill_rect(eye_x, 40, 24, 20, COLOR_GRAY); | ||
| 475 | display_render_text(eye_x + 8, 43, eye_label, COLOR_WHITE, COLOR_GRAY, 1); | ||
| 476 | |||
| 477 | int kb_y = kb->start_y; | ||
| 478 | for (int row = 0; row < kb->row_count; row++) { | ||
| 479 | const char *row_str; | ||
| 480 | int key_count = kb_get_row_keys(row, s_kb_state.layer, &row_str); | ||
| 481 | const char *tmp; | ||
| 482 | int total_keys = kb_get_row_keys(row, s_kb_state.layer, &tmp); | ||
| 483 | int x_off = (row == 0) ? (screen_w - total_keys * kb->key_w - (total_keys - 1) * kb->key_gap) / 2 | ||
| 484 | : (row == 1) ? (screen_w - total_keys * kb->key_w - (total_keys - 1) * kb->key_gap) / 2 + kb->key_w / 2 | ||
| 485 | : (row == 2) ? (screen_w - total_keys * kb->key_w - (total_keys - 1) * kb->key_gap) / 2 + kb->key_w | ||
| 486 | : (screen_w - total_keys * kb->key_w - (total_keys - 1) * kb->key_gap) / 2; | ||
| 487 | |||
| 488 | int cx = x_off; | ||
| 489 | for (int col = 0; col < key_count; col++) { | ||
| 490 | char c = row_str[col]; | ||
| 491 | int kw = kb->key_w; | ||
| 492 | if (row == 3) { | ||
| 493 | int margin = (screen_w - total_keys * kb->key_w - (total_keys - 1) * kb->key_gap) / 2; | ||
| 494 | int available = screen_w - margin * 2; | ||
| 495 | int side_w = (available - kb->key_gap) / 4; | ||
| 496 | if (col == 0) kw = side_w; | ||
| 497 | else if (col == key_count - 1) kw = side_w; | ||
| 498 | else kw = available - side_w * 2 - kb->key_gap * 2; | ||
| 499 | } | ||
| 500 | |||
| 501 | uint16_t bg = COLOR_DARKGRAY; | ||
| 502 | uint16_t fg = COLOR_WHITE; | ||
| 503 | char label[2] = {0, 0}; | ||
| 504 | |||
| 505 | if (c == '\001') { | ||
| 506 | label[0] = s_kb_state.layer == KB_ALPHA_UPPER ? 'A' : 'a'; | ||
| 507 | bg = COLOR_GRAY; | ||
| 508 | } else if (c == '\002') { | ||
| 509 | label[0] = s_kb_state.layer == KB_NUMSYM ? 'a' : '#'; | ||
| 510 | bg = COLOR_GRAY; | ||
| 511 | } else if (c == '\003') { | ||
| 512 | label[0] = '_'; | ||
| 513 | } else if (c == '\004') { | ||
| 514 | label[0] = '>'; | ||
| 515 | fg = COLOR_GREEN; | ||
| 516 | } else if (c == '\b') { | ||
| 517 | label[0] = '<'; | ||
| 518 | bg = COLOR_GRAY; | ||
| 519 | } else { | ||
| 520 | label[0] = c; | ||
| 521 | } | ||
| 522 | |||
| 523 | axs15231b_fill_rect(cx, kb_y, kw, kb->key_h, bg); | ||
| 524 | int lw = 8; | ||
| 525 | display_render_text(cx + (kw - lw) / 2, kb_y + (kb->key_h - 8) / 2, label, fg, bg, 1); | ||
| 526 | cx += kw + kb->key_gap; | ||
| 527 | } | ||
| 528 | kb_y += kb->key_h + kb->key_gap; | ||
| 529 | } | ||
| 530 | |||
| 531 | axs15231b_flush(); | ||
| 532 | } | ||
| 533 | |||
| 534 | static void render_wifi_setup_connecting(void) { | ||
| 535 | int screen_w = axs15231b_get_width(); | ||
| 536 | axs15231b_fill_screen(COLOR_BG); | ||
| 537 | |||
| 538 | const char *msg1 = "Connecting to"; | ||
| 539 | int w1 = strlen(msg1) * 8; | ||
| 540 | display_render_text((screen_w - w1) / 2, 200, msg1, COLOR_WHITE, COLOR_BG, 1); | ||
| 541 | |||
| 542 | int w2 = strlen(s_wifi_setup.selected_ssid) * 8; | ||
| 543 | display_render_text((screen_w - w2) / 2, 225, s_wifi_setup.selected_ssid, COLOR_CYAN, COLOR_BG, 1); | ||
| 544 | |||
| 545 | const char *dots = "..."; | ||
| 546 | int dw = strlen(dots) * 8; | ||
| 547 | display_render_text((screen_w - dw) / 2, 255, dots, COLOR_DIM, COLOR_BG, 1); | ||
| 548 | |||
| 549 | axs15231b_flush(); | ||
| 550 | } | ||
| 551 | |||
| 552 | static void render_wifi_setup_result(void) { | ||
| 553 | int screen_w = axs15231b_get_width(); | ||
| 554 | int screen_h = axs15231b_get_height(); | ||
| 555 | axs15231b_fill_screen(COLOR_BG); | ||
| 556 | |||
| 557 | if (s_wifi_setup.state == SETUP_SUCCESS) { | ||
| 558 | const char *msg = "Connected!"; | ||
| 559 | int mw = strlen(msg) * 8 * 2; | ||
| 560 | display_render_text((screen_w - mw) / 2, screen_h / 2 - 40, msg, COLOR_WHITE, COLOR_GREEN, 2); | ||
| 561 | 345 | ||
| 562 | if (s_wifi_setup.connect_ip[0]) { | 346 | const char *l3 = "AP still active"; |
| 563 | char ip_line[32]; | 347 | lw = strlen(l3) * 8; |
| 564 | snprintf(ip_line, sizeof(ip_line), "IP: %s", s_wifi_setup.connect_ip); | 348 | display_render_text((screen_w - lw) / 2, 360, l3, COLOR_GREEN, COLOR_BG, 1); |
| 565 | int iw = strlen(ip_line) * 8; | ||
| 566 | display_render_text((screen_w - iw) / 2, screen_h / 2, ip_line, COLOR_WHITE, COLOR_BG, 1); | ||
| 567 | } | ||
| 568 | } else { | ||
| 569 | const char *msg = "Connection failed"; | ||
| 570 | int mw = strlen(msg) * 8 * 2; | ||
| 571 | display_render_text((screen_w - mw) / 2, screen_h / 2 - 40, msg, COLOR_WHITE, COLOR_RED, 2); | ||
| 572 | |||
| 573 | const char *hint = "Wrong password?"; | ||
| 574 | int hw = strlen(hint) * 8; | ||
| 575 | display_render_text((screen_w - hw) / 2, screen_h / 2, hint, COLOR_YELLOW, COLOR_BG, 1); | ||
| 576 | |||
| 577 | int btn_y = screen_h / 2 + 30; | ||
| 578 | int btn_w = (screen_w - 40) / 2; | ||
| 579 | axs15231b_fill_rect(10, btn_y, btn_w, 30, COLOR_DARKGRAY); | ||
| 580 | display_render_text(10 + (btn_w - 40) / 2, btn_y + 8, "Retry", COLOR_WHITE, COLOR_DARKGRAY, 1); | ||
| 581 | axs15231b_fill_rect(20 + btn_w, btn_y, btn_w, 30, COLOR_DARKGRAY); | ||
| 582 | display_render_text(20 + btn_w + (btn_w - 50) / 2, btn_y + 8, "Change", COLOR_WHITE, COLOR_DARKGRAY, 1); | ||
| 583 | } | ||
| 584 | 349 | ||
| 585 | axs15231b_flush(); | 350 | axs15231b_flush(); |
| 586 | } | 351 | } |
| 587 | 352 | ||
| 588 | static void handle_wifi_setup_touch(uint16_t tx, uint16_t ty) { | ||
| 589 | int screen_w = axs15231b_get_width(); | ||
| 590 | int screen_h = axs15231b_get_height(); | ||
| 591 | |||
| 592 | switch (s_wifi_setup.state) { | ||
| 593 | case SETUP_SCAN: | ||
| 594 | break; | ||
| 595 | |||
| 596 | case SETUP_LIST: { | ||
| 597 | int btn_y = screen_h - 30; | ||
| 598 | if (touch_in_rect(tx, ty, 10, btn_y, 50, 26)) { | ||
| 599 | highlight_rect(10, btn_y, 50, 26); | ||
| 600 | wifi_setup_handle_cancel(&s_wifi_setup); | ||
| 601 | s_wifi_setup_active = false; | ||
| 602 | exit_wifi_setup_rotation(); | ||
| 603 | s_state = DISPLAY_ERROR; | ||
| 604 | return; | ||
| 605 | } | ||
| 606 | int item_w = screen_w - 20; | ||
| 607 | int y = 25; | ||
| 608 | int max_y = screen_h - 40; | ||
| 609 | int visible = wifi_setup_visible_count(&s_wifi_setup); | ||
| 610 | for (int i = 0; i < visible && y < max_y; i++) { | ||
| 611 | if (touch_in_rect(tx, ty, 10, y, item_w, 26)) { | ||
| 612 | highlight_rect(10, y, item_w, 26); | ||
| 613 | wifi_setup_handle_select(&s_wifi_setup, i); | ||
| 614 | kb_state_init(&s_kb_state); | ||
| 615 | return; | ||
| 616 | } | ||
| 617 | y += 30; | ||
| 618 | } | ||
| 619 | break; | ||
| 620 | } | ||
| 621 | |||
| 622 | case SETUP_PASSWORD: { | ||
| 623 | int field_w = screen_w - 80; | ||
| 624 | int eye_x = 10 + field_w + 5; | ||
| 625 | if (touch_in_rect(tx, ty, eye_x, 40, 24, 20)) { | ||
| 626 | s_kb_state.reveal = !s_kb_state.reveal; | ||
| 627 | return; | ||
| 628 | } | ||
| 629 | kb_result_t r = kb_hit_test(tx, ty, s_kb_state.layer); | ||
| 630 | if (r.action != KB_ACTION_NONE) { | ||
| 631 | axs15231b_fill_rect(tx - 20, ty - 20, 40, 40, COLOR_HIGHLIGHT); | ||
| 632 | axs15231b_flush(); | ||
| 633 | vTaskDelay(pdMS_TO_TICKS(60)); | ||
| 634 | if (r.action == KB_ACTION_DONE && s_kb_state.cursor > 0) { | ||
| 635 | wifi_setup_handle_connect(&s_wifi_setup); | ||
| 636 | wifi_config_t wifi_cfg = {0}; | ||
| 637 | strncpy((char *)wifi_cfg.sta.ssid, s_wifi_setup.selected_ssid, | ||
| 638 | sizeof(wifi_cfg.sta.ssid) - 1); | ||
| 639 | strncpy((char *)wifi_cfg.sta.password, s_kb_state.input, | ||
| 640 | sizeof(wifi_cfg.sta.password) - 1); | ||
| 641 | wifi_cfg.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK; | ||
| 642 | esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg); | ||
| 643 | esp_wifi_connect(); | ||
| 644 | return; | ||
| 645 | } | ||
| 646 | kb_apply(&s_kb_state, r); | ||
| 647 | } | ||
| 648 | break; | ||
| 649 | } | ||
| 650 | |||
| 651 | case SETUP_CONNECTING: | ||
| 652 | break; | ||
| 653 | |||
| 654 | case SETUP_SUCCESS: { | ||
| 655 | wifi_setup_handle_cancel(&s_wifi_setup); | ||
| 656 | s_wifi_setup_active = false; | ||
| 657 | exit_wifi_setup_rotation(); | ||
| 658 | s_state = DISPLAY_READY; | ||
| 659 | return; | ||
| 660 | } | ||
| 661 | |||
| 662 | case SETUP_FAILED: { | ||
| 663 | int btn_y = screen_h / 2 + 30; | ||
| 664 | int btn_w = (screen_w - 40) / 2; | ||
| 665 | if (touch_in_rect(tx, ty, 10, btn_y, btn_w, 30)) { | ||
| 666 | highlight_rect(10, btn_y, btn_w, 30); | ||
| 667 | wifi_setup_handle_retry(&s_wifi_setup); | ||
| 668 | kb_state_init(&s_kb_state); | ||
| 669 | } else if (touch_in_rect(tx, ty, 20 + btn_w, btn_y, btn_w, 30)) { | ||
| 670 | highlight_rect(20 + btn_w, btn_y, btn_w, 30); | ||
| 671 | wifi_setup_handle_change_network(&s_wifi_setup); | ||
| 672 | } | ||
| 673 | break; | ||
| 674 | } | ||
| 675 | |||
| 676 | default: | ||
| 677 | break; | ||
| 678 | } | ||
| 679 | } | ||
| 680 | |||
| 681 | static void display_task(void *pvParameters) { | 353 | static void display_task(void *pvParameters) { |
| 682 | ESP_LOGI(TAG, "Display task started"); | 354 | ESP_LOGI(TAG, "Display task started"); |
| 683 | 355 | ||
| @@ -692,72 +364,6 @@ static void display_task(void *pvParameters) { | |||
| 692 | } | 364 | } |
| 693 | } | 365 | } |
| 694 | 366 | ||
| 695 | touch_point_t tp; | ||
| 696 | if (s_touch_initialized && touch_read(&tp) && tp.touched) { | ||
| 697 | if (state == DISPLAY_WIFI_SETUP) { | ||
| 698 | handle_wifi_setup_touch(tp.x, tp.y); | ||
| 699 | state = s_state; | ||
| 700 | } else if (state == DISPLAY_ERROR || state == DISPLAY_READY) { | ||
| 701 | if (touch_in_rect(tp.x, tp.y, SETUP_BTN_X, SETUP_BTN_Y, SETUP_BTN_W, SETUP_BTN_H)) { | ||
| 702 | enter_wifi_setup_rotation(); | ||
| 703 | s_wifi_setup_active = true; | ||
| 704 | wifi_setup_init(&s_wifi_setup); | ||
| 705 | kb_state_init(&s_kb_state); | ||
| 706 | s_wifi_scan_pending = true; | ||
| 707 | s_state = DISPLAY_WIFI_SETUP; | ||
| 708 | state = DISPLAY_WIFI_SETUP; | ||
| 709 | } | ||
| 710 | } | ||
| 711 | } | ||
| 712 | |||
| 713 | if (s_wifi_scan_pending && s_wifi_setup.state == SETUP_SCAN) { | ||
| 714 | s_wifi_scan_pending = false; | ||
| 715 | esp_wifi_disconnect(); | ||
| 716 | vTaskDelay(pdMS_TO_TICKS(500)); | ||
| 717 | wifi_scan_config_t scan_cfg = {0}; | ||
| 718 | scan_cfg.scan_type = WIFI_SCAN_TYPE_ACTIVE; | ||
| 719 | scan_cfg.scan_time.active.min = 100; | ||
| 720 | scan_cfg.scan_time.active.max = 300; | ||
| 721 | esp_err_t scan_ret = esp_wifi_scan_start(&scan_cfg, true); | ||
| 722 | if (scan_ret != ESP_OK) { | ||
| 723 | ESP_LOGE(TAG, "WiFi scan failed: %s", esp_err_to_name(scan_ret)); | ||
| 724 | wifi_setup_set_aps(&s_wifi_setup, NULL, 0); | ||
| 725 | } else { | ||
| 726 | uint16_t ap_count = 0; | ||
| 727 | esp_wifi_scan_get_ap_num(&ap_count); | ||
| 728 | if (ap_count > WIFI_SETUP_MAX_APS) ap_count = WIFI_SETUP_MAX_APS; | ||
| 729 | wifi_ap_record_t ap_records[WIFI_SETUP_MAX_APS]; | ||
| 730 | esp_wifi_scan_get_ap_records(&ap_count, ap_records); | ||
| 731 | |||
| 732 | wifi_ap_info_t aps[WIFI_SETUP_MAX_APS]; | ||
| 733 | for (int i = 0; i < (int)ap_count; i++) { | ||
| 734 | strncpy(aps[i].ssid, (const char *)ap_records[i].ssid, WIFI_SETUP_SSID_LEN - 1); | ||
| 735 | aps[i].ssid[WIFI_SETUP_SSID_LEN - 1] = '\0'; | ||
| 736 | aps[i].rssi = ap_records[i].rssi; | ||
| 737 | aps[i].secured = (ap_records[i].authmode != WIFI_AUTH_OPEN); | ||
| 738 | } | ||
| 739 | |||
| 740 | for (int i = 0; i < (int)ap_count - 1; i++) { | ||
| 741 | for (int j = i + 1; j < (int)ap_count; j++) { | ||
| 742 | if (aps[j].rssi > aps[i].rssi) { | ||
| 743 | wifi_ap_info_t tmp = aps[i]; | ||
| 744 | aps[i] = aps[j]; | ||
| 745 | aps[j] = tmp; | ||
| 746 | } | ||
| 747 | } | ||
| 748 | } | ||
| 749 | |||
| 750 | wifi_setup_set_aps(&s_wifi_setup, aps, (int)ap_count); | ||
| 751 | } | ||
| 752 | } | ||
| 753 | |||
| 754 | if (s_wifi_setup_active && s_wifi_setup.state == SETUP_CONNECTING) { | ||
| 755 | wifi_ap_record_t ap_info; | ||
| 756 | if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) { | ||
| 757 | wifi_setup_handle_connect_result(&s_wifi_setup, true, NULL); | ||
| 758 | } | ||
| 759 | } | ||
| 760 | |||
| 761 | switch (state) { | 367 | switch (state) { |
| 762 | case DISPLAY_BOOT: | 368 | case DISPLAY_BOOT: |
| 763 | render_boot_screen(); | 369 | render_boot_screen(); |
| @@ -773,34 +379,8 @@ static void display_task(void *pvParameters) { | |||
| 773 | case DISPLAY_ERROR: | 379 | case DISPLAY_ERROR: |
| 774 | render_error_screen(); | 380 | render_error_screen(); |
| 775 | break; | 381 | break; |
| 776 | case DISPLAY_WIFI_SETUP: | 382 | case DISPLAY_SETUP_PENDING: |
| 777 | switch (s_wifi_setup.state) { | 383 | render_setup_pending_screen(); |
| 778 | case SETUP_SCAN: | ||
| 779 | render_wifi_setup_scanning(); | ||
| 780 | break; | ||
| 781 | case SETUP_LIST: | ||
| 782 | render_wifi_setup_list(); | ||
| 783 | break; | ||
| 784 | case SETUP_PASSWORD: | ||
| 785 | render_wifi_setup_password(); | ||
| 786 | break; | ||
| 787 | case SETUP_CONNECTING: | ||
| 788 | render_wifi_setup_connecting(); | ||
| 789 | break; | ||
| 790 | case SETUP_SUCCESS: | ||
| 791 | render_wifi_setup_result(); | ||
| 792 | vTaskDelay(pdMS_TO_TICKS(3000)); | ||
| 793 | wifi_setup_handle_cancel(&s_wifi_setup); | ||
| 794 | s_wifi_setup_active = false; | ||
| 795 | exit_wifi_setup_rotation(); | ||
| 796 | s_state = DISPLAY_READY; | ||
| 797 | break; | ||
| 798 | case SETUP_FAILED: | ||
| 799 | render_wifi_setup_result(); | ||
| 800 | break; | ||
| 801 | default: | ||
| 802 | break; | ||
| 803 | } | ||
| 804 | break; | 384 | break; |
| 805 | } | 385 | } |
| 806 | 386 | ||
| @@ -820,14 +400,6 @@ esp_err_t display_init(void) { | |||
| 820 | s_initialized = true; | 400 | s_initialized = true; |
| 821 | s_last_qr_switch = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS; | 401 | s_last_qr_switch = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS; |
| 822 | 402 | ||
| 823 | esp_err_t touch_ret = touch_init(); | ||
| 824 | if (touch_ret == ESP_OK) { | ||
| 825 | s_touch_initialized = true; | ||
| 826 | ESP_LOGI(TAG, "Touch controller initialized"); | ||
| 827 | } else { | ||
| 828 | ESP_LOGW(TAG, "Touch init failed (non-fatal): %s", esp_err_to_name(touch_ret)); | ||
| 829 | } | ||
| 830 | |||
| 831 | xTaskCreatePinnedToCore(display_task, "display", 24576, NULL, 2, NULL, 1); | 403 | xTaskCreatePinnedToCore(display_task, "display", 24576, NULL, 2, NULL, 1); |
| 832 | 404 | ||
| 833 | ESP_LOGI(TAG, "Display initialized"); | 405 | ESP_LOGI(TAG, "Display initialized"); |
| @@ -871,26 +443,8 @@ void display_notify_payment(int amount_sats, int64_t allotment_ms) { | |||
| 871 | } | 443 | } |
| 872 | 444 | ||
| 873 | void display_notify_wifi_connected(const char *ip) { | 445 | void display_notify_wifi_connected(const char *ip) { |
| 874 | if (s_wifi_setup_active && s_wifi_setup.state == SETUP_CONNECTING) { | 446 | (void)ip; |
| 875 | wifi_setup_handle_connect_result(&s_wifi_setup, true, ip); | ||
| 876 | if (s_kb_state.cursor > 0) { | ||
| 877 | tollgate_config_add_wifi(s_wifi_setup.selected_ssid, s_kb_state.input); | ||
| 878 | } | ||
| 879 | } | ||
| 880 | } | 447 | } |
| 881 | 448 | ||
| 882 | void display_notify_wifi_disconnected(void) { | 449 | void display_notify_wifi_disconnected(void) { |
| 883 | if (s_wifi_setup_active && s_wifi_setup.state == SETUP_CONNECTING) { | ||
| 884 | wifi_setup_handle_connect_result(&s_wifi_setup, false, NULL); | ||
| 885 | } | ||
| 886 | } | ||
| 887 | |||
| 888 | void display_enter_wifi_setup(void) { | ||
| 889 | if (!s_initialized) return; | ||
| 890 | enter_wifi_setup_rotation(); | ||
| 891 | s_wifi_setup_active = true; | ||
| 892 | wifi_setup_init(&s_wifi_setup); | ||
| 893 | kb_state_init(&s_kb_state); | ||
| 894 | s_wifi_scan_pending = true; | ||
| 895 | s_state = DISPLAY_WIFI_SETUP; | ||
| 896 | } | 450 | } |