github cvhviz/CAIRN cvhbuild-v83
Wio L1 Pro build 83 (archive)

pre-release28 days ago

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

CVHBuild 83 — an e-ink that rests, a third panel, and MeshCore v1.17.1

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

Builds 68–83. The theme is e-paper: the panel now stops refreshing when nobody is reading it, and
it draws values and annotation in different faces so a 250×122 screen reads like one. There is a
third panel option, a 2.42" SSD1309 OLED. The base moves from upstream v1.17.0 to v1.17.1.

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-CVHBuild83.uf2 Companion, SH1106 OLED — the main build
MeshCore-WioL1Eink-CVHBuild83.uf2 Companion, Waveshare 2.13" B/W e-ink
MeshCore-WioL1-1309-CVHBuild83.uf2 Companion, 2.42" SSD1309 OLED
MeshCore-WioL1RoomServer-CVHBuild83.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.

Upgrading from CVHBuild 67 keeps your settings — the preferences format is unchanged across this
span. One room-server nuance is noted under Upstream v1.17.1.

The tri-colour e-ink variant is no longer produced. Every refresh on a three-colour panel is a
blocking ~15 s full flash, which makes live pages snapshots rather than displays. The environment
and its EINK_3C paths remain in the tree and still build.


The e-ink panel finally rests

E-paper holds its image with no power, so the cost of a screen is not being on — it is
refreshing. Four separate things were refreshing an idle panel, each for its own reason, and none
of them was the screen-timeout setting:

  • The Radio page redrew every 30 s forever. Spectrum sampling lives inside render(), so one
    column is one redraw. That changed the frame, which defeated the driver's CRC gate, which cost a
    refresh on a page nobody was reading. Sampling is now gated on recent input and the readouts
    hold their captured values, so the frame goes byte-identical and the refresh is skipped.
  • The clock asked for a redraw on every minute boundary before consulting the timeout — the
    comment said "always". That was the periodic refresh that could not be switched off.
  • The message preview repainted every 10 seconds, ungated. It appears by itself when a message
    lands and never dismisses itself, and drew a seconds-resolution age — six refreshes in the first
    minute, then one a minute for as long as it stayed up.
  • The screen timeout was compiled out entirely on e-ink, on the reasoning that there is no
    panel to blank. On e-paper that is the wrong question. One predicate, isScreenLive(), now
    governs both builds: live when the timeout is Never, live while plugged in with keep-on-USB set,
    otherwise live until the timeout expires since the last input. It replaced three hardcoded
    60-second windows, so a bench node on USB keeps a live screen and a pocket node goes quiet.

The clock says SLEEPING rather than freezing. Simply stopping the clock would have been
worse than refreshing it: e-paper holds the last thing drawn, so a clock that stops ticking does
not go quiet, it goes wrong, and drifts further from the truth every minute it sits there.
SLEEPING costs one refresh to put up, after which every frame is identical and the CRC gate
suppresses all of them. Any key brings the real clock back on the next frame.

One honest caveat: the status bar is still drawn while asleep, so an arriving message can still
change the unread badge and cost a refresh. That is wanted — the count beside SLEEPING is
useful — but a busy mesh is not perfectly silent.


E-ink typography

The e-ink build draws the same 128×64 logical canvas as the OLED, so the bigger panel makes
everything larger, not roomier. What buys space is that its font is proportional and narrower
per glyph than the OLED's fixed 6 px advance.

  • Values are drawn in bold, labels and units in a smaller face. Differentiating by weight
    turned out to be the better lever: FreeSansBold9pt7b shares its metrics and its origin with the
    regular face, so there is no second baseline to reconcile and no risk of a value overrunning its
    column. There is no font one point above the built-in 6×8 cell — the whole bundled ladder below
    9pt is Tiny3x3, TomThumb, Picopixel and Org_01, all smaller — so an intermediate size would
    have meant generating and bundling a 7pt face. Bold sidesteps the question.
  • The treatment runs across the whole UI, not just the Radio page: clock and battery, GPS and
    compass, the message list, thread, read view, quick reply and both compose stages, contacts,
    settings and telemetry. Anything that names or qualifies a value — counters, ages, column
    headers, keybind hints, units — drops to the small face, and column values right-justify onto a
    shared rule.
  • Units get a reserved column the width of the widest unit on the page, rather than being
    appended. MHz, kHz, dBm and dB are all different widths, so appending would push each
    row's digits left by a different amount and unstack the column that reserving keeps aligned.
  • Header badge digits shrank. They annotate 7-row icons but were drawn in the value font,
    standing a head taller than the glyph they belong to.
  • The read-view footer moving to the small cell is a clipping fix as well as a restyle: at 9pt
    a glyph on row 56 descends past row 64.

Every one of these helpers carries a guarded contract — off e-ink, or on a board without a usable
small face, each is exactly the call it replaced, so the OLED build is byte-for-byte unaffected.


New: 2.42" SSD1309 build

WioTrackerL1_1309_companion_radio_ble drives a 2.42" SSD1309 OLED. It wears the e-ink
typography while behaving in every other respect like an OLED — isEink() stays false, so
blanking, brightness, the fast refresh cadence and every non-e-ink path apply unchanged.

That combination forced a real fix: the UI had been gating its layout on
UI_HAS_SMALL_FACE && isEink(), which conflates "this panel has a second, smaller face" with
"this panel is e-paper". Four defaulted DisplayDriver virtuals now separate them —
hasSmallFace(), smallFaceDrop(), smallFaceGap() and hasBoldFace(). Bold is a separate
capability from small precisely because this panel has one and not the other.

What the 128×64 panel would and would not take:

  • TomThumb (3×5) for annotation, chosen over Picopixel and Org_01 by pixel-identity test across
    all 62 alphanumerics: Picopixel collides 0/O, and Org_01 collides 0/O, 1/l, 2/Z and
    5/S — on a page printing SF 5 and CR 2, that matters.
  • No bold. Emulating weight by 1-px dilation lands on the right ink density but pushes 60 of 68
    glyphs into their single inter-glyph gap — RSSI becomes one continuous bar — and 20 lose their
    counters. At 5×7 the stem, counter and sidebearing are each already one pixel wide.
  • Baseline drop is 2 here rather than the e-ink's 3, and the unit gap is 0 rather than 2, because
    the fixed 6-px cell already carries a blank trailing column that a proportional face does not.

Upstream v1.17.1

Everything in upstream's release. What reaches this hardware:

  • Fixed upstream: replies were dropped when flood.max.unscoped was low. A scoped reply was
    being held to the unscoped hop limit, so on a mesh configured to damp unscoped flooding, the
    answer to a request could die before reaching the asker. Reply-routing moved into a shared
    RoutingPolicy.h with 14 unit tests behind it — the native suite goes from 33 cases to 40.
  • The nRF52 hardware crypto engine is initialised once, at board startup, instead of on every
    call; nRFCrypto.begin()/.end() used to bracket each individual hash, encrypt and decrypt.
  • The CC310 random number generator is mixed with the radio's entropy rather than trusted alone.
  • FEM gain preferences arrive for boards with an external front end, and they land differently
    in the two NodePrefs. The companion's copy has them #if 0'd upstream until they can actually
    be set, so it writes nothing new; the shared prefs used by the room server, repeater and sensor
    carry both keys live. Your ui settings are untouched either way — the serialiser is named-key,
    so added keys are additive.
  • Fixed upstream: fem_rxgain was aliased to the wrong field in the shared prefs. The key
    claimed to describe the external front end while actually reading and writing the SX126x
    boosted-gain setting. If you run the room server, note that it stops applying a stored
    fem_rxgain to rx_boosted_gain — the correctly-named rxgain key still carries that, and both
    had been written with the same value, so your setting survives.

On this hardware the new FEM calls are no-ops regardless — virtuals on mesh::MainBoard defaulting
to false, which WioTrackerL1Board overrides neither of.

The merge itself was cheap: one conflict, and not a disagreement — upstream added its two FEM gain
defaults at exactly the point this fork sets its five UI prefs defaults. Both blocks kept.


Security

Defects in the upstream v1.17.0 base, fixed here and still unfixed upstream:

  • Utils::decrypt rejects a ciphertext length that is not a multiple of the block size. The read
    loop consumed whole blocks, so an unaligned length ran off the end of the source — past the
    Packet object entirely on the anonymous-request path, reachable unauthenticated over the air.
  • IdentityStore::save() returns real success rather than "the file opened". A short write left a
    truncated key, and the next boot silently minted a new identity and changed the node's address.
  • CommonCLI bounds the four strcpy calls into its 132-byte scratch buffer, and the sprintf of
    an unrecognised get argument into the reply buffer.

Also fixed

  • The joystick centre press no longer floats. All six buttons declare pulldownup, so
    MomentaryButton::begin() stops re-running pinMode(pin, INPUT) — PULL_Disabled on this
    core — over the INPUT_PULLUP the board sets. Only user_btn has begin() called on it, which
    is why that one button, and only that one, answered intermittently.
  • The CPU can idle again. hasPendingWork() reported a scheduled contacts write as pending for
    its whole five-second window, holding the loop out of the firmware's only board.sleep() call
    after every advert from a known contact.
  • The GPS UART closes with the GPS. Its receiver PHY was left powered even with GPS off.
  • The build number is defined once, as CVH_BUILD_NAME in [arduino_base]. Build 68 shipped a
    room-server image filenamed 68 whose splash still said 67.
  • The OLED spectrum no longer stops sweeping. The idle gate added for e-ink was unconditional,
    so a lit OLED on USB stopped sampling after 60 s. An emissive panel redraws for free.
  • A quiet channel draws a bar on the compact band again. The h * 7 / 24 rescale truncated
    captured heights 1–3 to nothing, so a channel on its noise floor drew an empty band while the
    fullscreen view showed a baseline from the very same samples.
  • NOW is the sample the newest bar came from, not a second read taken a moment later — the
    old one was by construction outside the PK/LO window printed beside it.
  • NF prints the floor the bars were actually scaled against, instead of a raw driver value
    that reads 0 until the floor converges. 0 is not a dBm.
  • The room server's noise-floor guard matches the companion's, which its own comment always
    claimed. It caught only nf == 0, so any other uncalibrated reading clamped the whole band flat.

Withdrawn: builds 69, 70 and 71

Each tried to fix e-ink ghosting by changing the refresh policy, and each made the panel worse.
They were built and flashed; everything they did to the display was reverted in build 72, and the
non-display fixes they carried are what build 72 is for.

Worth recording, since it is the kind of thing that invites a second attempt: every shipping
MeshCore e-ink board — ThinkNode M1/M5, Mesh Pocket, LilyGo T-Echo and T-Echo Lite — uses partial
refresh unconditionally and never powers the panel down between frames. Mesh Pocket runs the same
GxEPD2_213_B74 panel that way. Reduced contrast is the accepted trade for never flashing. The
spectrum ghosting specifically came from scaling rectangle edges rather than origin and size: bars
drawn at pitch 2 width 2 stopped leaving a white column between them, turning the band into a solid
slab with about a third more ink on the one page that scrolls every second.


Memory

Build Flash RAM
Companion (OLED) 467 KB / 692 KB (67.6%) 150 KB / 230 KB (65.3%)
Companion (e-ink) 486 KB / 692 KB (70.2%) 154 KB / 230 KB (67.1%)
Companion (SSD1309) 471 KB / 692 KB (68.1%) 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 — 40 cases, up from 33.

The v1.17.1 merge was additionally audited for the class of defect a compiler cannot catch: a merge
that is textually clean and wrong at runtime. The crypto initialisation path, NodePrefs
persistence, CommonCLI, the display drivers, the new routing policy and the L1 variant files were
each checked against both sides of the merge. Nothing was found. One thing is worth passing on to
anyone else tracking upstream: test_companion_node_prefs.cpp wraps its only test in #if 0,
so it reports as passing while running zero assertions. It is not coverage, despite appearances.

CVHBuild 83 has not yet been run on hardware — its content is the upstream merge. The tri-colour
e-ink variant has never been run.

Full changelog: see CHANGELOG.md, builds 68–83.

Don't miss a new CAIRN release

NewReleases is sending notifications on new releases.