Added
-
Richer, more accurate install heartbeat. The daily heartbeat now
reports the real release channel (stablefor tag builds,edgefor
main-branch images,mainfor an untagged source checkout; it was
hard-coded tostable), a bucketed install age, one object per device
kind carrying the newest reported firmware plus the panel's declared
gamut, resolution and bucketed wake cadence, a bucketed lineup count,
four feature booleans (relay, touch, MCP, quiet hours) and a bucketed OTA
rollout snapshot. Per-kind firmware had been sent in a shape the API
ignored, so every install read asunknownfirmware. The first heartbeat
of an install's life now leaves within seconds of opting in (wizard,
Settings toggle, or the first page view) instead of after the daemon's
boot delay, which had left short-lived installs visible in the update
check but absent from the heartbeat. The privacy page lists the exact
payload. -
Opt-in question moved to the front of the wizard, right after the
welcome, so it is seen before the steps where setup is often abandoned.
Installs that never answered (skipped the wizard, or predate it) get a
one-click prompt in the header; either answer persists. -
Docker images are stamped with
TESSERAE_CHANNELat build time. -
M5Stack PaperMono hardware kind. New
m5stack_papermonokind for the
3.97" 480x800 4-level greyscale handheld (Pro and Lite share it): the
reTerminal Sticky's wire contract (esp32_bw_client,gray_4, a
96000-byte 2-bpp frame fromesp32_gray2_binat the portrait stride the
firmware transposes), paired with them5stack-papermonotarget in
tesserae-device-firmware.auto_select: falsebecause it is
indistinguishable on the relay wire from the Sticky and the Xteink X4
gray build. Confirmed on real hardware the same day (portal, register,
dashboard frame, battery telemetry) and added to the test matrix.
The kind now also declares touch (FT6336G) with the usualtouch_enabled
/touch_linger_sknobs, and afrontlight_pctsetting (0-100) for the
panel's built-in frontlight, which the firmware hands to the board's
power-management chip. -
Every bundled widget now translates its panel text. The 28 widgets that
still painted hardcoded English (the Home Assistant set, clocks, countdown,
year progress, todo, news, pictures, transit, status) moved their labels,
empty states, legends and status words behind the widgetctx.t()contract
with a generatedstrings/en.json, and the whole bundled set ships
Czech, Dutch, French, German, Italian, Lithuanian, Norwegian Bokmål, Polish,
Portuguese, Slovak, Spanish, Swedish, and Chinese (Simplified and
Traditional). The set follows where installs actually report from; the
language pickers list all of them automatically. A repo-wide test now
pins every widget's locales to its shipped files, the English key set to
every translation, and each literalt()call to its English text.
The German strings for the transit, climate, entities, locks, automation
history and status widgets were then reviewed by a native speaker (#315).
Fixed
-
A webhook tap no longer repaints the old dashboard before the real
update. A touch wake whose action is a fire-and-forgetwebhook:(or
webhook_refresh) now answers304for the frame the panel reports
holding, even when a newer render of the same page has appeared, instead
of serving that render and flashing an unchanged dashboard ahead of
whatever the receiver pushes back. The pushed frame lands in the linger
window as before, or on the next timer wake. Page, rotate and Home
Assistant actions still repaint on the same wake (#274). -
Upgrading a catalog bundle removes the widgets it dropped. When a
new release of a bundle renames or drops a folder, the marketplace
installer now deletes the old folder instead of leaving it behind as an
untracked plugin that still loads against the new shared code and could
never be uninstalled from the catalog page. -
A sleeping REST device woken for a daily lineup no longer collects the
previous day's frame. The device was told to poll 20 s after the lineup's
target, but the scheduler fires on a 30 s tick and renders each due lineup
in turn, so most mornings the poll landed before the new frame existed. The
miss then stuck: the projection reported the lineup as due now, a
fraction of a second in the past once truncated to the second, and the
poll path skipped it as stale and sent the device to sleep for its whole
interval on the old frame. The scheduler margin is now 70 s, and a change
overdue by less than a tick means "poll again after the margin". -
The Jarvis-Judice-Ninke and Stucki dithers no longer flood flat regions
of a colour panel with a single colour. The numpy error-diffusion loop
never bounded the value a pixel accumulated from its neighbours, so a
field the palette cannot reach banked the same residual on every pixel
and the wide kernels carried it into the next region as a solid swathe.
The diffused value is now clamped to 0-255 before matching, as Pillow's
Floyd-Steinberg does. Atkinson shifts imperceptibly. -
The Location picker on the weather, sky, and sunrise widgets now accepts a
pastedlat, lonpair. It only ever asked the city name search, which
returned "No matches." for coordinates, so a spot with no nearby town could
not be saved from the editor (#302). The pair is offered as a pick, Enter
takes it, and the Label auto-fills with the coordinates. -
Interval schedules whose page renders unchanged no longer re-fire every
scheduler tick. Ano_changepush never recorded the fire, so a stable page
paid a full render every 30 seconds and filled the History view; it now
re-arms the interval like a sent push does. -
The canvas editor's in-browser preview now paints cached webfonts. It built
its own font list from the bundled set, so a code element naming a cached
family showed the fallback in the editor while the device push had the real
face.
Added
-
The week calendar widget has a "Show daily event counts" toggle, so the
per-day count chip under each date can be turned off to save header
height. Event locations, when shown, now wrap to as many whole lines as
fit inside the block instead of being cut to one ellipsised line. -
The weather_now, weather_now_scenic, weather_hourly and weather_forecast
widgets now ship French, on the samelocalescontract as the calendar
widgets: titles, legends, metric labels, WMO condition text and empty
states go throughctx.t(), and the forecast's weekday abbreviations
come fromIntlin the panel's locale rather than a fixed English table. -
The Dashboards page has Active and Archived tabs. Archive parks a dashboard
you're not using without deleting it: it keeps its layout, device links,
and history, but leaves the "go to page" and lineup pickers, drops its
Home Assistant entities, and skips the background refresh passes. Restore
brings it back; Duplicate on an archived dashboard makes a working copy.
Only a dashboard no lineup references can be archived, so the button is
disabled on lineup members with the lineup named in its tooltip, and bulk
archive skips them and says so. -
Hardware catalog entry for the original M5Stack M5Paper (pre-S3), the 4.7"
960x540 16-level greyscale handheld on the classic ESP32-D0WDQ6-V3. Unlike
the PaperS3 its glass is driven through an IT8951 controller over SPI, so it
reuses the reTerminal E1003'sesp32_gray_binwire format at the PaperS3's
geometry over the existingesp32_clientprotocol. Marked
auto_select: false-- an M5Paper and a PaperS3 report the same model,
gamut and scan on the wire, so relay pairing can't tell them apart and this
kind is only ever set explicitly. Verified end-to-end on hardware
2026-09-10: dashboard frame, battery telemetry, the three side buttons, the
SHT30 sensor, and touch.touch: truewith atouch_enabled/
touch_linger_sform; the GT911 reports in composition space so taps
hit-test directly, and it needs Stay awake because the classic ESP32 can't
wake from deep sleep on the touch controller. -
Timed lineups take a display of their own. The setup wizard's "set time"
and "keep fresh" modes now ask which display to send to, defaulting to
every display the dashboard is on, so a dashboard shared by several
panels can be scheduled for just one of them. The full schedule form
gains the same picker, schedules carrydevice_idsthrough the
Lineups view and the MCP schedule API, and the scheduler already
honoured a bound timed lineup (discussion #300). -
Hardware catalog entries for the reMarkable 2, reMarkable 1 and reMarkable
Paper Pro, driven by the community tesserae.remarkable AppLoad client over
the v1 REST device API. The reMarkable 2 is confirmed on hardware; the
other two are built from the same code paths and await confirmation. -
Dynamic
refresh_rateon the TRMNL BYOS/api/displaypath. It echoed the
static configuredrefresh_rate_s; now it runs the same next-poll decision
the v1 REST path'snext_poll_shas used since #241 — the configured rate is
the ceiling, pulled earlier for the next projected dashboard change or a
widget staleness hint, reshaped onto a wake-alignment grid, and stretched to
sleep through a quiet window the device asked to sleep through (#299). A Kobo
or Kindle running KOReader with "Use server refresh interval" enabled now
wakes once at the end of quiet hours instead of every interval overnight. The
shared decision moved intoapp.device_poll; a fault anywhere in it degrades
to the static configured value. -
16-level greyscale over TRMNL BYOS. A new
trmnl_png_gray16renderer: the
same fit / flip / underscan / contrast / dither pipeline astrmnl_png, but
quantised to a 16-entry grey ramp and saved as an 8-bit greyscale PNG. For
panels that paint real greys over the BYOS/api/displaypath, jailbroken
e-readers running KOReader (confirmed on a Kobo Clara HD) and, untested, the
Seeed reTerminal E1003 / TRMNL X 16-level waveform under TRMNL firmware,
where 1-bit output turns every chart fill and shaded band into
coarse error-diffusion speckle. Honours a device's measured grey ramp when a
greyscale calibration profile is applied (same_gray_rampside channel
esp32_gray_binreads).trmnl_clientnow lists both renderers. -
Renderer picker on the device card (Settings → Devices → General). Shown only
when a device's kind offers more than one renderer of the same wire format
that the extension-matching auto-select can't tell apart — today that is
trmnl_client(1-bittrmnl_pngvs 16-greytrmnl_png_gray16). Switching
repins the instance and drops its stale render so the next poll re-renders. -
Any webfont in canvas pages and code elements, cached server-side. A new
add_fontMCP tool (POST /api/mcp/fonts) fetches a Google Fonts family by
name at chosen weights and styles, or one face from a direct .woff2 / .ttf
URL, once at author time; only the latin subset is kept unless asked
otherwise, so a CJK family stays small. Cached families then work exactly
like the bundled ones: autolibs inlines the face into a code element's
sandbox when its CSS names the family, the pagefontfield accepts the
cached id, and the appearance catalog lists them withsource: "cached".
Renders never touch the network; a missing cache entry degrades to the
author's fallback stack.list_fontsanddelete_fontcomplete the set,
and files are served from/page-fonts/<id>/<file>on the local origin. -
Hardware catalog entries for the paperlesspaper OpenPaper 7 (7.3" Spectra 6,
800x480) and OpenPaper L (13.3" Spectra 6, 1200x1600) picture frames, both
ESP32-C6 boards running the Tesserae device firmware over the existing
esp32_clientprotocol andesp32_binrenderer. The L entry is marked as
not yet confirmed on hardware. -
The OpenPaper 7 entry and the tested matrix now record the on-hardware
confirmation. -
The OpenPaper 7 kind composes portrait (480x800) by default, matching the
frame standing upright; the renderer rotates onto the panel's 800x480 native
stride when packing. Confirmed upright on hardware with the USB-C port at
the bottom. -
The GTFS widget gains a "GTFS-RT VehiclePositions URL" field, for agencies
that publish vehicle positions as their own feed beside TripUpdates and
ServiceAlerts. With "Show stops away" on, a row reads "2 stops" when the
feed reports a stop sequence, and falls back to the straight-line distance
from the stop ("1.4 km", or miles via the new "Distance units" option) when
the feed only carries coordinates, which is all some agencies publish.
Reports older than ten minutes are ignored.gtfs0.12.0. -
The History widget gains a "Current value" option (#282). "Large headline
above the chart" draws the single-sensor reading in display type with the
trend arrow and low / high beside it, so a weather panel shows the value at a
glance with the history underneath for context. The default keeps the value
in the legend strip under the chart.ha_history0.8.0. -
Quiet hours can be limited to chosen weekdays and can mark whole days quiet
(#299). Both the app-level window and a device's override gain a "Window
applies on" weekday picker and a "Quiet all day" picker, so an office panel
can be quiet overnight on weekdays and all weekend. Each day is judged by its
own clock: on a ticked day an overnight window covers that morning up to
the end time and that evening from the start time. Existing settings keep
their meaning: every day, no all-day days. -
"Sleep through quiet hours", off by default, tells battery panels to sleep
until the quiet window ends instead of waking on their interval inside it.
The REST status response stretchesnext_poll_s(andwake_at) to the end
of the window, capped at six days; always-on panels are unaffected. Wake
alignment now searches five days ahead so a quiet weekend does not push an
aligned device back onto its plain interval. -
The Home Assistant History widget takes optional "Y-axis minimum" and
"Y-axis maximum" cell options that pin the chart's value axis. Blank keeps
the auto-fit range, and one side can be set on its own. The Sensor widget's
description and sparkline help now point at the History widget for a full
chart with labelled axes (#282).
Fixed
-
The Gallery's sequential rotation keeps a cursor per device (#209). Two
panels showing the same folder used to share one position and advance it
for each other, so a panel woken between the other's renders skipped
photos. Each panel now walks the album at its own pace; the first render
after upgrading carries on from the album's current position rather than
restarting at the first photo, and a render with no device bound keeps the
shared cursor it always had. The cursor file is named after the device, so
deleting.sequential_index_<folder>_<device>restarts that one panel.
Because the widget now declaresrender.per_device_id, a page carrying it
is composed and captured once per bound device rather than once per panel
group, in random mode as well.picture_gallery0.1.3. -
Installing or removing a widget from Browse no longer shows "Install failed:
network error" under the success message. The success branch throws while
adding the topbar "Restart required" button (the topbar holds two theme
toggles and it picked the drawer's, which isn't a direct child), and the
request's error handler reported that as a network failure. The install had
succeeded, but the restart button only appeared after a reload. The button
now targets the topbar's own toggle, and the network-error message is
reserved for a failed request. -
A device's last heartbeat now survives a server restart (#282). The battery,
signal, temperature and humidity readings the status strip widget and the
Devices card draw from lived only in memory, so after an upgrade they were
blank until the device next reported, and a frame fetched on a button wake
is rendered before that same wake's heartbeat lands. The last merged
heartbeat is kept indata/core/device_status.jsonand seeds the cache at
boot with its original timestamp, so the Devices card still shows the
reading's real age. -
The History widget's low and high now always bracket its current value
(#282). The current reading comes from the live state while low and high
came from the history samples, which ha_core caches for longer, so a fresh
reading could sit below the window's low. A live state newer than the last
sample now joins the series as its final point, so the chart also ends where
the headline says.ha_history0.8.1. -
Min / max labels on the History widget's chart stay inside the canvas
(#282). A maximum sitting on the top gridline lost its label off the top
edge, and a marker at either end of the series lost half of it off the
side. The chart now reserves room above the plot for the max label and
keeps both labels within the drawing area, flipping a label to the other
side of its point when it cannot fit. -
The self-hosted relay container now sends an explicit
Content-Lengthon
every response instead of chunked transfer encoding. ESP32 firmware reads a
sealed frame only when the response carries a length, so a remote panel
paired against a container relay stayed on "waiting for first frame" even
though pairing and heartbeats worked. Relay image0.1.1. -
Older x86-64 processors (before 2009, no SSE4.2) no longer get a bare
Illegal instructionfrom the Docker image: the entrypoint explains that
the bundled numpy needs x86-64-v2 and points at the fix. The image accepts a
NUMPY_SPECbuild argument (--build-arg NUMPY_SPEC='numpy<2.4') to pin the
last numpy line whose wheels run on those machines, and the install docs
carry the same note for a bare install.
Added
- Hardware kind
waveshare_esp32_driver_75for Waveshare's E-Paper ESP32 Driver
Board carrying a 7.5" mono panel (V2, 800×480). Inheritsesp32_bw_client
and the 48000-byte mono frame shared with the reTerminal E1001 and the XIAO
7.5" boards; firmware is thewaveshare-esp32-driver-75build in
tesserae-device-firmware. Not yet confirmed on real hardware. - Calendar Core can use each event's own colour where the feed sets one.
A per-feed toggle on the feed row opts in; RFC 7986COLORnames and
X-APPLE-CALENDAR-COLORhex values are read, and an event with no colour of
its own keeps the feed colour. Off by default, so existing feeds render as
before. (#222) GET /api/mcp/appearanceserves the theme, style and font lists. (#257)
Changed
- PicPak frames now get the "update available" chip on the Devices card and
the Firmware page. The update check (api.tesserae.ink) also follows
community firmware repos, starting withpicpak_clientmapped to
varanu5/picpak-tesserae-client. Such releases carry no OTA descriptor, so
the chip links the release for a web-flasher update rather than queueing
an over-the-air one. /api/mcp/catalogreports a count of themes, styles and fonts and points at
the appearance endpoint instead of inlining the lists, taking roughly a
sixth off everylist_widgetsread. (#257)- tesserae-mcp 0.16.0.
list_widgets(section="appearance")fetches the
lists from the new endpoint, and the served tool docs point there. Upgrade
withpipx upgrade tesserae-mcp; an older bridge returns counts where the
docs promise lists. - The device setup guide gains an Update delivery section describing Auto
and Always full refresh. - The
cryptographydependency now allows the 50.x series, which carries the
fix for PYSEC-2026-3552. The affected PKCS#7 API is not used here; the pin
was the only reason the audit kept reporting it.
Fixed
- Background loops can be stopped as a set. The scheduler, the
relay-pairing poller, the OpenDisplay telemetry poller and HA discovery's
ticker register with a small registry when they start, and the transport
wiring keeps the two pollers threadless under the test suite. This is what
the intermittent hour-long CI stall was: each test that built an app left a
full set of these loops running, and the worker eventually wedged with a
hundred of them alive. The suite now stops them after every test and fails
the test that leaks one. - Palette profiles no longer swap primaries on 7-colour ACeP panels. A
profile emits its colours in Spectra 6 slot order and was applied to the
panel palette positionally, which oninky_7colourput the calibrated
yellow where the panel paints green and the blue where it paints red. The
override is now matched to the gamut's slots by colour. (#298) - The Gallery widget's Scale option now does something. Every choice
rendered as Fill: the client never read the option, and the shared
full-bleed rule crops to cover. Fit letterboxes, Stretch distorts, Center
shows native pixels, and Fit with blurred background paints a blurred copy
of the picture behind the letterboxed one. (#296) - CircuitPython BMP frames have a fixed colour table per gamut. The
circuitpython_bmprenderer used to compact the table to the colours a
page actually used, so a tri-colour page with no red on screen shipped as
a 2-colour 1-bpp file and the next frame with red in it no longer matched
a bitmap the client had sized from the earlier header. The table is now the
gamut's whole palette in a fixed order on every frame, withbiClrUsed
set to its length, so bit depth and table are constant for a given gamut
andadafruit_imageloadsizes a 3-colour frame's bitmap at 3 values
rather than 16. (#277)