JP drew a 53-cell MeshTerm at the original's height. It gets the same
colouring: the vertical ramp — white, yellow, light red, red, light
black — with every box-drawing character, shadow row and letter bevel
alike, in the light black.
The art moves to meshterm/assets/. It was sitting in meshterm/ui/ among
the modules, which is the wrong place for a file nothing imports; it
still ships inside the package, since the splash reads it at runtime and
an installed venv has no repo to read from. scripts/colour-logo.py paints
a .txt into its .ansi, so the palette is stated once in code instead of
retyped into escape sequences by hand, and re-styling stays a text edit.
Which mark is drawn is a question about the *screen*, not the platform: a
53-column console and a desktop terminal dragged narrow have the same
problem. load_logo(cols) answers it — widest that fits — and
compose_startup asks, since it is the only place that knows the width.
Below the smallest mark it returns nothing at all; no banner beats a torn
one. A test now draws the real wordmark at each platform's width, which
is the gate the splash never had (it is the one screen the gallery
doesn't cover, which is how a 71-cell mark shipped to a 53-cell console).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Writes the PSK straight to /var/lib/iwd/<SSID>.psk (same pattern
calculinux-setup.sh uses for first-boot provisioning) then asks iwd to
join right away. Self-elevates via sudo since that path is root-only.
Deployed to that user's ~/bin on the Lyra device (PATH updated in ~/.profile).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The board has no notion of local time -- glibc defaults to UTC -- so
astimezone() calls across the UI rendered heard-ages and timestamps in
UTC instead of JP's Eastern time. Adds a setup step that tries
timedatectl + tzdata's opkg feed first, falling back to a POSIX TZ
rule (America/Toronto and America/Montreal share the same US/Canada
Eastern DST rule since 2007) written to /etc/environment and that user's
~/.profile when the feed is unreachable, as it currently is.
Verified live on the deployed Lyra: UTC 03:17 now reads EDT 23:17
correctly through both a login shell and /etc/environment.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Lyra has no battery-backed RTC, so every cold boot starts at the
kernel's build-time epoch until something sets it -- wrong for
heard-node ages, TTL, and the SQLite observation log until then.
Adds time-sync.service alongside wifi-kick.service: a oneshot unit
that waits for a real default route, then steps the clock via
whichever NTP client the image ships (chronyd/ntpd/sntp), falling
back to an HTTPS response's Date header if none is present.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The console keeps its stock kernel palette - black background, classic
PC hues - instead of the custom setvtrgb remap. MESH_THEME_16 is
re-tuned against the stock slots, which hands back the map's own
language for free: clients wear bright pink (13), repeaters purple (5),
rooms cyan (6), sensors brown (3). The cost is one dark grey - muted,
faint and track all collapse onto slot 8. Heat quantizes on plain human
units (15 min / 1 h / 1 day). The custom design is archived, not
deleted: theme._VT_SLOTS_CUSTOM + MESHTERM_CUSTOM_PALETTE=1 on the font
script reinstalls it, with the parity test keeping the two in sync.
Undoing the remap on the device exposed a trap: setvtrgb overwrites the
kernel's DEFAULT colormap, so the OSC palette-reset "resets" straight
back to the custom values - the script's default mode now programs the
stock palette explicitly (verified live; specimen screenshot in
.claude/plans/p3-specimen-stock.png).
The header also follows JP's spec: the handheld brands the app -
"MeshTerm vX" - instead of naming a soldered port that never changes,
and the activity pulse takes whatever room remains.
Also root-causes the intermittent suite flake: Rich memoizes a parsed
Style's ANSI codes on first render (Style.parse is globally cached), so
a raw hex style first rendered by a truecolor console replays 38;2
verbatim on a standard console later in the process. The specimen's
nodes row now wears the type.* theme styles - which is also just
correct: the marks now match the names row. Six consecutive clean
full-suite runs.
Deferred at JP's request: the glyph substitution map (he will redo it),
the F-key lane design (awaits his explicit spec), the CLI fold gap.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- BatteryService grows the host provider (Platform.battery="host"): the
PicoCalc's own pack through the kernel power_supply driver - a true
percent and charging flag from sysfs, no voltage-curve estimation, no
radio connection needed. Unreadable supply = absent, gauge hidden.
- The console-font script now builds TWO 512-glyph PSF2 fonts: the 6x12
Terminus-based default (53x26) and a 6x8 A/B candidate (53x40) based
on the kernel's own font_6x8 (CP437 coverage - documented as having
no Cyrillic), with the braille block at 2px dot pitch and all 18
marks redrawn for the shorter cell. Built and verified live on the
Lyra: stty reports 53x40 under meshterm8, specimen legible
(.claude/plans/p5-specimen-6x8.png); flip with setfont, persist the
winner in vconsole.conf. JP judges the A/B by eye.
- calculinux-setup.sh puts the deploy user in input/dialout/video
(input is the modifier watch's /dev/input access).
- theme's platform binding now drops the fold table and cache on every
switch: its emoji pads bake in cell_len at build time, which the
regular platform's emoji-width calibration can re-measure - a stale
table survived platform swaps in randomized test runs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first P3 (a84abf3) ran on the wrong model and its palette never
reached the TUI: MESH_THEME_16 was written in 256-colour cube indices
instead of the 16 VT slots, and the platform theme pick was wired into
the scripted-CLI console while the real rasterizer stayed hardcoded to
truecolor. Replace it end to end:
- theme._VT_SLOTS: the canonical slot->RGB design (conventional hue
families kept; slots 5/6 become the track/faint slates), MESH_THEME_16
rewritten against color(0..15) with the VT's bold-as-brightness and
dim-bank-background rules pinned by tests. /etc/vtrgb + a boot oneshot
install it (console-font script), OSC fallback when kbd is absent.
- platforms.on_platform: set_platform now re-runs registered bindings,
so theme, rasterizer and widgets swap implementations once per switch
and hot paths read module globals - no per-call get_platform().
- The render-boundary fold (theme.fold_text): one str.translate pass
(NFKD accent folds - the Terminus base has no accented Latin - plus
symbol stand-ins and the width-padded emoji map), an embedded-SGR
quantizer for canvas truecolor, and a ?-at-own-width catch-all that
makes the no-wide-glyph short-circuit true by construction. Applied at
the only two places every visible byte passes: render_to_ansi and
MapCanvas.to_ansi_lines.
- ui/fontset.py freezes the installed font's inventory (dumped live);
the gallery now asserts rendered picocalc ANSI carries no truecolor/
256-colour SGR and no character outside it - the parity gate the
first attempt faked by asserting a dataclass field.
- Font build: nine new 6x12 marks (ellipsis, warn, backspace, shift,
gear, refresh, clock, crosshair, padlock) drawn into donor slots, with
donor misses now fatal and a keeper canary (donating pi once erased
Cyrillic pe - shared slot). Table re-dumped on-device: identical to
fontset.py, 519 = 519.
- Type-based name colouring: registry moved to core/nodetypes (services
no longer import ui), fed from monitor observations, heard_nodes and
merge_contacts. Heat gradient quantizes to heat.* steps on picocalc.
- ui/marks.py: the P2-deferred constant relocation - widgets no longer
drags map_render/mapcanvas/pathgraph for tuples and aliases.
- meshterm specimen: the visual language on one card through the real
funnels - the on-device acceptance screen (JP's request).
- tools/chat: delivered/no-ack state uses the standard check/query
marks, not the banned emoji pair.
Verified on the Lyra: font+palette installed, specimen framebuffer
captures in .claude/plans/. 1259 tests green, 13 xfailed (P6 list).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- .claude/plans/picocalc-platform.md: master plan for dual-platform work (P0–P7)
- .claude/housekeeping.md: parking lot for deferred tasks
- scripts/calculinux-radio-bridge.sh: WIP UART bridge setup (P0 blocker on pinmux)
Keep these checked in so context persists across work sessions.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Complete, reproducible bring-up for giving a PicoCalc (Luckfox Lyra, Calculinux) a
MeshCore radio via a Seeed XIAO nRF52840 + Wio-SX1262 on the Lyra's hardware UART1:
- README.md -- parts, wiring, build -> flash -> setup, the why, troubleshooting
- build-firmware.sh -- clone + patch MeshCore, build the _usb env, emit the .uf2
- flash.py -- flash the .uf2 via the XIAO UF2 bootloader (Win/Linux/Mac)
- lyra-setup.sh -- boot-persistent UART1->GP4/GP5 mux service, dialout, profile
- uart1-mux.py -- the RK3506 matrix-IO register poke that routes UART1
- meshcore-uart1.patch -- the two firmware edits (Serial1 bind; I2C off D6/D7)
The two firmware edits are both required: bind the companion to hardware UART1
(Serial1 = D6/D7) instead of USB, and move I2C off D6/D7 (MeshCore maps it there and
sensors.begin() would otherwise seize the UART pins). Also documented in the top-level
scripts/README.md alongside the Calculinux and uConsole-SPI targets.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sequences the full Lyra/PicoCalc bring-up: opkg python packages, the
rtl8xxxu Wi-Fi boot-scan kick (installed verbatim from the working device),
the deploy user, the read-only SSH clone, the venv + editable install with
TMPDIR off the RAM tmpfs, the login PATH, and the console font. Idempotent;
secrets (deploy key, Wi-Fi PSK) are checked for or read from the environment,
never embedded. README documents both scripts and their run order.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Builds, applies, and persists the 512-glyph "meshterm" PSF: Terminus base
+ 256 braille + node/status marks and the list cursor in donor slots +
rounded panel corners aliased onto the square corners. Idempotent; runs
on-device as root against the stock ter-u12n font.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pymc_core's ContactStore is memory-only, so every bridge restart forgot
every contact and DMs failed with ERR_CODE_NOT_FOUND until the peer was
heard advertising again. Snapshot the store to contacts.json after every
mutation and reload it at startup, alongside the identity the bridge
already persists.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Newer MeshCore firmware sends discrete ACK payloads longer than 4 bytes
(4-byte CRC first, then extras). pymc_core's AckHandler requires exactly
4 bytes and silently drops them, so DMs sent through the SPI bridge never
show delivery confirmation. Patch process_discrete_ack at bridge startup
to take the leading 4 bytes as the CRC, matching pymc_core's own
path-bundled ACK parsing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Expose an SPI-attached MeshCore radio (uConsole AIO, Waveshare HATs) to
MeshTerm over the companion protocol on local TCP. A single stdlib-only
file that is both a plain-text setup menu and the bridge process: it finds
the pymc_core runtime and re-execs the bridge under it, offers run-now /
install-as-a-service / uninstall, and can add a [profiles.bridge] block so
the endpoint shows up in MeshTerm's device picker.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>