Public Access
Add four Star Trek / Lower Decks pages with a 3D printer red alert
LCARS ship status, the Cerritos duty roster from the agenda, a warp core driven by solar power with Rutherford's advice, and a captain's log under a viewscreen. Original pixel art in tools/trek_sprites.py. When the 3D printer (Moonraker current_print_state, `printer` substitution) is paused or in error, the panel jumps to LCARS Status in red alert and holds the rotation until it carries on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4c6286e794
commit
13a499fd97
8 files changed
+811
-8
No files matched your search
@@ -85,6 +85,7 @@ components/el512/ shared driver + ESPHome component (one copy of the code)
|
|||||||
cyberpunk.* animated Cyberpunk-style ESPHome pages
|
cyberpunk.* animated Cyberpunk-style ESPHome pages
|
||||||
konosuba.* animated KonoSuba pages; konosuba_sprites.h from tools/
|
konosuba.* animated KonoSuba pages; konosuba_sprites.h from tools/
|
||||||
steins.* animated Steins;Gate pages; steins_sprites.h from tools/
|
steins.* animated Steins;Gate pages; steins_sprites.h from tools/
|
||||||
|
trek.* animated Star Trek / Lower Decks pages; trek_sprites.h from tools/
|
||||||
el512_pages.h home data and helpers shared by the themed pages
|
el512_pages.h home data and helpers shared by the themed pages
|
||||||
el512_display.* ESPHome display platform `el512`
|
el512_display.* ESPHome display platform `el512`
|
||||||
__init__.py, display.py ESPHome config schema
|
__init__.py, display.py ESPHome config schema
|
||||||
@@ -93,7 +94,8 @@ src/still_alive/ Portal-style end-credits demo (env still_alive)
|
|||||||
src/test_images/ test sketch: cycles through 12 test images (env esp32dev)
|
src/test_images/ test sketch: cycles through 12 test images (env esp32dev)
|
||||||
esphome/ el512-panel.yaml (Home Assistant display), el512-smoke.yaml
|
esphome/ el512-panel.yaml (Home Assistant display), el512-smoke.yaml
|
||||||
home_assistant/packages/ el512_panel.yaml: helper sensors for the ESPHome pages
|
home_assistant/packages/ el512_panel.yaml: helper sensors for the ESPHome pages
|
||||||
tools/ import_still_alive.py, konosuba_sprites.py, steins_sprites.py
|
tools/ import_still_alive.py, konosuba_sprites.py, steins_sprites.py,
|
||||||
|
trek_sprites.py
|
||||||
test/ unit tests (placeholder)
|
test/ unit tests (placeholder)
|
||||||
docs/ datasheet and notes
|
docs/ datasheet and notes
|
||||||
```
|
```
|
||||||
@@ -112,10 +114,11 @@ pio device monitor
|
|||||||
## ESPHome / Home Assistant
|
## ESPHome / Home Assistant
|
||||||
|
|
||||||
[esphome/el512-panel.yaml](esphome/el512-panel.yaml) turns the panel into a Home
|
[esphome/el512-panel.yaml](esphome/el512-panel.yaml) turns the panel into a Home
|
||||||
Assistant display with twenty pages: clock and weather, forecast, agenda, a sensor
|
Assistant display with twenty-four pages: clock and weather, forecast, agenda, a
|
||||||
dashboard with a message line, now playing, Plex, solar, Still Alive, and twelve
|
sensor dashboard with a message line, now playing, Plex, solar, Still Alive, and
|
||||||
animated themed pages ([Cyberpunk](#cyberpunk-pages), [KonoSuba](#konosuba-pages),
|
sixteen animated themed pages ([Cyberpunk](#cyberpunk-pages),
|
||||||
[Steins;Gate](#steinsgate-pages)). Pages
|
[KonoSuba](#konosuba-pages), [Steins;Gate](#steinsgate-pages),
|
||||||
|
[Star Trek](#star-trek-pages)). Pages
|
||||||
rotate on an interval, in a random order that is reshuffled every hour; Home Assistant gets a page select, a pause switch, the
|
rotate on an interval, in a random order that is reshuffled every hour; Home Assistant gets a page select, a pause switch, the
|
||||||
interval, a message text, a "Play Still Alive" button, and Display / Low power mode
|
interval, a message text, a "Play Still Alive" button, and Display / Low power mode
|
||||||
switches (ENABLE, _LOWPWR).
|
switches (ENABLE, _LOWPWR).
|
||||||
@@ -277,6 +280,35 @@ The pixel art lives in [tools/steins_sprites.py](tools/steins_sprites.py) (same
|
|||||||
These pages are fan art: the pixel art and all texts are original, and no material
|
These pages are fan art: the pixel art and all texts are original, and no material
|
||||||
from the series is used. Steins;Gate is © MAGES. and its other rights holders; this project is not affiliated with or endorsed by them.
|
from the series is used. Steins;Gate is © MAGES. and its other rights holders; this project is not affiliated with or endorsed by them.
|
||||||
|
|
||||||
|
## Star Trek pages
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
- **LCARS Status**: elbows, blocks and pills around the ship's systems: power
|
||||||
|
distribution (solar, house, grid), environmental (four rooms), long range sensors
|
||||||
|
(weather), the fabrication bay (3D printer state), a stardate and a data cascade.
|
||||||
|
- **Duty Roster**: the U.S.S. Cerritos roster: Mariner, Boimler, Tendi and Rutherford
|
||||||
|
each get one of your next agenda events as their assignment (or a chore of their
|
||||||
|
own), with a scrolling shipboard notice.
|
||||||
|
- **Warp Core**: plasma pulses rush into the reaction chamber faster as solar power
|
||||||
|
rises; engineering readouts for core output, containment (what the house load leaves)
|
||||||
|
and the plasma injectors (grid), and Rutherford with advice. A grid import above 3 kW
|
||||||
|
is a core breach (it's the oven).
|
||||||
|
- **Captain's Log**: a viewscreen at warp that drops out next to a planet, over a
|
||||||
|
captain's log typed from the weather, power and the next agenda event.
|
||||||
|
|
||||||
|
**Red alert:** when the 3D printer (`printer` substitution, the Moonraker
|
||||||
|
`current_print_state` sensor) goes to `paused` or `error`, the panel jumps to LCARS
|
||||||
|
Status in red alert and holds the rotation until the printer carries on.
|
||||||
|
|
||||||
|
The pixel art lives in [tools/trek_sprites.py](tools/trek_sprites.py).
|
||||||
|
|
||||||
|
These pages are fan art: the pixel art, layouts and texts are original, and no material
|
||||||
|
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.
|
||||||
|
|
||||||
## Using the driver
|
## Using the driver
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "el512_helpers.h"
|
#include "el512_helpers.h"
|
||||||
#include "konosuba.h"
|
#include "konosuba.h"
|
||||||
#include "steins.h"
|
#include "steins.h"
|
||||||
|
#include "trek.h"
|
||||||
#include "el512_panel.h"
|
#include "el512_panel.h"
|
||||||
#include "esphome/components/display/display_buffer.h"
|
#include "esphome/components/display/display_buffer.h"
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ struct HomeData {
|
|||||||
float grid{NAN}; // W, > 0 = import
|
float grid{NAN}; // W, > 0 = import
|
||||||
std::string agenda; // sensor.el512_agenda events: "day|time|title;..."
|
std::string agenda; // sensor.el512_agenda events: "day|time|title;..."
|
||||||
std::string message; // the panel's Message text
|
std::string message; // the panel's Message text
|
||||||
|
std::string printer; // 3D printer state (Moonraker: printing, paused, error, ...)
|
||||||
Reading rooms[4];
|
Reading rooms[4];
|
||||||
|
|
||||||
// Today's solar power in 15-minute slots (highest value seen per slot; -1 =
|
// Today's solar power in 15-minute slots (highest value seen per slot; -1 =
|
||||||
|
|||||||
@@ -0,0 +1,429 @@
|
|||||||
|
#include "trek.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "el512_display.h"
|
||||||
|
#include "esphome/core/hal.h"
|
||||||
|
#include "trek_sprites.h"
|
||||||
|
|
||||||
|
namespace esphome::el512::trek {
|
||||||
|
|
||||||
|
using display::COLOR_OFF;
|
||||||
|
using display::COLOR_ON;
|
||||||
|
using display::Display;
|
||||||
|
using display::TextAlign;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using namespace fx;
|
||||||
|
|
||||||
|
constexpr int W = 512, H = 256;
|
||||||
|
const uint8_t BAYER[4][4] = {{0, 8, 2, 10}, {12, 4, 14, 6}, {3, 11, 1, 9}, {15, 7, 13, 5}};
|
||||||
|
|
||||||
|
// --- LCARS shapes --------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Bar with round ends.
|
||||||
|
void pill(Display &it, int x, int y, int w, int h, Color c = COLOR_ON) {
|
||||||
|
const int r = h / 2;
|
||||||
|
it.filled_rectangle(x + r, y, w - 2 * r, h, c);
|
||||||
|
it.filled_circle(x + r, y + r, r, c);
|
||||||
|
it.filled_circle(x + w - 1 - r, y + r, r, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lit block with a dark label at its bottom right, as LCARS buttons have.
|
||||||
|
void block(Display &it, display::BaseFont *font, int x, int y, int w, int h, const char *label) {
|
||||||
|
it.filled_rectangle(x, y, w, h);
|
||||||
|
if (label != nullptr)
|
||||||
|
it.print(x + w - 4, y + h - 1, font, COLOR_OFF, TextAlign::BOTTOM_RIGHT, label);
|
||||||
|
}
|
||||||
|
|
||||||
|
// LCARS elbow: a vertical arm (vw wide) joined to a horizontal arm (hh thick)
|
||||||
|
// by a large outer curve; the horizontal arm runs along the top (top = true)
|
||||||
|
// or the bottom of the w x h box.
|
||||||
|
void elbow(Display &it, int x, int y, int w, int h, int vw, int hh, bool top) {
|
||||||
|
const int ro = vw * 3 / 4, ri = hh;
|
||||||
|
const int by = top ? y : y + h - hh; // horizontal arm
|
||||||
|
it.filled_rectangle(x, by, w, hh);
|
||||||
|
it.filled_rectangle(x, y, vw, h);
|
||||||
|
// Round the outer corner.
|
||||||
|
const int oy = top ? y : y + h - ro;
|
||||||
|
it.filled_rectangle(x, oy, ro, ro, COLOR_OFF);
|
||||||
|
it.start_clipping(x, oy, x + ro, oy + ro);
|
||||||
|
it.filled_circle(x + ro, top ? y + ro : y + h - 1 - ro, ro);
|
||||||
|
it.end_clipping();
|
||||||
|
// Fillet the inner corner.
|
||||||
|
const int iy = top ? by + hh : by - ri;
|
||||||
|
it.filled_rectangle(x + vw, iy, ri, ri);
|
||||||
|
it.start_clipping(x + vw, iy, x + vw + ri, iy + ri);
|
||||||
|
it.filled_circle(x + vw + ri, top ? iy + ri : iy - 1, ri, COLOR_OFF);
|
||||||
|
it.end_clipping();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Horizontal readout bar: dim track and a lit pill up to frac.
|
||||||
|
void readout(Display &it, int x, int y, int w, int h, float frac) {
|
||||||
|
it.horizontal_line(x, y + h / 2, w);
|
||||||
|
if (!std::isnan(frac) && frac > 0)
|
||||||
|
pill(it, x, y, std::max(h, (int) (w * std::clamp(frac, 0.0f, 1.0f))), h);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> wrap(Display &it, display::BaseFont *font, const std::string &text, int width,
|
||||||
|
size_t max_lines) {
|
||||||
|
std::vector<std::string> lines;
|
||||||
|
std::string line;
|
||||||
|
size_t i = 0;
|
||||||
|
while (i <= text.size()) {
|
||||||
|
size_t j = text.find(' ', i);
|
||||||
|
if (j == std::string::npos)
|
||||||
|
j = text.size();
|
||||||
|
const std::string word = text.substr(i, j - i);
|
||||||
|
const std::string joined = line.empty() ? word : line + " " + word;
|
||||||
|
if (!line.empty() && text_width(it, font, joined.c_str()) > width) {
|
||||||
|
lines.push_back(line);
|
||||||
|
line = word;
|
||||||
|
} else {
|
||||||
|
line = joined;
|
||||||
|
}
|
||||||
|
i = j + 1;
|
||||||
|
}
|
||||||
|
if (!line.empty())
|
||||||
|
lines.push_back(line);
|
||||||
|
if (lines.size() > max_lines) {
|
||||||
|
lines.resize(max_lines);
|
||||||
|
lines.back() += "...";
|
||||||
|
}
|
||||||
|
for (auto &l : lines)
|
||||||
|
l = fit(font, l, width);
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string typed(const std::string &s, uint32_t t, int cps) { return s.substr(0, std::min<size_t>(s.size(), t * cps / 1000)); }
|
||||||
|
|
||||||
|
std::string kw(float w) { return std::isnan(w) ? "--" : upper(watts(w)); }
|
||||||
|
std::string deg(float v) { return std::isnan(v) ? "--" : str("%.1f", v); }
|
||||||
|
|
||||||
|
float stardate(ESPTime now) {
|
||||||
|
if (!now.is_valid())
|
||||||
|
return 58000.0f;
|
||||||
|
return 58000.0f + (now.year - 2026) * 1000.0f + (now.day_of_year - 1 + now.hour / 24.0f) * 1000.0f / 365.25f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// LCARS-style serial numbers for blocks and the data cascade.
|
||||||
|
std::string serial(uint32_t seed) { return str("%02u-%04u", (unsigned) (seed % 100), (unsigned) ((seed >> 8) % 10000)); }
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool red_alert(const HomeData &d) { return d.printer == "paused" || d.printer == "error"; }
|
||||||
|
|
||||||
|
// --- 1. LCARS ship status (red alert when the printer needs a look) ----------------------
|
||||||
|
|
||||||
|
void lcars_status(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime now) {
|
||||||
|
if (f.large == nullptr)
|
||||||
|
return;
|
||||||
|
const uint32_t t = it.page_age_ms();
|
||||||
|
const bool alert = red_alert(d);
|
||||||
|
const bool blink = (t / 500) % 2;
|
||||||
|
|
||||||
|
// Frame: two elbows, sidebar blocks between them, bars along top and bottom.
|
||||||
|
if (!alert || blink) {
|
||||||
|
elbow(it, 0, 0, 300, 100, 72, 18, true);
|
||||||
|
elbow(it, 0, 156, 300, 100, 72, 18, false);
|
||||||
|
} else {
|
||||||
|
elbow(it, 0, 0, 300, 100, 72, 6, true);
|
||||||
|
elbow(it, 0, 156, 300, 100, 72, 6, false);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < 2; i++) {
|
||||||
|
const uint32_t r = hash(i, (t / 1700) * (i + 1));
|
||||||
|
if (!(alert ? blink : r % 7 == 0))
|
||||||
|
block(it, f.pixel, 0, 104 + i * 26, 72, 22, serial(hash(i, 4)).c_str());
|
||||||
|
}
|
||||||
|
pill(it, 306, 0, 50, 18);
|
||||||
|
pill(it, 306, 238, 70, 18);
|
||||||
|
it.print(508, 22, f.medium, TextAlign::BASELINE_RIGHT, alert ? "ALERT CONDITION RED" : "SHIP STATUS");
|
||||||
|
it.printf(508, 256, f.small, TextAlign::BASELINE_RIGHT, "STARDATE %.1f", stardate(now));
|
||||||
|
|
||||||
|
if (alert) {
|
||||||
|
// Red alert: bars sweep outward from the centre around the warning.
|
||||||
|
const int cx = 292, cy = 128;
|
||||||
|
const int phase = (int) (t % 900) * 120 / 900;
|
||||||
|
for (int k = 0; k < 3; k++) {
|
||||||
|
const int s = (phase + k * 40) % 120;
|
||||||
|
it.rectangle(cx - 70 - s, cy - 30 - s / 3, 140 + 2 * s, 60 + 2 * (s / 3));
|
||||||
|
}
|
||||||
|
if ((t % 1000) < 700) {
|
||||||
|
it.filled_rectangle(cx - 110, cy - 34, 220, 68, COLOR_OFF);
|
||||||
|
it.print(cx, cy + 4, f.large, TextAlign::CENTER, "RED ALERT");
|
||||||
|
}
|
||||||
|
it.print(cx, 196, f.medium, TextAlign::TOP_CENTER, d.printer == "error" ? "3D PRINTER ERROR" : "3D PRINTER PAUSED");
|
||||||
|
it.print(cx, 222, f.small, TextAlign::TOP_CENTER, "REPORT TO THE FABRICATION BAY");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Power distribution.
|
||||||
|
it.print(84, 24, f.small, TextAlign::TOP_LEFT, "POWER DISTRIBUTION");
|
||||||
|
const float grid = d.grid;
|
||||||
|
struct Row {
|
||||||
|
const char *name;
|
||||||
|
float w;
|
||||||
|
};
|
||||||
|
const Row rows[3] = {{"SOLAR", d.solar},
|
||||||
|
{"HOUSE", d.house},
|
||||||
|
{std::isnan(grid) || grid >= 0 ? "GRID IN" : "GRID OUT", std::fabs(grid)}};
|
||||||
|
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);
|
||||||
|
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;
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clock, sensors, fabrication bay.
|
||||||
|
it.vertical_line(306, 26, 206);
|
||||||
|
if (now.is_valid())
|
||||||
|
it.strftime(316, 22, f.large, TextAlign::TOP_LEFT, "%H:%M", now);
|
||||||
|
it.print(316, 96, f.small, TextAlign::TOP_LEFT, "LONG RANGE SENSORS");
|
||||||
|
it.printf(316, 114, f.small, TextAlign::TOP_LEFT, "%s %s", deg(d.temp).c_str(), upper(weather_text(d.weather)).c_str());
|
||||||
|
if (!std::isnan(d.humidity))
|
||||||
|
it.printf(316, 132, f.small, TextAlign::TOP_LEFT, "HUMIDITY %.0f%%", d.humidity);
|
||||||
|
it.print(316, 160, f.small, TextAlign::TOP_LEFT, "FABRICATION BAY");
|
||||||
|
it.print(316, 178, f.small, TextAlign::TOP_LEFT, d.printer.empty() ? "NO SIGNAL" : upper(d.printer).c_str());
|
||||||
|
|
||||||
|
// Data cascade: columns of numbers rolling over.
|
||||||
|
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;
|
||||||
|
if (r % 5 == 0)
|
||||||
|
continue;
|
||||||
|
it.printf(x, y, f.pixel, TextAlign::TOP_LEFT, "%03u", (unsigned) (r % 1000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 2. Lower Decks duty roster ---------------------------------------------------------
|
||||||
|
|
||||||
|
void duty_roster(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime now) {
|
||||||
|
if (f.text == nullptr)
|
||||||
|
return;
|
||||||
|
const uint32_t t = it.page_age_ms();
|
||||||
|
pill(it, 0, 0, 40, 18);
|
||||||
|
it.filled_rectangle(44, 0, 160, 18);
|
||||||
|
it.print(508, 22, f.medium, TextAlign::BASELINE_RIGHT, "U.S.S. CERRITOS DUTY ROSTER");
|
||||||
|
|
||||||
|
struct Ensign {
|
||||||
|
const char *name;
|
||||||
|
const Sprite *sprite;
|
||||||
|
const char *chore;
|
||||||
|
};
|
||||||
|
static const Ensign ENSIGNS[4] = {
|
||||||
|
{"MARINER", &MARINER, "Sneak off to the holodeck before anyone notices"},
|
||||||
|
{"BOIMLER", &BOIMLER, "Reorganize the cargo bay alphabetically, again"},
|
||||||
|
{"TENDI", &TENDI, "Run a full diagnostic of the science lab"},
|
||||||
|
{"RUTHERFORD", &RUTHERFORD, "Scrub the plasma conduits on deck 12"},
|
||||||
|
};
|
||||||
|
// The next agenda events go to the ensigns, in order; the rest get chores.
|
||||||
|
const auto events = split(d.agenda, ';');
|
||||||
|
const int alarmed = (int) ((t / 5000) % 4); // who gets the "!" this time
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
const int x = i * 128, cx = x + 64;
|
||||||
|
const Ensign &e = ENSIGNS[i];
|
||||||
|
const int bob = ((t / 350 + i) % 2) ? 1 : 0;
|
||||||
|
sprite(it, *e.sprite, cx - 16, 28 - bob, 2);
|
||||||
|
if (i == alarmed && (t % 5000) > 3000) {
|
||||||
|
it.filled_rectangle(cx + 18, 26, 12, 18);
|
||||||
|
it.print(cx + 24, 35, f.medium, COLOR_OFF, TextAlign::CENTER, "!");
|
||||||
|
}
|
||||||
|
pill(it, cx - 54, 80, 108, 18);
|
||||||
|
it.print(cx, 89, f.small, COLOR_OFF, TextAlign::CENTER, e.name);
|
||||||
|
|
||||||
|
std::string title = e.chore, when = "BETA SHIFT";
|
||||||
|
if ((size_t) i < events.size()) {
|
||||||
|
title = field(events[i], 2);
|
||||||
|
const std::string day = field(events[i], 0), time = field(events[i], 1);
|
||||||
|
when = upper(day) + " " + (time.empty() ? "ALL DAY" : time);
|
||||||
|
}
|
||||||
|
if (i > 0)
|
||||||
|
it.vertical_line(x, 104, 126);
|
||||||
|
it.print(x + 8, 106, f.pixel, TextAlign::TOP_LEFT, "ASSIGNMENT");
|
||||||
|
int y = 120;
|
||||||
|
for (const auto &l : wrap(it, f.text, title, 112, 4)) {
|
||||||
|
it.print(x + 8, y, f.text, TextAlign::TOP_LEFT, l.c_str());
|
||||||
|
y += 18;
|
||||||
|
}
|
||||||
|
it.print(x + 8, 206, f.pixel, TextAlign::TOP_LEFT, "REPORT");
|
||||||
|
it.print(x + 8, 218, f.pixel, TextAlign::TOP_LEFT, fit(f.pixel, when, 112).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bottom bar with a scrolling notice.
|
||||||
|
pill(it, 0, 238, 40, 18);
|
||||||
|
const std::string notice = "ALL ENSIGNS REPORT TO YOUR STATIONS // SENIOR STAFF MEETING IN CONFERENCE ROOM 1 // "
|
||||||
|
"REMINDER: THE HOLODECK IS NOT A NAP ROOM // ";
|
||||||
|
const int span = text_width(it, f.small, notice.c_str());
|
||||||
|
const int nx = 46 - (int) ((t / 25) % (uint32_t) span);
|
||||||
|
it.start_clipping(46, 236, W, H);
|
||||||
|
it.print(nx, 256, f.small, TextAlign::BASELINE_LEFT, notice.c_str());
|
||||||
|
it.print(nx + span, 256, f.small, TextAlign::BASELINE_LEFT, notice.c_str());
|
||||||
|
it.end_clipping();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 3. Warp core and engineering ---------------------------------------------------------
|
||||||
|
|
||||||
|
void warp_core(EL512Display &it, const Fonts &f, const HomeData &d) {
|
||||||
|
if (f.large == nullptr)
|
||||||
|
return;
|
||||||
|
const uint32_t t = it.page_age_ms();
|
||||||
|
const float solar = std::isnan(d.solar) ? 0 : std::max(0.0f, d.solar);
|
||||||
|
const bool breach = !std::isnan(d.grid) && d.grid > 3000;
|
||||||
|
const float speed = 40 + std::min(solar, 5000.0f) / 5000.0f * 160 + (breach ? 120 : 0); // px/s
|
||||||
|
|
||||||
|
// The core: a column of rings with plasma pulses rushing into the chamber.
|
||||||
|
constexpr int X0 = 224, X1 = 288, CH0 = 104, CH1 = 152;
|
||||||
|
it.vertical_line(X0, 0, H);
|
||||||
|
it.vertical_line(X1, 0, H);
|
||||||
|
for (int y = 4; y < H; y += 16) {
|
||||||
|
if (y > CH0 - 8 && y < CH1 + 8)
|
||||||
|
continue;
|
||||||
|
it.horizontal_line(X0 - 6, y, X1 - X0 + 13);
|
||||||
|
it.horizontal_line(X0 + 6, y + 6, X1 - X0 - 11);
|
||||||
|
}
|
||||||
|
const uint32_t travel = (uint32_t) (t * speed / 1000);
|
||||||
|
for (int k = 0; k < 4; k++) {
|
||||||
|
const int p = (int) ((travel + k * 26) % CH0);
|
||||||
|
it.filled_rectangle(X0 + 4, p - 3, X1 - X0 - 7, 6); // from the top
|
||||||
|
it.filled_rectangle(X0 + 4, H - p - 3, X1 - X0 - 7, 6); // from the bottom
|
||||||
|
}
|
||||||
|
// Reaction chamber.
|
||||||
|
it.filled_rectangle(X0 - 16, CH0, X1 - X0 + 33, CH1 - CH0, COLOR_OFF);
|
||||||
|
it.rectangle(X0 - 16, CH0, X1 - X0 + 33, CH1 - CH0);
|
||||||
|
const int level = 7 + (int) (6 * std::sin(t * speed / 9000.0f));
|
||||||
|
for (int y = CH0 + 3; y < CH1 - 3; y++)
|
||||||
|
for (int x = X0 - 13; x < X1 + 14; x++)
|
||||||
|
if (BAYER[y & 3][x & 3] < level)
|
||||||
|
it.draw_pixel_at(x, y, COLOR_ON);
|
||||||
|
|
||||||
|
// Engineering readouts on the left.
|
||||||
|
it.print(0, 0, f.medium, TextAlign::TOP_LEFT, "ENGINEERING");
|
||||||
|
const float containment = std::isnan(d.house) ? NAN : 1.0f - std::min(d.house, 5000.0f) / 5000.0f;
|
||||||
|
struct Row {
|
||||||
|
const char *name;
|
||||||
|
std::string value;
|
||||||
|
float frac;
|
||||||
|
};
|
||||||
|
const Row rows[] = {
|
||||||
|
{"WARP CORE OUTPUT", kw(solar), solar / 5000.0f},
|
||||||
|
{"CONTAINMENT FIELD", std::isnan(containment) ? "--" : str("%.0f%%", containment * 100), containment},
|
||||||
|
{"PLASMA INJECTORS", kw(std::fabs(d.grid)), std::fabs(d.grid) / 5000.0f},
|
||||||
|
};
|
||||||
|
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(0, 172, f.small, TextAlign::TOP_LEFT, "MATTER / ANTIMATTER 1:1");
|
||||||
|
it.print(0, 192, f.small, TextAlign::TOP_LEFT, "DILITHIUM MATRIX STABLE");
|
||||||
|
it.printf(0, 212, f.small, TextAlign::TOP_LEFT, "PULSE RATE %.0f", speed);
|
||||||
|
|
||||||
|
// Rutherford with advice.
|
||||||
|
sprite(it, RUTHERFORD, 382, 158 - ((t / 400) % 2), 3);
|
||||||
|
const std::string tips[] = {
|
||||||
|
"Core's purring! The sun's giving us " + kw(solar) + ".",
|
||||||
|
"Containment's holding. Don't touch the blue button.",
|
||||||
|
"If it glows brighter, that's normal. Probably.",
|
||||||
|
"House is pulling " + kw(d.house) + ". Totally manageable.",
|
||||||
|
};
|
||||||
|
const std::string &tip = breach ? "Uh... that's a LOT of grid draw. Is the oven on?" : tips[(t / 5000) % 4];
|
||||||
|
it.rectangle(306, 6, 204, 104);
|
||||||
|
it.filled_triangle(400, 109, 420, 109, 410, 128);
|
||||||
|
it.filled_triangle(402, 108, 418, 108, 410, 124, COLOR_OFF);
|
||||||
|
int y = 14;
|
||||||
|
for (const auto &l : wrap(it, f.text, typed(tip, t % 5000, 30), 188, 5)) {
|
||||||
|
it.print(314, y, f.text, TextAlign::TOP_LEFT, l.c_str());
|
||||||
|
y += 18;
|
||||||
|
}
|
||||||
|
if (breach && (t / 300) % 2) {
|
||||||
|
it.filled_rectangle(0, 236, 300, 20);
|
||||||
|
it.print(150, 246, f.small, COLOR_OFF, TextAlign::CENTER, "CORE BREACH IMMINENT (IT'S THE OVEN)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 4. Captain's log under the viewscreen -----------------------------------------------
|
||||||
|
|
||||||
|
void captains_log(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime now) {
|
||||||
|
if (f.text == nullptr)
|
||||||
|
return;
|
||||||
|
const uint32_t t = it.page_age_ms();
|
||||||
|
constexpr uint32_t CYCLE = 18000, DROP = 11000;
|
||||||
|
const uint32_t c = t % CYCLE;
|
||||||
|
const bool warp = c < DROP;
|
||||||
|
constexpr int VX = 4, VY = 2, VW = 504, VH = 146, CX = 256, CY = 74;
|
||||||
|
|
||||||
|
// Viewscreen: stars streak past at warp, then drift once we drop out.
|
||||||
|
it.start_clipping(VX + 2, VY + 2, VX + VW - 2, VY + VH - 2);
|
||||||
|
for (int i = 0; i < 70; i++) {
|
||||||
|
const uint32_t r = hash(i, 1701);
|
||||||
|
const float a = (r % 3600) / 10.0f;
|
||||||
|
if (warp) {
|
||||||
|
const float dmod = std::fmod((r >> 12) % 300 + c * (0.12f + (r >> 20) % 10 * 0.02f), 300.0f);
|
||||||
|
int x0, y0, x1, y1;
|
||||||
|
polar(CX, CY, dmod, a, &x0, &y0);
|
||||||
|
polar(CX, CY, dmod + 4 + dmod / 5, a, &x1, &y1);
|
||||||
|
it.line(x0, y0, x1, y1);
|
||||||
|
} else {
|
||||||
|
int x, y;
|
||||||
|
polar(CX, CY, (float) ((r >> 12) % 300) + (c - DROP) * 0.004f, a, &x, &y);
|
||||||
|
it.draw_pixel_at(x, y, COLOR_ON);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!warp) {
|
||||||
|
// A planet slides in after dropping out of warp.
|
||||||
|
const int px = 600 - (int) std::min<uint32_t>(c - DROP, 4000) / 25;
|
||||||
|
it.filled_circle(px, 170, 110, COLOR_OFF);
|
||||||
|
it.circle(px, 170, 110);
|
||||||
|
for (int y = 60; y < VY + VH; y++)
|
||||||
|
for (int x = px - 110; x < px; x++)
|
||||||
|
if ((x - px) * (x - px) + (y - 170) * (y - 170) < 108 * 108 && BAYER[y & 3][x & 3] < 3)
|
||||||
|
it.draw_pixel_at(x, y, COLOR_ON);
|
||||||
|
}
|
||||||
|
it.end_clipping();
|
||||||
|
const int bob = (t / 600) % 2;
|
||||||
|
it.filled_rectangle(CX - 50, CY - 13 + bob, 100, 26, COLOR_OFF);
|
||||||
|
sprite(it, SHIP, CX - 48, CY - 12 + bob, 2);
|
||||||
|
it.rectangle(VX, VY, VW, VH);
|
||||||
|
it.rectangle(VX + 1, VY + 1, VW - 2, VH - 2);
|
||||||
|
if (c >= DROP && c < DROP + 120)
|
||||||
|
it.invert_rect(VX, VY, VW, VH); // dropping out of warp
|
||||||
|
it.print(VX + VW - 8, VY + VH - 6, f.pixel, TextAlign::BOTTOM_RIGHT, warp ? "WARP 6" : "FULL STOP");
|
||||||
|
|
||||||
|
// Captain's log, written from today's data.
|
||||||
|
pill(it, 0, 154, 30, 18);
|
||||||
|
it.printf(36, 154, f.small, TextAlign::TOP_LEFT, "CAPTAIN'S LOG, STARDATE %.1f", stardate(now));
|
||||||
|
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) + ".");
|
||||||
|
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) + ".");
|
||||||
|
const auto events = split(d.agenda, ';');
|
||||||
|
entry += events.empty() ? " No away missions are scheduled."
|
||||||
|
: " Next mission: " + field(events[0], 2) + ", " + field(events[0], 0) +
|
||||||
|
(field(events[0], 1).empty() ? "" : " " + field(events[0], 1)) + ".";
|
||||||
|
if (d.printer == "printing")
|
||||||
|
entry += " The fabrication bay is busy printing.";
|
||||||
|
int y = 178;
|
||||||
|
for (const auto &l : wrap(it, f.text, typed(entry, c, 34), 500, 4)) {
|
||||||
|
it.print(6, y, f.text, TextAlign::TOP_LEFT, l.c_str());
|
||||||
|
y += 19;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace esphome::el512::trek
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
// Star Trek / Lower Decks-themed animated pages for the ESPHome display: an
|
||||||
|
// LCARS ship status screen (with red alert when the 3D printer pauses), the
|
||||||
|
// Cerritos duty roster from the agenda, a warp core driven by solar power,
|
||||||
|
// and a captain's log under a viewscreen. Fan art: the pixel art
|
||||||
|
// (trek_sprites.h, from tools/trek_sprites.py), layouts and texts are original.
|
||||||
|
//
|
||||||
|
// Each function draws a whole page. Call it from a page lambda and list that
|
||||||
|
// page under animated_pages; the animation clock is the display's page_age_ms().
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "el512_pages.h"
|
||||||
|
#include "esphome/components/display/display.h"
|
||||||
|
#include "esphome/core/time.h"
|
||||||
|
|
||||||
|
namespace esphome::el512 {
|
||||||
|
|
||||||
|
class EL512Display;
|
||||||
|
|
||||||
|
namespace trek {
|
||||||
|
|
||||||
|
struct Fonts {
|
||||||
|
display::BaseFont *small{nullptr}; // LCARS labels (Antonio 16)
|
||||||
|
display::BaseFont *medium{nullptr}; // LCARS headers (Antonio 26)
|
||||||
|
display::BaseFont *large{nullptr}; // LCARS big numbers (Antonio 60)
|
||||||
|
display::BaseFont *text{nullptr}; // log and roster text (Nunito 16)
|
||||||
|
display::BaseFont *pixel{nullptr}; // tiny data (Press Start 2P 8)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Red alert: the 3D printer is paused or in error and needs a look.
|
||||||
|
bool red_alert(const HomeData &d);
|
||||||
|
|
||||||
|
void lcars_status(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime now);
|
||||||
|
void duty_roster(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime now);
|
||||||
|
void warp_core(EL512Display &it, const Fonts &f, const HomeData &d);
|
||||||
|
void captains_log(EL512Display &it, const Fonts &f, const HomeData &d, ESPTime now);
|
||||||
|
|
||||||
|
} // namespace trek
|
||||||
|
} // namespace esphome::el512
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
// Pixel art for the Star Trek pages, generated by tools/trek_sprites.py;
|
||||||
|
// edit the art there. Original 1-bit fan art, '#' = lit pixel.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "el512_pages.h" // Sprite
|
||||||
|
|
||||||
|
namespace esphome::el512::trek {
|
||||||
|
|
||||||
|
static const char *const MARINER_ROWS[] = {
|
||||||
|
".......##.......",
|
||||||
|
"......####......",
|
||||||
|
"....########....",
|
||||||
|
"...##########...",
|
||||||
|
"...#.#....#.#...",
|
||||||
|
"...#........#...",
|
||||||
|
"...#.##..##.#...",
|
||||||
|
"...#........#...",
|
||||||
|
"...#......#.#...",
|
||||||
|
"....#..###.#....",
|
||||||
|
".....######.....",
|
||||||
|
"....#######.....",
|
||||||
|
"..###########...",
|
||||||
|
".#############..",
|
||||||
|
".##.###.#####...",
|
||||||
|
".##.##...###.#..",
|
||||||
|
".##.#######.##..",
|
||||||
|
".##.#.....#.##..",
|
||||||
|
".#..#.....#..#..",
|
||||||
|
"....#######.....",
|
||||||
|
"....##...##.....",
|
||||||
|
"....##...##.....",
|
||||||
|
"....##...##.....",
|
||||||
|
"...###...###....",
|
||||||
|
};
|
||||||
|
static constexpr Sprite MARINER{16, 24, MARINER_ROWS};
|
||||||
|
|
||||||
|
static const char *const BOIMLER_ROWS[] = {
|
||||||
|
"....#######.....",
|
||||||
|
"...#########....",
|
||||||
|
"...######...#...",
|
||||||
|
"...#........#...",
|
||||||
|
"...#.##..##.#...",
|
||||||
|
"...#........#...",
|
||||||
|
"...#...##...#...",
|
||||||
|
"...#..####..#...",
|
||||||
|
"...#..#..#..#...",
|
||||||
|
"....#.####.#....",
|
||||||
|
".....######.....",
|
||||||
|
"....#######.....",
|
||||||
|
"..###########...",
|
||||||
|
".#############..",
|
||||||
|
".##.###.#####...",
|
||||||
|
".##.##...###.#..",
|
||||||
|
".##.#######.##..",
|
||||||
|
".##.#.....#.##..",
|
||||||
|
".#..#.....#..#..",
|
||||||
|
"....#######.....",
|
||||||
|
"....##...##.....",
|
||||||
|
"....##...##.....",
|
||||||
|
"....##...##.....",
|
||||||
|
"...###...###....",
|
||||||
|
};
|
||||||
|
static constexpr Sprite BOIMLER{16, 24, BOIMLER_ROWS};
|
||||||
|
|
||||||
|
static const char *const TENDI_ROWS[] = {
|
||||||
|
".....######.....",
|
||||||
|
"...##########...",
|
||||||
|
"..############..",
|
||||||
|
"..##........##..",
|
||||||
|
"..#..##..##..#..",
|
||||||
|
"..#..........#..",
|
||||||
|
"..#.#......#.#..",
|
||||||
|
"..#..######..#..",
|
||||||
|
"..##........##..",
|
||||||
|
"..###......###..",
|
||||||
|
"..###.####.###..",
|
||||||
|
"....#######.....",
|
||||||
|
"..###########...",
|
||||||
|
".#############..",
|
||||||
|
".##.###.#####...",
|
||||||
|
".##.##...###.#..",
|
||||||
|
".##.#######.##..",
|
||||||
|
".##.#.....#.##..",
|
||||||
|
".#..#.....#..#..",
|
||||||
|
"....#######.....",
|
||||||
|
"....##...##.....",
|
||||||
|
"....##...##.....",
|
||||||
|
"....##...##.....",
|
||||||
|
"...###...###....",
|
||||||
|
};
|
||||||
|
static constexpr Sprite TENDI{16, 24, TENDI_ROWS};
|
||||||
|
|
||||||
|
static const char *const RUTHERFORD_ROWS[] = {
|
||||||
|
"....########....",
|
||||||
|
"...##########...",
|
||||||
|
"...#.#.#.#.##...",
|
||||||
|
"..####......#...",
|
||||||
|
"..#..#.##...#...",
|
||||||
|
"..####......#...",
|
||||||
|
"..####......#...",
|
||||||
|
"...##...##..#...",
|
||||||
|
"...#..#####.#...",
|
||||||
|
"....#......#....",
|
||||||
|
".....######.....",
|
||||||
|
"....#######.....",
|
||||||
|
"..###########...",
|
||||||
|
".#############..",
|
||||||
|
".##.###.#####...",
|
||||||
|
".##.##...###.#..",
|
||||||
|
".##.#######.##..",
|
||||||
|
".##.#.....#.##..",
|
||||||
|
".#..#.....#..#..",
|
||||||
|
"....#######.....",
|
||||||
|
"....##...##.....",
|
||||||
|
"....##...##.....",
|
||||||
|
"....##...##.....",
|
||||||
|
"...###...###....",
|
||||||
|
};
|
||||||
|
static constexpr Sprite RUTHERFORD{16, 24, RUTHERFORD_ROWS};
|
||||||
|
|
||||||
|
static const char *const SHIP_ROWS[] = {
|
||||||
|
"######################...........................",
|
||||||
|
"#....................###.........................",
|
||||||
|
"######################...........................",
|
||||||
|
".....#.......#...................................",
|
||||||
|
"......#.......#...................#############..",
|
||||||
|
".......#.......#...............##################",
|
||||||
|
"........###########......#######################.",
|
||||||
|
"........#.........######.........##############..",
|
||||||
|
"........#.....................#..................",
|
||||||
|
"........#######################..................",
|
||||||
|
".........#.....#####.........#...................",
|
||||||
|
"..........######...##########....................",
|
||||||
|
};
|
||||||
|
static constexpr Sprite SHIP{49, 12, SHIP_ROWS};
|
||||||
|
|
||||||
|
} // namespace esphome::el512::trek
|
||||||
@@ -10,6 +10,7 @@ substitutions:
|
|||||||
friendly_name: EL512 Panel
|
friendly_name: EL512 Panel
|
||||||
weather: weather.knmi_home
|
weather: weather.knmi_home
|
||||||
envoy: sensor.envoy_122225067403
|
envoy: sensor.envoy_122225067403
|
||||||
|
printer: sensor.mainsailos_current_print_state # Moonraker; "paused"/"error" = red alert
|
||||||
|
|
||||||
esphome:
|
esphome:
|
||||||
name: ${name}
|
name: ${name}
|
||||||
@@ -20,6 +21,7 @@ esphome:
|
|||||||
id(ks_fonts) = {id(f_ks_title), id(f_ks_big), id(f_ks_text), id(f_ks_pixel), id(f_ks_pixel_l), id(f_ks_jp),
|
id(ks_fonts) = {id(f_ks_title), id(f_ks_big), id(f_ks_text), id(f_ks_pixel), id(f_ks_pixel_l), id(f_ks_jp),
|
||||||
id(f_ks_jp_big), id(icons_l)};
|
id(f_ks_jp_big), id(icons_l)};
|
||||||
id(sg_fonts) = {id(f_sg_nixie), id(f_sg_title), id(f_cp_mono), id(f_cp_code), id(f_ks_pixel), id(f_sg_jp)};
|
id(sg_fonts) = {id(f_sg_nixie), id(f_sg_title), id(f_cp_mono), id(f_cp_code), id(f_ks_pixel), id(f_sg_jp)};
|
||||||
|
id(st_fonts) = {id(f_st_small), id(f_st_medium), id(f_st_large), id(f_ks_text), id(f_ks_pixel)};
|
||||||
|
|
||||||
esp32:
|
esp32:
|
||||||
board: wemos_d1_mini32
|
board: wemos_d1_mini32
|
||||||
@@ -65,13 +67,15 @@ globals:
|
|||||||
- id: plex_updated
|
- id: plex_updated
|
||||||
type: uint32_t
|
type: uint32_t
|
||||||
initial_value: "0"
|
initial_value: "0"
|
||||||
# Themed pages (Cyberpunk, KonoSuba, Steins;Gate): their fonts and the home data they show.
|
# Themed pages (Cyberpunk, KonoSuba, Steins;Gate, Star Trek): their fonts and the home data they show.
|
||||||
- id: cp_fonts
|
- id: cp_fonts
|
||||||
type: el512::cyber::Fonts
|
type: el512::cyber::Fonts
|
||||||
- id: ks_fonts
|
- id: ks_fonts
|
||||||
type: el512::konosuba::Fonts
|
type: el512::konosuba::Fonts
|
||||||
- id: sg_fonts
|
- id: sg_fonts
|
||||||
type: el512::steins::Fonts
|
type: el512::steins::Fonts
|
||||||
|
- id: st_fonts
|
||||||
|
type: el512::trek::Fonts
|
||||||
- id: cp_home
|
- id: cp_home
|
||||||
type: el512::HomeData
|
type: el512::HomeData
|
||||||
|
|
||||||
@@ -213,6 +217,20 @@ font:
|
|||||||
id: f_sg_jp
|
id: f_sg_jp
|
||||||
size: 18
|
size: 18
|
||||||
glyphs: "未来ガジェット研究所世界線"
|
glyphs: "未来ガジェット研究所世界線"
|
||||||
|
# Star Trek pages: Antonio stands in for the LCARS typeface (also use
|
||||||
|
# f_ks_text and f_ks_pixel)
|
||||||
|
- file: "gfonts://Antonio@600"
|
||||||
|
id: f_st_small
|
||||||
|
size: 16
|
||||||
|
glyphsets: [GF_Latin_Core]
|
||||||
|
- file: "gfonts://Antonio@600"
|
||||||
|
id: f_st_medium
|
||||||
|
size: 26
|
||||||
|
glyphsets: [GF_Latin_Core]
|
||||||
|
- file: "gfonts://Antonio@700"
|
||||||
|
id: f_st_large
|
||||||
|
size: 60
|
||||||
|
glyphs: " 0123456789:ADELRT"
|
||||||
|
|
||||||
# --- Data from Home Assistant -------------------------------------------------
|
# --- Data from Home Assistant -------------------------------------------------
|
||||||
text_sensor:
|
text_sensor:
|
||||||
@@ -245,6 +263,9 @@ text_sensor:
|
|||||||
attribute: streams
|
attribute: streams
|
||||||
on_value:
|
on_value:
|
||||||
- lambda: id(plex_updated) = millis();
|
- lambda: id(plex_updated) = millis();
|
||||||
|
- platform: homeassistant
|
||||||
|
id: printer_state
|
||||||
|
entity_id: ${printer}
|
||||||
- platform: homeassistant
|
- platform: homeassistant
|
||||||
id: plex_update
|
id: plex_update
|
||||||
entity_id: update.plex_media_server_mangavalkserver
|
entity_id: update.plex_media_server_mangavalkserver
|
||||||
@@ -414,6 +435,10 @@ select:
|
|||||||
- D-Mail
|
- D-Mail
|
||||||
- Gadget Lab
|
- Gadget Lab
|
||||||
- World Lines
|
- World Lines
|
||||||
|
- LCARS Status
|
||||||
|
- Duty Roster
|
||||||
|
- Warp Core
|
||||||
|
- Captain's Log
|
||||||
initial_option: Clock
|
initial_option: Clock
|
||||||
set_action:
|
set_action:
|
||||||
- lambda: |-
|
- lambda: |-
|
||||||
@@ -438,6 +463,10 @@ select:
|
|||||||
else if (x == "D-Mail") p = id(page_sg_dmail);
|
else if (x == "D-Mail") p = id(page_sg_dmail);
|
||||||
else if (x == "Gadget Lab") p = id(page_sg_lab);
|
else if (x == "Gadget Lab") p = id(page_sg_lab);
|
||||||
else if (x == "World Lines") p = id(page_sg_world);
|
else if (x == "World Lines") p = id(page_sg_world);
|
||||||
|
else if (x == "LCARS Status") p = id(page_st_lcars);
|
||||||
|
else if (x == "Duty Roster") p = id(page_st_roster);
|
||||||
|
else if (x == "Warp Core") p = id(page_st_warp);
|
||||||
|
else if (x == "Captain's Log") p = id(page_st_log);
|
||||||
id(panel).show_page(p);
|
id(panel).show_page(p);
|
||||||
id(panel).update();
|
id(panel).update();
|
||||||
|
|
||||||
@@ -456,6 +485,13 @@ interval:
|
|||||||
- interval: 1s
|
- interval: 1s
|
||||||
then:
|
then:
|
||||||
- lambda: |-
|
- lambda: |-
|
||||||
|
// Red alert (the 3D printer paused or failed): jump to LCARS once,
|
||||||
|
// then hold the rotation until the printer carries on.
|
||||||
|
static bool was_alert = false;
|
||||||
|
const bool alert = el512::trek::red_alert(id(cp_home));
|
||||||
|
if (alert && !was_alert) { id(panel).show_page(id(page_st_lcars)); id(panel).update(); }
|
||||||
|
was_alert = alert;
|
||||||
|
if (alert) { id(rotation_elapsed) = 0; return; }
|
||||||
if (id(sw_pause).state || id(panel).is_still_alive_playing()) { id(rotation_elapsed) = 0; return; }
|
if (id(sw_pause).state || id(panel).is_still_alive_playing()) { id(rotation_elapsed) = 0; return; }
|
||||||
if (++id(rotation_elapsed) < (int) id(page_interval).state) return;
|
if (++id(rotation_elapsed) < (int) id(page_interval).state) return;
|
||||||
id(rotation_elapsed) = 0;
|
id(rotation_elapsed) = 0;
|
||||||
@@ -470,7 +506,7 @@ interval:
|
|||||||
id(page_plex), id(page_solar), id(page_cyber_hud), id(page_cyber_breach), id(page_cyber_scan),
|
id(page_plex), id(page_solar), id(page_cyber_hud), id(page_cyber_breach), id(page_cyber_scan),
|
||||||
id(page_cyber_split), id(page_ks_explosion), id(page_ks_guild), id(page_ks_card),
|
id(page_cyber_split), id(page_ks_explosion), id(page_ks_guild), id(page_ks_card),
|
||||||
id(page_ks_battle), id(page_sg_meter), id(page_sg_dmail), id(page_sg_lab), id(page_sg_world),
|
id(page_ks_battle), id(page_sg_meter), id(page_sg_dmail), id(page_sg_lab), id(page_sg_world),
|
||||||
id(page_still_alive)};
|
id(page_st_lcars), id(page_st_roster), id(page_st_warp), id(page_st_log), id(page_still_alive)};
|
||||||
for (size_t i = order.size() - 1; i > 0; i--)
|
for (size_t i = order.size() - 1; i > 0; i--)
|
||||||
std::swap(order[i], order[random_uint32() % (i + 1)]);
|
std::swap(order[i], order[random_uint32() % (i + 1)]);
|
||||||
pos = order.size() - 1; // the next step starts at the front
|
pos = order.size() - 1; // the next step starts at the front
|
||||||
@@ -498,6 +534,7 @@ interval:
|
|||||||
d.solar_today = id(solar_today).state;
|
d.solar_today = id(solar_today).state;
|
||||||
d.agenda = id(agenda_events).state;
|
d.agenda = id(agenda_events).state;
|
||||||
d.message = id(panel_message).state;
|
d.message = id(panel_message).state;
|
||||||
|
d.printer = id(printer_state).state;
|
||||||
d.house = id(house_power).state;
|
d.house = id(house_power).state;
|
||||||
d.grid = id(grid_import).state - id(grid_export).state;
|
d.grid = id(grid_import).state - id(grid_export).state;
|
||||||
d.rooms[0] = {"Woonkamer", id(t_woonkamer).state};
|
d.rooms[0] = {"Woonkamer", id(t_woonkamer).state};
|
||||||
@@ -517,7 +554,8 @@ display:
|
|||||||
# Redrawn every animation_interval (~15 fps) while shown.
|
# Redrawn every animation_interval (~15 fps) while shown.
|
||||||
animated_pages: [page_cyber_hud, page_cyber_breach, page_cyber_scan, page_cyber_split,
|
animated_pages: [page_cyber_hud, page_cyber_breach, page_cyber_scan, page_cyber_split,
|
||||||
page_ks_explosion, page_ks_guild, page_ks_card, page_ks_battle,
|
page_ks_explosion, page_ks_guild, page_ks_card, page_ks_battle,
|
||||||
page_sg_meter, page_sg_dmail, page_sg_lab, page_sg_world]
|
page_sg_meter, page_sg_dmail, page_sg_lab, page_sg_world,
|
||||||
|
page_st_lcars, page_st_roster, page_st_warp, page_st_log]
|
||||||
animation_interval: 66ms
|
animation_interval: 66ms
|
||||||
on_page_change:
|
on_page_change:
|
||||||
- then:
|
- then:
|
||||||
@@ -542,6 +580,10 @@ display:
|
|||||||
else if (to == id(page_sg_dmail)) name = "D-Mail";
|
else if (to == id(page_sg_dmail)) name = "D-Mail";
|
||||||
else if (to == id(page_sg_lab)) name = "Gadget Lab";
|
else if (to == id(page_sg_lab)) name = "Gadget Lab";
|
||||||
else if (to == id(page_sg_world)) name = "World Lines";
|
else if (to == id(page_sg_world)) name = "World Lines";
|
||||||
|
else if (to == id(page_st_lcars)) name = "LCARS Status";
|
||||||
|
else if (to == id(page_st_roster)) name = "Duty Roster";
|
||||||
|
else if (to == id(page_st_warp)) name = "Warp Core";
|
||||||
|
else if (to == id(page_st_log)) name = "Captain's Log";
|
||||||
if (id(page_select).current_option() != name) id(page_select).publish_state(name);
|
if (id(page_select).current_option() != name) id(page_select).publish_state(name);
|
||||||
pages:
|
pages:
|
||||||
# 1. Clock, date and current weather
|
# 1. Clock, date and current weather
|
||||||
@@ -797,3 +839,17 @@ display:
|
|||||||
- id: page_sg_world
|
- id: page_sg_world
|
||||||
lambda: |-
|
lambda: |-
|
||||||
el512::steins::world_lines(*id(panel), id(sg_fonts), id(cp_home), id(ha_time).now());
|
el512::steins::world_lines(*id(panel), id(sg_fonts), id(cp_home), id(ha_time).now());
|
||||||
|
|
||||||
|
# 21-24. Star Trek / Lower Decks pages (components/el512/trek.cpp)
|
||||||
|
- id: page_st_lcars
|
||||||
|
lambda: |-
|
||||||
|
el512::trek::lcars_status(*id(panel), id(st_fonts), id(cp_home), id(ha_time).now());
|
||||||
|
- id: page_st_roster
|
||||||
|
lambda: |-
|
||||||
|
el512::trek::duty_roster(*id(panel), id(st_fonts), id(cp_home), id(ha_time).now());
|
||||||
|
- id: page_st_warp
|
||||||
|
lambda: |-
|
||||||
|
el512::trek::warp_core(*id(panel), id(st_fonts), id(cp_home));
|
||||||
|
- id: page_st_log
|
||||||
|
lambda: |-
|
||||||
|
el512::trek::captains_log(*id(panel), id(st_fonts), id(cp_home), id(ha_time).now());
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Pixel art for the Star Trek / Lower Decks pages (components/el512/trek.cpp).
|
||||||
|
|
||||||
|
Original 1-bit fan art: '#' is a lit pixel. Writes
|
||||||
|
components/el512/trek_sprites.h and, with --preview, a magnified PNG.
|
||||||
|
|
||||||
|
python tools/trek_sprites.py
|
||||||
|
python tools/trek_sprites.py --preview sprites.png
|
||||||
|
"""
|
||||||
|
|
||||||
|
from konosuba_sprites import main, padded
|
||||||
|
|
||||||
|
# Starfleet uniform shared by the ensigns: lit shoulders, combadge on the chest.
|
||||||
|
BODY = [
|
||||||
|
"....#######.....",
|
||||||
|
"..###########...",
|
||||||
|
".#############..",
|
||||||
|
".##.###.#####...",
|
||||||
|
".##.##...###.#..",
|
||||||
|
".##.#######.##..",
|
||||||
|
".##.#.....#.##..",
|
||||||
|
".#..#.....#..#..",
|
||||||
|
"....#######.....",
|
||||||
|
"....##...##.....",
|
||||||
|
"....##...##.....",
|
||||||
|
"....##...##.....",
|
||||||
|
"...###...###....",
|
||||||
|
]
|
||||||
|
|
||||||
|
SPRITES = {
|
||||||
|
# Mariner: hair up in a bun, a smirk.
|
||||||
|
"MARINER": [
|
||||||
|
".......##.......",
|
||||||
|
"......####......",
|
||||||
|
"....########....",
|
||||||
|
"...##########...",
|
||||||
|
"...#.#....#.#...",
|
||||||
|
"...#........#...",
|
||||||
|
"...#.##..##.#...",
|
||||||
|
"...#........#...",
|
||||||
|
"...#......#.#...",
|
||||||
|
"....#..###.#....",
|
||||||
|
".....######.....",
|
||||||
|
] + BODY,
|
||||||
|
# Boimler: neat side part, worried open mouth.
|
||||||
|
"BOIMLER": [
|
||||||
|
"....#######.....",
|
||||||
|
"...#########....",
|
||||||
|
"...######...#...",
|
||||||
|
"...#........#...",
|
||||||
|
"...#.##..##.#...",
|
||||||
|
"...#........#...",
|
||||||
|
"...#...##...#...",
|
||||||
|
"...#..####..#...",
|
||||||
|
"...#..#..#..#...",
|
||||||
|
"....#.####.#....",
|
||||||
|
".....######.....",
|
||||||
|
] + BODY,
|
||||||
|
# Tendi: long hair down both sides, big smile.
|
||||||
|
"TENDI": [
|
||||||
|
".....######.....",
|
||||||
|
"...##########...",
|
||||||
|
"..############..",
|
||||||
|
"..##........##..",
|
||||||
|
"..#..##..##..#..",
|
||||||
|
"..#..........#..",
|
||||||
|
"..#.#......#.#..",
|
||||||
|
"..#..######..#..",
|
||||||
|
"..##........##..",
|
||||||
|
"..###......###..",
|
||||||
|
"..###.####.###..",
|
||||||
|
] + BODY,
|
||||||
|
# Rutherford: cybernetic implant over his left eye and temple.
|
||||||
|
"RUTHERFORD": [
|
||||||
|
"....########....",
|
||||||
|
"...##########...",
|
||||||
|
"...#.#.#.#.##...",
|
||||||
|
"..####......#...",
|
||||||
|
"..#..#.##...#...",
|
||||||
|
"..####......#...",
|
||||||
|
"..####......#...",
|
||||||
|
"...##...##..#...",
|
||||||
|
"...#..#####.#...",
|
||||||
|
"....#......#....",
|
||||||
|
".....######.....",
|
||||||
|
] + BODY,
|
||||||
|
# A Starfleet ship in profile, heading right: nacelle, pylons, engineering
|
||||||
|
# hull and saucer.
|
||||||
|
"SHIP": [
|
||||||
|
"######################..........................",
|
||||||
|
"#....................###........................",
|
||||||
|
"######################..........................",
|
||||||
|
".....#.......#..................................",
|
||||||
|
"......#.......#...................#############.",
|
||||||
|
".......#.......#...............##################",
|
||||||
|
"........###########......#######################.",
|
||||||
|
"........#.........######.........##############..",
|
||||||
|
"........#.....................#..................",
|
||||||
|
"........#######################..................",
|
||||||
|
".........#.....#####.........#...................",
|
||||||
|
"..........######...##########....................",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
for _name in SPRITES:
|
||||||
|
SPRITES[_name] = padded(SPRITES[_name])
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main(SPRITES, "Star Trek", "trek_sprites.py", "trek", __doc__)
|
||||||
Reference in new issue
Block a user