diff options
| author | Your Name <you@example.com> | 2026-05-20 02:14:45 +0530 |
|---|---|---|
| committer | Your Name <you@example.com> | 2026-05-20 02:14:45 +0530 |
| commit | 899016795c389151e6b486ec470653f5688e5c5f (patch) | |
| tree | 64b4c732d6f585552fd14df5f47890764c66ac1a /main/display.c | |
| parent | 4f713120dbedd37a3512c2ec1af0766025a59d57 (diff) | |
feat: merge display-fix — touch driver, keyboard, WiFi setup UI
Squash-merge of feature/display-fix (27 commits):
- AXS15231B touch driver with coordinate parsing (touch.c/h)
- On-screen keyboard with layout/hit detection (keyboard.c/h)
- WiFi setup state machine + config_add_wifi (wifi_setup.c/h)
- Web-based WiFi setup via captive portal
- Display rotation fix (stride=480), color fixes, DMA byte-swap
- WiFi QR code on BOOT and ERROR screens
- ERROR state transition after WiFi retries exhausted
- Unit tests: test_touch, test_keyboard, test_wifi_setup
- Integration test: wifi_setup.mjs
- E2E test: wifi-setup.spec.mjs
- Per-board hardware locks for flash targets
Conflicts resolved: took master for config/cvm/api/main (more current),
took display-fix for display/axs15231b (newer fixes).
Diffstat (limited to 'main/display.c')
| -rw-r--r-- | main/display.c | 349 |
1 files changed, 280 insertions, 69 deletions
diff --git a/main/display.c b/main/display.c index 2b6cc88..ccd08b7 100644 --- a/main/display.c +++ b/main/display.c | |||
| @@ -2,7 +2,10 @@ | |||
| 2 | #include "axs15231b.h" | 2 | #include "axs15231b.h" |
| 3 | #include "qrcoded.h" | 3 | #include "qrcoded.h" |
| 4 | #include "font.h" | 4 | #include "font.h" |
| 5 | #include "nucula_wallet.h" | ||
| 6 | #include "config.h" | ||
| 5 | #include "esp_log.h" | 7 | #include "esp_log.h" |
| 8 | #include "esp_wifi.h" | ||
| 6 | #include "freertos/FreeRTOS.h" | 9 | #include "freertos/FreeRTOS.h" |
| 7 | #include "freertos/task.h" | 10 | #include "freertos/task.h" |
| 8 | #include <string.h> | 11 | #include <string.h> |
| @@ -12,15 +15,36 @@ | |||
| 12 | static const char *TAG = "display"; | 15 | static const char *TAG = "display"; |
| 13 | 16 | ||
| 14 | #define QR_CYCLE_MS 5000 | 17 | #define QR_CYCLE_MS 5000 |
| 18 | #define RENDER_INTERVAL_MS 2000 | ||
| 19 | |||
| 20 | #define COLOR_BG 0x0000 | ||
| 21 | #define COLOR_WHITE 0xFFFF | ||
| 22 | #define COLOR_CYAN 0x07FF | ||
| 23 | #define COLOR_YELLOW 0xFFE0 | ||
| 24 | #define COLOR_GREEN 0x07E0 | ||
| 25 | #define COLOR_ORANGE 0xFD20 | ||
| 26 | #define COLOR_RED 0xF800 | ||
| 27 | #define COLOR_DIM 0x8410 | ||
| 15 | 28 | ||
| 16 | static volatile display_state_t s_state = DISPLAY_BOOT; | 29 | static volatile display_state_t s_state = DISPLAY_BOOT; |
| 17 | static char s_ap_ssid[32] = ""; | 30 | static char s_ap_ssid[32] = ""; |
| 18 | static char s_portal_url[256] = ""; | 31 | static char s_portal_url[256] = ""; |
| 32 | static char s_mint_url[256] = ""; | ||
| 33 | static char s_wifi_status[32] = "starting..."; | ||
| 19 | static int s_active_clients = 0; | 34 | static int s_active_clients = 0; |
| 20 | static uint64_t s_wallet_balance = 0; | 35 | static uint64_t s_wallet_balance = 0; |
| 36 | static int s_price_per_step = 0; | ||
| 21 | static bool s_initialized = false; | 37 | static bool s_initialized = false; |
| 22 | static int64_t s_last_qr_switch = 0; | 38 | static int64_t s_last_qr_switch = 0; |
| 23 | static display_qr_mode_t s_qr_mode = DISPLAY_QR_WIFI; | 39 | static display_qr_mode_t s_qr_mode = DISPLAY_QR_WIFI; |
| 40 | static int s_last_payment_sats = 0; | ||
| 41 | static int64_t s_last_allotment_ms = 0; | ||
| 42 | |||
| 43 | static uint16_t wallet_color(void) { | ||
| 44 | if (s_wallet_balance == 0) return COLOR_RED; | ||
| 45 | if (s_wallet_balance < 100) return COLOR_YELLOW; | ||
| 46 | return COLOR_GREEN; | ||
| 47 | } | ||
| 24 | 48 | ||
| 25 | static int qr_version_from_strlen(int len) { | 49 | static int qr_version_from_strlen(int len) { |
| 26 | if (len <= 17) return 1; | 50 | if (len <= 17) return 1; |
| @@ -59,10 +83,60 @@ static int escape_wifi_field(const char *src, char *dst, int dst_size) { | |||
| 59 | return di; | 83 | return di; |
| 60 | } | 84 | } |
| 61 | 85 | ||
| 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 | |||
| 62 | static void build_wifi_qr_string(char *out, int out_size) { | 96 | static void build_wifi_qr_string(char *out, int out_size) { |
| 63 | char escaped_ssid[64]; | 97 | char escaped_ssid[64]; |
| 64 | escape_wifi_field(s_ap_ssid, escaped_ssid, sizeof(escaped_ssid)); | 98 | escape_wifi_field(s_ap_ssid, escaped_ssid, sizeof(escaped_ssid)); |
| 65 | snprintf(out, out_size, "WIFI:S:%s;T:nopass;;", escaped_ssid); | 99 | const tollgate_config_t *cfg = tollgate_config_get(); |
| 100 | if (strlen(cfg->ap_password) > 0) { | ||
| 101 | char escaped_pass[128]; | ||
| 102 | escape_wifi_field(cfg->ap_password, escaped_pass, sizeof(escaped_pass)); | ||
| 103 | snprintf(out, out_size, "WIFI:S:%s;T:WPA;P:%s;;", escaped_ssid, escaped_pass); | ||
| 104 | } else { | ||
| 105 | snprintf(out, out_size, "WIFI:S:%s;T:nopass;;", escaped_ssid); | ||
| 106 | } | ||
| 107 | } | ||
| 108 | |||
| 109 | static void render_qr_at(const char *text, int x_off, int y_off, int max_w, int max_h) { | ||
| 110 | int len = strlen(text); | ||
| 111 | int version = qr_version_from_strlen(len); | ||
| 112 | int px = qr_pixel_size(len); | ||
| 113 | |||
| 114 | uint16_t buf_size = qrcode_getBufferSize(version); | ||
| 115 | uint8_t *qr_buf = (uint8_t *)malloc(buf_size); | ||
| 116 | if (!qr_buf) return; | ||
| 117 | |||
| 118 | QRCode qrcode; | ||
| 119 | if (qrcode_initText(&qrcode, qr_buf, version, ECC_LOW, text) != 0) { | ||
| 120 | free(qr_buf); | ||
| 121 | return; | ||
| 122 | } | ||
| 123 | |||
| 124 | int qr_px_w = qrcode.size * px; | ||
| 125 | int qr_px_h = qrcode.size * px; | ||
| 126 | int cx = x_off + (max_w - qr_px_w) / 2; | ||
| 127 | int cy = y_off + (max_h - qr_px_h) / 2; | ||
| 128 | if (cx < 0) cx = 0; | ||
| 129 | if (cy < 0) cy = 0; | ||
| 130 | |||
| 131 | for (int y = 0; y < qrcode.size; y++) { | ||
| 132 | for (int x = 0; x < qrcode.size; x++) { | ||
| 133 | bool mod = qrcode_getModule(&qrcode, x, y); | ||
| 134 | uint16_t color = mod ? COLOR_WHITE : COLOR_BG; | ||
| 135 | axs15231b_fill_rect(cx + x * px, cy + y * px, px, px, color); | ||
| 136 | } | ||
| 137 | } | ||
| 138 | |||
| 139 | free(qr_buf); | ||
| 66 | } | 140 | } |
| 67 | 141 | ||
| 68 | 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) { |
| @@ -98,99 +172,206 @@ void display_render_text(int x, int y, const char *text, uint16_t fg, uint16_t b | |||
| 98 | } | 172 | } |
| 99 | } | 173 | } |
| 100 | 174 | ||
| 101 | static void render_qr_at(const char *text, int x_off, int y_off, int max_w, int max_h) { | ||
| 102 | int len = strlen(text); | ||
| 103 | int version = qr_version_from_strlen(len); | ||
| 104 | int px = qr_pixel_size(len); | ||
| 105 | |||
| 106 | uint16_t buf_size = qrcode_getBufferSize(version); | ||
| 107 | uint8_t *qr_buf = (uint8_t *)malloc(buf_size); | ||
| 108 | if (!qr_buf) { | ||
| 109 | ESP_LOGE(TAG, "Failed to allocate QR buffer"); | ||
| 110 | return; | ||
| 111 | } | ||
| 112 | |||
| 113 | QRCode qr; | ||
| 114 | if (qrcode_initText(&qr, qr_buf, version, ECC_LOW, text) != 0) { | ||
| 115 | ESP_LOGE(TAG, "QR generation failed"); | ||
| 116 | free(qr_buf); | ||
| 117 | return; | ||
| 118 | } | ||
| 119 | |||
| 120 | int qr_px_w = qr.size * px; | ||
| 121 | int qr_px_h = qr.size * px; | ||
| 122 | int cx = x_off + (max_w - qr_px_w) / 2; | ||
| 123 | int cy = y_off + (max_h - qr_px_h) / 2; | ||
| 124 | if (cx < 0) cx = 0; | ||
| 125 | if (cy < 0) cy = 0; | ||
| 126 | |||
| 127 | for (int y = 0; y < qr.size; y++) { | ||
| 128 | for (int x = 0; x < qr.size; x++) { | ||
| 129 | bool mod = qrcode_getModule(&qr, x, y); | ||
| 130 | uint16_t color = mod ? 0xFFFF : 0x0000; | ||
| 131 | axs15231b_fill_rect(cx + x * px, cy + y * px, px, px, color); | ||
| 132 | } | ||
| 133 | } | ||
| 134 | |||
| 135 | free(qr_buf); | ||
| 136 | } | ||
| 137 | |||
| 138 | void display_render_qr(const char *text) { | 175 | void display_render_qr(const char *text) { |
| 139 | int screen_w = axs15231b_get_width(); | 176 | int screen_w = axs15231b_get_width(); |
| 140 | int screen_h = axs15231b_get_height(); | 177 | int screen_h = axs15231b_get_height(); |
| 141 | axs15231b_fill_screen(0x0000); | 178 | axs15231b_fill_screen(COLOR_BG); |
| 142 | render_qr_at(text, 0, 0, screen_w, screen_h); | 179 | render_qr_at(text, 0, 0, screen_w, screen_h); |
| 143 | axs15231b_flush(); | 180 | axs15231b_flush(); |
| 144 | } | 181 | } |
| 145 | 182 | ||
| 146 | static void render_boot_screen(void) { | 183 | static void render_boot_screen(void) { |
| 147 | axs15231b_fill_screen(0x0000); | 184 | int screen_w = axs15231b_get_width(); |
| 148 | display_render_text(140, 100, "TollGate", 0xF79F, 0x0000, 3); | 185 | axs15231b_fill_screen(COLOR_BG); |
| 149 | display_render_text(140, 140, "starting...", 0xB5B6, 0x0000, 2); | 186 | |
| 187 | char qr_text[320]; | ||
| 188 | build_wifi_qr_string(qr_text, sizeof(qr_text)); | ||
| 189 | render_qr_at(qr_text, 0, 10, screen_w, 220); | ||
| 190 | |||
| 191 | const char *title = "TollGate"; | ||
| 192 | int title_w = strlen(title) * 8 * 2; | ||
| 193 | display_render_text((screen_w - title_w) / 2, 240, title, COLOR_CYAN, COLOR_BG, 2); | ||
| 194 | |||
| 195 | int status_w = strlen(s_wifi_status) * 8; | ||
| 196 | display_render_text((screen_w - status_w) / 2, 268, s_wifi_status, COLOR_YELLOW, COLOR_BG, 1); | ||
| 197 | |||
| 198 | snprintf(qr_text, sizeof(qr_text), "SSID: %s", s_ap_ssid); | ||
| 199 | int ssid_w = strlen(qr_text) * 8; | ||
| 200 | display_render_text((screen_w - ssid_w) / 2, 295, qr_text, COLOR_DIM, COLOR_BG, 1); | ||
| 201 | |||
| 202 | const char *hint = "Scan QR to connect"; | ||
| 203 | int hint_w = strlen(hint) * 8; | ||
| 204 | display_render_text((screen_w - hint_w) / 2, 315, hint, COLOR_DIM, COLOR_BG, 1); | ||
| 205 | |||
| 150 | axs15231b_flush(); | 206 | axs15231b_flush(); |
| 151 | } | 207 | } |
| 152 | 208 | ||
| 153 | static void render_ready_screen(void) { | 209 | static void render_ready_screen(void) { |
| 154 | axs15231b_fill_screen(0x0000); | ||
| 155 | |||
| 156 | int screen_w = axs15231b_get_width(); | 210 | int screen_w = axs15231b_get_width(); |
| 157 | int screen_h = axs15231b_get_height(); | 211 | int text_area_y = 330; |
| 158 | int text_area_y = screen_h - 55; | 212 | axs15231b_fill_screen(COLOR_BG); |
| 159 | 213 | ||
| 160 | char qr_text[320]; | 214 | char qr_text[320]; |
| 161 | const char *label; | ||
| 162 | |||
| 163 | if (s_qr_mode == DISPLAY_QR_WIFI) { | 215 | if (s_qr_mode == DISPLAY_QR_WIFI) { |
| 164 | build_wifi_qr_string(qr_text, sizeof(qr_text)); | 216 | build_wifi_qr_string(qr_text, sizeof(qr_text)); |
| 165 | label = "Scan to connect"; | ||
| 166 | } else { | 217 | } else { |
| 167 | strncpy(qr_text, s_portal_url, sizeof(qr_text) - 1); | 218 | strncpy(qr_text, s_portal_url, sizeof(qr_text) - 1); |
| 168 | qr_text[sizeof(qr_text) - 1] = '\0'; | 219 | qr_text[sizeof(qr_text) - 1] = '\0'; |
| 169 | label = "Portal URL"; | ||
| 170 | } | 220 | } |
| 171 | 221 | ||
| 172 | render_qr_at(qr_text, 0, 0, screen_w, text_area_y - 5); | 222 | render_qr_at(qr_text, 0, 5, screen_w, text_area_y - 10); |
| 223 | |||
| 224 | int y = text_area_y; | ||
| 225 | char line[48]; | ||
| 226 | |||
| 227 | if (s_qr_mode == DISPLAY_QR_WIFI) { | ||
| 228 | snprintf(line, sizeof(line), "Scan to connect"); | ||
| 229 | display_render_text(10, y, line, COLOR_CYAN, COLOR_BG, 1); | ||
| 230 | y += 16; | ||
| 231 | |||
| 232 | snprintf(line, sizeof(line), "SSID: %s", s_ap_ssid); | ||
| 233 | display_render_text(10, y, line, COLOR_WHITE, COLOR_BG, 1); | ||
| 234 | y += 16; | ||
| 235 | } else { | ||
| 236 | snprintf(line, sizeof(line), "Portal URL"); | ||
| 237 | display_render_text(10, y, line, COLOR_CYAN, COLOR_BG, 1); | ||
| 238 | y += 16; | ||
| 239 | |||
| 240 | char domain[48]; | ||
| 241 | extract_domain(s_mint_url, domain, sizeof(domain)); | ||
| 242 | snprintf(line, sizeof(line), "Mint: %.30s", domain); | ||
| 243 | display_render_text(10, y, line, COLOR_ORANGE, COLOR_BG, 1); | ||
| 244 | y += 16; | ||
| 245 | } | ||
| 246 | |||
| 247 | snprintf(line, sizeof(line), "%d sats/min", s_price_per_step); | ||
| 248 | display_render_text(10, y, line, COLOR_ORANGE, COLOR_BG, 1); | ||
| 249 | y += 16; | ||
| 250 | |||
| 251 | snprintf(line, sizeof(line), "Wallet: %llu sats", (unsigned long long)s_wallet_balance); | ||
| 252 | display_render_text(10, y, line, wallet_color(), COLOR_BG, 1); | ||
| 253 | y += 16; | ||
| 254 | |||
| 255 | if (s_active_clients > 0) { | ||
| 256 | snprintf(line, sizeof(line), "Clients: %d", s_active_clients); | ||
| 257 | display_render_text(10, y, line, COLOR_GREEN, COLOR_BG, 1); | ||
| 258 | } | ||
| 259 | |||
| 260 | axs15231b_flush(); | ||
| 261 | } | ||
| 262 | |||
| 263 | static void render_setup_pending_screen(void) { | ||
| 264 | int screen_w = axs15231b_get_width(); | ||
| 265 | axs15231b_fill_screen(COLOR_BG); | ||
| 173 | 266 | ||
| 174 | display_render_text(10, text_area_y, label, 0xB5B6, 0x0000, 2); | 267 | char qr_text[320]; |
| 268 | build_wifi_qr_string(qr_text, sizeof(qr_text)); | ||
| 269 | render_qr_at(qr_text, 0, 5, screen_w, 280); | ||
| 175 | 270 | ||
| 271 | int y = 290; | ||
| 176 | char line[64]; | 272 | char line[64]; |
| 273 | |||
| 274 | const char *title = "WiFi Setup"; | ||
| 275 | int tw = strlen(title) * 8; | ||
| 276 | display_render_text((screen_w - tw) / 2, y, title, COLOR_CYAN, COLOR_BG, 1); | ||
| 277 | y += 20; | ||
| 278 | |||
| 177 | snprintf(line, sizeof(line), "SSID: %s", s_ap_ssid); | 279 | snprintf(line, sizeof(line), "SSID: %s", s_ap_ssid); |
| 178 | display_render_text(10, text_area_y + 20, line, 0xB5B6, 0x0000, 2); | 280 | display_render_text(10, y, line, COLOR_WHITE, COLOR_BG, 1); |
| 281 | y += 18; | ||
| 282 | |||
| 283 | const char *hint1 = "1. Connect to WiFi above"; | ||
| 284 | display_render_text(10, y, hint1, COLOR_DIM, COLOR_BG, 1); | ||
| 285 | y += 16; | ||
| 286 | |||
| 287 | const char *hint2 = "2. Open browser, go to:"; | ||
| 288 | display_render_text(10, y, hint2, COLOR_DIM, COLOR_BG, 1); | ||
| 289 | y += 18; | ||
| 290 | |||
| 291 | const tollgate_config_t *cfg = tollgate_config_get(); | ||
| 292 | snprintf(line, sizeof(line), "http://%s/setup", cfg->ap_ip_str); | ||
| 293 | display_render_text(10, y, line, COLOR_YELLOW, COLOR_BG, 1); | ||
| 294 | y += 22; | ||
| 295 | |||
| 296 | const char *hint3 = "3. Configure upstream WiFi"; | ||
| 297 | display_render_text(10, y, hint3, COLOR_DIM, COLOR_BG, 1); | ||
| 179 | 298 | ||
| 180 | axs15231b_flush(); | 299 | axs15231b_flush(); |
| 181 | } | 300 | } |
| 182 | 301 | ||
| 183 | static void render_payment_screen(void) { | 302 | static void render_payment_screen(void) { |
| 184 | axs15231b_fill_screen(0x07E0); | 303 | int screen_w = axs15231b_get_width(); |
| 185 | display_render_text(140, 100, "Paid!", 0x0000, 0x07E0, 3); | 304 | axs15231b_fill_screen(COLOR_BG); |
| 186 | display_render_text(130, 140, "Access granted", 0x0000, 0x07E0, 2); | 305 | |
| 306 | axs15231b_fill_rect(0, 190, screen_w, 50, COLOR_GREEN); | ||
| 307 | const char *msg = "ACCESS GRANTED"; | ||
| 308 | int msg_w = strlen(msg) * 8 * 2; | ||
| 309 | display_render_text((screen_w - msg_w) / 2, 202, msg, COLOR_WHITE, COLOR_GREEN, 2); | ||
| 310 | |||
| 311 | char line[48]; | ||
| 312 | |||
| 313 | snprintf(line, sizeof(line), "Paid: %d sats", s_last_payment_sats); | ||
| 314 | int lw = strlen(line) * 8; | ||
| 315 | display_render_text((screen_w - lw) / 2, 270, line, COLOR_WHITE, COLOR_BG, 1); | ||
| 316 | |||
| 317 | int64_t secs = s_last_allotment_ms / 1000; | ||
| 318 | if (secs >= 60) { | ||
| 319 | snprintf(line, sizeof(line), "Time: %lld min", (long long)(secs / 60)); | ||
| 320 | } else { | ||
| 321 | snprintf(line, sizeof(line), "Time: %lld sec", (long long)secs); | ||
| 322 | } | ||
| 323 | lw = strlen(line) * 8; | ||
| 324 | display_render_text((screen_w - lw) / 2, 290, line, COLOR_WHITE, COLOR_BG, 1); | ||
| 325 | |||
| 326 | snprintf(line, sizeof(line), "Wallet: %llu sats", (unsigned long long)s_wallet_balance); | ||
| 327 | lw = strlen(line) * 8; | ||
| 328 | display_render_text((screen_w - lw) / 2, 320, line, wallet_color(), COLOR_BG, 1); | ||
| 329 | |||
| 187 | axs15231b_flush(); | 330 | axs15231b_flush(); |
| 188 | } | 331 | } |
| 189 | 332 | ||
| 190 | static void render_error_screen(void) { | 333 | static void render_error_screen(void) { |
| 191 | axs15231b_fill_screen(0xF800); | 334 | int screen_w = axs15231b_get_width(); |
| 192 | display_render_text(120, 100, "No upstream", 0xFFFF, 0xF800, 3); | 335 | axs15231b_fill_screen(COLOR_BG); |
| 193 | display_render_text(130, 140, "Check config", 0xFFFF, 0xF800, 2); | 336 | |
| 337 | char qr_text[320]; | ||
| 338 | build_wifi_qr_string(qr_text, sizeof(qr_text)); | ||
| 339 | render_qr_at(qr_text, 0, 5, screen_w, 150); | ||
| 340 | |||
| 341 | axs15231b_fill_rect(0, 160, screen_w, 36, COLOR_RED); | ||
| 342 | const char *msg = "NO UPSTREAM"; | ||
| 343 | int msg_w = strlen(msg) * 8 * 2; | ||
| 344 | display_render_text((screen_w - msg_w) / 2, 170, msg, COLOR_WHITE, COLOR_RED, 2); | ||
| 345 | |||
| 346 | char line[64]; | ||
| 347 | int lw; | ||
| 348 | int y = 210; | ||
| 349 | |||
| 350 | const char *l1 = "Internet unavailable"; | ||
| 351 | lw = strlen(l1) * 8; | ||
| 352 | display_render_text((screen_w - lw) / 2, y, l1, COLOR_WHITE, COLOR_BG, 1); | ||
| 353 | y += 20; | ||
| 354 | |||
| 355 | const char *l3 = "AP still active"; | ||
| 356 | lw = strlen(l3) * 8; | ||
| 357 | display_render_text((screen_w - lw) / 2, y, l3, COLOR_GREEN, COLOR_BG, 1); | ||
| 358 | y += 20; | ||
| 359 | |||
| 360 | snprintf(line, sizeof(line), "SSID: %s", s_ap_ssid); | ||
| 361 | lw = strlen(line) * 8; | ||
| 362 | display_render_text((screen_w - lw) / 2, y, line, COLOR_DIM, COLOR_BG, 1); | ||
| 363 | y += 20; | ||
| 364 | |||
| 365 | const tollgate_config_t *cfg = tollgate_config_get(); | ||
| 366 | snprintf(line, sizeof(line), "http://%s/setup", cfg->ap_ip_str); | ||
| 367 | lw = strlen(line) * 8; | ||
| 368 | display_render_text((screen_w - lw) / 2, y, line, COLOR_YELLOW, COLOR_BG, 1); | ||
| 369 | y += 16; | ||
| 370 | |||
| 371 | const char *hint = "Scan QR to connect"; | ||
| 372 | lw = strlen(hint) * 8; | ||
| 373 | display_render_text((screen_w - lw) / 2, y, hint, COLOR_DIM, COLOR_BG, 1); | ||
| 374 | |||
| 194 | axs15231b_flush(); | 375 | axs15231b_flush(); |
| 195 | } | 376 | } |
| 196 | 377 | ||
| @@ -200,6 +381,14 @@ static void display_task(void *pvParameters) { | |||
| 200 | while (1) { | 381 | while (1) { |
| 201 | display_state_t state = s_state; | 382 | display_state_t state = s_state; |
| 202 | 383 | ||
| 384 | if (state == DISPLAY_READY) { | ||
| 385 | int64_t now = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS; | ||
| 386 | if ((now - s_last_qr_switch) >= QR_CYCLE_MS) { | ||
| 387 | s_qr_mode = (s_qr_mode == DISPLAY_QR_WIFI) ? DISPLAY_QR_PORTAL : DISPLAY_QR_WIFI; | ||
| 388 | s_last_qr_switch = now; | ||
| 389 | } | ||
| 390 | } | ||
| 391 | |||
| 203 | switch (state) { | 392 | switch (state) { |
| 204 | case DISPLAY_BOOT: | 393 | case DISPLAY_BOOT: |
| 205 | render_boot_screen(); | 394 | render_boot_screen(); |
| @@ -209,21 +398,18 @@ static void display_task(void *pvParameters) { | |||
| 209 | break; | 398 | break; |
| 210 | case DISPLAY_PAYMENT_RECEIVED: | 399 | case DISPLAY_PAYMENT_RECEIVED: |
| 211 | render_payment_screen(); | 400 | render_payment_screen(); |
| 212 | vTaskDelay(pdMS_TO_TICKS(2000)); | 401 | vTaskDelay(pdMS_TO_TICKS(3000)); |
| 213 | s_state = DISPLAY_READY; | 402 | s_state = DISPLAY_READY; |
| 214 | break; | 403 | break; |
| 215 | case DISPLAY_ERROR: | 404 | case DISPLAY_ERROR: |
| 216 | render_error_screen(); | 405 | render_error_screen(); |
| 217 | break; | 406 | break; |
| 407 | case DISPLAY_SETUP_PENDING: | ||
| 408 | render_setup_pending_screen(); | ||
| 409 | break; | ||
| 218 | } | 410 | } |
| 219 | 411 | ||
| 220 | int64_t now = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS; | 412 | vTaskDelay(pdMS_TO_TICKS(RENDER_INTERVAL_MS)); |
| 221 | if (state == DISPLAY_READY && (now - s_last_qr_switch) >= QR_CYCLE_MS) { | ||
| 222 | s_qr_mode = (s_qr_mode == DISPLAY_QR_WIFI) ? DISPLAY_QR_PORTAL : DISPLAY_QR_WIFI; | ||
| 223 | s_last_qr_switch = now; | ||
| 224 | } | ||
| 225 | |||
| 226 | vTaskDelay(pdMS_TO_TICKS(1000)); | ||
| 227 | } | 413 | } |
| 228 | } | 414 | } |
| 229 | 415 | ||
| @@ -239,7 +425,7 @@ esp_err_t display_init(void) { | |||
| 239 | s_initialized = true; | 425 | s_initialized = true; |
| 240 | s_last_qr_switch = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS; | 426 | s_last_qr_switch = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS; |
| 241 | 427 | ||
| 242 | xTaskCreatePinnedToCore(display_task, "display", 16384, NULL, 2, NULL, 1); | 428 | xTaskCreatePinnedToCore(display_task, "display", 24576, NULL, 2, NULL, 1); |
| 243 | 429 | ||
| 244 | ESP_LOGI(TAG, "Display initialized"); | 430 | ESP_LOGI(TAG, "Display initialized"); |
| 245 | return ESP_OK; | 431 | return ESP_OK; |
| @@ -250,7 +436,9 @@ void display_set_state(display_state_t state) { | |||
| 250 | } | 436 | } |
| 251 | 437 | ||
| 252 | void display_update(const char *ap_ssid, int active_clients, | 438 | void display_update(const char *ap_ssid, int active_clients, |
| 253 | uint64_t wallet_balance, const char *portal_url) { | 439 | uint64_t wallet_balance, const char *portal_url, |
| 440 | const char *mint_url, int price_per_step, | ||
| 441 | const char *wifi_status) { | ||
| 254 | if (ap_ssid) { | 442 | if (ap_ssid) { |
| 255 | strncpy(s_ap_ssid, ap_ssid, sizeof(s_ap_ssid) - 1); | 443 | strncpy(s_ap_ssid, ap_ssid, sizeof(s_ap_ssid) - 1); |
| 256 | s_ap_ssid[sizeof(s_ap_ssid) - 1] = '\0'; | 444 | s_ap_ssid[sizeof(s_ap_ssid) - 1] = '\0'; |
| @@ -259,6 +447,29 @@ void display_update(const char *ap_ssid, int active_clients, | |||
| 259 | strncpy(s_portal_url, portal_url, sizeof(s_portal_url) - 1); | 447 | strncpy(s_portal_url, portal_url, sizeof(s_portal_url) - 1); |
| 260 | s_portal_url[sizeof(s_portal_url) - 1] = '\0'; | 448 | s_portal_url[sizeof(s_portal_url) - 1] = '\0'; |
| 261 | } | 449 | } |
| 450 | if (mint_url) { | ||
| 451 | strncpy(s_mint_url, mint_url, sizeof(s_mint_url) - 1); | ||
| 452 | s_mint_url[sizeof(s_mint_url) - 1] = '\0'; | ||
| 453 | } | ||
| 454 | if (wifi_status) { | ||
| 455 | strncpy(s_wifi_status, wifi_status, sizeof(s_wifi_status) - 1); | ||
| 456 | s_wifi_status[sizeof(s_wifi_status) - 1] = '\0'; | ||
| 457 | } | ||
| 458 | if (price_per_step > 0) s_price_per_step = price_per_step; | ||
| 262 | s_active_clients = active_clients; | 459 | s_active_clients = active_clients; |
| 263 | s_wallet_balance = wallet_balance; | 460 | s_wallet_balance = wallet_balance; |
| 264 | } | 461 | } |
| 462 | |||
| 463 | void display_notify_payment(int amount_sats, int64_t allotment_ms) { | ||
| 464 | s_last_payment_sats = amount_sats; | ||
| 465 | s_last_allotment_ms = allotment_ms; | ||
| 466 | s_wallet_balance = nucula_wallet_balance(); | ||
| 467 | display_set_state(DISPLAY_PAYMENT_RECEIVED); | ||
| 468 | } | ||
| 469 | |||
| 470 | void display_notify_wifi_connected(const char *ip) { | ||
| 471 | (void)ip; | ||
| 472 | } | ||
| 473 | |||
| 474 | void display_notify_wifi_disconnected(void) { | ||
| 475 | } | ||