From bf0f22dc7510eb6d098ae2205dae008f858d8195 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 18 May 2026 16:15:32 +0530 Subject: track: add display abstraction layer and bitmap font --- main/display.c | 264 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ main/display.h | 27 ++++++ main/font.c | 132 +++++++++++++++++++++++++++++ main/font.h | 11 +++ 4 files changed, 434 insertions(+) create mode 100644 main/display.c create mode 100644 main/display.h create mode 100644 main/font.c create mode 100644 main/font.h (limited to 'main') 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 @@ +#include "display.h" +#include "axs15231b.h" +#include "qrcoded.h" +#include "font.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include +#include +#include + +static const char *TAG = "display"; + +#define QR_CYCLE_MS 5000 + +static volatile display_state_t s_state = DISPLAY_BOOT; +static char s_ap_ssid[32] = ""; +static char s_portal_url[256] = ""; +static int s_active_clients = 0; +static uint64_t s_wallet_balance = 0; +static bool s_initialized = false; +static int64_t s_last_qr_switch = 0; +static display_qr_mode_t s_qr_mode = DISPLAY_QR_WIFI; + +static int qr_version_from_strlen(int len) { + if (len <= 17) return 1; + if (len <= 32) return 2; + if (len <= 53) return 3; + if (len <= 78) return 4; + if (len <= 106) return 5; + if (len <= 134) return 6; + if (len <= 154) return 7; + if (len <= 192) return 8; + if (len <= 230) return 9; + if (len <= 271) return 10; + return 11; +} + +static int qr_pixel_size(int len) { + if (len <= 53) return 4; + if (len <= 134) return 3; + return 2; +} + +static int escape_wifi_field(const char *src, char *dst, int dst_size) { + int si = 0, di = 0; + while (src[si] && di < dst_size - 2) { + char c = src[si]; + if (c == '\\' || c == ';' || c == ':' || c == ',' || c == '"') { + if (di + 2 >= dst_size) break; + dst[di++] = '\\'; + dst[di++] = c; + } else { + dst[di++] = c; + } + si++; + } + dst[di] = '\0'; + return di; +} + +static void build_wifi_qr_string(char *out, int out_size) { + char escaped_ssid[64]; + escape_wifi_field(s_ap_ssid, escaped_ssid, sizeof(escaped_ssid)); + snprintf(out, out_size, "WIFI:S:%s;T:nopass;;", escaped_ssid); +} + +void display_render_text(int x, int y, const char *text, uint16_t fg, uint16_t bg, int scale) { + int cx = x; + int cy = y; + int screen_w = axs15231b_get_width(); + int screen_h = axs15231b_get_height(); + + while (*text) { + uint8_t ch = (uint8_t)*text; + if (ch >= 128) ch = '?'; + + if (cx + FONT_GLYPH_W * scale > screen_w) { + cx = x; + cy += FONT_GLYPH_H * scale; + } + if (cy + FONT_GLYPH_H * scale > screen_h) break; + + const uint8_t *glyph = font8x8_basic[ch]; + for (int row = 0; row < FONT_GLYPH_H; row++) { + uint8_t bits = glyph[row]; + for (int col = 0; col < FONT_GLYPH_W; col++) { + uint16_t color = (bits & (0x80 >> col)) ? fg : bg; + int px = cx + col * scale; + int py = cy + row * scale; + if (px < screen_w && py < screen_h) { + axs15231b_fill_rect(px, py, scale, scale, color); + } + } + } + cx += FONT_GLYPH_W * scale; + text++; + } +} + +static void render_qr_at(const char *text, int x_off, int y_off, int max_w, int max_h) { + int len = strlen(text); + int version = qr_version_from_strlen(len); + int px = qr_pixel_size(len); + + uint16_t buf_size = qrcode_getBufferSize(version); + uint8_t *qr_buf = (uint8_t *)malloc(buf_size); + if (!qr_buf) { + ESP_LOGE(TAG, "Failed to allocate QR buffer"); + return; + } + + QRCode qr; + if (qrcode_initText(&qr, qr_buf, version, ECC_LOW, text) != 0) { + ESP_LOGE(TAG, "QR generation failed"); + free(qr_buf); + return; + } + + int qr_px_w = qr.size * px; + int qr_px_h = qr.size * px; + int cx = x_off + (max_w - qr_px_w) / 2; + int cy = y_off + (max_h - qr_px_h) / 2; + if (cx < 0) cx = 0; + if (cy < 0) cy = 0; + + for (int y = 0; y < qr.size; y++) { + for (int x = 0; x < qr.size; x++) { + bool mod = qrcode_getModule(&qr, x, y); + uint16_t color = mod ? 0xFFFF : 0x0000; + axs15231b_fill_rect(cx + x * px, cy + y * px, px, px, color); + } + } + + free(qr_buf); +} + +void display_render_qr(const char *text) { + int screen_w = axs15231b_get_width(); + int screen_h = axs15231b_get_height(); + axs15231b_fill_screen(0x0000); + render_qr_at(text, 0, 0, screen_w, screen_h); + axs15231b_flush(); +} + +static void render_boot_screen(void) { + axs15231b_fill_screen(0x0000); + display_render_text(140, 100, "TollGate", 0xF79F, 0x0000, 3); + display_render_text(140, 140, "starting...", 0xB5B6, 0x0000, 2); + axs15231b_flush(); +} + +static void render_ready_screen(void) { + axs15231b_fill_screen(0x0000); + + int screen_w = axs15231b_get_width(); + int screen_h = axs15231b_get_height(); + int text_area_y = screen_h - 55; + + char qr_text[320]; + const char *label; + + if (s_qr_mode == DISPLAY_QR_WIFI) { + build_wifi_qr_string(qr_text, sizeof(qr_text)); + label = "Scan to connect"; + } else { + strncpy(qr_text, s_portal_url, sizeof(qr_text) - 1); + qr_text[sizeof(qr_text) - 1] = '\0'; + label = "Portal URL"; + } + + render_qr_at(qr_text, 0, 0, screen_w, text_area_y - 5); + + display_render_text(10, text_area_y, label, 0xB5B6, 0x0000, 2); + + char line[64]; + snprintf(line, sizeof(line), "SSID: %s", s_ap_ssid); + display_render_text(10, text_area_y + 20, line, 0xB5B6, 0x0000, 2); + + axs15231b_flush(); +} + +static void render_payment_screen(void) { + axs15231b_fill_screen(0x07E0); + display_render_text(140, 100, "Paid!", 0x0000, 0x07E0, 3); + display_render_text(130, 140, "Access granted", 0x0000, 0x07E0, 2); + axs15231b_flush(); +} + +static void render_error_screen(void) { + axs15231b_fill_screen(0xF800); + display_render_text(120, 100, "No upstream", 0xFFFF, 0xF800, 3); + display_render_text(130, 140, "Check config", 0xFFFF, 0xF800, 2); + axs15231b_flush(); +} + +static void display_task(void *pvParameters) { + ESP_LOGI(TAG, "Display task started"); + + while (1) { + display_state_t state = s_state; + + switch (state) { + case DISPLAY_BOOT: + render_boot_screen(); + break; + case DISPLAY_READY: + render_ready_screen(); + break; + case DISPLAY_PAYMENT_RECEIVED: + render_payment_screen(); + vTaskDelay(pdMS_TO_TICKS(2000)); + s_state = DISPLAY_READY; + break; + case DISPLAY_ERROR: + render_error_screen(); + break; + } + + int64_t now = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS; + if (state == DISPLAY_READY && (now - s_last_qr_switch) >= QR_CYCLE_MS) { + s_qr_mode = (s_qr_mode == DISPLAY_QR_WIFI) ? DISPLAY_QR_PORTAL : DISPLAY_QR_WIFI; + s_last_qr_switch = now; + } + + vTaskDelay(pdMS_TO_TICKS(1000)); + } +} + +esp_err_t display_init(void) { + if (s_initialized) return ESP_OK; + + esp_err_t ret = axs15231b_init(); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Display hardware init failed: %s", esp_err_to_name(ret)); + return ret; + } + + s_initialized = true; + s_last_qr_switch = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS; + + xTaskCreatePinnedToCore(display_task, "display", 16384, NULL, 2, NULL, 1); + + ESP_LOGI(TAG, "Display initialized"); + return ESP_OK; +} + +void display_set_state(display_state_t state) { + s_state = state; +} + +void display_update(const char *ap_ssid, int active_clients, + uint64_t wallet_balance, const char *portal_url) { + if (ap_ssid) { + strncpy(s_ap_ssid, ap_ssid, sizeof(s_ap_ssid) - 1); + s_ap_ssid[sizeof(s_ap_ssid) - 1] = '\0'; + } + if (portal_url) { + strncpy(s_portal_url, portal_url, sizeof(s_portal_url) - 1); + s_portal_url[sizeof(s_portal_url) - 1] = '\0'; + } + s_active_clients = active_clients; + s_wallet_balance = wallet_balance; +} 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 @@ +#ifndef DISPLAY_H +#define DISPLAY_H + +#include "esp_err.h" +#include +#include + +typedef enum { + DISPLAY_BOOT, + DISPLAY_READY, + DISPLAY_PAYMENT_RECEIVED, + DISPLAY_ERROR +} display_state_t; + +typedef enum { + DISPLAY_QR_WIFI, + DISPLAY_QR_PORTAL +} display_qr_mode_t; + +esp_err_t display_init(void); +void display_set_state(display_state_t state); +void display_update(const char *ap_ssid, int active_clients, + uint64_t wallet_balance, const char *portal_url); +void display_render_text(int x, int y, const char *text, uint16_t fg, uint16_t bg, int scale); +void display_render_qr(const char *text); + +#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 @@ +#include "font.h" + +const uint8_t font8x8_basic[128][8] = { + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x18,0x18,0x18,0x18,0x18,0x00,0x18,0x00}, + {0x66,0x66,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x66,0xFF,0x66,0x66,0xFF,0x66,0x00,0x00}, + {0x18,0x3E,0x58,0x3C,0x1A,0x7C,0x18,0x00}, + {0x62,0x66,0x0C,0x18,0x30,0x66,0x46,0x00}, + {0x3C,0x66,0x3C,0x38,0x67,0x66,0x3F,0x00}, + {0x18,0x18,0x30,0x00,0x00,0x00,0x00,0x00}, + {0x0C,0x18,0x30,0x30,0x30,0x18,0x0C,0x00}, + {0x30,0x18,0x0C,0x0C,0x0C,0x18,0x30,0x00}, + {0x00,0x66,0x3C,0xFF,0x3C,0x66,0x00,0x00}, + {0x00,0x18,0x18,0x7E,0x18,0x18,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x30}, + {0x00,0x00,0x00,0x7E,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x00}, + {0x06,0x0C,0x18,0x30,0x60,0xC0,0x80,0x00}, + {0x3C,0x66,0x6E,0x7E,0x76,0x66,0x3C,0x00}, + {0x18,0x38,0x18,0x18,0x18,0x18,0x7E,0x00}, + {0x3C,0x66,0x06,0x1C,0x30,0x60,0x7E,0x00}, + {0x3C,0x66,0x06,0x1C,0x06,0x66,0x3C,0x00}, + {0x1C,0x3C,0x6C,0x6C,0x7E,0x0C,0x0C,0x00}, + {0x7E,0x60,0x7C,0x06,0x06,0x66,0x3C,0x00}, + {0x1C,0x30,0x60,0x7C,0x66,0x66,0x3C,0x00}, + {0x7E,0x06,0x0C,0x18,0x30,0x30,0x30,0x00}, + {0x3C,0x66,0x66,0x3C,0x66,0x66,0x3C,0x00}, + {0x3C,0x66,0x66,0x3E,0x06,0x0C,0x38,0x00}, + {0x00,0x00,0x18,0x18,0x00,0x18,0x18,0x00}, + {0x00,0x00,0x18,0x18,0x00,0x18,0x18,0x30}, + {0x0C,0x18,0x30,0x60,0x30,0x18,0x0C,0x00}, + {0x00,0x00,0x7E,0x00,0x7E,0x00,0x00,0x00}, + {0x30,0x18,0x0C,0x06,0x0C,0x18,0x30,0x00}, + {0x3C,0x66,0x0C,0x18,0x18,0x00,0x18,0x00}, + {0x3C,0x66,0x6E,0x6A,0x6E,0x60,0x3C,0x00}, + {0x3C,0x66,0x66,0x7E,0x66,0x66,0x66,0x00}, + {0x7C,0x66,0x66,0x7C,0x66,0x66,0x7C,0x00}, + {0x3C,0x66,0x60,0x60,0x60,0x66,0x3C,0x00}, + {0x78,0x6C,0x66,0x66,0x66,0x6C,0x78,0x00}, + {0x7E,0x60,0x60,0x7C,0x60,0x60,0x7E,0x00}, + {0x7E,0x60,0x60,0x7C,0x60,0x60,0x60,0x00}, + {0x3C,0x66,0x60,0x6E,0x66,0x66,0x3C,0x00}, + {0x66,0x66,0x66,0x7E,0x66,0x66,0x66,0x00}, + {0x3C,0x18,0x18,0x18,0x18,0x18,0x3C,0x00}, + {0x1E,0x0C,0x0C,0x0C,0x0C,0x6C,0x38,0x00}, + {0x66,0x6C,0x78,0x70,0x78,0x6C,0x66,0x00}, + {0x60,0x60,0x60,0x60,0x60,0x60,0x7E,0x00}, + {0x63,0x77,0x7F,0x6B,0x63,0x63,0x63,0x00}, + {0x66,0x76,0x7E,0x7E,0x6E,0x66,0x66,0x00}, + {0x3C,0x66,0x66,0x66,0x66,0x66,0x3C,0x00}, + {0x7C,0x66,0x66,0x7C,0x60,0x60,0x60,0x00}, + {0x3C,0x66,0x66,0x66,0x6A,0x6C,0x36,0x00}, + {0x7C,0x66,0x66,0x7C,0x6C,0x66,0x66,0x00}, + {0x3C,0x66,0x60,0x3C,0x06,0x66,0x3C,0x00}, + {0x7E,0x18,0x18,0x18,0x18,0x18,0x18,0x00}, + {0x66,0x66,0x66,0x66,0x66,0x66,0x3C,0x00}, + {0x66,0x66,0x66,0x66,0x66,0x3C,0x18,0x00}, + {0x63,0x63,0x63,0x6B,0x7F,0x77,0x63,0x00}, + {0x66,0x66,0x3C,0x18,0x3C,0x66,0x66,0x00}, + {0x66,0x66,0x66,0x3C,0x18,0x18,0x18,0x00}, + {0x7E,0x06,0x0C,0x18,0x30,0x60,0x7E,0x00}, + {0x3C,0x30,0x30,0x30,0x30,0x30,0x3C,0x00}, + {0xC0,0x60,0x30,0x18,0x0C,0x06,0x03,0x00}, + {0x3C,0x0C,0x0C,0x0C,0x0C,0x0C,0x3C,0x00}, + {0x18,0x3C,0x66,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0x00}, + {0x18,0x18,0x0C,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x3C,0x06,0x3E,0x66,0x3E,0x00}, + {0x60,0x60,0x7C,0x66,0x66,0x66,0x7C,0x00}, + {0x00,0x00,0x3C,0x66,0x60,0x66,0x3C,0x00}, + {0x06,0x06,0x3E,0x66,0x66,0x66,0x3E,0x00}, + {0x00,0x00,0x3C,0x66,0x7E,0x60,0x3C,0x00}, + {0x1C,0x36,0x30,0x7C,0x30,0x30,0x30,0x00}, + {0x00,0x00,0x3E,0x66,0x66,0x3E,0x06,0x3C}, + {0x60,0x60,0x7C,0x66,0x66,0x66,0x66,0x00}, + {0x18,0x00,0x38,0x18,0x18,0x18,0x3C,0x00}, + {0x0C,0x00,0x1C,0x0C,0x0C,0x0C,0x6C,0x38}, + {0x60,0x60,0x66,0x6C,0x78,0x6C,0x66,0x00}, + {0x38,0x18,0x18,0x18,0x18,0x18,0x3C,0x00}, + {0x00,0x00,0xEC,0xFE,0xD6,0xD6,0xD6,0x00}, + {0x00,0x00,0x7C,0x66,0x66,0x66,0x66,0x00}, + {0x00,0x00,0x3C,0x66,0x66,0x66,0x3C,0x00}, + {0x00,0x00,0x7C,0x66,0x66,0x7C,0x60,0x60}, + {0x00,0x00,0x3E,0x66,0x66,0x3E,0x06,0x06}, + {0x00,0x00,0x7C,0x66,0x60,0x60,0x60,0x00}, + {0x00,0x00,0x3E,0x60,0x3C,0x06,0x7C,0x00}, + {0x30,0x30,0x7C,0x30,0x30,0x36,0x1C,0x00}, + {0x00,0x00,0x66,0x66,0x66,0x66,0x3E,0x00}, + {0x00,0x00,0x66,0x66,0x66,0x3C,0x18,0x00}, + {0x00,0x00,0xD6,0xD6,0xD6,0xFE,0x6C,0x00}, + {0x00,0x00,0x66,0x3C,0x18,0x3C,0x66,0x00}, + {0x00,0x00,0x66,0x66,0x66,0x3E,0x06,0x3C}, + {0x00,0x00,0x7E,0x0C,0x18,0x30,0x7E,0x00}, + {0x0C,0x18,0x18,0x70,0x18,0x18,0x0C,0x00}, + {0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x00}, + {0x30,0x18,0x18,0x0E,0x18,0x18,0x30,0x00}, + {0x00,0x00,0x31,0x6B,0x46,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, +}; 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 @@ +#ifndef FONT_H +#define FONT_H + +#include + +#define FONT_GLYPH_W 8 +#define FONT_GLYPH_H 8 + +extern const uint8_t font8x8_basic[128][8]; + +#endif -- cgit v1.2.3