diff options
| author | Your Name <you@example.com> | 2026-05-18 19:58:12 +0530 |
|---|---|---|
| committer | Your Name <you@example.com> | 2026-05-18 19:58:12 +0530 |
| commit | 7820837d79ebc8e00221b5206bdd8e3ca0ae4c15 (patch) | |
| tree | 98070e83e4e8bfa0b46009d202e7b81410c960f6 | |
| parent | 7dd532cee475d486552e886dd7173c8853f80b60 (diff) | |
Sync display with live TollGate state
- Periodic refresh: wallet balance + client count every 5s from main loop
- display_notify_payment() API: passes payment amount and time allotment
- tollgate_api.c: triggers PAYMENT_RECEIVED state after wallet receive
- AP station events update client count in real-time
- Config data (price, mint) passed to display during boot phase
- Payment screen shows actual sats paid and time purchased
- Updated DISPLAY_UI_PLAN.md with state-sync audit and checklist
| -rw-r--r-- | DISPLAY_UI_PLAN.md | 171 | ||||
| -rw-r--r-- | main/display.c | 23 | ||||
| -rw-r--r-- | main/display.h | 1 | ||||
| -rw-r--r-- | main/tollgate_api.c | 3 | ||||
| -rw-r--r-- | main/tollgate_main.c | 13 |
5 files changed, 136 insertions, 75 deletions
diff --git a/DISPLAY_UI_PLAN.md b/DISPLAY_UI_PLAN.md index 3050b8a..e78db44 100644 --- a/DISPLAY_UI_PLAN.md +++ b/DISPLAY_UI_PLAN.md | |||
| @@ -18,7 +18,6 @@ | |||
| 18 | | Orange | `0xFD20` | Accent (Bitcoin orange) | | 18 | | Orange | `0xFD20` | Accent (Bitcoin orange) | |
| 19 | | Red | `0xF800` | Errors, alerts | | 19 | | Red | `0xF800` | Errors, alerts | |
| 20 | | Dim gray | `0x8410` | Secondary info | | 20 | | Dim gray | `0x8410` | Secondary info | |
| 21 | | Dark bg | `0x2104` | Card backgrounds | | ||
| 22 | 21 | ||
| 23 | ## Screen States | 22 | ## Screen States |
| 24 | 23 | ||
| @@ -26,111 +25,141 @@ | |||
| 26 | Shown during startup until WiFi connects and services start. | 25 | Shown during startup until WiFi connects and services start. |
| 27 | 26 | ||
| 28 | ``` | 27 | ``` |
| 29 | ┌──────────────────────────┐ 0 | 28 | ┌──────────────────────────┐ |
| 30 | │ │ | 29 | │ TollGate │ cyan, scale 2 |
| 31 | │ TollGate │ y=180, cyan, scale 2 | 30 | │ connecting... │ yellow, scale 1 |
| 32 | │ connecting... │ y=205, yellow, scale 1 | 31 | │ WiFi: trying... │ dim, scale 1 |
| 33 | │ │ | 32 | └──────────────────────────┘ |
| 34 | │ WiFi: trying... │ y=260, dim, scale 1 | ||
| 35 | │ │ | ||
| 36 | └──────────────────────────┘ 479 | ||
| 37 | ``` | 33 | ``` |
| 38 | 34 | ||
| 39 | WiFi status line shows: "trying...", "connected!", "failed (retry)" | 35 | ### 2. READY — QR Cycling |
| 40 | |||
| 41 | ### 2. READY — QR Cycling (primary screen) | ||
| 42 | Cycles every 5 seconds between WiFi QR and Portal QR. | 36 | Cycles every 5 seconds between WiFi QR and Portal QR. |
| 43 | 37 | ||
| 44 | **View A — WiFi QR:** | 38 | **View A — WiFi QR:** |
| 45 | ``` | 39 | ``` |
| 46 | ┌──────────────────────────┐ 0 | 40 | ┌──────────────────────────┐ |
| 47 | │ ┌──────┐ │ | 41 | │ ┌──────┐ │ |
| 48 | │ │ QR │ │ QR: WIFI:S:<ssid>;T:nopass;; | 42 | │ │ QR │ │ WIFI:S:<ssid>;T:nopass;; |
| 49 | │ │ │ │ Centered in top 2/3 of screen | ||
| 50 | │ └──────┘ │ | 43 | │ └──────┘ │ |
| 51 | │ │ ~y=320 | 44 | │ Scan to connect │ cyan |
| 52 | │ Scan to connect │ cyan, scale 1 | 45 | │ SSID: TollGate-XXXX │ white |
| 53 | │ SSID: TollGate-XXXX │ white, scale 1 | 46 | │ 21 sats/min │ orange |
| 54 | │ 21 sats/min │ orange, scale 1 | 47 | │ Wallet: 420 sats │ green/yellow/red |
| 55 | │ Wallet: 420 sats │ green, scale 1 | 48 | └──────────────────────────┘ |
| 56 | └──────────────────────────┘ 479 | ||
| 57 | ``` | 49 | ``` |
| 58 | 50 | ||
| 59 | **View B — Portal QR:** | 51 | **View B — Portal QR:** |
| 60 | ``` | 52 | ``` |
| 61 | ┌──────────────────────────┐ 0 | 53 | ┌──────────────────────────┐ |
| 62 | │ ┌──────┐ │ | 54 | │ ┌──────┐ │ |
| 63 | │ │ QR │ │ QR: http://10.x.x.x/ | 55 | │ │ QR │ │ http://10.x.x.x/ |
| 64 | │ │ │ │ | ||
| 65 | │ └──────┘ │ | 56 | │ └──────┘ │ |
| 66 | │ │ ~y=320 | 57 | │ Portal URL │ cyan |
| 67 | │ Portal URL │ cyan, scale 1 | 58 | │ Mint: testnut... │ orange |
| 68 | │ testnut.cashu.space │ orange, scale 1 (mint domain) | 59 | │ 21 sats/min │ orange |
| 69 | │ 21 sats/min │ yellow, scale 1 | 60 | │ Clients: 3 │ green |
| 70 | │ Clients: 3 │ green, scale 1 | 61 | └──────────────────────────┘ |
| 71 | └──────────────────────────┘ 479 | ||
| 72 | ``` | 62 | ``` |
| 73 | 63 | ||
| 74 | ### 3. PAYMENT_RECEIVED | 64 | ### 3. PAYMENT_RECEIVED |
| 75 | Shows for 3 seconds after payment, then returns to READY. | 65 | Shows for 3 seconds after payment, then returns to READY. |
| 76 | 66 | ||
| 77 | ``` | 67 | ``` |
| 78 | ┌──────────────────────────┐ 0 | 68 | ┌──────────────────────────┐ |
| 79 | │ │ | 69 | │ ████████████████████ │ green bar |
| 80 | │ ████████████████████ │ green filled bar, y=190..230 | ||
| 81 | │ ACCESS GRANTED │ white on green, scale 2 | 70 | │ ACCESS GRANTED │ white on green, scale 2 |
| 82 | │ ████████████████████ │ | 71 | │ ████████████████████ │ |
| 83 | │ │ | 72 | │ Paid: 42 sats │ white |
| 84 | │ Paid: 21 sats │ white, scale 1 | 73 | │ Time: 2 min │ white |
| 85 | │ Time: 1 min │ white, scale 1 | 74 | │ Wallet: 462 sats │ green |
| 86 | │ │ | 75 | └──────────────────────────┘ |
| 87 | │ Wallet: 441 sats │ green, scale 1 | ||
| 88 | └──────────────────────────┘ 479 | ||
| 89 | ``` | 76 | ``` |
| 90 | 77 | ||
| 91 | ### 4. ERROR | 78 | ### 4. ERROR |
| 92 | Shown when upstream WiFi is disconnected. | 79 | Shown when upstream WiFi is disconnected. |
| 93 | 80 | ||
| 94 | ``` | 81 | ``` |
| 95 | ┌──────────────────────────┐ 0 | 82 | ┌──────────────────────────┐ |
| 96 | │ ████████████████████ │ red filled bar, y=190..230 | 83 | │ ████████████████████ │ red bar |
| 97 | │ NO UPSTREAM │ white on red, scale 2 | 84 | │ NO UPSTREAM │ white on red, scale 2 |
| 98 | │ ████████████████████ │ | 85 | │ ████████████████████ │ |
| 99 | │ │ | 86 | │ Internet unavailable │ white |
| 100 | │ Internet unavailable │ white, scale 1 | 87 | │ Check WiFi config │ yellow |
| 101 | │ Check WiFi config │ yellow, scale 1 | 88 | │ AP still active │ green |
| 102 | │ │ | 89 | │ SSID: TollGate-XXXX │ dim |
| 103 | │ AP still active │ green, scale 1 | 90 | └──────────────────────────┘ |
| 104 | │ SSID: TollGate-XXXX │ dim, scale 1 | ||
| 105 | └──────────────────────────┘ 479 | ||
| 106 | ``` | 91 | ``` |
| 107 | 92 | ||
| 108 | ## Data Flow | 93 | ## State Synchronization |
| 109 | 94 | ||
| 110 | ### display_update() receives: | 95 | ### Data sources and update triggers |
| 111 | ```c | ||
| 112 | void display_update(const char *ap_ssid, int active_clients, | ||
| 113 | uint64_t wallet_balance, const char *portal_url); | ||
| 114 | ``` | ||
| 115 | 96 | ||
| 116 | ### Enhanced to also receive: | 97 | | Display data | Source function | Update trigger | |
| 117 | ```c | 98 | |-------------|----------------|----------------| |
| 118 | void display_update(const char *ap_ssid, int active_clients, | 99 | | SSID | `config.ap_ssid` | Once at `start_services()` | |
| 119 | uint64_t wallet_balance, const char *portal_url, | 100 | | Portal URL | `config.ap_ip_str` | Once at `start_services()` | |
| 120 | const char *mint_url, int price_per_step, | 101 | | Mint URL | `config.mint_url` | Once at `start_services()` | |
| 121 | const char *wifi_status); | 102 | | Price | `config.price_per_step` | Once at `start_services()` | |
| 103 | | **Wallet balance** | `nucula_wallet_balance()` | **Every 5s in main loop** | | ||
| 104 | | **Client count** | `session_active_count()` | **Every 5s in main loop + AP events** | | ||
| 105 | | **Last payment** | `display_notify_payment()` | **On each payment in API handler** | | ||
| 106 | | WiFi status | Event handler | On STA connect/disconnect | | ||
| 107 | |||
| 108 | ### State transitions | ||
| 109 | |||
| 110 | ``` | ||
| 111 | app_main() | ||
| 112 | └─ display_set_state(BOOT) | ||
| 113 | └─ display_update(price, mint, ssid) ← config data available after config_init | ||
| 114 | |||
| 115 | wifi_event_handler(STA_DISCONNECTED) | ||
| 116 | └─ display_set_state(ERROR) | ||
| 117 | └─ display_update(wifi_status="retrying...") | ||
| 118 | |||
| 119 | ip_event_handler(STA_GOT_IP) | ||
| 120 | └─ start_services() | ||
| 121 | └─ display_set_state(READY) | ||
| 122 | └─ display_update(ssid, portal_url, mint, price) | ||
| 123 | |||
| 124 | tollgate_api (POST / payment) | ||
| 125 | └─ session_create() | ||
| 126 | └─ nucula_wallet_receive() | ||
| 127 | └─ display_notify_payment(amount_sats, allotment) ← NEW | ||
| 128 | └─ display_set_state(PAYMENT_RECEIVED) | ||
| 129 | |||
| 130 | display_task (3s timeout) | ||
| 131 | └─ auto-return PAYMENT_RECEIVED → READY | ||
| 132 | |||
| 133 | app_main() main loop (every 5s) | ||
| 134 | └─ display_update(wallet_balance, client_count) ← NEW periodic refresh | ||
| 122 | ``` | 135 | ``` |
| 123 | 136 | ||
| 124 | ### display_set_state() triggers: | 137 | ### New API: `display_notify_payment()` |
| 125 | - `DISPLAY_BOOT` → at startup | ||
| 126 | - `DISPLAY_READY` → when services start (WiFi connected) | ||
| 127 | - `DISPLAY_PAYMENT_RECEIVED` → on successful payment (auto-returns to READY) | ||
| 128 | - `DISPLAY_ERROR` → when upstream WiFi disconnects | ||
| 129 | 138 | ||
| 130 | ## Implementation Notes | 139 | ```c |
| 140 | void display_notify_payment(int amount_sats, int64_t allotment_ms); | ||
| 141 | ``` | ||
| 131 | 142 | ||
| 132 | - Render every 2 seconds (reduces SPI bus load vs 1 second) | 143 | Stores the last payment amount and time purchased for the PAYMENT screen to display. |
| 133 | - QR codes: auto-size based on string length, centered in top portion | 144 | |
| 134 | - Mint URL: show only domain part (truncate at first `/`) | 145 | ## Implementation Checklist |
| 135 | - Wallet balance: color-coded (green > 100, yellow > 0, red = 0) | 146 | |
| 136 | - Client count: "Clients: N" or empty string if 0 | 147 | ### Done |
| 148 | - [x] QSPI driver working with correct colors (DMA byte-swap + RAMWR) | ||
| 149 | - [x] BOOT screen with title and WiFi status | ||
| 150 | - [x] READY screen with QR cycling, price, mint, balance, clients | ||
| 151 | - [x] PAYMENT screen layout (green banner, amount, time, wallet) | ||
| 152 | - [x] ERROR screen layout (red banner, guidance, AP status) | ||
| 153 | - [x] WiFi disconnect → ERROR state transition | ||
| 154 | - [x] WiFi connect → READY state transition | ||
| 155 | |||
| 156 | ### TODO — State Sync | ||
| 157 | - [ ] Periodic display data refresh in main loop (wallet balance, client count every 5s) | ||
| 158 | - [ ] `display_notify_payment()` API to pass payment amount and allotment | ||
| 159 | - [ ] Call `display_set_state(PAYMENT_RECEIVED)` from tollgate_api.c after payment | ||
| 160 | - [ ] Pass config data (price, mint) to display during boot phase | ||
| 161 | - [ ] Update client count on AP station connect/disconnect events | ||
| 162 | |||
| 163 | ### TODO — Polish | ||
| 164 | - [ ] Run `make test-unit` to check for regressions | ||
| 165 | - [ ] Commit, push, prepare for merge | ||
diff --git a/main/display.c b/main/display.c index add97db..0935743 100644 --- a/main/display.c +++ b/main/display.c | |||
| @@ -2,6 +2,7 @@ | |||
| 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" | ||
| 5 | #include "esp_log.h" | 6 | #include "esp_log.h" |
| 6 | #include "freertos/FreeRTOS.h" | 7 | #include "freertos/FreeRTOS.h" |
| 7 | #include "freertos/task.h" | 8 | #include "freertos/task.h" |
| @@ -34,6 +35,8 @@ static int s_price_per_step = 0; | |||
| 34 | static bool s_initialized = false; | 35 | static bool s_initialized = false; |
| 35 | static int64_t s_last_qr_switch = 0; | 36 | static int64_t s_last_qr_switch = 0; |
| 36 | static display_qr_mode_t s_qr_mode = DISPLAY_QR_WIFI; | 37 | static display_qr_mode_t s_qr_mode = DISPLAY_QR_WIFI; |
| 38 | static int s_last_payment_sats = 0; | ||
| 39 | static int64_t s_last_allotment_ms = 0; | ||
| 37 | 40 | ||
| 38 | static int qr_version_from_strlen(int len) { | 41 | static int qr_version_from_strlen(int len) { |
| 39 | if (len <= 17) return 1; | 42 | if (len <= 17) return 1; |
| @@ -251,13 +254,18 @@ static void render_payment_screen(void) { | |||
| 251 | 254 | ||
| 252 | char line[48]; | 255 | char line[48]; |
| 253 | 256 | ||
| 254 | snprintf(line, sizeof(line), "Paid: %d sats", s_price_per_step); | 257 | snprintf(line, sizeof(line), "Paid: %d sats", s_last_payment_sats); |
| 255 | int lw = strlen(line) * 8; | 258 | int lw = strlen(line) * 8; |
| 256 | display_render_text((screen_w - lw) / 2, 270, line, COLOR_WHITE, COLOR_BG, 1); | 259 | display_render_text((screen_w - lw) / 2, 270, line, COLOR_WHITE, COLOR_BG, 1); |
| 257 | 260 | ||
| 258 | const char *time_msg = "Time: 1 min"; | 261 | int64_t secs = s_last_allotment_ms / 1000; |
| 259 | int tw = strlen(time_msg) * 8; | 262 | if (secs >= 60) { |
| 260 | display_render_text((screen_w - tw) / 2, 290, time_msg, COLOR_WHITE, COLOR_BG, 1); | 263 | snprintf(line, sizeof(line), "Time: %lld min", (long long)(secs / 60)); |
| 264 | } else { | ||
| 265 | snprintf(line, sizeof(line), "Time: %lld sec", (long long)secs); | ||
| 266 | } | ||
| 267 | lw = strlen(line) * 8; | ||
| 268 | display_render_text((screen_w - lw) / 2, 290, line, COLOR_WHITE, COLOR_BG, 1); | ||
| 261 | 269 | ||
| 262 | snprintf(line, sizeof(line), "Wallet: %llu sats", (unsigned long long)s_wallet_balance); | 270 | snprintf(line, sizeof(line), "Wallet: %llu sats", (unsigned long long)s_wallet_balance); |
| 263 | lw = strlen(line) * 8; | 271 | lw = strlen(line) * 8; |
| @@ -378,3 +386,10 @@ void display_update(const char *ap_ssid, int active_clients, | |||
| 378 | s_active_clients = active_clients; | 386 | s_active_clients = active_clients; |
| 379 | s_wallet_balance = wallet_balance; | 387 | s_wallet_balance = wallet_balance; |
| 380 | } | 388 | } |
| 389 | |||
| 390 | void display_notify_payment(int amount_sats, int64_t allotment_ms) { | ||
| 391 | s_last_payment_sats = amount_sats; | ||
| 392 | s_last_allotment_ms = allotment_ms; | ||
| 393 | s_wallet_balance = nucula_wallet_balance(); | ||
| 394 | display_set_state(DISPLAY_PAYMENT_RECEIVED); | ||
| 395 | } | ||
diff --git a/main/display.h b/main/display.h index 1530e57..49b3ffd 100644 --- a/main/display.h +++ b/main/display.h | |||
| @@ -23,6 +23,7 @@ void display_update(const char *ap_ssid, int active_clients, | |||
| 23 | uint64_t wallet_balance, const char *portal_url, | 23 | uint64_t wallet_balance, const char *portal_url, |
| 24 | const char *mint_url, int price_per_step, | 24 | const char *mint_url, int price_per_step, |
| 25 | const char *wifi_status); | 25 | const char *wifi_status); |
| 26 | void display_notify_payment(int amount_sats, int64_t allotment_ms); | ||
| 26 | void display_render_text(int x, int y, const char *text, uint16_t fg, uint16_t bg, int scale); | 27 | void display_render_text(int x, int y, const char *text, uint16_t fg, uint16_t bg, int scale); |
| 27 | void display_render_qr(const char *text); | 28 | void display_render_qr(const char *text); |
| 28 | 29 | ||
diff --git a/main/tollgate_api.c b/main/tollgate_api.c index 650b0f3..7ddfb9e 100644 --- a/main/tollgate_api.c +++ b/main/tollgate_api.c | |||
| @@ -4,6 +4,7 @@ | |||
| 4 | #include "session.h" | 4 | #include "session.h" |
| 5 | #include "firewall.h" | 5 | #include "firewall.h" |
| 6 | #include "nucula_wallet.h" | 6 | #include "nucula_wallet.h" |
| 7 | #include "display.h" | ||
| 7 | #include "esp_log.h" | 8 | #include "esp_log.h" |
| 8 | #include "cJSON.h" | 9 | #include "cJSON.h" |
| 9 | #include "lwip/sockets.h" | 10 | #include "lwip/sockets.h" |
| @@ -313,6 +314,8 @@ static esp_err_t api_post_payment(httpd_req_t *req) | |||
| 313 | 314 | ||
| 314 | nucula_wallet_receive(body_copy); | 315 | nucula_wallet_receive(body_copy); |
| 315 | 316 | ||
| 317 | display_notify_payment(token->total_amount, allotment); | ||
| 318 | |||
| 316 | free(states); | 319 | free(states); |
| 317 | free(token); | 320 | free(token); |
| 318 | return ESP_OK; | 321 | return ESP_OK; |
diff --git a/main/tollgate_main.c b/main/tollgate_main.c index 7fd50ad..345295a 100644 --- a/main/tollgate_main.c +++ b/main/tollgate_main.c | |||
| @@ -77,11 +77,13 @@ static void wifi_event_handler(void *arg, esp_event_base_t event_base, | |||
| 77 | ESP_LOGI(TAG, "Station connected: MAC=%02x:%02x:%02x:%02x:%02x:%02x", | 77 | ESP_LOGI(TAG, "Station connected: MAC=%02x:%02x:%02x:%02x:%02x:%02x", |
| 78 | event->mac[0], event->mac[1], event->mac[2], | 78 | event->mac[0], event->mac[1], event->mac[2], |
| 79 | event->mac[3], event->mac[4], event->mac[5]); | 79 | event->mac[3], event->mac[4], event->mac[5]); |
| 80 | display_update(NULL, session_active_count(), 0, NULL, NULL, 0, NULL); | ||
| 80 | } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STADISCONNECTED) { | 81 | } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STADISCONNECTED) { |
| 81 | wifi_event_ap_stadisconnected_t *event = (wifi_event_ap_stadisconnected_t *)event_data; | 82 | wifi_event_ap_stadisconnected_t *event = (wifi_event_ap_stadisconnected_t *)event_data; |
| 82 | ESP_LOGI(TAG, "Station disconnected: MAC=%02x:%02x:%02x:%02x:%02x:%02x", | 83 | ESP_LOGI(TAG, "Station disconnected: MAC=%02x:%02x:%02x:%02x:%02x:%02x", |
| 83 | event->mac[0], event->mac[1], event->mac[2], | 84 | event->mac[0], event->mac[1], event->mac[2], |
| 84 | event->mac[3], event->mac[4], event->mac[5]); | 85 | event->mac[3], event->mac[4], event->mac[5]); |
| 86 | display_update(NULL, session_active_count(), 0, NULL, NULL, 0, NULL); | ||
| 85 | } | 87 | } |
| 86 | } | 88 | } |
| 87 | 89 | ||
| @@ -314,6 +316,9 @@ void app_main(void) | |||
| 314 | 316 | ||
| 315 | ESP_LOGI(TAG, "WiFi AP+STA started, waiting for connection..."); | 317 | ESP_LOGI(TAG, "WiFi AP+STA started, waiting for connection..."); |
| 316 | 318 | ||
| 319 | display_update(tcfg->ap_ssid, 0, 0, NULL, | ||
| 320 | tcfg->mint_url, tcfg->price_per_step, "connecting..."); | ||
| 321 | |||
| 317 | if (tollgate_config_get_wifi(&(wifi_config_t){0}) != ESP_OK) { | 322 | if (tollgate_config_get_wifi(&(wifi_config_t){0}) != ESP_OK) { |
| 318 | ESP_LOGI(TAG, "No STA network configured, starting services immediately"); | 323 | ESP_LOGI(TAG, "No STA network configured, starting services immediately"); |
| 319 | xTaskCreate(services_start_task, "svc_start", 32768, NULL, 5, NULL); | 324 | xTaskCreate(services_start_task, "svc_start", 32768, NULL, 5, NULL); |
| @@ -324,5 +329,13 @@ void app_main(void) | |||
| 324 | session_tick(); | 329 | session_tick(); |
| 325 | tollgate_client_tick(); | 330 | tollgate_client_tick(); |
| 326 | lightning_payout_tick(); | 331 | lightning_payout_tick(); |
| 332 | |||
| 333 | static int display_tick = 0; | ||
| 334 | if (++display_tick >= 5) { | ||
| 335 | display_tick = 0; | ||
| 336 | display_update(NULL, session_active_count(), | ||
| 337 | nucula_wallet_balance(), NULL, | ||
| 338 | NULL, 0, NULL); | ||
| 339 | } | ||
| 327 | } | 340 | } |
| 328 | } | 341 | } |