#ifndef ENABLE_EMULATION #include "Arduino.h" #endif #include "font_atlas.c" #include "driver.h" #ifdef ENABLE_EMULATION #include "driver_emulator.cpp" #else #include "driver.cpp" #endif // === Config ================================================================== #define WIFI_SSID "Klatka schodowa (ekran)" #define WIFI_PASSWORD "12345678" #define WIFI_TIMEOUT_MS 300000 // 5 minutes #define SCREEN_TIMEOUT_MS 60000 // 1 minute #define DISPLAY_MARGIN 4 // === Color =================================================================== struct Color24 { uint8_t r, g, b; }; static uint16_t color565(uint8_t r, uint8_t g, uint8_t b) { return ((r / 8) << 11) | ((g / 4) << 5) | (b / 8); } static uint16_t color565(Color24 c) { return color565(c.r, c.g, c.b); } static Color24 color_lerp(Color24 a, Color24 b, float t) { return { (uint8_t)(a.r * (1-t) + b.r * t), (uint8_t)(a.g * (1-t) + b.g * t), (uint8_t)(a.b * (1-t) + b.b * t), }; } static uint8_t parse_hex2(const char *s) { char buf[3] = { s[0], s[1], 0 }; return (uint8_t)strtol(buf, nullptr, 16); } static Color24 parse_color(const char *hex) { if (!hex || hex[0] != '#' || strlen(hex) < 7) return {0, 0, 0}; return { parse_hex2(hex + 1), parse_hex2(hex + 3), parse_hex2(hex + 5), }; } // === Display primitives ====================================================== static void display_progress_bar( int32_t x, int32_t y, int32_t w, int32_t h, float t, uint16_t fg, uint16_t bg ) { int32_t fill = (int32_t)(w * t); for (int32_t py = y; py < y + h; py++) { for (int32_t px = x; px < x + w; px++) { display_set_pixel(px, py, px < x + fill ? fg : bg); } } } static void display_gradient_rect( int32_t x, int32_t y, int32_t w, int32_t h, Color24 top, Color24 bot ) { for (int32_t py = y; py < y + h; py++) { float t = (float)(py - y) / (float)(h - 1); uint16_t row_color = color565(color_lerp(top, bot, t)); for (int32_t px = x; px < x + w; px++) display_set_pixel(px, py, row_color); } } // === Text ==================================================================== static void display_write_char(int32_t *x, int32_t *y, uint8_t c, uint16_t color, int32_t scale) { FontGlyph glyph = font_glyphs[c]; for (int row = 0; row < FONT_CHAR_H; row++) { for (int col = 0; col < glyph.width; col++) { if (glyph.data[row] & (1 << col)) { for (int sy = 0; sy < scale; sy++) { for (int sx = 0; sx < scale; sx++) { display_set_pixel(*x + col * scale + sx, *y + row * scale + sy, color); } } } } } *x += (glyph.width + 1) * scale; } static int32_t measure_word(const uint8_t *text) { int32_t w = 0; while (*text && *text != ' ' && *text != '\n') { w += (font_glyphs[*text].width + 1); text++; } return w; } static void display_write_text( int32_t *x, int32_t *y, uint8_t *text, uint16_t color, int32_t scale, int32_t max_x, bool word_wrap ) { int32_t x_start = *x; while (*text != 0) { if (*text == '\n') { *x = x_start; *y += (FONT_CHAR_H + 1) * scale; text++; continue; } if (word_wrap) { if (*text == ' ') { if (max_x > 0) { int32_t word_w = measure_word(text + 1) * scale; int32_t space_w = (font_glyphs[' '].width + 1) * scale; if (*x + space_w + word_w > max_x) { *x = x_start; *y += (FONT_CHAR_H + 1) * scale; text++; continue; } } display_write_char(x, y, ' ', color, scale); text++; continue; } if (max_x > 0 && *x > x_start) { int32_t word_w = measure_word(text) * scale; int32_t line_w = max_x - x_start; if (*x + word_w > max_x && word_w <= line_w) { *x = x_start; *y += (FONT_CHAR_H + 1) * scale; } } } // Per-character wrap (always active; also handles oversized words in word_wrap mode). if (max_x > 0 && *x + (int32_t)(font_glyphs[*text].width + 1) * scale > max_x) { *x = x_start; *y += (FONT_CHAR_H + 1) * scale; } display_write_char(x, y, *text, color, scale); text++; } } // === HTTP helpers ============================================================ static char *url_decode(const char *src, size_t src_len, char *dst, size_t dst_size) { size_t i = 0; while (src_len-- > 0 && *src && i < dst_size - 1) { if (*src == '+') { dst[i++] = ' '; src++; } else if (*src == '%' && src[1] && src[2]) { char hex[3] = { src[1], src[2], 0 }; dst[i++] = (char)strtol(hex, nullptr, 16); src += 3; src_len -= 2; } else { dst[i++] = *src++; } } dst[i] = 0; return dst; } static bool form_get(const char *body, const char *key, char *out, size_t out_size) { char search[64]; snprintf(search, sizeof(search), "%s=", key); const char *p = strstr(body ? body : "", search); if (!p) return false; p += strlen(search); const char *end = strchr(p, '&'); size_t len = end ? (size_t)(end - p) : strlen(p); url_decode(p, len, out, out_size); return true; } // === Display state =========================================================== static uint8_t display_text[1024] = ""; static Color24 display_fg = {255, 255, 255}; static Color24 display_bg1 = { 0, 0, 0}; static Color24 display_bg2 = { 0, 0, 0}; static int32_t display_scale = 2; static bool display_wrap = true; static bool display_dirty = true; // === App state =============================================================== static bool display_active = true; static bool wifi_active = false; static uint32_t wifi_on_until = 0; static uint32_t screen_on_until = 0; // === WiFi ==================================================================== static WifiResponse on_request(const WifiRequest *req); // forward decl static void wifi_start(void) { wifi_hotspot_start(WIFI_SSID, WIFI_PASSWORD, on_request); wifi_active = true; wifi_on_until = millis() + WIFI_TIMEOUT_MS; display_active = true; display_dirty = true; display_on(); } static void wifi_stop(void) { wifi_hotspot_stop(); wifi_active = false; screen_on_until = millis() + SCREEN_TIMEOUT_MS; display_active = true; display_dirty = true; display_on(); } #include "frontend.h" static WifiResponse on_request(const WifiRequest *req) { wifi_on_until = millis() + WIFI_TIMEOUT_MS; if (strcmp(req->method, "POST") == 0 && strcmp(req->uri, "/submit") == 0) { const char *body = req->body ? req->body : ""; char buf[16] = {}; if (!form_get(body, "text", (char *)display_text, sizeof(display_text))) return { 400, "text/html; charset=utf-8", err_html }; if (form_get(body, "fg", buf, sizeof(buf))) display_fg = parse_color(buf); if (form_get(body, "bg1", buf, sizeof(buf))) display_bg1 = parse_color(buf); if (form_get(body, "bg2", buf, sizeof(buf))) display_bg2 = parse_color(buf); if (form_get(body, "scale", buf, sizeof(buf))) { int32_t s = atoi(buf); if (s >= 1 && s <= 4) display_scale = s; } display_wrap = strstr(body, "wrap=1") != nullptr; display_dirty = true; return { 200, "text/html; charset=utf-8", ok_html }; } if (strcmp(req->method, "POST") == 0 && strcmp(req->uri, "/wifi-off") == 0) { wifi_stop(); return { 200, "text/html; charset=utf-8", wifi_off_html }; } return { 200, "text/html; charset=utf-8", page_html }; } // === Button ================================================================== static uint32_t button_held_since = 0; static bool button_was_pressed = false; static bool button_hold_fired = false; static void button_tick(uint32_t now) { bool pressed = button_get(); if (pressed && !button_was_pressed) { button_held_since = now; button_hold_fired = false; display_dirty = true; } if (pressed && !button_hold_fired && (now - button_held_since) > 2000) { button_hold_fired = true; if (wifi_active) wifi_stop(); else wifi_start(); display_dirty = true; } if (!pressed && button_was_pressed && !button_hold_fired) { display_active = !display_active; if (display_active) { display_on(); screen_on_until = now + SCREEN_TIMEOUT_MS; } else if (!wifi_active) { display_off(); } display_dirty = true; } button_was_pressed = pressed; } // === Drawing ================================================================= static void draw_frame(uint32_t now) { display_begin(); if (!display_active && !wifi_active) { display_end(); return; } if (display_dirty) display_clear(color565(0, 0, 0)); // progress bar (top 3px) { float t = wifi_active ? (float)(wifi_on_until - now) / (float)WIFI_TIMEOUT_MS : (float)(screen_on_until - now) / (float)SCREEN_TIMEOUT_MS; display_progress_bar(0, 0, DISPLAY_WIDTH, 3, t, color565(display_fg), color565(display_bg1)); } if (display_dirty) { int32_t x = DISPLAY_MARGIN; int32_t y = DISPLAY_MARGIN + 3; // wifi info section if (wifi_active) { // "Połącz się z siecią" display_write_text(&x, &y, (uint8_t *)"Po""\xb3""\xb1""cz si""\xea"" z sieci""\xb1"" i ustaw tekst\n", color565(0, 255, 0), 2, DISPLAY_WIDTH - DISPLAY_MARGIN, false); display_write_text(&x, &y, (uint8_t *)"SSID: " WIFI_SSID "\n", color565(180, 180, 180), 2, 0, false); display_write_text(&x, &y, (uint8_t *)"PASS: " WIFI_PASSWORD "\n", color565(180, 180, 180), 2, 0, false); display_write_text(&x, &y, (uint8_t *)WIFI_HOTSPOT_IP "\n", color565(180, 180, 180), 2, 0, false); } // text section: gradient background, then text on top display_gradient_rect(0, y - DISPLAY_MARGIN, DISPLAY_WIDTH, DISPLAY_HEIGHT - y + DISPLAY_MARGIN, display_bg1, display_bg2); display_write_text(&x, &y, display_text, color565(display_fg), display_scale, DISPLAY_WIDTH - DISPLAY_MARGIN, display_wrap); } display_end(); display_dirty = false; } // === Entry points ==================================================== void setup(void) { display_setup(); display_on(); button_setup(); wifi_setup(); wifi_start(); } void loop(void) { uint32_t now = millis(); button_tick(now); // wifi timeout if (wifi_active && now >= wifi_on_until) { wifi_stop(); display_dirty = true; } // screen timeout (only when wifi is off) if (!wifi_active && display_active && now >= screen_on_until) { display_active = false; display_off(); display_dirty = true; } wifi_tick(); draw_frame(now); }