From d2e81a5a369f6f95cdfc3915901760e624cf609c Mon Sep 17 00:00:00 2001 From: MangaValk Date: Mon, 19 Oct 2026 16:46:31 +0200 Subject: [PATCH] Add a PC renderer for the themed pages; fix overlapping layouts tools/render_pages.py compiles the page code natively with g++ against ESPHome's own display and font code and the font bitmaps of the last `esphome compile`, and draws pages to PNG at chosen moments with sample data (normal, empty, alert, busy), as a contact sheet, or as one example per page in docs/images/pages (now shown in the README). Fixes found with it: Cyber HUD zone names ran into the temperatures, the breach header label hit the tag and the column marker the matrix label; KonoSuba subtitle, guild weather icon, cabbage text and all-day deadline, and hills through the party; Steins;Gate D-mail recipient, the microwave display, the gadget card value and world line labels; LCARS room rows, readout bars and data cascade, warp core readouts, and the captain's log weather wording. Co-Authored-By: Claude Opus 5.5 --- .gitignore | 3 + README.md | 43 ++++- components/el512/cyberpunk.cpp | 13 +- components/el512/el512_pages.h | 6 + components/el512/konosuba.cpp | 14 +- components/el512/steins.cpp | 41 +++-- components/el512/trek.cpp | 14 +- docs/images/pages/cyber_breach.png | Bin 0 -> 7469 bytes docs/images/pages/cyber_hud.png | Bin 0 -> 8455 bytes docs/images/pages/cyber_scan.png | Bin 0 -> 7368 bytes docs/images/pages/cyber_split.png | Bin 0 -> 7148 bytes docs/images/pages/ks_battle.png | Bin 0 -> 6434 bytes docs/images/pages/ks_card.png | Bin 0 -> 6922 bytes docs/images/pages/ks_explosion.png | Bin 0 -> 7052 bytes docs/images/pages/ks_guild.png | Bin 0 -> 8834 bytes docs/images/pages/sg_dmail.png | Bin 0 -> 6131 bytes docs/images/pages/sg_lab.png | Bin 0 -> 9727 bytes docs/images/pages/sg_meter.png | Bin 0 -> 8551 bytes docs/images/pages/sg_world.png | Bin 0 -> 6269 bytes docs/images/pages/st_lcars.png | Bin 0 -> 9414 bytes docs/images/pages/st_log.png | Bin 0 -> 8157 bytes docs/images/pages/st_roster.png | Bin 0 -> 8083 bytes docs/images/pages/st_warp.png | Bin 0 -> 7297 bytes tools/render/el512_display.h | 104 +++++++++++++ tools/render/host_stubs.cpp | 29 ++++ tools/render/render_main.cpp | 157 +++++++++++++++++++ tools/render_pages.py | 241 +++++++++++++++++++++++++++++ 27 files changed, 627 insertions(+), 38 deletions(-) create mode 100644 docs/images/pages/cyber_breach.png create mode 100644 docs/images/pages/cyber_hud.png create mode 100644 docs/images/pages/cyber_scan.png create mode 100644 docs/images/pages/cyber_split.png create mode 100644 docs/images/pages/ks_battle.png create mode 100644 docs/images/pages/ks_card.png create mode 100644 docs/images/pages/ks_explosion.png create mode 100644 docs/images/pages/ks_guild.png create mode 100644 docs/images/pages/sg_dmail.png create mode 100644 docs/images/pages/sg_lab.png create mode 100644 docs/images/pages/sg_meter.png create mode 100644 docs/images/pages/sg_world.png create mode 100644 docs/images/pages/st_lcars.png create mode 100644 docs/images/pages/st_log.png create mode 100644 docs/images/pages/st_roster.png create mode 100644 docs/images/pages/st_warp.png create mode 100644 tools/render/el512_display.h create mode 100644 tools/render/host_stubs.cpp create mode 100644 tools/render/render_main.cpp create mode 100644 tools/render_pages.py diff --git a/.gitignore b/.gitignore index c246de7..4944140 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,6 @@ esphome/*-local.yaml # Python bytecode (ESPHome component) __pycache__/ + +# tools/render_pages.py build folder and renders +.render/ diff --git a/README.md b/README.md index 37f562e..8cb6ea5 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ src/test_images/ test sketch: cycles through 12 test images (env esp32dev esphome/ el512-panel.yaml (Home Assistant display), el512-smoke.yaml home_assistant/packages/ el512_panel.yaml: helper sensors for the ESPHome pages tools/ import_still_alive.py, konosuba_sprites.py, steins_sprites.py, - trek_sprites.py + trek_sprites.py, render_pages.py (+ render/: PC renderer) test/ unit tests (placeholder) docs/ datasheet and notes ``` @@ -198,6 +198,11 @@ endorsed by Valve. ## Cyberpunk pages +| | | +|---|---| +| ![Cyber HUD](docs/images/pages/cyber_hud.png) | ![Cyber Breach](docs/images/pages/cyber_breach.png) | +| ![Cyber Scan](docs/images/pages/cyber_scan.png) | ![Cyber Split](docs/images/pages/cyber_split.png) | + Four animated ESPHome pages in a Cyberpunk look, drawn by [components/el512/cyberpunk.cpp](components/el512/cyberpunk.cpp) with Orbitron, Rajdhani and Share Tech Mono (Google Fonts). All of them take part in the rotation: @@ -224,6 +229,11 @@ with or endorsed by CD PROJEKT RED. ## KonoSuba pages +| | | +|---|---| +| ![KonoSuba Explosion](docs/images/pages/ks_explosion.png) | ![KonoSuba Guild](docs/images/pages/ks_guild.png) | +| ![KonoSuba Card](docs/images/pages/ks_card.png) | ![KonoSuba Battle](docs/images/pages/ks_battle.png) | + Four animated pages around the KonoSuba party, drawn by [components/el512/konosuba.cpp](components/el512/konosuba.cpp) with Cinzel, Nunito, Press Start 2P, Noto Sans JP and Dela Gothic One (Google Fonts), all in the rotation: @@ -256,6 +266,11 @@ affiliated with or endorsed by them. ## Steins;Gate pages +| | | +|---|---| +| ![Divergence Meter](docs/images/pages/sg_meter.png) | ![D-Mail](docs/images/pages/sg_dmail.png) | +| ![Gadget Lab](docs/images/pages/sg_lab.png) | ![World Lines](docs/images/pages/sg_world.png) | + Four animated pages from the Future Gadget Lab, drawn by [components/el512/steins.cpp](components/el512/steins.cpp) with Nixie One, Special Elite and Noto Sans JP (plus fonts shared with the other themed pages), all in the @@ -282,6 +297,11 @@ from the series is used. Steins;Gate is © MAGES. and its other rights holders; ## Star Trek pages +| | | +|---|---| +| ![LCARS Status](docs/images/pages/st_lcars.png) | ![Duty Roster](docs/images/pages/st_roster.png) | +| ![Warp Core](docs/images/pages/st_warp.png) | ![Captain's Log](docs/images/pages/st_log.png) | + Four animated pages aboard a Lower Decks-era ship, drawn by [components/el512/trek.cpp](components/el512/trek.cpp) with Antonio (standing in for the LCARS typeface), all in the rotation: @@ -309,6 +329,27 @@ These pages are fan art: the pixel art, layouts and texts are original, and no m from the series is used. Star Trek and Star Trek: Lower Decks are trademarks of CBS Studios Inc.; this project is not affiliated with or endorsed by them. +## Rendering pages on the PC + +[tools/render_pages.py](tools/render_pages.py) draws the themed pages to PNG without +the panel, to check layouts before flashing and to make the images above. It compiles +the page code natively with g++ against ESPHome's own display and font code (from the +installed `esphome` package) and the exact font bitmaps of the last `esphome compile`, +so text sizes and positions match the device. Pages are drawn at chosen moments after +they were shown, with sample data: + +```bash +esphome compile esphome/el512-panel-local.yaml # once, and after font changes +python tools/render_pages.py # all pages -> .render/out +python tools/render_pages.py --page st_lcars --at 0,2000 --scenario alert +python tools/render_pages.py --scenario busy --sheet # long names, high values +python tools/render_pages.py --scenario empty # no data yet +python tools/render_pages.py --examples # docs/images/pages/*.png +``` + +It needs a C++20 g++ (MinGW-w64 works on Windows) and Pillow. The host stand-in for +the display and the sample data are in [tools/render/](tools/render/). + ## Using the driver ```cpp diff --git a/components/el512/cyberpunk.cpp b/components/el512/cyberpunk.cpp index c934aa2..464dd5b 100644 --- a/components/el512/cyberpunk.cpp +++ b/components/el512/cyberpunk.cpp @@ -404,9 +404,10 @@ void netrunner_hud(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime cut_frame(it, 254, 150, 258, 86); tag(it, f.mono, 254, 150, 14, "ZONES"); for (int i = 0; i < 4; i++) { - const int x = 264 + (i % 2) * 124, y = 192 + (i / 2) * 34; - it.print(x, y, f.label, TextAlign::BASELINE_LEFT, el512::fit(f.label, d.rooms[i].name, 62).c_str()); - it.print(x + 116, y, f.value, TextAlign::BASELINE_RIGHT, temp_text(d.rooms[i].value).c_str()); + // Name on top, temperature below it. + const int x = 264 + (i % 2) * 124, y = 164 + (i / 2) * 36; + it.print(x, y + 11, f.mono, TextAlign::BASELINE_LEFT, el512::fit(f.mono, upper(d.rooms[i].name), 114).c_str()); + it.print(x, y + 33, f.value, TextAlign::BASELINE_LEFT, temp_text(d.rooms[i].value).c_str()); } // Bottom: ticker. @@ -432,7 +433,7 @@ void breach_protocol(EL512Display &it, const Fonts &f) { // Header and timer. tag(it, f.title, 0, 0, 24, "BREACH PROTOCOL"); - it.print(420, 18, f.label, TextAlign::BASELINE_RIGHT, "BREACH TIME REMAINING"); + it.print(420, 18, f.label, TextAlign::BASELINE_RIGHT, "TIME REMAINING"); cut_frame(it, 428, 0, 84, 26, 6); const bool low = v.remain < 10.0f && (t / 250) % 2; if (!low) @@ -441,8 +442,8 @@ void breach_protocol(EL512Display &it, const Fonts &f) { // Code matrix. cut_frame(it, 0, 40, 296, 216); - it.print(10, 54, f.mono, TextAlign::BASELINE_LEFT, "CODE MATRIX"); - draw_matrix(it, {10, 62, 46, 31, f.code}, p, v); + it.print(10, 52, f.mono, TextAlign::BASELINE_LEFT, "CODE MATRIX"); + draw_matrix(it, {10, 64, 46, 31, f.code}, p, v); // Buffer. it.print(304, 54, f.mono, TextAlign::BASELINE_LEFT, "BUFFER"); diff --git a/components/el512/el512_pages.h b/components/el512/el512_pages.h index 4462c0b..bc89996 100644 --- a/components/el512/el512_pages.h +++ b/components/el512/el512_pages.h @@ -96,6 +96,12 @@ inline std::string upper(std::string s) { return s; } +inline std::string lower(std::string s) { + for (auto &c : s) + c = (char) tolower((unsigned char) c); + return s; +} + inline int text_width(display::Display &it, display::BaseFont *font, const char *text) { int x, y, w, h; it.get_text_bounds(0, 0, text, font, display::TextAlign::TOP_LEFT, &x, &y, &w, &h); diff --git a/components/el512/konosuba.cpp b/components/el512/konosuba.cpp index 49ccb30..c6dc6eb 100644 --- a/components/el512/konosuba.cpp +++ b/components/el512/konosuba.cpp @@ -215,7 +215,7 @@ void explosion(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime now) // Title and clock (hidden while the sky is on fire). if (!blasting) { it.print(8, 4, f.jp_big, TextAlign::TOP_LEFT, "爆裂魔法"); - it.print(10, 52, f.pixel, TextAlign::TOP_LEFT, "EXPLOSION MAGIC"); + it.print(10, 56, f.pixel, TextAlign::TOP_LEFT, "EXPLOSION MAGIC"); if (now.is_valid()) it.strftime(504, 6, f.pixel_l, TextAlign::TOP_RIGHT, "%H:%M", now); } @@ -372,7 +372,7 @@ void guild_board(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime no if (quests.size() == 3) break; const std::string day = field(e, 0), time = field(e, 1); - quests.push_back({field(e, 2), upper(day) + " " + (time.empty() ? "ALL DAY" : time), true, day == today}); + quests.push_back({field(e, 2), upper(day) + (time.empty() ? "" : " " + time), true, day == today}); } static const char *const STANDING[][2] = {{"Exterminate five giant toads", "BEFORE DUSK"}, {"Escort a merchant cart to the next town", "THIS WEEK"}, @@ -412,13 +412,13 @@ void guild_board(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime no // Hazard notice (weather) and the cabbage bounty (solar). note(it, 386, 48, 116, 94); it.print(444, 60, f.pixel, TextAlign::TOP_CENTER, "HAZARD"); - it.print(416, 102, f.weather, TextAlign::CENTER, weather_icon(d.weather)); + it.print(416, 98, f.weather, TextAlign::CENTER, weather_icon(d.weather)); it.print(492, 94, f.text, TextAlign::BASELINE_RIGHT, std::isnan(d.temp) ? "--" : str("%.0f°", d.temp).c_str()); - it.print(444, 128, f.pixel, TextAlign::TOP_CENTER, fit(f.pixel, upper(weather_text(d.weather)), 100).c_str()); + it.print(444, 131, f.pixel, TextAlign::TOP_CENTER, fit(f.pixel, upper(weather_text(d.weather)), 100).c_str()); note(it, 386, 150, 116, 94); it.print(444, 162, f.pixel, TextAlign::TOP_CENTER, "BOUNTY"); - it.print(444, 176, f.text, TextAlign::TOP_CENTER, "Cabbage harvest"); + it.print(444, 176, f.text, TextAlign::TOP_CENTER, "Cabbage crop"); it.print(444, 198, f.text, TextAlign::TOP_CENTER, std::isnan(d.solar_today) ? "--" : str("%.1f kWh", d.solar_today / 1000.0f).c_str()); it.printf(444, 222, f.pixel, TextAlign::TOP_CENTER, "NOW %s", upper(watts_text(d.solar)).c_str()); @@ -535,8 +535,8 @@ void party_battle(EL512Display &it, const Fonts &f, const HomeData &d) { // Scenery. it.print(8, 6, f.jp, TextAlign::TOP_LEFT, "戦闘"); it.printf(56, 12, f.pixel, TextAlign::TOP_LEFT, "ROUND %u", (unsigned) (round + 1)); - arc(it, 110, GROUND + 70, 110, -52, 52); - arc(it, 300, GROUND + 90, 120, -40, 40); + // A hill between the party and the toad, clear of both. + arc(it, 270, GROUND + 50, 80, -46, 46); it.horizontal_line(0, GROUND, W); for (int i = 0; i < 18; i++) { const int x = (int) (hash(i, 21) % W); diff --git a/components/el512/steins.cpp b/components/el512/steins.cpp index 70a6116..7301cea 100644 --- a/components/el512/steins.cpp +++ b/components/el512/steins.cpp @@ -223,7 +223,7 @@ void d_mail(EL512Display &it, const Fonts &f, const HomeData &d) { for (int b = 0; b < 4; b++) it.filled_rectangle(146 + b * 6, 26 - b * 2 - 2, 4, b * 2 + 2, COLOR_OFF); if (c < SEND) { - it.print(35, 34, f.pixel, TextAlign::TOP_LEFT, "TO: ME, 1 WEEK AGO"); + it.print(35, 34, f.pixel, TextAlign::TOP_LEFT, "TO: ME, -1 WEEK"); it.horizontal_line(34, 45, 138); std::string body = c < TYPE_AT ? "" : typed(msg, c - TYPE_AT, CPS); if ((t / 300) % 2) @@ -267,7 +267,11 @@ void d_mail(EL512Display &it, const Fonts &f, const HomeData &d) { it.draw_pixel_at(x, y, COLOR_ON); it.printf(MX + 209, MY + 23, f.mono, TextAlign::CENTER, "0:%02u", (unsigned) ((ZAP + 300 - c) / 1000)); } else { - it.print(MX + 209, MY + 23, f.mono, TextAlign::CENTER, upper(watts_text(d.house)).c_str()); + // The microwave's little display: "850W" or "12.0kW". + const std::string lcd = std::isnan(d.house) ? "--" + : d.house >= 1000 ? str("%.1fkW", d.house / 1000.0f) + : str("%.0fW", d.house); + it.print(MX + 209, MY + 23, f.mono, TextAlign::CENTER, lcd.c_str()); } // Turntable with a banana on it. for (int a = 0; a < 360; a += 15) { @@ -374,17 +378,19 @@ void gadget_lab(EL512Display &it, const Fonts &f, const HomeData &d) { // Detail card for the selected gadget. const Gadget &g = gadgets[sel]; const uint32_t since = t % STEP; - it.rectangle(340, 38, 170, 128); - it.rectangle(342, 40, 166, 124); - it.printf(350, 46, f.title, TextAlign::TOP_LEFT, "NO.%s", g.no.c_str()); - it.print(502, 46, f.title, TextAlign::TOP_RIGHT, g.value.c_str()); - int y = 74; + // Number, name, value, then the description. + it.rectangle(340, 38, 170, 140); + it.rectangle(342, 40, 166, 136); + it.printf(350, 44, f.title, TextAlign::TOP_LEFT, "NO.%s", g.no.c_str()); + int y = 68; for (const auto &l : wrap(it, f.mono, g.name, 150, 2)) { it.print(350, y, f.mono, TextAlign::TOP_LEFT, l.c_str()); y += 15; } - it.horizontal_line(350, y + 2, 150); - y += 8; + it.print(350, y + 2, f.title, TextAlign::TOP_LEFT, g.value.c_str()); + y += 26; + it.horizontal_line(350, y, 150); + y += 5; for (const auto &l : wrap(it, f.mono, typed(g.desc, since, 40), 150, 3)) { it.print(350, y, f.mono, TextAlign::TOP_LEFT, l.c_str()); y += 15; @@ -429,7 +435,7 @@ void world_lines(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime no } for (int h = 0; h <= 24; h += 6) { it.vertical_line(xmap(h), PY1, 4); - it.printf(xmap(h), PY1 + 8, f.pixel, TextAlign::TOP_CENTER, "%02d:00", h % 24); + it.printf(xmap(h), PY1 + 8, f.pixel, h == 24 ? TextAlign::TOP_RIGHT : TextAlign::TOP_CENTER, "%02d:00", h); } // Attractor fields. @@ -441,14 +447,15 @@ void world_lines(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime no const int phase = (int) (t / 80); for (const auto &fl : fields) { const int y = ymap(fl.div); - if (fl.div == 1.048596f) { + // Labels on the left, where the chart stays empty until sunrise: Beta's + // above its line, the others below theirs (Steins Gate and Beta are only a + // few pixels apart). + if (fl.div == 1.048596f) it.horizontal_line(PX0 + 1, y, PX1 - PX0); - it.print(PX1, y - 3, f.pixel, TextAlign::BOTTOM_RIGHT, fl.name); - } else { + else dashed_hline(it, PX0 + 1, y, PX1 - PX0, 4, 4, phase); - it.print(PX1, fl.div > 1.1f ? y - 3 : y + 3, f.pixel, fl.div > 1.1f ? TextAlign::BOTTOM_RIGHT : TextAlign::TOP_RIGHT, - fl.name); - } + const bool above = fl.div > 1.1f; + it.print(PX0 + 6, above ? y - 3 : y + 3, f.pixel, above ? TextAlign::BOTTOM_LEFT : TextAlign::TOP_LEFT, fl.name); } // Today's world line, drawn in over the first 1.5 s. @@ -472,7 +479,7 @@ void world_lines(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime no points++; } if (points == 0 && (t / 600) % 2) - it.print((PX0 + PX1) / 2, (PY0 + PY1) / 2 + 30, f.mono, TextAlign::CENTER, "COLLECTING WORLD LINE DATA..."); + it.print((PX0 + PX1) / 2 - 40, PY1 - 10, f.mono, TextAlign::CENTER, "COLLECTING WORLD LINE DATA..."); // Where we are now, and the futures branching off it. if (now.is_valid() && t >= 1500) { diff --git a/components/el512/trek.cpp b/components/el512/trek.cpp index 70d3e86..9836a0e 100644 --- a/components/el512/trek.cpp +++ b/components/el512/trek.cpp @@ -173,16 +173,16 @@ void lcars_status(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime n for (int i = 0; i < 3; i++) { const int y = 48 + i * 24; block(it, f.small, 84, y, 66, 18, rows[i].name); - readout(it, 156, y + 4, 90, 10, rows[i].w / 5000.0f); + readout(it, 156, y + 4, 80, 10, rows[i].w / 5000.0f); it.print(298, y + 18, f.small, TextAlign::BOTTOM_RIGHT, kw(rows[i].w).c_str()); } // Environmental. it.print(84, 128, f.small, TextAlign::TOP_LEFT, "ENVIRONMENTAL DECKS 1-4"); for (int i = 0; i < 4; i++) { - const int x = 84 + (i % 2) * 110, y = 150 + (i / 2) * 40; + const int x = 84 + (i % 2) * 110, y = 148 + (i / 2) * 44; it.print(x, y, f.small, TextAlign::TOP_LEFT, fit(f.small, upper(d.rooms[i].name), 104).c_str()); - it.print(x, y + 16, f.medium, TextAlign::TOP_LEFT, deg(d.rooms[i].value).c_str()); + it.print(x, y + 14, f.medium, TextAlign::TOP_LEFT, deg(d.rooms[i].value).c_str()); } // Clock, sensors, fabrication bay. @@ -200,7 +200,7 @@ void lcars_status(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime n for (int col = 0; col < 2; col++) { for (int row = 0; row < 9; row++) { const uint32_t r = hash(col * 31 + row, (t / 180) + row * 3); - const int x = 446 + col * 32, y = 96 + row * 12; + const int x = 456 + col * 30, y = 96 + row * 12; if (r % 5 == 0) continue; it.printf(x, y, f.pixel, TextAlign::TOP_LEFT, "%03u", (unsigned) (r % 1000)); @@ -325,8 +325,8 @@ void warp_core(EL512Display &it, const Fonts &f, const HomeData &d) { for (int i = 0; i < 3; i++) { const int y = 36 + i * 44; it.print(0, y, f.small, TextAlign::TOP_LEFT, rows[i].name); - it.print(204, y, f.small, TextAlign::TOP_RIGHT, rows[i].value.c_str()); - readout(it, 0, y + 22, 200, 10, rows[i].frac); + it.print(196, y, f.small, TextAlign::TOP_RIGHT, rows[i].value.c_str()); + readout(it, 0, y + 22, 196, 10, rows[i].frac); } it.print(0, 172, f.small, TextAlign::TOP_LEFT, "MATTER / ANTIMATTER 1:1"); it.print(0, 192, f.small, TextAlign::TOP_LEFT, "DILITHIUM MATRIX STABLE"); @@ -409,7 +409,7 @@ void captains_log(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime n std::string entry = "We are holding position at home."; if (!std::isnan(d.temp)) entry += str(" Outside it is %.0f degrees", d.temp) + - (d.weather.empty() ? std::string(".") : " and " + weather_text(d.weather) + "."); + (d.weather.empty() ? std::string(".") : " and " + lower(weather_text(d.weather)) + "."); if (!std::isnan(d.solar)) entry += " The solar collectors deliver " + watts(d.solar) + (std::isnan(d.house) ? std::string(".") : " while the replicators draw " + watts(d.house) + "."); diff --git a/docs/images/pages/cyber_breach.png b/docs/images/pages/cyber_breach.png new file mode 100644 index 0000000000000000000000000000000000000000..d6d29662eb17769fb02b27b8d1f1bbfc71924eb2 GIT binary patch literal 7469 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ξvwYD?<^I^k(IB(PV=Z=X>CAGYSKZ$N0iX%%1 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 IyO00zKO?X1v;Y7A literal 0 HcmV?d00001 diff --git a/docs/images/pages/cyber_scan.png b/docs/images/pages/cyber_scan.png new file mode 100644 index 0000000000000000000000000000000000000000..03d2bf8f98ede9b1118ab824398234716082953f GIT binary patch literal 7368 zcmb7Jc|6qH-#?ZXD%x~YqT;4oDUpgWk}M@lVzMh+5n`^LnW=lb>1HV+gi+Ii!Bnyg zjnQ%`+X!O{S+dMzvV@t*jG6BC3Yjg4fF4=JFEe2kMURrvoYjs{A9%`jQ^*d(QimY@#A1xMM^DNfYh3N9@oO?kEM5~ z)Iw~|g-vwf*9tC##!Nu5O?>V{4z*b9OfzX zv%H(;LQnzV5fMzK?>zL6%OE31lRw^}3_KQMi@y)@BrD97Ir(LP39 zYTG9`aJ3t;E51}y97ms&0q`;nfF2)(eS1+ReA{N!IF#*{b_OR72+^IGmRjrbH^3U! z1%45!YG^CoAa?M|1;6WpDMhD`i^1qNSi7yy=xm%@*?dP)c90D4`xTzv<~)d|Bzuq| z`uZwDQ&xe31rKY0<$eVDJUgflQ9qm~1-h*k9EII&?8so*N*5ElQJrP6hU!n)!DXB+ z;Nn8QTY~LoCDm8M$({%qlLagOmJ2cqbEY)vo^BXVL_~Y!T-jIa`6JY@bD8lTW$S7# zq@ZL0O5ze3Vw|mF-b#R#1PFF_S0KOGKSTHL5>44*eW1M+Ho(O@g6@k=d7x8s#Xz*{ z+@Nq`s|8Va{BswH^Nf8i%y@xPhy@qj`tU5AbE@3B@W%aSKTmc zAGWf-CtOGpZ$-RDwGDX5rzG8DpUjQjCpkJ-qv*>s5wEC|9P>9Vn#iwji0CXQJ4 z5X)5ETbjrGXR?NJocLP7kV4om67@`S1EkV(@90+h+?O&zh~2zZYT!yZ+GnYL7s?VkCYpF&5`T>}?Kf`fZ{81mGrx7E*$O;A2=O5fgJt12PZ#e2tbZR zFS)`Lodfzng8!?rg$07NBMJa8;I#Kz9RT^H%78k1_>SxIx)E=rK)yus;)fi_T9NDZ zOAnen7Apn#Fvw_Y=(X1Hrb-M1H8|t%M|^>!vd*hhNM)Zc=fyNVqFwYU0MV~%a zP->X53WK?O1vb(#huHUHX^QC|eGy-{tIe0e>6IWmuqCT}o~4H@>mklQH`eITo%up_ z&(*NkeLL^#uK*h+>P?K`YKDNz|rXO6k3>b_6meYMc4-%lN=*&%%*c))cw~ zbL;Rf5z#s28Eq${bJ_0+PkIVeu!4}+?irJQOR%Il!P#9Bxh-9#H_o#shv%5;#<@Cc z^a1r0D#bX>Bt9b2?{|1nX2isK7qYV+H)b;4=b_YNTnopmD=NP9Qu8qw1dsZ!X>|$D z*KM$Y7TMJJ)2sGjEvIYzps z=Fo#tD|k8cY;w_@ip2|Sed{ZGKZLF2n5xpJn~PW}1%j+3K_oMjBgi{>6~5Yb7k^(T z3l!GA=rjJ=8USb1Mcwk5#^Oc$G8+3IvUd$;k6U-uKCMxPzNF;j?A)jiuc|{7Slew$ z5}^(5!`@pgUhXdhnO~>KehUM>zg<_(Okx_;7D#A*bZP=)Z6aOp(lfk;y4th3uz9 zfwjB-BZiQZrMbdOQmS_s?uYi=9cd2gotqu1P{Hnlk59mxlI{n%rp-tL!Ge!}BHdAo zH=o-kNC-mzrOePvit;eOpbO56P4vIG99-&mm|RV&s%;Q$$z!_)Z20;-@x32RKYwQP zQH#C8{I=*WDI1d-*bQrf#n_$y%U-9MjSoWTnyb zVgi@<>iK10Xkn_Z_3iAh7bgmO5S0P}Zq7z9(Th`p<8%Pbe7+RXem|03&|ZFu8yna4 z4#_JwaFH`EB2#h7x>`8&$22X>XdJl*H<`K+$#u_8VN305O5R4_< zA8mweN4cE>5}aS6cgSE%<;j*BVO}-jSJBp$A})Dnv}WjPsJo~0^O2BPL;tW~rqeim9gIX97>|J2WP40-GTQefi_w5J_LsmNz*+@V z%du`FH_GsyR&IU6-b# z{%0BlnN=7;bF+`i-~Um;IoDnuY%)7mR*^!asFR^tR>$nAP0`)@Qr)KQ(1_ z=#FoG!fq7o8)$Sc?YbJdw^9`(!_EaGh9oGs<e&KUNG-J3WXQ{LiMP6z;j za3Fex(UD)G`x1f{19J&2`_x&8ryM#&u|WV}D-qc-hV;bLMg(&`b%Msx@spIv$ya0u zd3>DM8zUf3QiKG&K)V!gd$Pn6gG9{6`d694ezQJT>-c-cSOg=nN73#@*pjaqAsr;2 z{1Jg`5kJtCm^0MamO6HUT&q^o>X0PHv*l@Ew$=iNpMG(=wS^gUhTH$7s+RVoIGM^d z%4S!Up0kqkG8SJz$67ky)b`~UNZ``R)|4{;e05*zGU}a>5nQ9svQrD3<&_4Qier!P zcrAF%)i-7m&1{k)CWhQq-LKrjk2vq4EQMLT2GCA-Cd6p4Dm~WC(DJxksi^5F>bT4b zs^rn5vj0=FurcTlgE)s#yFtU?^1H1wa`5X=RGajX7SXl$D2U_XP3YL>zyI2K>dfaL zfy!mp{t!w1dj7nRqZ^nbGaQ9(QCojqd|ON41^l2R`~wdniz?!vjB>Z+3I>zk@pR3_ zSlk|Hq|X=^tLRt1RUCqzi2jfRcPpNZu1cnL8xUqy;-Q`6`YwFZblyzTpV{8%j}Tv7 zbMU#3>Ylpo|I$7n!*oAv$P*&FJBCi5I^ADB0cC{aa`Zsog4ESm8R{3pdnu&OrHoA` z?*4#zLXvcQ>^?6USSVrtM|0w=Qhwq4a;!=O17K1A0WeP#m&)XLmv03{k{tbWSjpBY zf^O6=<)^pQMcUFrvUY%cnj^*3R=4czxl z8g|e-Uzl4^T*nUS%_x~1aQaoK>^dH*R}c{&9QnH$J6KUg^5m+TBP|=)%|$<&C?f6d zZ>X%TMf;ESp4JH~kWi71r|a7EM}3_}zW_$*M*ZZ{Wo8Q;=O$3ef0#GmM!rved4GS} z5s%PG$I8*jhDpSYD&v#sXF#E!)8@NC#onWQQ;sWQOIs?6@^u}Dr zLJEuC&>~pOG%xNI6}a`Gy~*!KRcB>PT6UI+{qVhTCVxx_+V&yAJN)&28Mx2#-yg-H zhH@tHe(f^3ajs(=Uj%-^~B+Yo|A|V z%^BC&EFzn3^m3~8)Eg&;McnS2q$*K!u7^+epLD)z06Xkr-D%qF&XSTeZ*8@qzXQBO z<|cQP<70(|&p9`TxVI$B+Wt{dn$=IfY%se1I7(%aHUZC)zPZr6&;av_VW8AO&wp)c zluG!l)gNg3t5~iq+#M73%+5VL)^6SKGR<#;uI{E~7l{VQzwlb2zD<(yt#Cu8&BpN> zF9Y>(9MqA%mU)J%`MQzLK7!NFB(lt@g>h)>gwC;l2)XZ!D@(|iL><0WPi=B|C5t*! zkG~ffWWm{X@PaeaPTBWuRK5-YKQ*7w-FQ2uQ|LG=A!gXC%{+~|;)Z|h%aSiSrvxTD z(^)fN$VI;2=La=m%Rm=$VdrS|i) zp(-i*>iNz{DK9fPWujj6rcaYnl2e|vPQL3(4*wloyag-BJWKw&B4&(PZ*<389qh6a z(G^jPYd}v9HFCQ_k9)Hcf9*}yn>61;~~0ekz`VQ}8Upn60_ud+OA`v4m2b+;MXp1XXucv3+~FZ>L?reB|xm#GavQV(qCNjX>RjY;j3DG+CHs0MgYh%2Gr zv%elsP-Lu6La7{e2?;5&Du?Flg*zwXp$ywCbCsh^K5pY%3)e8CMd3#`iJ|b*`z6@+ z;kt&z6n zykg=*doyHgt0NEm?ilwkAqVEf=bR3mHIcQdvE20*Uu^p~VFf#ftB_Hx65!Sz2IJ(} z=72_WQqQzOr|=Sro+M9UlLKcWuGqrOFYJpqQmyRwqz$$d7k&*c&tx3qq@~yNr~jq* zVf5gZkfba@Jm8f~b+yvM6<`OiPgNNxHI571RuJQZC~`mrI^#b!vfh@uxwG7ic04fcDL|#sd@<^Y)uRqxb zJ|y!`6|8jtgzD*5V)wm!8plBKmur`nltJ%|b!!C<9Ej7mxBn%Cd9#p6;xN5bQ+O@4 z?jVt>FI^Drn<3`WRA9~I#Tkbm+M#PKH6Up?ShL&|@+*${)@uSulM?&3ggZwl>)7&4 zJ4Om-fm@{#9vAZ_C$IdbmapKw8pOacW|G4mGZ`yBdyZgQr>hX*WXtsbU#`BED#ZcfPWJF!= zE1pWc23HO=i^`q2Rc{#Jezc;;bVT}^PSmwPzEZ3pJC-;sNRFfk z_z?P;5oKe{5R)%rqNN;936=HO-?c^>)kaOU)biAVcu`TK2S%8W5{1X<9)}Ug-ze>= zvHcW56!HFUYNDj~V`te!j-?zLa%NQZ)2=odNsY38-QmDT@+pyqvs1&5iX+b{3#vc1 zzr;7R{!6W0_;Iz3s@N#eKyR@wO z>U%{3f&ihfTI$+*0^^6dV?WCEv`5uL{kax&T|82J|6`h~_;dA$t3^W|+H zby@(#$LU30sm=CBvoMI%ZR{V7=1GTdaZdR5-4VFB1-*W-~Iekzaai3-WdYL408FQG|~kI5`QqUE+f%-66vV7C0P`k=Wg*yErM zbP8!gV#?LN^ikBc*Xb(wm+}a$abjDmJts@pV^aq}9a-FS*W=~vHxG@!uT1FV&TnUv zL4kMl3C?nnTV=m~AFTs(8;*(oe*Xwz=r0Yd&#|0$!Feqba*9JgBWX&D7T@z#T=(P0 zscx?4ZPDKE9U%bBI)w;{ekR=NwLtKU{?p90zt;ko!T&@re)=*4zvlnv2b-U#=Z3}? z*De44i7n)SsFQh3R&?zD^_6bz|L?H|S literal 0 HcmV?d00001 diff --git a/docs/images/pages/cyber_split.png b/docs/images/pages/cyber_split.png new file mode 100644 index 0000000000000000000000000000000000000000..f8c5df1f5c0cb83ea0881ff6f0850547d98a4c36 GIT binary patch literal 7148 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 D;)Vd7 literal 0 HcmV?d00001 diff --git a/docs/images/pages/ks_battle.png b/docs/images/pages/ks_battle.png new file mode 100644 index 0000000000000000000000000000000000000000..17cd02039c2c037e5d03db2c1c5ad0a2b7b719ac GIT binary patch literal 6434 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 WR4x9?Wfb^-uzkz!&86Rlp7;-MsM>b` literal 0 HcmV?d00001 diff --git a/docs/images/pages/ks_card.png b/docs/images/pages/ks_card.png new file mode 100644 index 0000000000000000000000000000000000000000..2d71d4b820419c52dc4f533d59944cffb71d469f GIT binary patch literal 6922 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 literal 0 HcmV?d00001 diff --git a/docs/images/pages/sg_dmail.png b/docs/images/pages/sg_dmail.png new file mode 100644 index 0000000000000000000000000000000000000000..ab22010a89c11407ea3f1925537ef0c710768704 GIT binary patch literal 6131 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 literal 0 HcmV?d00001 diff --git a/docs/images/pages/sg_lab.png b/docs/images/pages/sg_lab.png new file mode 100644 index 0000000000000000000000000000000000000000..c25c31795dd5de97750291ca2285955978da335c GIT binary patch literal 9727 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 literal 0 HcmV?d00001 diff --git a/docs/images/pages/st_lcars.png b/docs/images/pages/st_lcars.png new file mode 100644 index 0000000000000000000000000000000000000000..fc308f5d6dfa073c05f37595a37c0b7d3eff3416 GIT binary patch literal 9414 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 ZWjL7@l-~Ls7|p=(qjpDfzd!%ue*rSLEd2lg literal 0 HcmV?d00001 diff --git a/docs/images/pages/st_roster.png b/docs/images/pages/st_roster.png new file mode 100644 index 0000000000000000000000000000000000000000..d9a45833a67590b3be234ee21053bf99c9aa1e8b GIT binary patch literal 8083 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 GPyG*UYf3f% literal 0 HcmV?d00001 diff --git a/tools/render/el512_display.h b/tools/render/el512_display.h new file mode 100644 index 0000000..bef5b2f --- /dev/null +++ b/tools/render/el512_display.h @@ -0,0 +1,104 @@ +// Host stand-in for components/el512/el512_display.h, used by +// tools/render_pages.py to draw the themed pages on a PC: the same drawing +// surface (ESPHome's Display, 512 x 256, 1 bpp) and the same buffer effects, +// without the panel, DMA or ESPHome components. +#pragma once + +#include +#include +#include + +#include "cyberpunk.h" +#include "el512_helpers.h" +#include "esphome/components/display/display.h" +#include "esphome/core/hal.h" +#include "konosuba.h" +#include "steins.h" +#include "trek.h" + +namespace esphome::el512 { + +class EL512Display : public display::Display { + public: + static constexpr int WIDTH = 512, HEIGHT = 256, ROW = WIDTH / 8; + + uint8_t fb[ROW * HEIGHT]{}; // row-major, MSB = leftmost pixel (also PBM P4 order) + uint32_t shown_at{0}; // millis() when the page was "shown" + + void update() override {} + display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_BINARY; } + + void fill(Color color) override { + if (this->get_clipping().is_set()) { + Display::fill(color); + return; + } + memset(this->fb, color.is_on() ? 0xFF : 0x00, sizeof(this->fb)); + } + void draw_pixel_at(int x, int y, Color color) override { + if (!this->get_clipping().inside(x, y)) + return; + this->set(x, y, color.is_on()); + } + + uint32_t page_age_ms() { return millis() - this->shown_at; } + uint32_t page_shown_at() const { return this->shown_at; } + + // Same effects as the device (el512_display.cpp). + void shift_rows(int y, int height, int dx) { + dx %= WIDTH; + if (dx < 0) + dx += WIDTH; + if (dx == 0) + return; + uint8_t src[ROW]; + for (int r = std::max(y, 0); r < std::min(y + height, HEIGHT); r++) { + uint8_t *row = this->fb + r * ROW; + memcpy(src, row, ROW); + memset(row, 0, ROW); + for (int x = 0; x < WIDTH; x++) { + const int s = (x - dx + WIDTH) % WIDTH; + if (src[s >> 3] & (0x80 >> (s & 7))) + row[x >> 3] |= 0x80 >> (x & 7); + } + } + } + void invert_rect(int x, int y, int width, int height) { + for (int py = y; py < y + height; py++) + for (int px = x; px < x + width; px++) + if (px >= 0 && py >= 0 && px < WIDTH && py < HEIGHT) + this->set(px, py, !this->get(px, py)); + } + void dither_mask(int level) { + if (level >= 16) + return; + static const uint8_t BAYER[4][4] = {{0, 8, 2, 10}, {12, 4, 14, 6}, {3, 11, 1, 9}, {15, 7, 13, 5}}; + uint8_t masks[4]; + for (int r = 0; r < 4; r++) { + masks[r] = 0; + for (int x = 0; x < 8; x++) + if (BAYER[r][x & 3] < level) + masks[r] |= 0x80 >> x; + } + for (int y = 0; y < HEIGHT; y++) + for (int i = 0; i < ROW; i++) + this->fb[y * ROW + i] &= masks[y & 3]; + } + + bool get(int x, int y) const { return this->fb[y * ROW + (x >> 3)] & (0x80 >> (x & 7)); } + void set(int x, int y, bool on) { + if (x < 0 || y < 0 || x >= WIDTH || y >= HEIGHT) + return; + uint8_t &b = this->fb[y * ROW + (x >> 3)]; + if (on) + b |= 0x80 >> (x & 7); + else + b &= ~(0x80 >> (x & 7)); + } + + protected: + int get_width_internal() override { return WIDTH; } + int get_height_internal() override { return HEIGHT; } +}; + +} // namespace esphome::el512 diff --git a/tools/render/host_stubs.cpp b/tools/render/host_stubs.cpp new file mode 100644 index 0000000..f5d96d1 --- /dev/null +++ b/tools/render/host_stubs.cpp @@ -0,0 +1,29 @@ +// The parts of ESPHome's core that the display code links against but the +// renderer never uses: there is no component lifecycle, polling, MAC or RNG on +// the PC side. +#include + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +namespace esphome { + +void Component::setup() {} +void Component::loop() {} +void Component::dump_config() {} +float Component::get_setup_priority() const { return 0.0f; } +bool Component::can_proceed() { return true; } +void Component::call_setup() {} + +PollingComponent::PollingComponent(uint32_t update_interval) : update_interval_(update_interval) {} +void PollingComponent::call_setup() {} +uint32_t PollingComponent::get_update_interval() const { return this->update_interval_; } +void PollingComponent::stop_poller() {} + +bool random_bytes(uint8_t *data, size_t len) { + memset(data, 0x5A, len); + return true; +} +void get_mac_address_raw(uint8_t *mac) { memset(mac, 0, 6); } + +} // namespace esphome diff --git a/tools/render/render_main.cpp b/tools/render/render_main.cpp new file mode 100644 index 0000000..5076543 --- /dev/null +++ b/tools/render/render_main.cpp @@ -0,0 +1,157 @@ +// Renders the themed ESPHome pages on a PC with ESPHome's own display and +// font code and the fonts of the last `esphome compile`. Built and run by +// tools/render_pages.py; see there for usage. +// +// render list page names and default moments +// render : ... one PBM per page and moment +// +// Scenarios: normal, empty (no data), alert (3D printer paused), busy (long +// names, high values, export). +#include +#include +#include +#include + +#include "el512_display.h" +#include "fonts.h" + +using namespace esphome; +using namespace esphome::el512; + +static uint32_t g_millis = 0; +namespace esphome { +uint32_t millis() { return g_millis; } +uint64_t millis_64() { return g_millis; } +uint32_t micros() { return g_millis * 1000; } +void delay(uint32_t) {} +void delayMicroseconds(uint32_t) {} +} // namespace esphome + +// Monday 2026-10-19 14:37:25 (UTC, so renders don't depend on the PC's zone). +static ESPTime sample_time() { return ESPTime::from_epoch_utc(1792420645); } + +static HomeData sample(const std::string &scenario) { + HomeData d; + if (scenario == "empty") + return d; + d.weather = "partlycloudy"; + d.temp = 12.4f; + d.humidity = 81; + d.wind = 14; + d.solar = 3420; + d.solar_today = 9800; + d.house = 1240; + d.grid = -2180; + d.agenda = "Mon 19|16:00|Dentist appointment;Tue 20||Holiday;Wed 21|09:30|Team meeting about the roadmap;" + "Thu 22|19:00|Dinner at Stans & Paul"; + d.message = ""; + d.printer = scenario == "alert" ? "paused" : "printing"; + d.rooms[0] = {"Woonkamer", 21.3f}; + d.rooms[1] = {"Voorkamer", 19.8f}; + d.rooms[2] = {"Patrick", 20.5f}; + d.rooms[3] = {"Badkamer", 22.1f}; + if (scenario == "busy") { + d.weather = "lightning-rainy"; + d.temp = -12.6f; + d.solar = 12450; + d.solar_today = 48700; + d.house = 11980; + d.grid = 8520; + d.humidity = 100; + d.wind = 104; + d.message = "Remember to take the bins out tonight before 22:00!"; + d.agenda = "Mon 19|16:00|Very long appointment title that keeps going and going to test wrapping;" + "Tue 20||Holiday;Wed 21|09:30|Quarterly planning session with the entire department;" + "Thu 22|19:00|Dinner"; + d.rooms[0] = {"Vloerverwarming", 23.9f}; + d.rooms[1] = {"Stans & Paul", 18.2f}; + } + // Today's solar curve up to now (14:37), for the world line chart. + for (int s = 7 * 4; s <= 14 * 4 + 2; s++) { + const float h = s / 4.0f; + d.solar_slots[s] = std::max(0.0f, 4200.0f * std::sin((h - 7.0f) / 12.0f * 3.14159f)); + } + return d; +} + +struct Page { + const char *name; + std::vector moments; // ms after the page was shown; the first is its example + std::function draw; +}; + +static std::vector pages() { + const cyber::Fonts cp{f_cp_title, f_cp_label, f_cp_value, f_cp_huge, f_cp_mono, f_cp_code}; + const konosuba::Fonts ks{f_ks_title, f_ks_big, f_ks_text, f_ks_pixel, f_ks_pixel_l, f_ks_jp, f_ks_jp_big, icons_l}; + const steins::Fonts sg{f_sg_nixie, f_sg_title, f_cp_mono, f_cp_code, f_ks_pixel, f_sg_jp}; + const trek::Fonts st{f_st_small, f_st_medium, f_st_large, f_ks_text, f_ks_pixel}; + return { + {"cyber_hud", {2000}, [=](auto &it, auto &d, auto now) { cyber::netrunner_hud(it, cp, d, now); }}, + {"cyber_breach", {4500, 9000}, [=](auto &it, auto &, auto) { cyber::breach_protocol(it, cp); }}, + {"cyber_scan", {7000, 2500}, [=](auto &it, auto &d, auto now) { cyber::optics_scan(it, cp, d, now); }}, + {"cyber_split", {4500}, [=](auto &it, auto &d, auto now) { cyber::split_hud(it, cp, d, now); }}, + {"ks_explosion", {6900, 800, 5200, 10500, 15000}, + [=](auto &it, auto &d, auto now) { konosuba::explosion(it, ks, d, now); }}, + {"ks_guild", {3000}, [=](auto &it, auto &d, auto now) { konosuba::guild_board(it, ks, d, now); }}, + {"ks_card", {2000}, [=](auto &it, auto &d, auto now) { konosuba::adventurer_card(it, ks, d, now); }}, + {"ks_battle", {2500, 10500, 18300, 20000}, [=](auto &it, auto &d, auto) { konosuba::party_battle(it, ks, d); }}, + {"sg_meter", {3000, 11000}, [=](auto &it, auto &, auto now) { steins::divergence_meter(it, sg, now); }}, + {"sg_dmail", {8000, 3000, 13500}, [=](auto &it, auto &d, auto) { steins::d_mail(it, sg, d); }}, + {"sg_lab", {1500}, [=](auto &it, auto &d, auto) { steins::gadget_lab(it, sg, d); }}, + {"sg_world", {3000}, [=](auto &it, auto &d, auto now) { steins::world_lines(it, sg, d, now); }}, + {"st_lcars", {2000}, [=](auto &it, auto &d, auto now) { trek::lcars_status(it, st, d, now); }}, + {"st_roster", {1500}, [=](auto &it, auto &d, auto now) { trek::duty_roster(it, st, d, now); }}, + {"st_warp", {2000}, [=](auto &it, auto &d, auto) { trek::warp_core(it, st, d); }}, + {"st_log", {14000, 6000}, [=](auto &it, auto &d, auto now) { trek::captains_log(it, st, d, now); }}, + }; +} + +int main(int argc, char **argv) { + const auto all = pages(); + if (argc >= 2 && std::string(argv[1]) == "list") { + for (const auto &p : all) { + printf("%s", p.name); + for (size_t i = 0; i < p.moments.size(); i++) + printf("%c%u", i ? ',' : ' ', (unsigned) p.moments[i]); + printf("\n"); + } + return 0; + } + if (argc < 4) { + fprintf(stderr, "usage: render list | render : ...\n"); + return 2; + } + const std::string out = argv[1]; + const HomeData data = sample(argv[2]); + for (int a = 3; a < argc; a++) { + const std::string arg = argv[a]; + const size_t colon = arg.find(':'); + const std::string name = arg.substr(0, colon); + const uint32_t at = colon == std::string::npos ? 0 : (uint32_t) std::stoul(arg.substr(colon + 1)); + const Page *page = nullptr; + for (const auto &p : all) + if (name == p.name) + page = &p; + if (page == nullptr) { + fprintf(stderr, "unknown page %s\n", name.c_str()); + return 1; + } + EL512Display it; + it.shown_at = 1000000; + g_millis = it.shown_at + at; + // Like the device: start from a clear buffer, then the page draws. + it.fill(display::COLOR_OFF); + page->draw(it, data, sample_time()); + const std::string path = out + "/" + name + "_" + std::to_string(at) + ".pbm"; + FILE *f = fopen(path.c_str(), "wb"); + if (f == nullptr) { + fprintf(stderr, "cannot write %s\n", path.c_str()); + return 1; + } + fprintf(f, "P4\n%d %d\n", EL512Display::WIDTH, EL512Display::HEIGHT); + fwrite(it.fb, 1, sizeof(it.fb), f); + fclose(f); + printf("%s\n", path.c_str()); + } + return 0; +} diff --git a/tools/render_pages.py b/tools/render_pages.py new file mode 100644 index 0000000..4e344dc --- /dev/null +++ b/tools/render_pages.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python3 +"""Render the themed ESPHome pages to PNG on this PC, for checking layouts +without flashing and for example images. + +Compiles the page code (components/el512/cyberpunk, konosuba, steins, trek) +natively with g++ against ESPHome's own display and font code (from the +installed esphome package) and the exact font bitmaps of the last +`esphome compile` (its generated main.cpp), then draws each page at given +moments after it was shown. Run `esphome compile esphome/el512-panel-local.yaml` +first, and again after changing fonts in the YAML. + + python tools/render_pages.py # all pages, default moments + python tools/render_pages.py --page ks_explosion --at 0,6900,15000 + python tools/render_pages.py --scenario busy # also: normal, empty, alert + python tools/render_pages.py --sheet # plus one contact sheet + python tools/render_pages.py --list # page names and moments + python tools/render_pages.py --examples # docs/images/pages/.png + +Output goes to .render/out (git-ignored) unless --out says otherwise. Needs a +g++ with C++20 (MinGW-w64 on Windows works) and Pillow. +""" + +import argparse +import re +import shutil +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +WORK = ROOT / ".render" +PAGE_SOURCES = ["cyberpunk", "konosuba", "steins", "trek"] +PAGE_HEADERS = ["el512_pages.h", "el512_helpers.h", "konosuba_sprites.h", "steins_sprites.h", "trek_sprites.h"] +ESPHOME_PARTS = ["core", "components/display", "components/font", "components/host", "components/time", + "components/lvgl", "components/graph"] +ESPHOME_SOURCES = ["components/display/display.cpp", "components/display/rect.cpp", + "components/font/font.cpp", "core/time.cpp", "core/color.cpp", "core/helpers.cpp"] +# ESPHome's headers include "defines.h" from their own folder; on the host only +# the display and font code is wanted. +HOST_DEFINES = "#pragma once\n#define USE_HOST\n#define USE_DISPLAY\n#define USE_FONT\n#define ESPHOME_LOG_LEVEL 0\n" +COMPAT = """#pragma once +// MinGW lacks strcasestr, which ESPHome's helpers use. +#ifdef __cplusplus +#include +#include +inline char *strcasestr(const char *h, const char *n) { + const size_t len = strlen(n); + for (; *h; h++) + if (strncasecmp(h, n, len) == 0) + return (char *) h; + return len == 0 ? (char *) h : nullptr; +} +#endif +""" +AMBER = (255, 170, 30) +BACKGROUND = (20, 12, 0) + + +def run(cmd, **kw): + r = subprocess.run(cmd, capture_output=True, text=True, **kw) + if r.returncode != 0: + sys.stderr.write(r.stdout + r.stderr) + raise SystemExit(f"failed: {' '.join(str(c) for c in cmd[:3])} ...") + return r.stdout + + +def write_if_changed(path, text): + if not path.exists() or path.read_text(encoding="utf-8") != text: + path.write_text(text, encoding="utf-8") + + +def stage_esphome(stage): + """Copy the needed part of the installed ESPHome sources, with host defines.""" + import esphome + + src = Path(esphome.__file__).parent + marker = stage / "esphome" / ".version" + version = getattr(__import__("esphome.const", fromlist=["__version__"]), "__version__", "?") + if marker.exists() and marker.read_text() == version: + return version + shutil.rmtree(stage / "esphome", ignore_errors=True) + for part in ESPHOME_PARTS: + dst = stage / "esphome" / part + dst.mkdir(parents=True, exist_ok=True) + for f in (src / part).iterdir(): + if f.suffix in (".h", ".cpp", ".c"): + shutil.copy2(f, dst / f.name) + (stage / "esphome" / "core" / "defines.h").write_text(HOST_DEFINES) + shutil.rmtree(stage / "obj", ignore_errors=True) # rebuild ESPHome objects + marker.write_text(version) + return version + + +def extract_fonts(main_cpp, stage): + """Turn the fonts of ESPHome's generated main.cpp into fonts.cpp / fonts.h.""" + text = main_cpp.read_text(encoding="utf-8") + cpp = ['#include "fonts.h"', "", "using esphome::font::Font;", "using esphome::font::Glyph;", ""] + hdr = ["#pragma once", '#include "esphome/components/font/font.h"', ""] + for name, glyphs, args in re.findall(r"new\((\w+)\) font::Font\((\w+), ([^;]*)\);", text): + gdef = re.search(rf"static const font::Glyph {glyphs}\[\] = (\{{.*?\}});\n", text) + if gdef is None: + raise SystemExit(f"glyphs of {name} not found in {main_cpp}") + for data in sorted(set(re.findall(r"\((uint8_t_id\w*) \+", gdef.group(1)))): + ddef = re.search(rf"static constexpr uint8_t {data}\[\] PROGMEM = (\{{.*?\}});\n", text) + cpp.append(f"static constexpr uint8_t {data}[] = {ddef.group(1)};") + cpp.append(f"static const Glyph {glyphs}[] = {gdef.group(1)};") + cpp.append(f"Font *const {name} = new Font({glyphs}, {args});") + cpp.append("") + hdr.append(f"extern esphome::font::Font *const {name};") + if len(hdr) == 3: + raise SystemExit(f"no fonts found in {main_cpp}") + write_if_changed(stage / "fonts.cpp", "\n".join(cpp) + "\n") + write_if_changed(stage / "fonts.h", "\n".join(hdr) + "\n") + + +def stage_pages(stage): + """Page sources, with the host EL512Display in place of the panel's.""" + pages = stage / "el512" + pages.mkdir(parents=True, exist_ok=True) + comp = ROOT / "components" / "el512" + for name in PAGE_HEADERS + [f"{p}.h" for p in PAGE_SOURCES] + [f"{p}.cpp" for p in PAGE_SOURCES]: + write_if_changed(pages / name, (comp / name).read_text(encoding="utf-8")) + here = Path(__file__).resolve().parent / "render" + write_if_changed(pages / "el512_display.h", (here / "el512_display.h").read_text(encoding="utf-8")) + for name in ("render_main.cpp", "host_stubs.cpp"): + write_if_changed(stage / name, (here / name).read_text(encoding="utf-8")) + write_if_changed(stage / "compat.h", COMPAT) + + +def build(stage, cxx): + obj = stage / "obj" + obj.mkdir(exist_ok=True) + flags = ["-std=gnu++20", "-O1", "-include", str(stage / "compat.h"), "-I", str(stage), "-I", str(stage / "el512")] + objects = [] + # ESPHome's own sources: compiled once per ESPHome version. + for src in ESPHOME_SOURCES: + o = obj / (src.replace("/", "_") + ".o") + if not o.exists(): + run([cxx, *flags, "-w", "-c", str(stage / "esphome" / src), "-o", str(o)]) + objects.append(o) + # Ours: rebuilt when they change. + for src in [stage / "fonts.cpp", stage / "render_main.cpp", stage / "host_stubs.cpp"] + [stage / "el512" / f"{p}.cpp" for p in PAGE_SOURCES]: + o = obj / (src.stem + ".o") + deps = [src] + list((stage / "el512").glob("*.h")) + [stage / "fonts.h"] + if not o.exists() or any(d.stat().st_mtime > o.stat().st_mtime for d in deps): + warn = ["-w"] if src.stem == "fonts" else ["-Wall"] + out = run([cxx, *flags, *warn, "-c", str(src), "-o", str(o)]) + sys.stdout.write(out) + objects.append(o) + exe = stage / ("render.exe" if sys.platform == "win32" else "render") + if not exe.exists() or any(o.stat().st_mtime > exe.stat().st_mtime for o in objects): + run([cxx, *[str(o) for o in objects], "-o", str(exe), "-static"]) + return exe + + +def to_png(pbm, png, scale): + from PIL import Image + + # PBM stores lit pixels as 1, which Pillow reads as black: invert for the mask. + lit = Image.open(pbm).convert("L").point(lambda v: 255 - v) + img = Image.new("RGB", lit.size, BACKGROUND) + img.paste(AMBER, mask=lit) + if scale != 1: + img = img.resize((img.width * scale, img.height * scale), Image.NEAREST) + img.save(png) + pbm.unlink() + + +def contact_sheet(pngs, path, cols=2): + from PIL import Image, ImageDraw + + imgs = [Image.open(p) for p in pngs] + w, h = imgs[0].size + label = 18 + sheet = Image.new("RGB", (cols * (w + 8) + 8, ((len(imgs) + cols - 1) // cols) * (h + label + 8) + 8), (0, 0, 0)) + draw = ImageDraw.Draw(sheet) + for i, (p, im) in enumerate(zip(pngs, imgs)): + x, y = 8 + (i % cols) * (w + 8), 8 + (i // cols) * (h + label + 8) + draw.text((x, y), p.stem, fill=(180, 120, 20)) + sheet.paste(im, (x, y + label)) + sheet.save(path) + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--page", action="append", help="page name (repeatable; default: all)") + ap.add_argument("--at", help="moments in ms after the page was shown, e.g. 0,2000 (default: per page)") + ap.add_argument("--scenario", default="normal", choices=["normal", "empty", "alert", "busy"]) + ap.add_argument("--out", type=Path, default=WORK / "out") + ap.add_argument("--scale", type=int, default=2) + ap.add_argument("--sheet", action="store_true", help="also write sheet.png with all renders") + ap.add_argument("--list", action="store_true", help="list pages and their default moments") + ap.add_argument("--examples", action="store_true", + help="one image per page (its first moment) as docs/images/pages/.png") + ap.add_argument("--build-dir", type=Path, default=ROOT / "esphome/.esphome/build/el512-panel", + help="ESPHome build folder with src/main.cpp") + ap.add_argument("--cxx", default=shutil.which("g++") or "g++") + args = ap.parse_args() + + main_cpp = args.build_dir / "src" / "main.cpp" + if not main_cpp.exists(): + raise SystemExit(f"{main_cpp} not found: run `esphome compile esphome/el512-panel-local.yaml` first") + WORK.mkdir(exist_ok=True) + stage_esphome(WORK) + extract_fonts(main_cpp, WORK) + stage_pages(WORK) + exe = build(WORK, args.cxx) + + listing = {} + for line in run([str(exe), "list"]).splitlines(): + name, _, moments = line.partition(" ") + listing[name] = [int(m) for m in moments.split(",") if m] + if args.list: + for name, moments in listing.items(): + print(f"{name:14} {', '.join(map(str, moments))}") + return + if args.examples: + args.out, args.at = ROOT / "docs/images/pages", None + listing = {n: m[:1] for n, m in listing.items()} + names = args.page or list(listing) + for n in names: + if n not in listing: + raise SystemExit(f"unknown page {n}; pages: {', '.join(listing)}") + jobs = [f"{n}:{m}" for n in names + for m in ([int(x) for x in args.at.split(",")] if args.at else listing[n])] + args.out.mkdir(parents=True, exist_ok=True) + pngs = [] + for pbm in run([str(exe), str(args.out), args.scenario, *jobs]).splitlines(): + pbm = Path(pbm) + stem = pbm.stem.rsplit("_", 1)[0] if args.examples else pbm.stem + png = pbm.with_name(f"{stem}{'' if args.scenario == 'normal' else '_' + args.scenario}.png") + to_png(pbm, png, args.scale) + pngs.append(png) + if args.sheet and pngs: + contact_sheet(pngs, args.out / f"sheet_{args.scenario}.png") + pngs.append(args.out / f"sheet_{args.scenario}.png") + print(f"{len(pngs)} image(s) in {args.out}") + + +if __name__ == "__main__": + main()