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:
authorYour Name <you@example.com>2026-05-19 02:31:19 +0530
committerYour Name <you@example.com>2026-05-19 02:32:41 +0530
commit81f2dc52dc42d01c89dff45a5407ec40b8863052 (patch)
tree15018c2438639ca89dc6d33a5144c10d0b1c2af0 /main/display.c
parent75688d55b3c8d13c8c9a50da9668ec408f684cb3 (diff)
feat: local Nostr relay with relay selection, sync, and integration tests
Local Nostr relay (NIP-01) on port 4869 with LittleFS 4MB storage. All events published locally first, then synced to public relays via REQ-diff. Relay selection via NIP-11 HTTP probing with NIP-77 scoring and auto-failover. Components: - wisp_relay: 16-file local relay (ws_server, storage_engine, sub_manager, broadcaster, relay_validator, router, handlers, rate_limiter, nip11, deletion, flash_monitor, relay_types) - esp_littlefs: LittleFS VFS integration (git submodule) - negentropy: for future NIP-77 binary sync (git submodule) New source files: - local_relay.c/h: thin wrapper for relay init/start/publish - relay_selector.c/h: NIP-11 probe + scoring + auto-failover - sync_manager.c/h: REQ-diff sync (primary 30min, fallback 6h) Bug fixes: - config.c: use-after-free (cJSON_Delete before seed_relays/sync parsing) - local_relay: moved init to app_main for boot-time start (not gated on STA IP) Flash layout: 4MB LittleFS partition at 0x500000 for relay_store Test results (Board B, live hardware): - Smoke: ping + HTTP 4869 + NIP-11: PASS - NIP-11 info document: 10/11 PASS - WS pub/sub (connect, REQ/EOSE, EVENT/OK, CLOSE, concurrent): 6/6 PASS - Unit tests (relay_validator + relay_selector): 13/13 PASS Hardware test make targets in physical-router-test-automation/: - make relay-build, relay-flash-b, relay-test-smoke/nip11/pubsub/sync/full
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}