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 16:15:32 +0530
committerYour Name <you@example.com>2026-05-18 16:15:32 +0530
commitbf0f22dc7510eb6d098ae2205dae008f858d8195 (patch)
treefc0496521338c9963ae482a1fbbb7d70599e8ca4
parent52f852f5b987750537b71e2a5f2363a48783a985 (diff)
track: add display abstraction layer and bitmap font
-rw-r--r--main/display.c264
-rw-r--r--main/display.h27
-rw-r--r--main/font.c132
-rw-r--r--main/font.h11
4 files changed, 434 insertions, 0 deletions
diff --git a/main/display.c b/main/display.c
new file mode 100644
index 0000000..2b6cc88
--- /dev/null
+++ b/main/display.c
@@ -0,0 +1,264 @@
1#include "display.h"
2#include "axs15231b.h"
3#include "qrcoded.h"
4#include "font.h"
5#include "esp_log.h"
6#include "freertos/FreeRTOS.h"
7#include "freertos/task.h"
8#include <string.h>
9#include <stdio.h>
10#include <stdlib.h>
11
12static const char *TAG = "display";
13
14#define QR_CYCLE_MS 5000
15
16static volatile display_state_t s_state = DISPLAY_BOOT;
17static char s_ap_ssid[32] = "";
18static char s_portal_url[256] = "";
19static int s_active_clients = 0;
20static uint64_t s_wallet_balance = 0;
21static bool s_initialized = false;
22static int64_t s_last_qr_switch = 0;
23static display_qr_mode_t s_qr_mode = DISPLAY_QR_WIFI;
24
25static int qr_version_from_strlen(int len) {
26 if (len <= 17) return 1;
27 if (len <= 32) return 2;
28 if (len <= 53) return 3;
29 if (len <= 78) return 4;
30 if (len <= 106) return 5;
31 if (len <= 134) return 6;
32 if (len <= 154) return 7;
33 if (len <= 192) return 8;
34 if (len <= 230) return 9;
35 if (len <= 271) return 10;
36 return 11;
37}
38
39static int qr_pixel_size(int len) {
40 if (len <= 53) return 4;
41 if (len <= 134) return 3;
42 return 2;
43}
44
45static int escape_wifi_field(const char *src, char *dst, int dst_size) {
46 int si = 0, di = 0;
47 while (src[si] && di < dst_size - 2) {
48 char c = src[si];
49 if (c == '\\' || c == ';' || c == ':' || c == ',' || c == '"') {
50 if (di + 2 >= dst_size) break;
51 dst[di++] = '\\';
52 dst[di++] = c;
53 } else {
54 dst[di++] = c;
55 }
56 si++;
57 }
58 dst[di] = '\0';
59 return di;
60}
61
62static void build_wifi_qr_string(char *out, int out_size) {
63 char escaped_ssid[64];
64 escape_wifi_field(s_ap_ssid, escaped_ssid, sizeof(escaped_ssid));
65 snprintf(out, out_size, "WIFI:S:%s;T:nopass;;", escaped_ssid);
66}
67
68void display_render_text(int x, int y, const char *text, uint16_t fg, uint16_t bg, int scale) {
69 int cx = x;
70 int cy = y;
71 int screen_w = axs15231b_get_width();
72 int screen_h = axs15231b_get_height();
73
74 while (*text) {
75 uint8_t ch = (uint8_t)*text;
76 if (ch >= 128) ch = '?';
77
78 if (cx + FONT_GLYPH_W * scale > screen_w) {
79 cx = x;
80 cy += FONT_GLYPH_H * scale;
81 }
82 if (cy + FONT_GLYPH_H * scale > screen_h) break;
83
84 const uint8_t *glyph = font8x8_basic[ch];
85 for (int row = 0; row < FONT_GLYPH_H; row++) {
86 uint8_t bits = glyph[row];
87 for (int col = 0; col < FONT_GLYPH_W; col++) {
88 uint16_t color = (bits & (0x80 >> col)) ? fg : bg;
89 int px = cx + col * scale;
90 int py = cy + row * scale;
91 if (px < screen_w && py < screen_h) {
92 axs15231b_fill_rect(px, py, scale, scale, color);
93 }
94 }
95 }
96 cx += FONT_GLYPH_W * scale;
97 text++;
98 }
99}
100
101static 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
138void display_render_qr(const char *text) {
139 int screen_w = axs15231b_get_width();
140 int screen_h = axs15231b_get_height();
141 axs15231b_fill_screen(0x0000);
142 render_qr_at(text, 0, 0, screen_w, screen_h);
143 axs15231b_flush();
144}
145
146static void render_boot_screen(void) {
147 axs15231b_fill_screen(0x0000);
148 display_render_text(140, 100, "TollGate", 0xF79F, 0x0000, 3);
149 display_render_text(140, 140, "starting...", 0xB5B6, 0x0000, 2);
150 axs15231b_flush();
151}
152
153static void render_ready_screen(void) {
154 axs15231b_fill_screen(0x0000);
155
156 int screen_w = axs15231b_get_width();
157 int screen_h = axs15231b_get_height();
158 int text_area_y = screen_h - 55;
159
160 char qr_text[320];
161 const char *label;
162
163 if (s_qr_mode == DISPLAY_QR_WIFI) {
164 build_wifi_qr_string(qr_text, sizeof(qr_text));
165 label = "Scan to connect";
166 } else {
167 strncpy(qr_text, s_portal_url, sizeof(qr_text) - 1);
168 qr_text[sizeof(qr_text) - 1] = '\0';
169 label = "Portal URL";
170 }
171
172 render_qr_at(qr_text, 0, 0, screen_w, text_area_y - 5);
173
174 display_render_text(10, text_area_y, label, 0xB5B6, 0x0000, 2);
175
176 char line[64];
177 snprintf(line, sizeof(line), "SSID: %s", s_ap_ssid);
178 display_render_text(10, text_area_y + 20, line, 0xB5B6, 0x0000, 2);
179
180 axs15231b_flush();
181}
182
183static void render_payment_screen(void) {
184 axs15231b_fill_screen(0x07E0);
185 display_render_text(140, 100, "Paid!", 0x0000, 0x07E0, 3);
186 display_render_text(130, 140, "Access granted", 0x0000, 0x07E0, 2);
187 axs15231b_flush();
188}
189
190static void render_error_screen(void) {
191 axs15231b_fill_screen(0xF800);
192 display_render_text(120, 100, "No upstream", 0xFFFF, 0xF800, 3);
193 display_render_text(130, 140, "Check config", 0xFFFF, 0xF800, 2);
194 axs15231b_flush();
195}
196
197static void display_task(void *pvParameters) {
198 ESP_LOGI(TAG, "Display task started");
199
200 while (1) {
201 display_state_t state = s_state;
202
203 switch (state) {
204 case DISPLAY_BOOT:
205 render_boot_screen();
206 break;
207 case DISPLAY_READY:
208 render_ready_screen();
209 break;
210 case DISPLAY_PAYMENT_RECEIVED:
211 render_payment_screen();
212 vTaskDelay(pdMS_TO_TICKS(2000));
213 s_state = DISPLAY_READY;
214 break;
215 case DISPLAY_ERROR:
216 render_error_screen();
217 break;
218 }
219
220 int64_t now = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_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 }
228}
229
230esp_err_t display_init(void) {
231 if (s_initialized) return ESP_OK;
232
233 esp_err_t ret = axs15231b_init();
234 if (ret != ESP_OK) {
235 ESP_LOGE(TAG, "Display hardware init failed: %s", esp_err_to_name(ret));
236 return ret;
237 }
238
239 s_initialized = true;
240 s_last_qr_switch = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS;
241
242 xTaskCreatePinnedToCore(display_task, "display", 16384, NULL, 2, NULL, 1);
243
244 ESP_LOGI(TAG, "Display initialized");
245 return ESP_OK;
246}
247
248void display_set_state(display_state_t state) {
249 s_state = state;
250}
251
252void display_update(const char *ap_ssid, int active_clients,
253 uint64_t wallet_balance, const char *portal_url) {
254 if (ap_ssid) {
255 strncpy(s_ap_ssid, ap_ssid, sizeof(s_ap_ssid) - 1);
256 s_ap_ssid[sizeof(s_ap_ssid) - 1] = '\0';
257 }
258 if (portal_url) {
259 strncpy(s_portal_url, portal_url, sizeof(s_portal_url) - 1);
260 s_portal_url[sizeof(s_portal_url) - 1] = '\0';
261 }
262 s_active_clients = active_clients;
263 s_wallet_balance = wallet_balance;
264}
diff --git a/main/display.h b/main/display.h
new file mode 100644
index 0000000..407521b
--- /dev/null
+++ b/main/display.h
@@ -0,0 +1,27 @@
1#ifndef DISPLAY_H
2#define DISPLAY_H
3
4#include "esp_err.h"
5#include <stdint.h>
6#include <stdbool.h>
7
8typedef enum {
9 DISPLAY_BOOT,
10 DISPLAY_READY,
11 DISPLAY_PAYMENT_RECEIVED,
12 DISPLAY_ERROR
13} display_state_t;
14
15typedef enum {
16 DISPLAY_QR_WIFI,
17 DISPLAY_QR_PORTAL
18} display_qr_mode_t;
19
20esp_err_t display_init(void);
21void display_set_state(display_state_t state);
22void display_update(const char *ap_ssid, int active_clients,
23 uint64_t wallet_balance, const char *portal_url);
24void 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);
26
27#endif
diff --git a/main/font.c b/main/font.c
new file mode 100644
index 0000000..b23928f
--- /dev/null
+++ b/main/font.c
@@ -0,0 +1,132 @@
1#include "font.h"
2
3const uint8_t font8x8_basic[128][8] = {
4 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
5 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
6 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
7 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
8 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
9 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
10 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
11 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
12 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
13 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
14 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
15 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
16 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
17 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
18 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
19 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
20 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
21 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
22 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
23 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
24 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
25 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
26 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
27 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
28 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
29 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
30 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
31 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
32 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
33 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
34 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
35 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
36 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
37 {0x18,0x18,0x18,0x18,0x18,0x00,0x18,0x00},
38 {0x66,0x66,0x00,0x00,0x00,0x00,0x00,0x00},
39 {0x66,0xFF,0x66,0x66,0xFF,0x66,0x00,0x00},
40 {0x18,0x3E,0x58,0x3C,0x1A,0x7C,0x18,0x00},
41 {0x62,0x66,0x0C,0x18,0x30,0x66,0x46,0x00},
42 {0x3C,0x66,0x3C,0x38,0x67,0x66,0x3F,0x00},
43 {0x18,0x18,0x30,0x00,0x00,0x00,0x00,0x00},
44 {0x0C,0x18,0x30,0x30,0x30,0x18,0x0C,0x00},
45 {0x30,0x18,0x0C,0x0C,0x0C,0x18,0x30,0x00},
46 {0x00,0x66,0x3C,0xFF,0x3C,0x66,0x00,0x00},
47 {0x00,0x18,0x18,0x7E,0x18,0x18,0x00,0x00},
48 {0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x30},
49 {0x00,0x00,0x00,0x7E,0x00,0x00,0x00,0x00},
50 {0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x00},
51 {0x06,0x0C,0x18,0x30,0x60,0xC0,0x80,0x00},
52 {0x3C,0x66,0x6E,0x7E,0x76,0x66,0x3C,0x00},
53 {0x18,0x38,0x18,0x18,0x18,0x18,0x7E,0x00},
54 {0x3C,0x66,0x06,0x1C,0x30,0x60,0x7E,0x00},
55 {0x3C,0x66,0x06,0x1C,0x06,0x66,0x3C,0x00},
56 {0x1C,0x3C,0x6C,0x6C,0x7E,0x0C,0x0C,0x00},
57 {0x7E,0x60,0x7C,0x06,0x06,0x66,0x3C,0x00},
58 {0x1C,0x30,0x60,0x7C,0x66,0x66,0x3C,0x00},
59 {0x7E,0x06,0x0C,0x18,0x30,0x30,0x30,0x00},
60 {0x3C,0x66,0x66,0x3C,0x66,0x66,0x3C,0x00},
61 {0x3C,0x66,0x66,0x3E,0x06,0x0C,0x38,0x00},
62 {0x00,0x00,0x18,0x18,0x00,0x18,0x18,0x00},
63 {0x00,0x00,0x18,0x18,0x00,0x18,0x18,0x30},
64 {0x0C,0x18,0x30,0x60,0x30,0x18,0x0C,0x00},
65 {0x00,0x00,0x7E,0x00,0x7E,0x00,0x00,0x00},
66 {0x30,0x18,0x0C,0x06,0x0C,0x18,0x30,0x00},
67 {0x3C,0x66,0x0C,0x18,0x18,0x00,0x18,0x00},
68 {0x3C,0x66,0x6E,0x6A,0x6E,0x60,0x3C,0x00},
69 {0x3C,0x66,0x66,0x7E,0x66,0x66,0x66,0x00},
70 {0x7C,0x66,0x66,0x7C,0x66,0x66,0x7C,0x00},
71 {0x3C,0x66,0x60,0x60,0x60,0x66,0x3C,0x00},
72 {0x78,0x6C,0x66,0x66,0x66,0x6C,0x78,0x00},
73 {0x7E,0x60,0x60,0x7C,0x60,0x60,0x7E,0x00},
74 {0x7E,0x60,0x60,0x7C,0x60,0x60,0x60,0x00},
75 {0x3C,0x66,0x60,0x6E,0x66,0x66,0x3C,0x00},
76 {0x66,0x66,0x66,0x7E,0x66,0x66,0x66,0x00},
77 {0x3C,0x18,0x18,0x18,0x18,0x18,0x3C,0x00},
78 {0x1E,0x0C,0x0C,0x0C,0x0C,0x6C,0x38,0x00},
79 {0x66,0x6C,0x78,0x70,0x78,0x6C,0x66,0x00},
80 {0x60,0x60,0x60,0x60,0x60,0x60,0x7E,0x00},
81 {0x63,0x77,0x7F,0x6B,0x63,0x63,0x63,0x00},
82 {0x66,0x76,0x7E,0x7E,0x6E,0x66,0x66,0x00},
83 {0x3C,0x66,0x66,0x66,0x66,0x66,0x3C,0x00},
84 {0x7C,0x66,0x66,0x7C,0x60,0x60,0x60,0x00},
85 {0x3C,0x66,0x66,0x66,0x6A,0x6C,0x36,0x00},
86 {0x7C,0x66,0x66,0x7C,0x6C,0x66,0x66,0x00},
87 {0x3C,0x66,0x60,0x3C,0x06,0x66,0x3C,0x00},
88 {0x7E,0x18,0x18,0x18,0x18,0x18,0x18,0x00},
89 {0x66,0x66,0x66,0x66,0x66,0x66,0x3C,0x00},
90 {0x66,0x66,0x66,0x66,0x66,0x3C,0x18,0x00},
91 {0x63,0x63,0x63,0x6B,0x7F,0x77,0x63,0x00},
92 {0x66,0x66,0x3C,0x18,0x3C,0x66,0x66,0x00},
93 {0x66,0x66,0x66,0x3C,0x18,0x18,0x18,0x00},
94 {0x7E,0x06,0x0C,0x18,0x30,0x60,0x7E,0x00},
95 {0x3C,0x30,0x30,0x30,0x30,0x30,0x3C,0x00},
96 {0xC0,0x60,0x30,0x18,0x0C,0x06,0x03,0x00},
97 {0x3C,0x0C,0x0C,0x0C,0x0C,0x0C,0x3C,0x00},
98 {0x18,0x3C,0x66,0x00,0x00,0x00,0x00,0x00},
99 {0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0x00},
100 {0x18,0x18,0x0C,0x00,0x00,0x00,0x00,0x00},
101 {0x00,0x00,0x3C,0x06,0x3E,0x66,0x3E,0x00},
102 {0x60,0x60,0x7C,0x66,0x66,0x66,0x7C,0x00},
103 {0x00,0x00,0x3C,0x66,0x60,0x66,0x3C,0x00},
104 {0x06,0x06,0x3E,0x66,0x66,0x66,0x3E,0x00},
105 {0x00,0x00,0x3C,0x66,0x7E,0x60,0x3C,0x00},
106 {0x1C,0x36,0x30,0x7C,0x30,0x30,0x30,0x00},
107 {0x00,0x00,0x3E,0x66,0x66,0x3E,0x06,0x3C},
108 {0x60,0x60,0x7C,0x66,0x66,0x66,0x66,0x00},
109 {0x18,0x00,0x38,0x18,0x18,0x18,0x3C,0x00},
110 {0x0C,0x00,0x1C,0x0C,0x0C,0x0C,0x6C,0x38},
111 {0x60,0x60,0x66,0x6C,0x78,0x6C,0x66,0x00},
112 {0x38,0x18,0x18,0x18,0x18,0x18,0x3C,0x00},
113 {0x00,0x00,0xEC,0xFE,0xD6,0xD6,0xD6,0x00},
114 {0x00,0x00,0x7C,0x66,0x66,0x66,0x66,0x00},
115 {0x00,0x00,0x3C,0x66,0x66,0x66,0x3C,0x00},
116 {0x00,0x00,0x7C,0x66,0x66,0x7C,0x60,0x60},
117 {0x00,0x00,0x3E,0x66,0x66,0x3E,0x06,0x06},
118 {0x00,0x00,0x7C,0x66,0x60,0x60,0x60,0x00},
119 {0x00,0x00,0x3E,0x60,0x3C,0x06,0x7C,0x00},
120 {0x30,0x30,0x7C,0x30,0x30,0x36,0x1C,0x00},
121 {0x00,0x00,0x66,0x66,0x66,0x66,0x3E,0x00},
122 {0x00,0x00,0x66,0x66,0x66,0x3C,0x18,0x00},
123 {0x00,0x00,0xD6,0xD6,0xD6,0xFE,0x6C,0x00},
124 {0x00,0x00,0x66,0x3C,0x18,0x3C,0x66,0x00},
125 {0x00,0x00,0x66,0x66,0x66,0x3E,0x06,0x3C},
126 {0x00,0x00,0x7E,0x0C,0x18,0x30,0x7E,0x00},
127 {0x0C,0x18,0x18,0x70,0x18,0x18,0x0C,0x00},
128 {0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x00},
129 {0x30,0x18,0x18,0x0E,0x18,0x18,0x30,0x00},
130 {0x00,0x00,0x31,0x6B,0x46,0x00,0x00,0x00},
131 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
132};
diff --git a/main/font.h b/main/font.h
new file mode 100644
index 0000000..8ef1955
--- /dev/null
+++ b/main/font.h
@@ -0,0 +1,11 @@
1#ifndef FONT_H
2#define FONT_H
3
4#include <stdint.h>
5
6#define FONT_GLYPH_W 8
7#define FONT_GLYPH_H 8
8
9extern const uint8_t font8x8_basic[128][8];
10
11#endif