2026-10-19 16:46:31 +02:00
|
|
|
// 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 <out_dir> <scenario> <page>:<ms> ... one PBM per page and moment
|
|
|
|
|
//
|
|
|
|
|
// Scenarios: normal, empty (no data), alert (3D printer paused), busy (long
|
|
|
|
|
// names, high values, export).
|
|
|
|
|
#include <cstdio>
|
|
|
|
|
#include <functional>
|
|
|
|
|
#include <string>
|
|
|
|
|
#include <vector>
|
|
|
|
|
|
|
|
|
|
#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};
|
2026-10-19 18:59:00 +02:00
|
|
|
// Sunset 40 minutes after the sample time: twilight.
|
|
|
|
|
d.sunset = 1792420645 + 40 * 60;
|
|
|
|
|
d.sunrise = 1792420645 + 17 * 3600;
|
|
|
|
|
d.sun_elevation = 3.5f;
|
2026-10-19 16:46:31 +02:00
|
|
|
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};
|
2026-10-19 18:59:00 +02:00
|
|
|
d.sunset = 1792420645 + 3 * 3600 + 25 * 60;
|
|
|
|
|
d.sun_elevation = 24.0f;
|
2026-10-19 16:46:31 +02:00
|
|
|
}
|
|
|
|
|
// 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<uint32_t> moments; // ms after the page was shown; the first is its example
|
|
|
|
|
std::function<void(EL512Display &, const HomeData &, ESPTime)> draw;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static std::vector<Page> 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};
|
2026-10-19 18:59:00 +02:00
|
|
|
const interstellar::Fonts is{f_is_title, f_is_large, f_cp_mono, f_cp_code};
|
|
|
|
|
const yourname::Fonts yn{f_yn_jp, f_yn_hand, f_yn_clock, f_cp_mono};
|
2026-10-19 16:46:31 +02:00
|
|
|
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); }},
|
2026-10-19 18:59:00 +02:00
|
|
|
{"is_gargantua", {3000}, [=](auto &it, auto &, auto now) { interstellar::gargantua(it, is, now); }},
|
|
|
|
|
{"is_tars", {3500, 38500}, [=](auto &it, auto &d, auto) { interstellar::tars(it, is, d); }},
|
|
|
|
|
{"is_endurance", {8000, 2000, 12000}, [=](auto &it, auto &d, auto now) { interstellar::endurance(it, is, d, now); }},
|
|
|
|
|
{"is_tesseract", {1700, 900}, [=](auto &it, auto &d, auto now) { interstellar::tesseract(it, is, d, now); }},
|
|
|
|
|
{"yn_comet", {14000, 6000}, [=](auto &it, auto &d, auto now) { yourname::comet(it, yn, d, now); }},
|
|
|
|
|
{"yn_twilight", {3000}, [=](auto &it, auto &d, auto now) { yourname::twilight(it, yn, d, now); }},
|
2026-10-19 16:46:31 +02:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 <out_dir> <scenario> <page>:<ms> ...\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;
|
|
|
|
|
}
|