epaper42.tc¶
epaper42.tc — 4.2" e-paper full-house dashboard.
// ═════════════════════════════════════════════════════════════════════
// epaper42.tc — 4.2" e-paper full-house dashboard.
//
// Receives smart-meter / Powerwall / inverter / outdoor-sensor values
// from other house devices via Tasmota's UDP multicast — every
// `global float` declaration below auto-syncs on read AND on write
// (receive-only names just sit and wait for incoming packets;
// publish names get broadcast back). Renders the lot onto a 400×300
// e-paper panel via uDisplay with two scrolling 7-day graphs
// (Powerwall % and solar W), plus a Tasmota WebUI sensor table via
// WebCall for the same data.
//
// Callback shape: EverySecond + WebCall + TaskLoop. SHT31 values are
// pulled via `sensorGet("SHT3X#Temperature")` once per EverySecond —
// safe because all callbacks on the same slot serialise through the
// VM mutex. (An earlier diagnostic port did sensorGet from inside the
// TaskLoop worker — that hung the device because the worker raced
// EverySecond on the same VM mutex while MqttShowSensor fanned the
// JsonCall out to all slots. Don't move the call back into TaskLoop.)
//
// The `>S` / `>B` blocks of the original Scripter dashboard port
// 1:1 here: sprintf the variable parts into a buffer, hand the
// bracketed uDisplay markup to dspText(); persist arrays replace
// the Scripter's M:array=0 N declarations.
//
// Refresh strategy — IMPORTANT:
// The 4.2" panel is a FULL-refresh device. Each `[d]` flush takes
// roughly 3-5 s while the display driver clocks the framebuffer out
// via SPI and waits for BUSY. To avoid that blocking call living on
// Tasmota's main loop task (which would trip the loop watchdog after
// enough cumulative blocks → software reset), the actual rendering
// runs in a worker — TaskLoop on its own FreeRTOS task. EverySecond
// just raises a `display_dirty` flag once per minute; TaskLoop sees
// the flag, clears it, runs update_display(). The TinyC VM mutex is
// released around long syscalls so EverySecond / WebCall / MQTT
// keep firing on the loop task throughout the [d] flush.
//
// First port of this dashboard ran update_display() inline from
// EverySecond and software-reset roughly once per day — same cause
// (loop-task held too long during [d]). Worker-task split fixed it.
// ═════════════════════════════════════════════════════════════════════
// ─── House-wide UDP-shared globals ───────────────────────────────────
//
// Every `global float` declaration auto-syncs via Tasmota's UDP
// multicast (239.255.255.250:1999). Values published by other devices
// land here on the next packet; values we set here are broadcast back.
// Initial values are sane defaults until the first packet arrives.
//
// All names match exactly what the publishing devices send — DON'T
// rename without coordinating across the house. Receive-only globals
// (most of them) just sit and wait; publish globals (btemp/bhumi)
// are written from EverySecond's sensor read below.
// ── Tesla Powerwall (.99) ──
global float pwl = 0.0; // battery percent
global float sip = 0.0; // grid in (W; negative = exporting)
global float sop = 0.0; // solar power (W)
global float bip = 0.0; // battery in (W; negative = discharging)
global float hip = 0.0; // house power (W)
global float tcap = 0.0; // total capacity (Wh)
global float rcap = 0.0; // remaining capacity (Wh)
global float rper = 0.0; // remaining percent
// ── Solar / inverters / wallbox ──
global float sedc = 0.0; // Dach Solar DC (W) — current
global float sedt = 0.0; // Dach total Einspeisung (kWh) — counter
global float wrgh = 0.0; // Wechselrichter Gartenhaus (W)
global float wrga = 0.0; // Wechselrichter Garage (W)
global float wrgg = 0.0; // Wechselrichter Garten (W)
global float auto = 0.0; // Wallbox aktuell (W)
global float ssp = 0.0; // Solarspeicher (°C)
// ── Smart-meter (Hauptzähler) ──
global float zwzc = 0.0; // current draw (W; signed)
global float zwzi = 0.0; // consumption total (kWh)
global float zwzo = 0.0; // export total (kWh)
// ── Outdoor (Bresser) ──
global float atmp = 0.0; // outside temperature (°C)
// ── This device's local sensor — published to the rest of the house ──
// SHT31 on I2C, read in EverySecond below. The originals from the
// Scripter version (`g:btemp`, `g:bhumi`, `g:bpress`) were also
// publish-from-this-device globals, so name + semantics carry over 1:1.
global float btemp = 20.0; // room temperature (°C)
global float bhumi = 50.0; // room humidity (%)
// global float bpress = 1013.0; // BMP280 commented out in original; skip
global float ledbar;
// ─── Sensor readiness flags ──────────────────────────────────────────
// SHT31 may not be present at boot (i2cExists race) — start as
// "unknown", flip to 1 on first successful read, 0 on persistent
// failure so the WebUI can show a clear "—" instead of a stale 20.0.
int sht_seen = 0;
// ─── Daily/weekly counters (persist + Scripter-format /wd_log.txt) ───
//
// Three 8-element arrays mirroring the Scripter's `M:mez1=0 7` etc.:
// mez1[1..7] — daily Dach Einspeisung delta per weekday (kWh)
// mezh[1..7] — daily Hauszähler Einspeisung delta (kWh)
// mvzh[1..7] — daily Hauszähler Verbrauch delta (kWh)
// Index 0 holds the "current weekday pointer" (= tasm_wday - 1, with
// 0 → 7 wraparound) so a future WebChart call can use it as the ring-
// buffer head — same convention as the Scripter and as epaper29.tc.
//
// The `persist` keyword auto-loads from /epaper42.pvs across
// TinyCStop+TinyCRun cycles and reboots; load_wd_log() additionally
// reads the legacy Scripter file `/wd_log.txt` at boot so the
// historical 7-day window from before the TinyC migration carries
// over unchanged. save_wd_log() writes back at midnight to keep the
// file in sync (lets users fall back to Scripter or hand-edit).
//
// sezh / svzh / sez1 are the absolute-counter snapshots taken at the
// last midnight; "today's delta" = current zwzo - sezh, etc. Used in
// update_display()'s column 3.
persist float mez1[8];
persist float mezh[8];
persist float mvzh[8];
persist float sezh = 0.0;
persist float svzh = 0.0;
persist float sez1 = 0.0;
// Last seen hour — used to detect the 23 → 0 transition for the
// midnight rollover. -1 means "haven't seen any hour yet".
int last_hr = -1;
// Worker-task signal flag. EverySecond raises it on the minute
// boundary; TaskLoop notices it, clears it, and runs update_display()
// from a separate FreeRTOS task so the 3-5 s `[d]` SPI dump no
// longer holds Tasmota's main loop task (which would trip Tasmota's
// loop-watchdog after enough cumulative blocks → software reset).
//
// Both tasks read/write the flag under the TinyC VM mutex (which
// serialises callback dispatch), so the volatile semantics come for
// free — no atomic primitives needed at the language level.
int display_dirty = 0;
// Read the Scripter-format /wd_log.txt into the mez*/mezh/mvzh arrays.
// File layout (tab-separated, 7 rows):
// <mez1>\t<mezh>\t<mvzh>\n × 7 (rows 0..6 → indices [1..7])
// Missing or short file → leftover persist values remain (no zeroing).
// Called once from main(); EverySecond writes back via save_wd_log()
// at midnight.
void load_wd_log() {
if (!fileExists("/wd_log.txt")) return;
char buf[256];
int f = fileOpen("/wd_log.txt", r);
if (f < 0) return;
int n = fileRead(f, buf, 255);
fileClose(f);
if (n <= 0) return;
buf[n] = 0;
char rowbuf[64];
char fldbuf[24];
for (int row = 0; row < 7; row = row + 1) {
strToken(rowbuf, buf, '\n', row);
if (strlen(rowbuf) == 0) continue;
strToken(fldbuf, rowbuf, '\t', 0);
if (strlen(fldbuf) > 0) mez1[row + 1] = atof(fldbuf);
strToken(fldbuf, rowbuf, '\t', 1);
if (strlen(fldbuf) > 0) mezh[row + 1] = atof(fldbuf);
strToken(fldbuf, rowbuf, '\t', 2);
if (strlen(fldbuf) > 0) mvzh[row + 1] = atof(fldbuf);
}
}
// Mirror of the Scripter `#savgraf` write loop. Called once at the
// midnight rollover (after the new day's deltas are stored) so the
// legacy /wd_log.txt file always reflects the latest 7-day window.
void save_wd_log() {
char buf[256];
char tmp[40];
buf[0] = 0;
for (int row = 1; row <= 7; row = row + 1) {
sprintf(tmp, "%.3f\t%.3f\t%.3f\n", mez1[row], mezh[row], mvzh[row]);
strcat(buf, tmp);
}
int f = fileOpen("/wd_log.txt", w);
if (f >= 0) {
fileWrite(f, buf, strlen(buf));
fileClose(f);
}
}
// ═══════════════ BEGIN CLOCK HEADER BLOCK ═══════════════════════════
// Pasted from examples/clock_header.tc — see that file for the
// rationale and the EN-localised version. Required: a `char scratch[256]`
// buffer in scope (declared in WebCall below).
int web_clock_tick = 0; // increments per WebCall poll
void web_clock_header() {
web_clock_tick = web_clock_tick + 1;
char wd_names[] = "So|Mo|Di|Mi|Do|Fr|Sa";
char mo_names[] = "Jan|Feb|Mar|Apr|Mai|Jun|Jul|Aug|Sep|Okt|Nov|Dez";
char wd_label[4];
char mo_label[4];
strToken(wd_label, wd_names, '|', tasm_wday);
strToken(mo_label, mo_names, '|', tasm_month);
char scratch[256];
sprintf(scratch, "<tr><td colspan=2 style='text-align:center;background:#333;padding:8px;border-radius:8px'><span style='color:green;font-size:40px;font-weight:bold'>%02d:%02d:%02d</span><br>%s %d. %s %d <span style='font-size:0.7em;color:#888;'>● %d</span><br>",
tasm_hour, tasm_minute, tasm_second,
wd_label, tasm_day, mo_label, tasm_year, web_clock_tick);
webSend(scratch);
int sr = tasm_sunrise;
int ss = tasm_sunset;
int dl = ss - sr;
sprintf(scratch, "🌞 %02d:%02d <--- %02d:%02d ---> %02d:%02d 🌙</td></tr>",
sr / 60, sr % 60,
dl / 60, dl % 60,
ss / 60, ss % 60);
webSend(scratch);
}
// ═══════════════ END CLOCK HEADER BLOCK ═════════════════════════════
// ─── update_display — port of the original Scripter `>S` dt block ────
//
// Defined before EverySecond so the single-pass TinyC compiler sees
// the symbol at the call site. Layout (400×300 e-paper, anchored
// top-left, y-down):
// y= 0.. 19 header row: btemp ─ horizontal line ─ bhumi
// y= 20.. 119 graph 0 (Powerwall % over 7 days)
// y=120.. 219 graph 1 (Solar power W over 7 days)
// y=220.. 299 4-line counter table (WR / Einsp / Verbr / DachEinsp)
// + outside-temp readout in the right column
//
// The `[…]` strings are uDisplay markup interpreted by the display
// driver — pass-through from the Scripter version, only the variable
// substitutions (`%var%` → sprintf %f) change. Precision matches the
// original: dp2 for kWh / btemp / bhumi, dp1 for daily increments,
// dp0 for W and percent values.
//
// No `[Id]` (full clear) here — that's done once at boot. Subsequent
// updates use the panel's existing buffer, just overwriting the dirty
// regions, then `[d]` triggers ONE full refresh of the whole panel.
// On the 4.2" SSD1683-style controller this is ~3-5 s; the Scripter
// has run that cadence for years without watchdog issues.
void update_display() {
char dt[128];
// Row 1 — temperature, separator line, humidity. The Scripter mixed
// multiple primitives into one `[…][…]` string; we keep that style
// so the markup matches the reference 1:1.
sprintf(dt, "[f1p7x0y5]%.2f C", btemp);
dspText(dt);
dspText("[x0y20h400x250y5T][x350t]");
sprintf(dt, "[f1p10x70y5]%.2f %%", bhumi);
dspText(dt);
// bpress on .51 was BMP280-only and isn't published from any house
// device anymore — original Scripter line `dt [p10x140y5]%bpress%
// hPa` left out intentionally so we don't render a stale 0.
// Big-number readouts on the right (Powerwall % / Solar W) and
// the rolling-graph append. Sticks to the original's [g0:Vg1:V]
// pair-syntax — the display driver dispatches the two separate
// graph appends from the one bracketed call.
sprintf(dt, "[p5x360y75]%.0f %%", pwl);
dspText(dt);
sprintf(dt, "[p6x360y180]%.0fW", sedc);
dspText(dt);
sprintf(dt, "[g0:%.0fg1:%.0f]", pwl, sedc);
dspText(dt);
// 4 inverters on one line (Dach / Garage / Gartenhaus / Garten).
// The Scripter used `%-wrga%` etc. to flip sign — wrga/wrgh/wrgg
// are stored as negative-going (consumption-as-positive convention)
// so we negate to display as positive solar production.
sprintf(dt, "[p40x60y230] %.0f W : %.0f W : %.0f W : %.0f W",
sedc, -wrga, -wrgh, -wrgg);
dspText(dt);
// 3-row counter table.
// col 1 (x=75) today's absolute total (running counter)
// col 2 (x=150) 7-day average (sum / 7) — kWh, dp2
// col 3 (x=250) today's delta vs. last midnight snapshot — dp1
// Indices 1..7 hold the 7 weekday daily-deltas; index 0 is the
// ring-buffer pointer (set in EverySecond), not a value.
sprintf(dt, "[p-10x75y245]%.2f kWh", zwzo);
dspText(dt);
sprintf(dt, "[p-10x75y260]%.2f kWh", zwzi);
dspText(dt);
sprintf(dt, "[p-10x75y275]%.2f kWh", sedt);
dspText(dt);
// Column 2 — weekly average (Scripter: `t1=mezh[1]+...+mezh[7]/7`).
// The Scripter's broken precedence (`a+b+c+d+e+f+g/7` evaluates the
// /7 only on g) was probably unintentional but is the visible
// behaviour the user has lived with for years. We do the math
// properly here — sum then divide.
float t1;
t1 = (mezh[1] + mezh[2] + mezh[3] + mezh[4] + mezh[5] + mezh[6] + mezh[7]) / 7.0;
sprintf(dt, "[p-10x150y245]: %.2f kWh", t1);
dspText(dt);
t1 = (mvzh[1] + mvzh[2] + mvzh[3] + mvzh[4] + mvzh[5] + mvzh[6] + mvzh[7]) / 7.0;
sprintf(dt, "[p-10x150y260]: %.2f kWh", t1);
dspText(dt);
t1 = (mez1[1] + mez1[2] + mez1[3] + mez1[4] + mez1[5] + mez1[6] + mez1[7]) / 7.0;
sprintf(dt, "[p-10x150y275]: %.2f kWh", t1);
dspText(dt);
// Column 3 — today's delta against last midnight snapshot.
sprintf(dt, "[p12x250y245]: %.1f kWh", zwzo - sezh);
dspText(dt);
sprintf(dt, "[p12x250y260]: %.1f kWh", zwzi - svzh);
dspText(dt);
sprintf(dt, "[p12x250y275]: %.1f kWh", sedt - sez1);
dspText(dt);
// Outside temp at x=320 in the lower band — sized larger via [f2]
// to fill the otherwise-empty bottom-right cell.
sprintf(dt, "[f2p5x320y250] %.0fC", atmp);
dspText(dt);
// Trigger panel refresh. This is the blocking call — everything
// above just buffers; `[d]` starts the SPI dump + waits for BUSY
// to clear. ~3-5 s on a 4.2" full-refresh; nothing else runs in
// the loop task during that.
dspText("[d]");
}
// ─── EverySecond — drives the e-paper update gate ─────────────────────
//
// btemp/bhumi are NOT read here (no sensorGet). They land via UDP from
// sht31_publisher.tc (slot 5 on this same device). Falling back to the
// publisher avoids the JsonCall fan-out that hung v2.
//
// es_ticks / disp_updates surface in WebCall's diag row so a glance
// at the Tasmota main page tells you whether EverySecond is alive
// AND how many display refreshes have run.
int es_ticks = 0;
int disp_updates = 0;
int last_disp_min = -1;
void EverySecond() {
es_ticks = es_ticks + 1;
// ── Local SHT31 read via Tasmota SensorJSON ──
// Slot 5 (sht31.tcb) publishes SHT3X.Temperature / SHT3X.Humidity
// through its JsonCall. sensorGet parses the live SensorJSON and
// returns the numeric value (NAN if the key is missing).
// Earlier ports hung the device when this was done from a TaskLoop
// worker — sensorGet → MqttShowSensor → JsonCall fan-out raced the
// worker against EverySecond on the same VM. Running it from
// EverySecond (one fixed call site, serialised with all other
// callbacks via the VM mutex) is the supported pattern.
float t = sensorGet("SHT3X#Temperature");
float h = sensorGet("SHT3X#Humidity");
if (t > -100.0 && t < 200.0) { // sanity-check, NaN compares false
btemp = t;
bhumi = h;
sht_seen = 1;
}
// ── Midnight rollover (Scripter: `if chg[hr]>0 and hr==0`) ──
// Detect the 23 → 0 hour transition. On the first tick after boot
// last_hr is -1, so we don't fire a spurious rollover unless we
// happened to boot exactly at 00:xx — in that edge case sezh has
// either been carried over from .pvs (correct delta) or starts
// at 0 (the bootstrap branch below seeds it to current zwzo etc.
// so the very next midnight produces a meaningful delta).
if (last_hr != 0 && tasm_hour == 0) {
// Yesterday's weekday: Scripter's `tmp=wday-1; if tmp==0 tmp=7`.
// Mon (wday=2) → tmp=1; Sun (wday=1) → tmp=7.
int dow = tasm_wday - 1;
if (dow == 0) dow = 7;
mezh[dow] = zwzo - sezh;
sezh = zwzo;
mvzh[dow] = zwzi - svzh;
svzh = zwzi;
mez1[dow] = sedt - sez1;
sez1 = sedt;
save_wd_log();
saveVars(); // flush .pvs immediately so a crash
// before next TinyCStop doesn't lose
// the new day's snapshot.
addLog("midnight rollover: arrays + /wd_log.txt updated");
}
last_hr = tasm_hour;
// First-run bootstrap: if sezh is still zero (fresh install, no
// .pvs, no /wd_log.txt to migrate from) AND we have non-zero
// counter values from the UDP-shared globals, snapshot them so
// tomorrow's daily delta starts measuring from "now". Once seeded
// this branch never fires again because sezh != 0.
if (sezh == 0.0 && zwzo > 0.0) {
sezh = zwzo;
svzh = zwzi;
sez1 = sedt;
}
// Keep the [0]-element ring pointer in sync for any future
// WebChart calls (the chart API uses arr[0] as the head index).
int dow_now = tasm_wday - 1;
if (dow_now == 0) dow_now = 7;
mezh[0] = (float)dow_now;
mvzh[0] = (float)dow_now;
mez1[0] = (float)dow_now;
// Match the Scripter's `if upsecs%60==0` gate. tasm_uptime is in
// seconds since Tasmota boot; once a minute we just RAISE the
// display-dirty flag. The actual update_display() call (with its
// 3-5 s blocking `[d]` flush) runs in TaskLoop on a dedicated
// worker task, so Tasmota's main loop keeps yielding within its
// watchdog window — no more cumulative-block software resets.
int now_min = tasm_uptime / 60;
if (now_min != last_disp_min && tasm_uptime > 0) {
last_disp_min = now_min;
display_dirty = 1;
}
// Periodic graph save — every 5 min, Scripter ported from `>S`'s
// `if upsecs%300==0 then =#savgraf endif` block. Two trivial dt
// strings, file I/O happens inside the display driver.
if (tasm_uptime > 0 && tasm_uptime % 300 == 0) {
dspText("[Gs0:/g0_sav.txt:]");
dspText("[Gs1:/g1_sav.txt:]");
}
ledbar=0-(wrgh+wrga+wrgg);
// otmp=abs(zwzc)
// if otmp<80
// then
// ledbar=tmp
// else
// ledbar=-zwzc
// endif
}
// ─── TaskLoop — runs update_display() off the main loop task ─────────
//
// Lives on its own FreeRTOS task with a 5 KB stack (TinyC's default
// is fine — no HTTP / TLS / JSON parsing here). Polls the dirty flag
// every 50 ms; when EverySecond raises it, runs the display update
// (which includes the long-blocking `[d]` SPI dump). Tasmota's main
// loop keeps yielding the whole time because the [d] only blocks
// THIS task — the TinyC VM mutex is released around long syscalls,
// so EverySecond / WebCall / MQTT / etc. continue firing on the
// loop task without missing a beat.
//
// Why not just tighten the gate / add a periodic clear instead? Both
// were considered; this is the architectural fix — the Tasmota loop
// watchdog can't fire on us because we never block the loop task.
void TaskLoop() {
while (1) {
if (display_dirty) {
display_dirty = 0;
update_display();
disp_updates = disp_updates + 1;
}
delay(50);
}
}
// ─── WebCall — Tasmota main page rows ────────────────────────────────
//
// Tasmota AJAX-polls our main page every ~2 s and the rows we emit
// here are spliced into the standard sensor table. {s}/{m}/{e} tags
// = <tr><th>/</th><td>/</td></tr> after Tasmota's client-side
// substitution. Group separators use plain {s}<hr>{m}<hr>{e} since
// Tasmota's CSS centers <hr> inside <td>.
//
// `buf` (sprintf scratch) is intentionally 64 chars — ALL sprintf
// calls into webSend stay below that limit because each one builds
// just ONE row's value+suffix. Anything longer (multi-field rows)
// would need splitting; we don't have any here.
void WebCall() {
char buf[64];
// ── Top: clock + date + sun row ──
web_clock_header();
// ── This room (SHT31) ──
webSend("{s}<hr>{m}<hr>{e}{s}<b style='color:magenta'>Arbeitszimmer Gerhard</b>{m}{e}");
if (sht_seen) {
webSend("{s}Temperatur{m}<span style='color:yellow'>");
sprintf(buf, "%.1f ℃</span>{e}", btemp);
webSend(buf);
webSend("{s}Luftfeuchte{m}<span style='color:yellow'>");
sprintf(buf, "%.1f %%</span>{e}", bhumi);
webSend(buf);
} else {
webSend("{s}Temperatur{m}<span style='color:#666'>—</span>{e}");
webSend("{s}Luftfeuchte{m}<span style='color:#666'>—</span>{e}");
}
// ── Powerwall ──
webSend("{s}<hr>{m}<hr>{e}{s}<b style='color:magenta'>Powerwall</b>{m}{e}");
webSend("{s}Batterie{m}<span style='color:yellow'>");
sprintf(buf, "%.1f %% (%.2f kWh)</span>{e}", pwl, pwl/100.0*13.5);
webSend(buf);
webSend("{s}Netz{m}<span style='color:yellow'>");
sprintf(buf, "%.0f W</span>{e}", sip);
webSend(buf);
webSend("{s}Solar{m}<span style='color:green'>");
sprintf(buf, "%.0f W</span>{e}", sop);
webSend(buf);
webSend("{s}Batterie-Fluss{m}<span style='color:yellow'>");
sprintf(buf, "%.0f W</span>{e}", bip);
webSend(buf);
webSend("{s}Haus{m}<span style='color:red'>");
sprintf(buf, "%.0f W</span>{e}", hip);
webSend(buf);
// ── Solar / Wechselrichter ──
webSend("{s}<hr>{m}<hr>{e}{s}<b style='color:magenta'>Solar</b>{m}{e}");
webSend("{s}Dach DC{m}<span style='color:green'>");
sprintf(buf, "%.0f W</span>{e}", sedc);
webSend(buf);
webSend("{s}Garage{m}<span style='color:green'>");
sprintf(buf, "%.0f W</span>{e}", wrga);
webSend(buf);
webSend("{s}Gartenhaus{m}<span style='color:green'>");
sprintf(buf, "%.0f W</span>{e}", wrgh);
webSend(buf);
webSend("{s}Garten{m}<span style='color:green'>");
sprintf(buf, "%.0f W</span>{e}", wrgg);
webSend(buf);
webSend("{s}Solarspeicher{m}<span style='color:yellow'>");
sprintf(buf, "%.1f ℃</span>{e}", ssp);
webSend(buf);
// ── Hauptzähler + Wallbox ──
webSend("{s}<hr>{m}<hr>{e}{s}<b style='color:magenta'>Zähler</b>{m}{e}");
webSend("{s}aktuell{m}<span style='color:yellow'>");
sprintf(buf, "%.0f W</span>{e}", zwzc);
webSend(buf);
webSend("{s}Verbrauch{m}<span style='color:yellow'>");
sprintf(buf, "%.3f kWh</span>{e}", zwzi);
webSend(buf);
webSend("{s}Einspeisung{m}<span style='color:yellow'>");
sprintf(buf, "%.3f kWh</span>{e}", zwzo);
webSend(buf);
webSend("{s}Wallbox{m}<span style='color:yellow'>");
sprintf(buf, "%.0f W</span>{e}", auto);
webSend(buf);
// ── Aussen ──
webSend("{s}<hr>{m}<hr>{e}{s}<b style='color:magenta'>Aussen</b>{m}{e}");
webSend("{s}Temperatur{m}<span style='color:yellow'>");
sprintf(buf, "%.1f ℃</span>{e}", atmp);
webSend(buf);
// ── Diag — uptime, EverySecond tick count, EPaper refresh count ──
sprintf(buf, "{s}diag{m}uptime=%d s, es=%d, disp=%d{e}",
tasm_uptime, es_ticks, disp_updates);
webSend(buf);
}
// ─── TaskLoop and Every50ms intentionally REMOVED in v3 ──────────────
// Hypothesis: TaskLoop's persistent tc_vm_task interferes with the
// short-lived tc_vm_task spawned for sht31.tcb's main(). With both
// removed, slot 0 has the same callback shape as examples/epaper29.tc
// (which the user runs alongside 5 concurrent sensor drivers without
// issue): EverySecond + WebCall, no separate FreeRTOS task.
// ─── main — boot init mirrors the original Scripter `>B` block ───────
int main() {
// Initial clear in partial-mode framebuffer. Same `[IzD0]` start
// the Scripter used: I = init/full-clear, z = clear buffer,
// D0 = drawing-mode 0 (text + lines, no rotate).
dspText("[IzD0]");
// Graph 0 — Powerwall % over 7 days, sample every minute (10080
// minutes total = 7 days). Scale 0..100, axis label "100 %%" /
// "0 %%" anchored at right edge (x=360). The big numeric readout
// in update_display() shares the same x.
dspText("[zG10352:5:40:-350:80:10080:0:100f3x360y40]100 %%[x360y115]0 %%");
dspText("[f1x100y25]Powerwall - 7 Tage[f1x360y75] 0 %%");
// Graph 1 — Solar power W, same 7-day window, scale 0..5000.
dspText("[G10353:5:140:-350:80:10080:0:5000f3x360y140]+5000 W[x360y215]0 W");
dspText("[f1x70y125]Volleinspeisung - 7 Tage[f1x360y180] 0 W");
// Static labels for the bottom counter table.
dspText("[p13x10y230]WR 1-4:");
dspText("[p13x10y245]H-Einsp.:");
dspText("[p13x10y260]H-Verbr.:");
dspText("[p13x10y275]D-Einsp.:");
// First flush — gives a blank dashboard skeleton on screen until
// EverySecond's first 60-s tick fires the real values in.
dspText("[d]");
// Restore the persisted graph buffers. Scripter `>B` did this AFTER
// the static drawing (so the graph rectangle is clear, then the
// restore paints the saved curve back into it). The two files
// accumulate roughly 70 KB each (10080 samples × 4 bytes); the
// Scripter's same files live on .51's filesystem already.
dspText("[Gr0:/g0_sav.txt:]");
dspText("[Gr1:/g1_sav.txt:]");
// Pull the 7-day weekday daily-delta arrays from the legacy
// /wd_log.txt if it's still around. After the first midnight
// rollover under TinyC the persist .pvs has authoritative data
// and this becomes a redundant overwrite-with-same-values, but
// it also lets the Scripter and TinyC versions share state
// bidirectionally during the transition.
load_wd_log();
addLog("epaper42 ready: uptime=%d s (display init done)", tasm_uptime);
return 0;
}