github cvhviz/CAIRN cvhbuild-v98
Wio L1 Pro build 98 (archive)

pre-release28 days ago

Archived from the old WioL1Pro-CVHBuild repository (first published 2026-09-05). These are the Wio Tracker L1 Pro builds from before Cairn; current builds are in the latest Cairn release.

CVHBuild 98 — messages that survive a reboot, and a Nodes page

A Seeed Wio Tracker L1 Pro fork of MeshCore
(nRF52840, SH1106 128×64 OLED, 5-way joystick, buzzer, L76K GNSS), on upstream v1.17.1.

Six builds' worth of work since the last release, all cut the same day: messages now survive a
power cycle, messages you send from the device finally appear in its own list, the Recent page
grew into a Nodes page that also browses saved contacts, and the page indicator is legible.

Build 92 was a DV1-only diagnostic image and was never released, so every target moves from
91 straight to 98.

This fork versions by build number — CVHBuild N, shown on the splash screen — so a device can be
matched to a tag from its own display.


Downloads

File Target
MeshCore-WioL1Pro-CVHBuild98-2026-09-05.uf2 Companion, SH1106 OLED — the main build
MeshCore-WioL1Eink-CVHBuild98-2026-09-05.uf2 Companion, Waveshare 2.13" B/W e-ink
MeshCore-DV1-CVHBuild98-2026-09-05.uf2 Companion, 2.42" SSD1309 OLED on the Grove connector
MeshCore-WioL1RoomServer-CVHBuild98-2026-09-05.uf2 Room server, SH1106 OLED

Flashing: double-tap reset to expose the TRACKER L1 volume and copy the .uf2 onto it. The
"could not copy extended attributes" error is normal, and the volume ejecting itself means the
flash succeeded.

Time the copy. A flash that finishes in about a second with "Device not configured" is a
truncated write and leaves the app bricked — a good flash takes several seconds (≈6 s for the
OLED images, ≈16 s for e-ink). If it was quick, double-tap reset and copy it again.


Added

The un-synced message queue now survives a power-off or reboot. (build 93)

The companion's offline queue holds up to 256 messages that have arrived but that the phone app has
not pulled yet. It lived only in RAM, so switching the node off discarded all of them — including
in the case the queue exists for, a node collecting messages unattended with no app connected.

It is now snapshotted to /offline_q on the way down and restored at the next boot. On the L1 that
file lands on the 2 MB QSPI flash alongside contacts and channels. Records are length-prefixed, so
a queue holding three messages writes a few hundred bytes rather than the whole 45 KB array, and a
write cut short by power loss reads back as fewer messages instead of garbage.

The snapshot is deliberately one-shot — the loader deletes the file after reading it. Queue reads
are destructive (the app takes each message into its own database), so a file left behind would
replay messages the app already had the next time power was lost ungracefully, showing up as
duplicates. Deleting at load means only never-synced messages ever come back.

Hooked into every path that ends in SYSTEM OFF or a reset: the menu power-off and the menu
restart, the app's reboot command, and the serial CLI reboot.

Scope: this survives a clean shutdown, not a battery pull, a brownout or a crash. Closing that
gap needs a debounced write-through, which would put blocking flash I/O in the packet-receive
callback. The device's own on-screen message history is a separate RAM-only list and is still lost
on reboot; only the app-sync queue is restored.


Fixed

A message sent from the device never appeared in its own Messages list. (build 94)

The message store only ever recorded received messages, so a message composed on the device went
out over the radio and left no trace on screen — the conversation showed only the other side of it.

Both send paths now record on success, keyed by the peer rather than by us. That is what threads
it correctly: an outgoing DM stores the recipient's key and name, so it joins that peer's
conversation instead of opening a new one titled with your own node name. Your own messages never
count toward the unread badge, thread rows mark them with >, and the read view titles them
To <peer> rather than showing the peer as if they had sent it.

Sent messages now share the 24-entry ring, so a busy conversation ages received messages out
somewhat sooner than before. That is inherent to showing a conversation rather than an inbox.

The Nodes page age column read 49707d for saved contacts. (build 97)

Caught on hardware the moment the contact list was first shown against a real mesh. The age is
now - last_advert_timestamp in unsigned arithmetic. A node with a drifting clock genuinely
advertises timestamps ahead of yours, and a contact carries whatever that node claimed — so the
subtraction underflowed and wrapped to just under 2^32 seconds, which divides out to about 49707
days. The contacts in question were roughly 3.3 days ahead, exactly the observed wrap.

It could not have shown up on the recently-heard list, where every entry is by definition something
this node received in its own past. Reaching the saved contact list is what made future timestamps
displayable for the first time.

Elapsed time is now clamped at zero, and anything more than a minute ahead of our clock renders as
-, the same dash a never-heard contact gets. Past clock jitter there is no age that can honestly
be stated, and now would have been a confident lie about a node last heard days ago.


Changed

The Recent page is now the Nodes page — recently-heard adverts or saved contacts. (build 95)

Same tab position and icon. The list gains a mode row at the top naming which list is on screen
and how long it is, with Enter switching between them:

> Recent                    7      <- Enter switches to Contacts
  BaseCamp                 2m
  KG7XYZ                  14m
  Repeater-N               1h
                Back:exit Enter:ping

The cursor model extends the old one rather than replacing it. With no cursor, Enter still opens the
zero-hop/flood advert chooser exactly as before. Row 0 is the mode row. Rows 1 and beyond are nodes,
where Enter pings. Each position gets its own hint line — at 128px the annotation row holds about 21
characters, so a combined hint was never going to fit.

The mode row scrolls off once the cursor moves down into the list, so it does not permanently cost a
row of a four-row viewport. The cursor wraps through 1 + count rows, so the mode row stays
reachable and an empty Recent list can still be switched over to Contacts.

The contact list is windowed, not materialised — only the rows about to be drawn are fetched, so
350 contacts cost the same four rows of RAM the recently-heard table already used. Ping needed no
new logic: contacts render into the same row type the advert list uses, so both feed one render path
and one ping. Ages now roll over to days as well, since a contact can be weeks stale where a heard
advert never is.

The active page indicator is an inverted block instead of a 1px underline. (build 96)

The underline sat on the bottom row of the panel, where it was hard to tell from the panel edge and
near-impossible to place among six identical icons at a glance. The active icon is now knocked out
of a filled block — the same idiom as the inverted title bar, so the top and bottom of the frame
read alike.

The title bar names the page, not the node. (build 98)

It carried the node's own name on every page — the one thing that never changes — while the
sub-views, which you can get lost in, had no label at all. It now reads Messages, Recent,
Contacts, Radio, Traffic, Spectrum, GPS, Compass, Battery or Settings.

The clock face is the exception: the giant clock already says which page it is, so the bar carries
the node name there. That is also the page the device rests on, so the name stays glanceable
without changing pages.

The Nodes page loses its Recent/Contacts mode row. (build 98)

The title bar names the list now, so the row was saying it twice and costing a row of a four-row
viewport. With it gone the switch needed a home, and every gesture on that page was already spoken
for. The one that freed up cleanly was Enter — the send-advert chooser it used to open also lives
on the Radio page, reachable in the same two keypresses, so moving it off Nodes costs nothing:

  • Not in the list — Up/Down switches Recent ↔ Contacts, the same gesture that switches the
    sub-view on Clock, Radio and GPS. Enter steps into the list.
  • In the list — Up/Down moves the cursor, Enter pings, Back steps back out.

The page now behaves like every other page when idle, and like a list once you are in one.


Still open

  • The SSD1309 back-button report — Back on the message-reading screen sleeps the display instead
    of navigating out — is not fixed here. The input-latch mechanism that looked like the culprit
    turned out to be inert on every OLED build, so that lead is closed. The symptom means the key
    reaches the screen and is not consumed, so the reading screen's own cancel handling is where to
    look next.
  • No add or remove action on contacts. Auto-add is on by default, so heard nodes are already
    saved, and the recently-heard table keeps only a 7-byte key prefix — not enough to encrypt to
    anyone. Manual promotion would need the full 32-byte key retained (~400 bytes).

Memory

Build Flash RAM
Companion (SH1106 OLED) 468 KB / 692 KB (67.6%) 150 KB / 230 KB (65.4%)
Companion (e-ink) 488 KB / 692 KB (70.6%) 154 KB / 230 KB (67.1%)
Companion (DV1 / SSD1309) 475 KB / 692 KB (68.6%) 150 KB / 230 KB (65.4%)
Room server 417 KB / 792 KB (52.6%) 37 KB / 230 KB (16.1%)

Testing

All four environments build and the native suite passes 48/48 (40 core + 8 KISS modem). Every image
was verified to report CVHBuild 98 / 5 Sep 2026 against its firmware.elf.

Build 96 was run on a DV1, which is where the age-column wrap above was caught and fixed. Every
page rendered correctly there. The title-bar and Nodes-page changes in build 98 came after that
session and have not been on hardware yet.

Still unconfirmed: power the node off and back on with unread messages queued and check the app
still pulls them; send a message from the device and check it threads into the conversation; and
scroll a contact list longer than four rows on the Nodes page, since that is where the windowing
arithmetic lives.

Full changelog: see CHANGELOG.md, entries [cvhbuild-v93] through [cvhbuild-v98].

Don't miss a new CAIRN release

NewReleases is sending notifications on new releases.