upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYour Name <you@example.com>2026-05-18 19:25:32 +0530
committerYour Name <you@example.com>2026-05-18 19:25:32 +0530
commite1da6ffc0221abf4d0fb3a8c3e6be0b90d69397a (patch)
tree3a0055b5af2bb09492c8c38fdf2712922ecc7123
parentf0f8cca10196b265c3b453f71322239d2ecaf4ae (diff)
Implement full TollGate display UI
- BOOT screen: centered title + WiFi status line - READY screen: QR cycling (WiFi/Portal), price, mint domain, wallet balance (color-coded), client count - PAYMENT screen: green banner 'ACCESS GRANTED', paid amount, time - ERROR screen: red banner 'NO UPSTREAM', guidance, AP reassurance - Enhanced display_update() API with mint_url, price, wifi_status - Extract domain helper for clean mint URL display - Render interval: 2s (reduced SPI load) - Color palette: cyan, yellow, orange, green, red, dim gray - WiFi events update display status and trigger ERROR state - Board C now on /dev/ttyACM3
-rw-r--r--DISPLAY_UI_PLAN.md136
-rw-r--r--main/display.c168
-rw-r--r--main/display.h4
-rw-r--r--main/tollgate_main.c9
4 files changed, 285 insertions, 32 deletions
diff --git a/DISPLAY_UI_PLAN.md b/DISPLAY_UI_PLAN.md
new file mode 100644
index 0000000..3050b8a
--- /dev/null
+++ b/DISPLAY_UI_PLAN.md
@@ -0,0 +1,136 @@
1# TollGate Display UI Design
2
3## Display Hardware
4- **Panel:** 3.5" IPS, 320x480 portrait (AXS15231B QSPI)
5- **Font:** 8x8 bitmap, scalable (1x=8px, 2x=16px, 3x=24px)
6- **Capabilities:** Text rendering, QR codes, filled rectangles
7- **No touch input** — display is output-only signage
8
9## Color Palette
10
11| Color | RGB565 | Usage |
12|-------|--------|-------|
13| Black | `0x0000` | Background |
14| White | `0xFFFF` | Primary text |
15| Cyan | `0x07FF` | Titles, labels |
16| Yellow | `0xFFE0` | Price, warnings |
17| Green | `0x07E0` | Success, wallet OK |
18| Orange | `0xFD20` | Accent (Bitcoin orange) |
19| Red | `0xF800` | Errors, alerts |
20| Dim gray | `0x8410` | Secondary info |
21| Dark bg | `0x2104` | Card backgrounds |
22
23## Screen States
24
25### 1. BOOT
26Shown during startup until WiFi connects and services start.
27
28```
29┌──────────────────────────┐ 0
30│ │
31│ TollGate │ y=180, cyan, scale 2
32│ connecting... │ y=205, yellow, scale 1
33│ │
34│ WiFi: trying... │ y=260, dim, scale 1
35│ │
36└──────────────────────────┘ 479
37```
38
39WiFi status line shows: "trying...", "connected!", "failed (retry)"
40
41### 2. READY — QR Cycling (primary screen)
42Cycles every 5 seconds between WiFi QR and Portal QR.
43
44**View A — WiFi QR:**
45```
46┌──────────────────────────┐ 0
47│ ┌──────┐ │
48│ │ QR │ │ QR: WIFI:S:<ssid>;T:nopass;;
49│ │ │ │ Centered in top 2/3 of screen
50│ └──────┘ │
51│ │ ~y=320
52│ Scan to connect │ cyan, scale 1
53│ SSID: TollGate-XXXX │ white, scale 1
54│ 21 sats/min │ orange, scale 1
55│ Wallet: 420 sats │ green, scale 1
56└──────────────────────────┘ 479
57```
58
59**View B — Portal QR:**
60```
61┌──────────────────────────┐ 0
62│ ┌──────┐ │
63│ │ QR │ │ QR: http://10.x.x.x/
64│ │ │ │
65│ └──────┘ │
66│ │ ~y=320
67│ Portal URL │ cyan, scale 1
68│ testnut.cashu.space │ orange, scale 1 (mint domain)
69│ 21 sats/min │ yellow, scale 1
70│ Clients: 3 │ green, scale 1
71└──────────────────────────┘ 479
72```
73
74### 3. PAYMENT_RECEIVED
75Shows for 3 seconds after payment, then returns to READY.
76
77```
78┌──────────────────────────┐ 0
79│ │
80│ ████████████████████ │ green filled bar, y=190..230
81│ ACCESS GRANTED │ white on green, scale 2
82│ ████████████████████ │
83│ │
84│ Paid: 21 sats │ white, scale 1
85│ Time: 1 min │ white, scale 1
86│ │
87│ Wallet: 441 sats │ green, scale 1
88└──────────────────────────┘ 479
89```
90
91### 4. ERROR
92Shown when upstream WiFi is disconnected.
93
94```
95┌──────────────────────────┐ 0
96│ ████████████████████ │ red filled bar, y=190..230
97│ NO UPSTREAM │ white on red, scale 2
98│ ████████████████████ │
99│ │
100│ Internet unavailable │ white, scale 1
101│ Check WiFi config │ yellow, scale 1
102│ │
103│ AP still active │ green, scale 1
104│ SSID: TollGate-XXXX │ dim, scale 1
105└──────────────────────────┘ 479
106```
107
108## Data Flow
109
110### display_update() receives:
111```c
112void display_update(const char *ap_ssid, int active_clients,
113 uint64_t wallet_balance, const char *portal_url);
114```
115
116### Enhanced to also receive:
117```c
118void display_update(const char *ap_ssid, int active_clients,
119 uint64_t wallet_balance, const char *portal_url,
120 const char *mint_url, int price_per_step,
121 const char *wifi_status);
122```
123
124### display_set_state() triggers:
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
130## Implementation Notes
131
132- Render every 2 seconds (reduces SPI bus load vs 1 second)
133- QR codes: auto-size based on string length, centered in top portion
134- Mint URL: show only domain part (truncate at first `/`)
135- Wallet balance: color-coded (green > 100, yellow > 0, red = 0)
136- Client count: "Clients: N" or empty string if 0
diff --git a/main/display.c b/main/display.c
index 0b641ea..add97db 100644
--- a/main/display.c
+++ b/main/display.c
@@ -12,18 +12,28 @@
12static const char *TAG = "display"; 12static const char *TAG = "display";
13 13
14#define QR_CYCLE_MS 5000 14#define QR_CYCLE_MS 5000
15#define RENDER_INTERVAL_MS 2000
16
17#define COLOR_BG 0x0000
18#define COLOR_WHITE 0xFFFF
19#define COLOR_CYAN 0x07FF
20#define COLOR_YELLOW 0xFFE0
21#define COLOR_GREEN 0x07E0
22#define COLOR_ORANGE 0xFD20
23#define COLOR_RED 0xF800
24#define COLOR_DIM 0x8410
15 25
16static volatile display_state_t s_state = DISPLAY_BOOT; 26static volatile display_state_t s_state = DISPLAY_BOOT;
17static char s_ap_ssid[32] = ""; 27static char s_ap_ssid[32] = "";
18static char s_portal_url[256] = ""; 28static char s_portal_url[256] = "";
29static char s_mint_url[256] = "";
30static char s_wifi_status[32] = "starting...";
19static int s_active_clients = 0; 31static int s_active_clients = 0;
20static uint64_t s_wallet_balance = 0; 32static uint64_t s_wallet_balance = 0;
33static int s_price_per_step = 0;
21static bool s_initialized = false; 34static bool s_initialized = false;
22static int64_t s_last_qr_switch = 0; 35static int64_t s_last_qr_switch = 0;
23static display_qr_mode_t s_qr_mode = DISPLAY_QR_WIFI; 36static display_qr_mode_t s_qr_mode = DISPLAY_QR_WIFI;
24static display_state_t s_rendered_state = DISPLAY_BOOT;
25static display_qr_mode_t s_rendered_qr_mode = DISPLAY_QR_WIFI;
26static bool s_force_render = true;
27 37
28static int qr_version_from_strlen(int len) { 38static int qr_version_from_strlen(int len) {
29 if (len <= 17) return 1; 39 if (len <= 17) return 1;
@@ -68,6 +78,16 @@ static void build_wifi_qr_string(char *out, int out_size) {
68 snprintf(out, out_size, "WIFI:S:%s;T:nopass;;", escaped_ssid); 78 snprintf(out, out_size, "WIFI:S:%s;T:nopass;;", escaped_ssid);
69} 79}
70 80
81static void extract_domain(const char *url, char *domain, int domain_size) {
82 const char *start = url;
83 if (strncmp(url, "https://", 8) == 0) start = url + 8;
84 else if (strncmp(url, "http://", 7) == 0) start = url + 7;
85 strncpy(domain, start, domain_size - 1);
86 domain[domain_size - 1] = '\0';
87 char *slash = strchr(domain, '/');
88 if (slash) *slash = '\0';
89}
90
71void display_render_text(int x, int y, const char *text, uint16_t fg, uint16_t bg, int scale) { 91void display_render_text(int x, int y, const char *text, uint16_t fg, uint16_t bg, int scale) {
72 int cx = x; 92 int cx = x;
73 int cy = y; 93 int cy = y;
@@ -146,54 +166,134 @@ void display_render_qr(const char *text) {
146 axs15231b_flush(); 166 axs15231b_flush();
147} 167}
148 168
169static uint16_t wallet_color(void) {
170 if (s_wallet_balance == 0) return COLOR_RED;
171 if (s_wallet_balance < 100) return COLOR_YELLOW;
172 return COLOR_GREEN;
173}
174
149static void render_boot_screen(void) { 175static void render_boot_screen(void) {
150 axs15231b_fill_screen(0x0000); 176 int screen_w = axs15231b_get_width();
151 display_render_text(96, 220, "TollGate", 0x07FF, 0x0000, 2); 177 axs15231b_fill_screen(COLOR_BG);
152 display_render_text(116, 245, "starting...", 0xFFE0, 0x0000, 1); 178
179 const char *title = "TollGate";
180 int title_w = strlen(title) * 8 * 2;
181 display_render_text((screen_w - title_w) / 2, 200, title, COLOR_CYAN, COLOR_BG, 2);
182
183 int status_w = strlen(s_wifi_status) * 8;
184 display_render_text((screen_w - status_w) / 2, 228, s_wifi_status, COLOR_YELLOW, COLOR_BG, 1);
185
153 axs15231b_flush(); 186 axs15231b_flush();
154} 187}
155 188
156static void render_ready_screen(void) { 189static void render_ready_screen(void) {
157 axs15231b_fill_screen(0x0000);
158
159 int screen_w = axs15231b_get_width(); 190 int screen_w = axs15231b_get_width();
160 int screen_h = axs15231b_get_height(); 191 int text_area_y = 330;
161 int text_area_y = screen_h - 55; 192 axs15231b_fill_screen(COLOR_BG);
162 193
163 char qr_text[320]; 194 char qr_text[320];
164 const char *label;
165
166 if (s_qr_mode == DISPLAY_QR_WIFI) { 195 if (s_qr_mode == DISPLAY_QR_WIFI) {
167 build_wifi_qr_string(qr_text, sizeof(qr_text)); 196 build_wifi_qr_string(qr_text, sizeof(qr_text));
168 label = "Scan to connect";
169 } else { 197 } else {
170 strncpy(qr_text, s_portal_url, sizeof(qr_text) - 1); 198 strncpy(qr_text, s_portal_url, sizeof(qr_text) - 1);
171 qr_text[sizeof(qr_text) - 1] = '\0'; 199 qr_text[sizeof(qr_text) - 1] = '\0';
172 label = "Portal URL";
173 } 200 }
174 201
175 render_qr_at(qr_text, 0, 0, screen_w, text_area_y - 5); 202 render_qr_at(qr_text, 0, 5, screen_w, text_area_y - 10);
176 203
177 display_render_text(10, text_area_y, label, 0xB5B6, 0x0000, 2); 204 int y = text_area_y;
205 char line[48];
178 206
179 char line[64]; 207 if (s_qr_mode == DISPLAY_QR_WIFI) {
180 snprintf(line, sizeof(line), "SSID: %s", s_ap_ssid); 208 snprintf(line, sizeof(line), "Scan to connect");
181 display_render_text(10, text_area_y + 20, line, 0xB5B6, 0x0000, 2); 209 display_render_text(10, y, line, COLOR_CYAN, COLOR_BG, 1);
210 y += 16;
211
212 snprintf(line, sizeof(line), "SSID: %s", s_ap_ssid);
213 display_render_text(10, y, line, COLOR_WHITE, COLOR_BG, 1);
214 y += 16;
215 } else {
216 snprintf(line, sizeof(line), "Portal URL");
217 display_render_text(10, y, line, COLOR_CYAN, COLOR_BG, 1);
218 y += 16;
219
220 char domain[48];
221 extract_domain(s_mint_url, domain, sizeof(domain));
222 snprintf(line, sizeof(line), "Mint: %.30s", domain);
223 display_render_text(10, y, line, COLOR_ORANGE, COLOR_BG, 1);
224 y += 16;
225 }
226
227 snprintf(line, sizeof(line), "%d sats/min", s_price_per_step);
228 display_render_text(10, y, line, COLOR_ORANGE, COLOR_BG, 1);
229 y += 16;
230
231 snprintf(line, sizeof(line), "Wallet: %llu sats", (unsigned long long)s_wallet_balance);
232 display_render_text(10, y, line, wallet_color(), COLOR_BG, 1);
233 y += 16;
234
235 if (s_active_clients > 0) {
236 snprintf(line, sizeof(line), "Clients: %d", s_active_clients);
237 display_render_text(10, y, line, COLOR_GREEN, COLOR_BG, 1);
238 }
182 239
183 axs15231b_flush(); 240 axs15231b_flush();
184} 241}
185 242
186static void render_payment_screen(void) { 243static void render_payment_screen(void) {
187 axs15231b_fill_screen(0x07E0); 244 int screen_w = axs15231b_get_width();
188 display_render_text(128, 225, "Paid!", 0x0000, 0x07E0, 2); 245 axs15231b_fill_screen(COLOR_BG);
189 display_render_text(104, 245, "Access granted", 0x0000, 0x07E0, 1); 246
247 axs15231b_fill_rect(0, 190, screen_w, 50, COLOR_GREEN);
248 const char *msg = "ACCESS GRANTED";
249 int msg_w = strlen(msg) * 8 * 2;
250 display_render_text((screen_w - msg_w) / 2, 202, msg, COLOR_WHITE, COLOR_GREEN, 2);
251
252 char line[48];
253
254 snprintf(line, sizeof(line), "Paid: %d sats", s_price_per_step);
255 int lw = strlen(line) * 8;
256 display_render_text((screen_w - lw) / 2, 270, line, COLOR_WHITE, COLOR_BG, 1);
257
258 const char *time_msg = "Time: 1 min";
259 int tw = strlen(time_msg) * 8;
260 display_render_text((screen_w - tw) / 2, 290, time_msg, COLOR_WHITE, COLOR_BG, 1);
261
262 snprintf(line, sizeof(line), "Wallet: %llu sats", (unsigned long long)s_wallet_balance);
263 lw = strlen(line) * 8;
264 display_render_text((screen_w - lw) / 2, 320, line, wallet_color(), COLOR_BG, 1);
265
190 axs15231b_flush(); 266 axs15231b_flush();
191} 267}
192 268
193static void render_error_screen(void) { 269static void render_error_screen(void) {
194 axs15231b_fill_screen(0xF800); 270 int screen_w = axs15231b_get_width();
195 display_render_text(104, 225, "No upstream", 0xFFFF, 0xF800, 2); 271 axs15231b_fill_screen(COLOR_BG);
196 display_render_text(120, 245, "Check config", 0xFFFF, 0xF800, 1); 272
273 axs15231b_fill_rect(0, 190, screen_w, 50, COLOR_RED);
274 const char *msg = "NO UPSTREAM";
275 int msg_w = strlen(msg) * 8 * 2;
276 display_render_text((screen_w - msg_w) / 2, 202, msg, COLOR_WHITE, COLOR_RED, 2);
277
278 char line[48];
279 int lw;
280
281 const char *l1 = "Internet unavailable";
282 lw = strlen(l1) * 8;
283 display_render_text((screen_w - lw) / 2, 270, l1, COLOR_WHITE, COLOR_BG, 1);
284
285 const char *l2 = "Check WiFi config";
286 lw = strlen(l2) * 8;
287 display_render_text((screen_w - lw) / 2, 290, l2, COLOR_YELLOW, COLOR_BG, 1);
288
289 const char *l3 = "AP still active";
290 lw = strlen(l3) * 8;
291 display_render_text((screen_w - lw) / 2, 320, l3, COLOR_GREEN, COLOR_BG, 1);
292
293 snprintf(line, sizeof(line), "SSID: %s", s_ap_ssid);
294 lw = strlen(line) * 8;
295 display_render_text((screen_w - lw) / 2, 340, line, COLOR_DIM, COLOR_BG, 1);
296
197 axs15231b_flush(); 297 axs15231b_flush();
198} 298}
199 299
@@ -220,7 +320,7 @@ static void display_task(void *pvParameters) {
220 break; 320 break;
221 case DISPLAY_PAYMENT_RECEIVED: 321 case DISPLAY_PAYMENT_RECEIVED:
222 render_payment_screen(); 322 render_payment_screen();
223 vTaskDelay(pdMS_TO_TICKS(2000)); 323 vTaskDelay(pdMS_TO_TICKS(3000));
224 s_state = DISPLAY_READY; 324 s_state = DISPLAY_READY;
225 break; 325 break;
226 case DISPLAY_ERROR: 326 case DISPLAY_ERROR:
@@ -228,7 +328,7 @@ static void display_task(void *pvParameters) {
228 break; 328 break;
229 } 329 }
230 330
231 vTaskDelay(pdMS_TO_TICKS(1000)); 331 vTaskDelay(pdMS_TO_TICKS(RENDER_INTERVAL_MS));
232 } 332 }
233} 333}
234 334
@@ -252,11 +352,12 @@ esp_err_t display_init(void) {
252 352
253void display_set_state(display_state_t state) { 353void display_set_state(display_state_t state) {
254 s_state = state; 354 s_state = state;
255 s_force_render = true;
256} 355}
257 356
258void display_update(const char *ap_ssid, int active_clients, 357void display_update(const char *ap_ssid, int active_clients,
259 uint64_t wallet_balance, const char *portal_url) { 358 uint64_t wallet_balance, const char *portal_url,
359 const char *mint_url, int price_per_step,
360 const char *wifi_status) {
260 if (ap_ssid) { 361 if (ap_ssid) {
261 strncpy(s_ap_ssid, ap_ssid, sizeof(s_ap_ssid) - 1); 362 strncpy(s_ap_ssid, ap_ssid, sizeof(s_ap_ssid) - 1);
262 s_ap_ssid[sizeof(s_ap_ssid) - 1] = '\0'; 363 s_ap_ssid[sizeof(s_ap_ssid) - 1] = '\0';
@@ -265,6 +366,15 @@ void display_update(const char *ap_ssid, int active_clients,
265 strncpy(s_portal_url, portal_url, sizeof(s_portal_url) - 1); 366 strncpy(s_portal_url, portal_url, sizeof(s_portal_url) - 1);
266 s_portal_url[sizeof(s_portal_url) - 1] = '\0'; 367 s_portal_url[sizeof(s_portal_url) - 1] = '\0';
267 } 368 }
369 if (mint_url) {
370 strncpy(s_mint_url, mint_url, sizeof(s_mint_url) - 1);
371 s_mint_url[sizeof(s_mint_url) - 1] = '\0';
372 }
373 if (wifi_status) {
374 strncpy(s_wifi_status, wifi_status, sizeof(s_wifi_status) - 1);
375 s_wifi_status[sizeof(s_wifi_status) - 1] = '\0';
376 }
377 if (price_per_step > 0) s_price_per_step = price_per_step;
268 s_active_clients = active_clients; 378 s_active_clients = active_clients;
269 s_wallet_balance = wallet_balance; 379 s_wallet_balance = wallet_balance;
270} 380}
diff --git a/main/display.h b/main/display.h
index 407521b..1530e57 100644
--- a/main/display.h
+++ b/main/display.h
@@ -20,7 +20,9 @@ typedef enum {
20esp_err_t display_init(void); 20esp_err_t display_init(void);
21void display_set_state(display_state_t state); 21void display_set_state(display_state_t state);
22void display_update(const char *ap_ssid, int active_clients, 22void 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,
25 const char *wifi_status);
24void display_render_text(int x, int y, const char *text, uint16_t fg, uint16_t bg, int scale); 26void display_render_text(int x, int y, const char *text, uint16_t fg, uint16_t bg, int scale);
25void display_render_qr(const char *text); 27void display_render_qr(const char *text);
26 28
diff --git a/main/tollgate_main.c b/main/tollgate_main.c
index c0ff65f..7fd50ad 100644
--- a/main/tollgate_main.c
+++ b/main/tollgate_main.c
@@ -54,7 +54,11 @@ 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 if (s_services_running) stop_services(); 57 if (s_services_running) {
58 stop_services();
59 display_set_state(DISPLAY_ERROR);
60 }
61 display_update(NULL, 0, 0, NULL, NULL, 0, "WiFi retry...");
58 if (s_retry_count < MAX_STA_RETRY) { 62 if (s_retry_count < MAX_STA_RETRY) {
59 esp_wifi_connect(); 63 esp_wifi_connect();
60 } else { 64 } else {
@@ -173,7 +177,8 @@ static void start_services(void)
173 display_set_state(DISPLAY_READY); 177 display_set_state(DISPLAY_READY);
174 char portal_url[128]; 178 char portal_url[128];
175 snprintf(portal_url, sizeof(portal_url), "http://%s/", cfg->ap_ip_str); 179 snprintf(portal_url, sizeof(portal_url), "http://%s/", cfg->ap_ip_str);
176 display_update(cfg->ap_ssid, 0, 0, portal_url); 180 display_update(cfg->ap_ssid, 0, 0, portal_url,
181 cfg->mint_url, cfg->price_per_step, NULL);
177} 182}
178 183
179static void stop_services(void) 184static void stop_services(void)