Skip to content

Commit 5a3e4f1

Browse files
committed
release: crumble-v4.7.2
1 parent 2519aff commit 5a3e4f1

2 files changed

Lines changed: 175 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@
22

33
## [Unreleased]
44

5+
## [crumble-v4.7.2] - 2026-08-05
6+
7+
Single firmware: `crumble-v4.7.2-tiny-bitter.bin` (Bitter built-in, fits the stock 6.25 MB slot for SD-card flashing).
8+
9+
### Added
10+
- **Newer X4 units that only stayed awake on USB power now run on battery.** One X4 hardware revision does not self-latch its battery MOSFET, so it appeared dead or unresponsive the moment it was unplugged. CrumBLE now asserts that latch (GPIO13) at boot, matching the fix CrossInk shipped in 1.5.0-rc-2. This is a no-op on every X4 sold before that revision — those self-latch through a pull and never needed firmware to hold the rail — so nothing changes for existing devices.
11+
12+
### Changed
13+
- **Vendored `freeink-sdk` updated to upstream `e6a8048`** (154 commits, from the 2026-07-01 snapshot shipped since 4.6.0). For existing panels this brings: the refresh-completion wait now sleeps on the BUSY interrupt edge instead of polling on a 1 ms tick (X4 joins X3, which already did this), power-up sequencing before a grayscale refresh, and an X4 RED-plane baseline fix. The X4 grayscale waveform table and the X3 LUT bank are byte-for-byte unchanged. The X3 grayscale `wb_gc` fix CrumBLE has carried since v4.6.0 is now upstream's own value, so it survives the update unmodified. The update also compiles in upstream's UC8179 and UC8279 drivers for the newer UltraChip panel controllers — see the known limitation below.
14+
15+
### Fixed
16+
- **Opening a book no longer floods the log with deferred-save errors.** Two heap thresholds disagreed: the settings-save retry admitted a write whenever `maxAlloc` cleared 20 KB, but the re-check immediately before serializing rejected anything under 28 KB. In that 20–28 KB band — easily reached while a book is open — every main-loop tick ran the directory/filesystem work, failed the floor, logged an error, and tried again, hundreds of times a second. Both now use the same floor, so the save defers quietly and retries once there is genuinely room. The save behaviour itself is unchanged; only the futile retry loop is gone.
17+
18+
### Known limitations
19+
- **Newer X3/X4 units with UltraChip UC8279/UC8179 panel controllers are still unsupported.** The drivers are compiled in, but nothing selects them: CrumBLE does not fingerprint the panel controller at boot. The bus probe that would do it hung both an X4 and an X3 during development, and upstream marks the UC8279 path itself as "Pending hardware validation — no UC8279 X3 unit has been on the bench yet". Shipping detection that cannot be validated, and that demonstrably broke working devices, was not a trade worth making. If your display never comes up on CrumBLE, use CrossInk 1.4.0.1 or newer.
20+
521
### Fixed
622
- **UI font fallback no longer taxes the reader, and reader UI keeps CJK coverage for free when the primary is CJK-capable.** All three reader activities (EPUB, TXT, XTC) release the standalone UI glyph fallback family on entry and re-arm it on exit, matching the pattern that File Transfer, OPDS, and KOReader auth already use. The fallback exists to fill glyph misses in carousel/shelf/settings labels; book text renders through the primary SD font, which already carries whatever glyphs the book itself needs. Previously the fallback stayed resident through the reading session (~10 KB scattered across the heap) and starved contiguous allocations the reader depends on (page DOM 25-40 KB, CSS rule-table grow, BT font subset). Users can now leave the "UI Font Fallback" setting on and still have full contiguous heap while reading. On top of the release-and-suppress path, if the reader's primary SD font is loaded on entry (the normal case when the user has picked an SD font family like Bitter-LXGWWenKai), the reader points the UI fallback at the primary's already-loaded `EpdFontFamily` -- zero extra heap, and CJK glyph misses in reader UI (title bar, chapter label, progress bar, drawer, in-reader menu) render through the primary. Rendered at the primary's point size, so CJK reads visibly larger than surrounding 10-12pt UI text, but far better than tofu. The alias survives font-change / reindex within the reader (ensureLoaded breaks the fallback pointer before unloading the primary and re-establishes it after the new primary loads).
723
- **Progressive JPEG EPUB covers now render smoothly in generated BMP cover assets.** The cover/thumbnail BMP path already detected progressive JPEGs and forced the required 1/8 JPEGDEC decode, but it still upscaled that reduced grid with blocky sampling. `JpegToBmpConverter` now uses a progressive-only bilinear smoothing pass before dithering when those covers are enlarged for home thumbnails and sleep covers, matching the higher-quality behavior already used in the framebuffer renderer while keeping memory bounded to a small line buffer.

UC8179-PORT-PLAN.md

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,165 @@ split) unless the facade is also updated, which is most of strategy A anyway.
9494
release checklist. Third frozen-dependency incident this cycle (prebake wasm,
9595
sim shim, SDK) and the only one with hardware consequences.
9696

97+
## Execution record (steps 1-4 done)
98+
99+
### Step 1 — local-patch direction: resolved
100+
101+
The vendored SDK was an unmodified snapshot of upstream **`6ee2d06`** (2026-07-01),
102+
found by matching blob hashes across all 293 upstream commits: 37 of the 42 files
103+
in the six compiled libs matched that tree exactly. The other 5 carried content
104+
never seen in any upstream commit — that is the complete local-patch surface:
105+
106+
| File | Local patch | Direction |
107+
|---|---|---|
108+
| `lut/Uc8253X3Luts.h` | X3 `lut_x3_wb_gc` lead byte `0x54` -> `0x00` (the v321 fix) | **take upstream** — HEAD is byte-identical; only our comment differed |
109+
| `include/FreeInkDisplay.h`, `src/FreeInkDisplay.cpp` | heap framebuffers, `releaseBuffers`/`reallocBuffers`, `syncWriteBufferFromActive`, secondary-buffer release, `cleanupGrayscale*` restore memcpy, `swapBuffers` null guard | **take upstream** — HEAD has all of them, several comment-for-comment identical (they were backports that landed upstream) |
110+
| `driver/Ssd1677Driver.cpp` | initial paint FULL -> HALF | **take upstream** — converged; on X4 (`halfSeqOverride=0xD7`) upstream also resolves to HALF |
111+
| `driver/Ssd1677Driver.h` | `skipInitialResync()` override defusing `_needsInitialFull` | **KEEP — re-applied.** Upstream made `skipInitialResync` a `PanelDriver` virtual and overrides it on X3/UC8179/UC8279 but *not* SSD1677; the base is a no-op, so without this the X4 seamless silent-restart flash returns |
112+
113+
The `Uc8253X3Driver.{h,cpp}` hunks the plan flagged are **entirely upstream
114+
evolution** — the async `displayStart`/`displayFinish` split, the `factoryP1/P2`
115+
reference banks, and a `forcedFullSync` -> `_forceFullSyncNext` fix. Nothing of
116+
ours to lose there.
117+
118+
### Step 2 — strategy A taken
119+
120+
`freeink-sdk/` mirrored to upstream `e6a8048`, excluding `libs/book/` (4 MB
121+
EPUB engine — expat/miniz/libunibreak plus a test TTF — that CrumBLE never
122+
compiles) and the nested VCS/`__pycache__` dirs. Re-applied the one kept hunk.
123+
**Zero firmware call-site changes were needed**; `pio run -e tiny-bitter` built
124+
clean on the first attempt.
125+
126+
Only six SDK libs are actually compiled (`lib_deps`): FreeInkDisplay,
127+
BatteryMonitor, InputManager, SDCardManager, BoardConfig, PowerManager —
128+
FreeInkUI, network and book are vendored but never built, which is why the
129+
"312 diff lines in FreeInkDisplay.h" scoped down to nothing.
130+
131+
### Step 3 — detection: a gap the plan did not anticipate
132+
133+
Copying the drivers is **not** sufficient, and neither is updating the facade.
134+
`selectDriver()` routes on `BoardConfig::ACTIVE.displayController`, which is set
135+
by `XteinkDetect::applyXteinkDisplayController()` — a lib that was **not in our
136+
`lib_deps` and is called from nowhere in our firmware**. Without wiring it, the
137+
UC8179/UC8279 drivers link but can never be selected, so a new-panel unit still
138+
gets a dead display. Added `XteinkDetect` to `lib_deps` and called the probe from
139+
`HalDisplay::begin()`, after the X3/X4 profile is chosen and before
140+
`einkDisplay.begin()`.
141+
142+
Regression safety, verified by reading the probe:
143+
- It promotes **only** on `Uc81xxConfirmed` — two independent passes must both
144+
match the UC81xx VER/FLG signature *and* agree byte-for-byte. An SSD1677 or
145+
UC8253 does not answer register `0x70`, so the bus floats and both passes fail
146+
-> `PrimaryAssumed` -> the profile default stands. Inconclusive also falls back.
147+
- Pins are read from `BoardConfig::ACTIVE` and released to `INPUT` afterwards;
148+
`EpdBus::begin()` re-runs `SPI.begin()` immediately after.
149+
- SD shares SCLK/MOSI and is already mounted at this point, but
150+
`HalSpiBus::Lock` is held across all of `HalDisplay::begin()`, so no SD
151+
transaction can interleave.
152+
- Cost: roughly 70 ms at boot.
153+
154+
### Step 3b — waveform regression audit: clears
155+
156+
The plan's headline worry ("Replace grayscale LUT waveforms with stock 2-frame
157+
set" changing appearance on existing panels) does not apply. That commit
158+
(`0647bfd`), plus `6662faf` "Restore OLD plane baseline after grayscale refresh"
159+
and `7babfab` "Fix e-ink ghosting by tracking previous frame", touch **only
160+
`Uc8179Driver.cpp`** — the new-panel driver.
161+
162+
- **X4 `lut_grayscale`: unchanged.** What actually changed in `Ssd1677Luts.h` is
163+
that `lut_grayscale_revert` was deleted and its slot repurposed for a Seeed
164+
Sticky table, plus an additive X4 Pro partial LUT. Neither is our X4.
165+
- **X3 LUT bank: unchanged**, plus additive `factory_p1/p2` reference tables that
166+
are deliberately not wired up.
167+
- Removing the X4 revert waveform is a no-op in our flows: the reader always
168+
calls `cleanupGrayscaleWithFrameBuffer()` after `displayGrayBuffer()`, and
169+
`cleanupGrayscaleBuffers()` (byte-identical old vs new) already cleared
170+
`_inGrayscaleMode`, so the old `grayscaleRevert()` branch was dead code.
171+
- `POWER_BUTTON_PIN` and the ADC ladder ranges `{3100, 2090, 750}` are unchanged;
172+
the large InputManager/BoardConfig diffs are clang-format reflow plus X4 Pro
173+
additions that are not compiled here.
174+
175+
Real behavior deltas that do land on existing panels, and are therefore what
176+
hardware testing is for: the completion wait moved from 1 ms polling to an
177+
ISR edge wait on BUSY (`waitRefreshComplete`) on **both** X4 and X3;
178+
power-up sequencing before grayscale refresh; and the X4 RED-RAM baseline /
179+
boot first-paint fix. The ISR itself checks out against the repo's rules —
180+
`IRAM_ATTR`, `xSemaphoreGiveFromISR` (never `Take`) in the handler, a
181+
polling fallback when the semaphore can't be created, a bounded 20 ms
182+
arm-confirmation, an already-done fast path, and a 30 s timeout.
183+
184+
The async `displayStart`/`displayFinish` split is inert for us: our app never
185+
calls `displayAsync`/`triggerDisplay`, so `display()` runs the blocking path,
186+
which on both drivers is exactly `displayStart()` + `displayFinish()`.
187+
188+
### Step 4 — builds and size
189+
190+
All six envs build clean, no warnings from SDK or probe code.
191+
192+
| env | 4.7.1 | new | delta | vs 6,553,600 SD slot |
193+
|---|---:|---:|---:|---|
194+
| tiny-bitter | 5,352,608 | 5,361,888 | +9,280 | 1,191,712 under |
195+
| tiny || 5,361,744 || 1,191,856 under |
196+
| tiny-lexend || 5,024,864 || 1,528,736 under |
197+
| tiny-chareink || 5,316,976 || 1,236,624 under |
198+
| xlarge | 7,655,840 | 7,665,136 | +9,296 | over — **already over at 4.7.1** |
199+
| no_emoji | 7,235,984 | 7,245,280 | +9,296 | over — **already over at 4.7.1** |
200+
201+
The whole change costs **+9.3 KB**: +7,344 for the SDK sync including both new
202+
drivers, +1,936 for the probe. `xlarge` / `no_emoji` exceed the 6.25 MB SD-flash
203+
slot but are unchanged in that respect — they were 7.66 / 7.24 MB on 4.7.1 and
204+
fit only the 7,864,320-byte app partition (USB flash), which is why the shipping
205+
SD-flashable build is a `tiny-*` variant.
206+
207+
Simulator envs are unaffected: `[env:simulator*]` declares its own `lib_deps`
208+
without the freeink-sdk symlinks and sets `lib_ignore = hal`, so neither the SDK
209+
nor `HalDisplay.cpp` compiles there.
210+
211+
### Step 5 — hardware gate: what actually happened
212+
213+
The plan's premise was wrong in one important way, and hardware is what proved it.
214+
215+
**The newer-X4 problem is not a panel-controller swap.** CrossInk's release notes
216+
split it: 1.4.0.1 (Jul 29) added display-driver detection for newer **X3** units,
217+
while 1.5.0-rc-2 (Aug 1) added "support for latest X4 battery latch on newer
218+
models (without this patch, newer X4's could seem unresponsive unless connected
219+
to USB power)". Upstream's own consumer entry point agrees —
220+
`selectXteinkDevice()` probes the display bus **only on a confirmed X3**; on X4 it
221+
just selects the profile. No shipping firmware probes an X4's panel bus.
222+
223+
Three hardware failures, all caused by this port, all from the same root mistake
224+
(running things at boot that upstream does not run, or runs in a different order):
225+
226+
1. **X4 hang, probe inside `HalDisplay::begin()`**`applyXteinkDisplayController()`
227+
was called unconditionally on every device. Verdict was *correct*
228+
(`VER=FF..FF -> default controller`, SSD1677 kept) but display init never
229+
completed. Bisected with a `-DCRUMBLE_DISABLE_PANEL_PROBE` build, which booted
230+
clean and cleared the SDK update + both new drivers.
231+
2. **X4 hang, probe moved before `Storage.begin()`** — same symptom, which
232+
disproved the "SD was already mounted" theory. The real problem was simply
233+
that the X4 bus must not be probed at all.
234+
3. **X3 hang in `Storage.begin()`**`[SD] SD card detected` never printed. Calling
235+
`selectDevice(XteinkX3)` early switched `sd.powerEnable` from `PIN_UNASSIGNED`
236+
(X4 default) to GPIO13, so `SDCardManager` began driving the X3 SD rail for the
237+
first time ever (upstream `2da0700`, unvalidated here). Every prior CrumBLE
238+
release mounts SD while `ACTIVE` is still `XTEINK_X4`; copying upstream's call
239+
site into a different init order is not "1:1".
240+
241+
**Shipped state:** no panel-controller probe on either device. X4 gets
242+
`selectDevice(XteinkX4)` for the battery latch only. X3's boot path is byte-for-byte
243+
4.7.1. UC8179/UC8279 drivers compile in but nothing selects them — documented as a
244+
known limitation rather than pretended-to-work.
245+
246+
Verified on hardware: X4 boots and opens books (init waits 2/5/5 ms); X3 boots
247+
past SD mount. Settings log-spam fix verified at `maxAlloc=19444`, below both old
248+
thresholds, with zero spam.
249+
250+
Nothing has been released.
251+
252+
Working tree is uncommitted. The 21 new SDK paths (including
253+
`Uc8179Driver.{h,cpp}` and `Uc8279Driver.{h,cpp}`) are untracked — they compile
254+
because PlatformIO globs the source dir, but they need `git add` before a commit.
255+
97256
## Do not
98257

99258
- Do not ship without the on-device X4 + X3 regression pass. A bad waveform

0 commit comments

Comments
 (0)