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:FreeSansBold9pt7bshares 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,dBmanddBare 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 collides0/O, and Org_01 collides0/O,1/l,2/Zand
5/S— on a page printingSF 5andCR 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 —RSSIbecomes 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.unscopedwas 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.hwith 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 twoNodePrefs. 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. Youruisettings are untouched either way — the serialiser is named-key,
so added keys are additive. - Fixed upstream:
fem_rxgainwas 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_rxgaintorx_boosted_gain— the correctly-namedrxgainkey 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::decryptrejects 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
Packetobject 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.CommonCLIbounds the fourstrcpycalls into its 132-byte scratch buffer, and thesprintfof
an unrecognisedgetargument into the reply buffer.
Also fixed
- The joystick centre press no longer floats. All six buttons declare
pulldownup, so
MomentaryButton::begin()stops re-runningpinMode(pin, INPUT)—PULL_Disabledon this
core — over theINPUT_PULLUPthe board sets. Onlyuser_btnhasbegin()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 onlyboard.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_NAMEin[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 / 24rescale 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. NOWis the sample the newest bar came from, not a second read taken a moment later — the
old one was by construction outside thePK/LOwindow printed beside it.NFprints the floor the bars were actually scaled against, instead of a raw driver value
that reads0until the floor converges.0is not a dBm.- The room server's noise-floor guard matches the companion's, which its own comment always
claimed. It caught onlynf == 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.