upleb.uk

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

summaryrefslogtreecommitdiff
path: root/main/display.c
diff options
context:
space:
mode:
Diffstat (limited to 'main/display.c')
-rw-r--r--main/display.c264
1 files changed, 264 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}