Compare commits

..
Author SHA1 Message Date
Uri Tauber 95a847c721 Merge branch 'develop' 2026-08-07 10:04:31 +03:00
53 changed files with 112 additions and 2223 deletions

No files matched your search

-5
View File
@@ -369,11 +369,6 @@ int FontDecompressor::prewarmCache(const EpdFontData* fontData, const char* utf8
}
stats.pageBufferBytes += totalBytes;
stats.pageGlyphsBytes += glyphCount * sizeof(PageGlyphEntry);
// MEMFIX-PORT: page-slot address landmark for the heap map; portable
// Landmark for the heap block map: page slots are the largest flash-font
// allocations and otherwise show up as anonymous ~4-20 KB used blocks.
LOG_DBG("FDC", "page slot buffer=%p bytes=%u glyphs=%u", static_cast<void*>(slot.buffer), (unsigned)totalBytes,
(unsigned)glyphCount);
slot.fontData = fontData;
slot.glyphCount = glyphCount;
+28 -140
View File
@@ -99,11 +99,8 @@ void SdCardFont::freeStyleMiniData(PerStyle& s) {
s.miniIntervals = nullptr;
delete[] s.miniGlyphs;
s.miniGlyphs = nullptr;
for (auto& chunk : s.miniBitmapChunks) {
delete[] chunk;
chunk = nullptr;
}
s.miniBitmapChunkCount = 0;
delete[] s.miniBitmap;
s.miniBitmap = nullptr;
s.miniIntervalCount = 0;
s.miniGlyphCount = 0;
s.miniIntervalCapacity = 0;
@@ -191,10 +188,6 @@ void SdCardFont::freeStyleAll(PerStyle& s) {
void SdCardFont::freeAll() {
clearOverflow();
clearPersistentCache();
delete[] overflowEmergencyBuf_;
overflowEmergencyBuf_ = nullptr;
overflowEmergencyCap_ = 0;
overflowEmergency_ = {};
for (uint8_t i = 0; i < MAX_STYLES; i++) {
freeStyleAll(styles_[i]);
}
@@ -711,12 +704,6 @@ bool SdCardFont::load(const char* path) {
applyGlyphMissCallback(i);
}
// Reserve the overflow fallback buffer now, while the heap is at its healthiest
// (font load runs before the per-page render allocations). Best-effort: if it
// can't allocate, onGlyphMiss() simply keeps its per-glyph malloc path.
overflowEmergencyBuf_ = new (std::nothrow) uint8_t[OVERFLOW_EMERGENCY_BYTES];
overflowEmergencyCap_ = overflowEmergencyBuf_ ? OVERFLOW_EMERGENCY_BYTES : 0;
loaded_ = true;
LOG_DBG("SDCF", "Loaded: %s (v%u, %u styles)", path, CPFONT_VERSION, styleCount_);
@@ -1023,65 +1010,31 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
totalBitmapSize += s.miniGlyphs[i].dataLength;
}
// Read bitmap data sorted by file offset (sequential glyphs then avoid a seek).
if (!ensureArrayCapacity(s.miniBitmap, s.miniBitmapCapacity, totalBitmapSize)) {
LOG_ERR("SDCF", "Failed to allocate mini bitmap (%u bytes) for style %u", totalBitmapSize, styleIdx);
delete[] readOrder;
delete[] mappings;
freeStyleMiniData(s);
return static_cast<int>(cpCount);
}
s.miniBitmapUsed = totalBitmapSize; // underuse-hysteresis signal for resetStyleMiniData
// Read bitmap data sorted by file offset
std::sort(readOrder, readOrder + validCount,
[&](uint32_t a, uint32_t b) { return s.miniGlyphs[a].dataOffset < s.miniGlyphs[b].dataOffset; });
// Place each glyph wholly within one fixed-size chunk (padding to the next chunk
// when it wouldn't fit the remainder), so a large page's bitmap fits a fragmented
// heap yet the render still gets a contiguous per-glyph pointer. `span` is the
// byte offset across the logical chunk sequence and becomes glyph->dataOffset;
// chunks are allocated on demand and kept across pages. Any per-chunk alloc
// failure drops to the stub (overflow path), exactly as the old single-buffer
// allocation did.
uint32_t span = 0;
uint32_t miniBitmapOffset = 0;
uint32_t lastBitmapEnd = UINT32_MAX;
for (uint32_t i = 0; i < validCount; i++) {
const uint32_t mapIdx = readOrder[i];
uint32_t mapIdx = readOrder[i];
EpdGlyph& glyph = s.miniGlyphs[mapIdx];
const uint32_t len = glyph.dataLength;
if (len == 0) {
glyph.dataOffset = span;
if (glyph.dataLength == 0) {
glyph.dataOffset = miniBitmapOffset;
continue;
}
if (len > MINI_BM_CHUNK_SIZE) {
// A glyph bigger than one chunk can't be placed contiguously. Reading fonts
// never hit this; bail to the stub rather than corrupt the layout.
LOG_ERR("SDCF", "Prewarm: glyph %u B exceeds chunk %u B (style %u)", len, MINI_BM_CHUNK_SIZE, styleIdx);
delete[] readOrder;
delete[] mappings;
freeStyleMiniData(s);
return static_cast<int>(cpCount);
}
// Avoid straddling a chunk boundary.
const uint32_t within = span & (MINI_BM_CHUNK_SIZE - 1);
if (within != 0 && within + len > MINI_BM_CHUNK_SIZE) {
span += MINI_BM_CHUNK_SIZE - within;
}
const uint32_t chunkIdx = span >> MINI_BM_CHUNK_SHIFT;
if (chunkIdx >= MINI_BM_MAX_CHUNKS) {
LOG_ERR("SDCF", "Prewarm: mini bitmap needs > %u chunks (style %u)", MINI_BM_MAX_CHUNKS, styleIdx);
delete[] readOrder;
delete[] mappings;
freeStyleMiniData(s);
return static_cast<int>(cpCount);
}
if (!s.miniBitmapChunks[chunkIdx]) {
s.miniBitmapChunks[chunkIdx] = new (std::nothrow) uint8_t[MINI_BM_CHUNK_SIZE];
if (!s.miniBitmapChunks[chunkIdx]) {
LOG_ERR("SDCF", "Failed to allocate mini bitmap chunk %u (style %u)", chunkIdx, styleIdx);
delete[] readOrder;
delete[] mappings;
freeStyleMiniData(s);
return static_cast<int>(cpCount);
}
if (chunkIdx + 1 > s.miniBitmapChunkCount) s.miniBitmapChunkCount = chunkIdx + 1;
}
const uint32_t off = span & (MINI_BM_CHUNK_SIZE - 1);
const uint32_t fileOff = s.bitmapFileOffset + glyph.dataOffset;
uint32_t fileOff = s.bitmapFileOffset + glyph.dataOffset;
if (fileOff != lastBitmapEnd) {
if (!file.seekSet(fileOff)) {
LOG_ERR("SDCF", "Prewarm: failed to seek to bitmap (style %u)", styleIdx);
@@ -1093,20 +1046,18 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
}
seekCount++;
}
if (file.read(s.miniBitmapChunks[chunkIdx] + off, len) != static_cast<int>(len)) {
if (file.read(s.miniBitmap + miniBitmapOffset, glyph.dataLength) != static_cast<int>(glyph.dataLength)) {
LOG_ERR("SDCF", "Prewarm: short bitmap read (style %u)", styleIdx);
delete[] readOrder;
delete[] mappings;
freeStyleMiniData(s);
return static_cast<int>(cpCount);
}
lastBitmapEnd = fileOff + len;
lastBitmapEnd = fileOff + glyph.dataLength;
glyph.dataOffset = span;
span += len;
glyph.dataOffset = miniBitmapOffset;
miniBitmapOffset += glyph.dataLength;
}
s.miniBitmapUsed = span; // footprint incl. padding: underuse-hysteresis signal
s.miniBitmapCapacity = s.miniBitmapChunkCount * MINI_BM_CHUNK_SIZE;
}
uint32_t sdTime = millis() - sdStart;
@@ -1129,10 +1080,7 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
s.miniMetadataOnly = metadataOnly;
s.miniHysteresisPending = !metadataOnly; // one hysteresis evaluation per rebuild
memset(&s.miniData, 0, sizeof(s.miniData));
// Mini bitmap is chunked (non-contiguous) so there is no single base pointer.
// SD mini glyphs are resolved via SdCardFont::miniGlyphBitmap() in the renderer;
// leave bitmap null so any accidental &bitmap[dataOffset] deref faults loudly.
s.miniData.bitmap = nullptr;
s.miniData.bitmap = s.miniBitmap;
s.miniData.glyph = s.miniGlyphs;
s.miniData.intervals = s.miniIntervals;
s.miniData.intervalCount = s.miniIntervalCount;
@@ -1516,45 +1464,25 @@ const EpdGlyph* SdCardFont::onGlyphMiss(void* ctx, uint32_t codepoint) {
// Read bitmap data into temporary (if any)
uint8_t* tempBitmap = nullptr;
bool usingEmergency = false;
if (tempGlyph.dataLength > 0) {
tempBitmap = new (std::nothrow) uint8_t[tempGlyph.dataLength];
if (!tempBitmap) {
// Per-glyph malloc failed under heap pressure. Fall back to the pre-reserved
// emergency buffer so the glyph still renders instead of blanking (the whole
// last-loaded style, in practice bold-italic, otherwise disappears while BLE
// is resident). Read directly into it; this path bypasses the cache ring.
if (self->overflowEmergencyBuf_ && tempGlyph.dataLength <= self->overflowEmergencyCap_) {
tempBitmap = self->overflowEmergencyBuf_;
usingEmergency = true;
} else {
LOG_ERR("SDCF", "Overflow: failed to allocate %u bytes for U+%04X bitmap", tempGlyph.dataLength, codepoint);
return nullptr;
}
LOG_ERR("SDCF", "Overflow: failed to allocate %u bytes for U+%04X bitmap", tempGlyph.dataLength, codepoint);
return nullptr;
}
if (!file.seekSet(s.bitmapFileOffset + tempGlyph.dataOffset)) {
LOG_ERR("SDCF", "Overflow: failed to seek to bitmap for U+%04X", codepoint);
if (!usingEmergency) delete[] tempBitmap;
delete[] tempBitmap;
file.close();
return nullptr;
}
if (file.read(tempBitmap, tempGlyph.dataLength) != static_cast<int>(tempGlyph.dataLength)) {
LOG_ERR("SDCF", "Overflow: failed to read bitmap for U+%04X", codepoint);
if (!usingEmergency) delete[] tempBitmap;
delete[] tempBitmap;
return nullptr;
}
}
// Emergency-buffer path: return the glyph directly, without touching the ring
// (the buffer is a single shared slot, overwritten on the next miss).
if (usingEmergency) {
self->overflowEmergency_.glyph = tempGlyph;
self->overflowEmergency_.bitmap = tempBitmap;
self->overflowEmergency_.codepoint = codepoint;
self->overflowEmergency_.styleIdx = styleIdx;
return &self->overflowEmergency_.glyph;
}
// All reads succeeded — commit to slot and advance ring buffer
if (wasAtCapacity) {
delete[] self->overflow_[slot].bitmap;
@@ -1567,42 +1495,13 @@ const EpdGlyph* SdCardFont::onGlyphMiss(void* ctx, uint32_t codepoint) {
self->overflow_[slot].codepoint = codepoint;
self->overflow_[slot].styleIdx = styleIdx;
// No per-glyph log here: when the prewarm mini-data doesn't cover a page every
// glyph takes this path, which floods the console. Overflow-heavy pages are
// instead visible as slow renders.
LOG_DBG("SDCF", "Overflow: loaded U+%04X style %u on demand (slot %u/%u)", codepoint, styleIdx, slot,
OVERFLOW_CAPACITY);
return &self->overflow_[slot].glyph;
}
size_t SdCardFont::reportMemory() const {
size_t total = 0;
for (uint8_t si = 0; si < MAX_STYLES; ++si) {
const auto& s = styles_[si];
if (!s.present) continue;
size_t fixed = 0; // loaded once per family: interval/kern/lig tables
if (s.fullIntervals) fixed += s.header.intervalCount * sizeof(EpdUnicodeInterval);
if (s.bmpIntervals) fixed += s.header.intervalCount * sizeof(PerStyle::BmpInterval16);
if (s.kernLeftClasses) fixed += s.header.kernLeftEntryCount * sizeof(EpdKernClassEntry);
if (s.kernRightClasses) fixed += s.header.kernRightEntryCount * sizeof(EpdKernClassEntry);
if (s.ligaturePairs) fixed += s.header.ligaturePairCount * sizeof(EpdLigaturePair);
// kept-if-fits mini arenas: capacity (not count) is what stays resident
size_t mini = s.miniIntervalCapacity * sizeof(EpdUnicodeInterval) + s.miniGlyphCapacity * sizeof(EpdGlyph) +
s.miniBitmapCapacity + s.miniKernLeftCapacity * sizeof(EpdKernClassEntry) +
s.miniKernRightCapacity * sizeof(EpdKernClassEntry) + s.miniKernMatrixCapacity;
const size_t adv = advanceTableSize_[si] * sizeof(AdvanceEntry);
LOG_DBG("SDCF", "mem style%u: fixed=%u mini=%u adv=%u", si, (unsigned)fixed, (unsigned)mini, (unsigned)adv);
total += fixed + mini + adv;
}
size_t overflowBytes = 0;
for (uint32_t i = 0; i < overflowCount_; ++i) {
if (overflow_[i].bitmap) overflowBytes += overflow_[i].glyph.dataLength;
}
total += overflowBytes + overflowCount_ * sizeof(OverflowEntry);
return total;
}
bool SdCardFont::isOverflowGlyph(const EpdGlyph* glyph) const {
if (glyph == &overflowEmergency_.glyph) return true;
for (uint32_t i = 0; i < overflowCount_; i++) {
if (&overflow_[i].glyph == glyph) return true;
}
@@ -1610,7 +1509,6 @@ bool SdCardFont::isOverflowGlyph(const EpdGlyph* glyph) const {
}
const uint8_t* SdCardFont::getOverflowBitmap(const EpdGlyph* glyph) const {
if (glyph == &overflowEmergency_.glyph) return overflowEmergency_.bitmap;
for (uint32_t i = 0; i < overflowCount_; i++) {
if (&overflow_[i].glyph == glyph) {
return overflow_[i].bitmap;
@@ -1619,14 +1517,4 @@ const uint8_t* SdCardFont::getOverflowBitmap(const EpdGlyph* glyph) const {
return nullptr;
}
const uint8_t* SdCardFont::miniGlyphBitmap(const void* ctx, uint32_t dataOffset) const {
const auto* octx = static_cast<const OverflowContext*>(ctx);
const PerStyle& s = styles_[octx->styleIdx];
const uint32_t chunkIdx = dataOffset >> MINI_BM_CHUNK_SHIFT;
if (chunkIdx >= s.miniBitmapChunkCount) return nullptr;
const uint8_t* chunk = s.miniBitmapChunks[chunkIdx];
if (!chunk) return nullptr;
return chunk + (dataOffset & (MINI_BM_CHUNK_SIZE - 1));
}
SdCardFont* SdCardFont::fromMissCtx(void* ctx) { return static_cast<OverflowContext*>(ctx)->self; }
+3 -47
View File
@@ -94,14 +94,6 @@ class SdCardFont {
// Returns the bitmap for an on-demand-loaded (overflow) glyph.
const uint8_t* getOverflowBitmap(const EpdGlyph* glyph) const;
// Resolve a prewarmed mini glyph's bitmap. The mini bitmap is chunked (see
// MINI_BM_* above), so glyph->dataOffset is a byte offset across the fixed-size
// chunk sequence; each glyph lives wholly within one chunk. ctx is the glyph's
// EpdFontData::glyphMissCtx (an OverflowContext carrying the style index). Called
// by GfxRenderer::getGlyphBitmap for SD-card mini glyphs. Returns nullptr if the
// chunk isn't resident.
const uint8_t* miniGlyphBitmap(const void* ctx, uint32_t dataOffset) const;
// Extract SdCardFont* from an opaque glyphMissCtx pointer.
// Used by GfxRenderer::getGlyphBitmap() to recover the SdCardFont from EpdFontData::glyphMissCtx.
static SdCardFont* fromMissCtx(void* ctx);
@@ -113,11 +105,6 @@ class SdCardFont {
uint32_t uniqueGlyphs = 0;
uint32_t bitmapBytes = 0;
};
// MEMFIX-PORT: SD font resident-bytes audit; portable
// Log per-style resident heap (full tables + kept-if-fits mini arenas +
// advance tables + overflow bitmaps) and return the total in bytes. Pure
// accounting — no allocation, no state change.
size_t reportMemory() const;
void logStats(const char* label = "SDCF");
void resetStats();
const Stats& getStats() const { return stats_; }
@@ -142,16 +129,6 @@ class SdCardFont {
uint8_t ligaturePairCount = 0;
};
// Per-page prewarm bitmap chunking. A page of a large 2-bpp font needs a
// 20-40 KB bitmap; as one contiguous block it fails to allocate on a fragmented,
// BLE-resident heap, dropping the whole style to the slow overflow path. Splitting
// it into fixed 4 KB chunks (each glyph kept whole within one chunk) lets it fit
// scattered free space, so prewarm succeeds and the render still gets a contiguous
// per-glyph pointer. Mirrors the image .pxc slot's chunking.
static constexpr uint32_t MINI_BM_CHUNK_SHIFT = 12; // 4 KB chunks
static constexpr uint32_t MINI_BM_CHUNK_SIZE = 1u << MINI_BM_CHUNK_SHIFT;
static constexpr uint32_t MINI_BM_MAX_CHUNKS = 24; // 96 KB max per-style page bitmap
// All per-style data: file offsets, intervals, kern/lig, prewarm cache, EpdFont
struct PerStyle {
CpFontHeader header{};
@@ -211,16 +188,12 @@ class SdCardFont {
EpdFontData miniData{};
EpdUnicodeInterval* miniIntervals = nullptr;
EpdGlyph* miniGlyphs = nullptr;
// Chunked mini bitmap (see MINI_BM_* above): fixed-size chunks so a large page's
// bitmap fits a fragmented heap. Each glyph is placed wholly within one chunk;
// EpdGlyph::dataOffset is a byte offset across the logical chunk sequence.
uint8_t* miniBitmapChunks[MINI_BM_MAX_CHUNKS] = {};
uint32_t miniBitmapChunkCount = 0; // chunks currently allocated (0..count-1), kept across pages
uint8_t* miniBitmap = nullptr;
uint32_t miniIntervalCount = 0;
uint32_t miniGlyphCount = 0;
uint32_t miniIntervalCapacity = 0;
uint32_t miniGlyphCapacity = 0;
uint32_t miniBitmapCapacity = 0; // miniBitmapChunkCount * MINI_BM_CHUNK_SIZE (hysteresis/report)
uint32_t miniBitmapCapacity = 0;
// Bitmap bytes the current page actually used (set by prewarmStyle), the
// underuse-hysteresis signal; 0 = no bitmap built this scope (metadata-only
// prewarm), which leaves the hysteresis counter untouched.
@@ -270,10 +243,7 @@ class SdCardFont {
};
OverflowContext overflowCtx_[MAX_STYLES] = {};
// Shared on-demand overflow ring (glyphs loaded via glyphMissHandler) -- the
// fallback for glyphs the per-page prewarm didn't cover. Kept small: the chunked
// mini-bitmap (below) lets prewarm succeed on a fragmented heap, so this ring
// only serves the rare uncovered glyph rather than a whole page.
// Shared on-demand overflow buffer (ring buffer of glyphs loaded via glyphMissHandler)
static constexpr uint32_t OVERFLOW_CAPACITY = 8;
struct OverflowEntry {
EpdGlyph glyph;
@@ -285,20 +255,6 @@ class SdCardFont {
uint32_t overflowCount_ = 0;
uint32_t overflowNext_ = 0;
// Single pre-reserved fallback buffer for the on-demand overflow path.
// onGlyphMiss() normally mallocs each glyph's bitmap; under heap pressure (BLE
// resident) that per-glyph malloc fails and the glyph -- and thus the whole
// last-loaded style, in practice bold-italic -- renders blank. This buffer is
// reserved once at load(), when the heap is healthy. When the per-glyph malloc
// fails, the current glyph is read into this buffer and rendered directly. It is
// NOT part of the cache ring above (it is overwritten on every use), which is
// fine: getGlyphBitmap() consumes the bitmap immediately after the miss handler
// returns, before the next glyph is fetched.
static constexpr uint32_t OVERFLOW_EMERGENCY_BYTES = 1024;
uint8_t* overflowEmergencyBuf_ = nullptr;
uint32_t overflowEmergencyCap_ = 0;
OverflowEntry overflowEmergency_ = {};
// Compact advance-only table for layout measurement (per-style).
// Built by buildAdvanceTable(), queried by getAdvance().
struct AdvanceEntry {
-8
View File
@@ -107,14 +107,6 @@ void SdCardFontManager::unloadAll(GfxRenderer& renderer) {
loadedPointSize_ = 0;
}
size_t SdCardFontManager::reportMemory() const {
size_t total = 0;
for (const auto& lf : loaded_) {
if (lf.font) total += lf.font->reportMemory();
}
return total;
}
int SdCardFontManager::getFontId(const std::string& familyName) const {
if (familyName != loadedFamilyName_ || loaded_.empty()) return 0;
return loaded_.front().fontId;
-4
View File
@@ -39,10 +39,6 @@ class SdCardFontManager {
// Get name of currently loaded family (empty if none).
const std::string& currentFamilyName() const { return loadedFamilyName_; };
// MEMFIX-PORT: font manager audit passthrough; portable
// Sum of loaded fonts' resident heap (see SdCardFont::reportMemory).
size_t reportMemory() const;
// Point size that was actually loaded.
// 0 if nothing loaded.
uint8_t currentPointSize() const { return loadedPointSize_; };
-12
View File
@@ -44,18 +44,6 @@ class Epub {
}
~Epub() = default;
std::string& getBasePath() { return contentBasePath; }
// MEMFIX-PORT: epub resident-bytes audit accessor; portable
// Approximate resident heap of the open book (audit): path strings, the CSS
// file list, and the parsed stylesheet. BookMetadataCache is file-backed
// (counts + HalFile handles) and contributes little.
size_t residentBytes() const {
size_t total = sizeof(Epub) + tocNcxItem.capacity() + tocNavItem.capacity() + filepath.capacity() +
contentBasePath.capacity() + cachePath.capacity();
for (const auto& f : cssFiles) total += sizeof(f) + (f.capacity() > 15 ? f.capacity() : 0);
if (cssParser) total += cssParser->residentBytes();
return total;
}
size_t cssRuleCount() const { return cssParser ? cssParser->ruleCount() : 0; }
bool load(bool buildIfMissing = true, bool skipLoadingCss = false);
bool clearCache() const;
void setupCacheDir() const;
-4
View File
@@ -173,10 +173,6 @@ std::unique_ptr<Page> Page::deserialize(HalFile& file) {
uint16_t count;
serialization::readPod(file, count);
// Reserve up front: growth-by-doubling needs old + new capacity live at once and
// reallocates repeatedly — a field crash (bad_alloc -> abort under -fno-exceptions)
// hit exactly this append path on a heavily fragmented heap.
page->elements.reserve(count);
// Reserve up front so a page load costs one allocation for the element vector
// instead of a grow-copy-free cycle every doubling. `count` is untrusted (it
-15
View File
@@ -72,7 +72,6 @@ class Section {
// Builds write here and are swapped over filePath only on commit, so a prior
// partial/finalized file stays readable while a rebuild is in progress.
std::string binTmpPath() const { return filePath + ".part"; }
std::unique_ptr<Page> loadPageAt(int page) const;
// Read a page already laid out by the in-progress build (page < build LUT size), from
// the partially-written tmp .bin without disturbing the build's write cursor.
@@ -125,20 +124,6 @@ class Section {
// (covers finalized sections and partials from a previous session).
std::optional<uint16_t> findAnchor(const std::string& anchor) const;
// MEMFIX-PORT: section resident-bytes audit accessor; portable
// Approximate resident heap for the audit log. Steady state (no build) a
// Section holds little beyond itself; during a build the page LUT and path
// strings dominate (the parser's internal footprint is not walked here).
size_t residentBytes() const {
size_t total = sizeof(Section) + filePath.capacity();
if (build_) {
total += sizeof(BuildContext) + build_->lut.capacity() * sizeof(PageLutEntry) +
build_->parsePath.capacity() + build_->contentBase.capacity() + build_->imageBasePath.capacity() +
build_->htmlPath.capacity() + build_->tmpHtmlPath.capacity();
}
return total;
}
// True if this spine's unzipped HTML is already cached, so a build won't pay the (multi-second on a
// giant spine) zip inflation. Lets the reader skip the indexing popup on a fast reopen/rebuild.
bool hasHtmlCache() const;
-19
View File
@@ -327,25 +327,6 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
FontCacheManager* fcm = renderer.getFontCacheManager();
if (fcm && fcm->isScanning()) return;
// Grayscale strip passes re-invoke render() once per horizontal band and once
// per plane. When the image's pixels are resident in the RAM slot, redrawing
// from RAM is cheap and preserves 4-level AA. When they are NOT resident -- the
// payload was too big to cache on this heap (a full-page image is ~80 KB vs
// ~48 KB free with BLE up), or another image on the page owns the single slot --
// each band would re-stream the entire .pxc from SD. For a full-page image
// that is ~13 full-file reads (BW pass + ~6 column bands x 2 planes in
// portrait), the dominant cost of an image page (measured bw_render ~7.8 s).
// The image already has its 1bpp rendering from the BW pass, so skip the
// grayscale redraw instead of re-streaming: cacheable images keep full AA,
// oversized ones stay BW (already dithered in the .pxc). Orientation-agnostic --
// gated on slot residency, not on Portrait vs Landscape.
const GfxRenderer::RenderMode renderMode = renderer.getRenderMode();
if (renderMode == GfxRenderer::GRAYSCALE_LSB || renderMode == GfxRenderer::GRAYSCALE_MSB) {
if (pxcSlotHash != imagePathHash(getCachePath(imagePath))) {
return; // not slot-resident; leave the BW-pass pixels untouched
}
}
LOG_DBG("IMG", "Rendering image at %d,%d: %s (%dx%d)", x, y, imagePath.c_str(), width, height);
const int screenWidth = renderer.getScreenWidth();
-14
View File
@@ -67,20 +67,6 @@ class CssParser {
*/
[[nodiscard]] static CssStyle parseInlineStyle(std::string_view styleValue);
// MEMFIX-PORT: stylesheet resident-bytes audit accessor; portable
// Approximate resident heap of the parsed stylesheet, for the audit log.
// unordered_map cost model: bucket array + one node per rule (libstdc++ node
// overhead ~= 2 pointers + hash) + key string capacity when it exceeds SSO.
size_t residentBytes() const {
size_t total = rulesBySelector_.bucket_count() * sizeof(void*);
for (const auto& kv : rulesBySelector_) {
total += sizeof(void*) * 2 + sizeof(size_t); // node overhead
total += sizeof(kv);
if (kv.first.capacity() > 15) total += kv.first.capacity(); // beyond SSO
}
return total;
}
/**
* Check if any rules have been loaded
*/
+23 -38
View File
@@ -62,22 +62,16 @@ void FontCacheManager::resetStats() {
bool FontCacheManager::isScanning() const { return scanMode_ == ScanMode::Scanning; }
void FontCacheManager::recordText(const char* text, int fontId, EpdFontFamily::Style style) {
if (!text) return;
// Bucket the text under its base style so each style is prewarmed only for what
// it actually renders (see the header comment on scanText_).
scanText_ += text;
if (scanFontId_ < 0) scanFontId_ = fontId;
const uint8_t baseStyle = static_cast<uint8_t>(style) & 0x03;
size_t& len = scanTextLen_[baseStyle];
const size_t remaining = (len < SCAN_TEXT_CAPACITY - 1) ? (SCAN_TEXT_CAPACITY - 1 - len) : 0;
if (remaining > 0) {
const size_t textLen = strnlen(text, remaining);
memcpy(scanText_[baseStyle] + len, text, textLen);
len += textLen;
scanText_[baseStyle][len] = '\0';
}
if (!scanRecorded_) {
scanFontId_ = fontId; // capture the first drawn font; may be negative (SD font)
scanRecorded_ = true;
const unsigned char* p = reinterpret_cast<const unsigned char*>(text);
uint32_t cpCount = 0;
while (*p) {
if ((*p & 0xC0) != 0x80) cpCount++;
p++;
}
scanStyleCounts_[baseStyle] += cpCount;
}
// --- PrewarmScope implementation ---
@@ -86,37 +80,28 @@ FontCacheManager::PrewarmScope::PrewarmScope(FontCacheManager& manager) : manage
manager_->scanMode_ = ScanMode::Scanning;
manager_->clearCache();
manager_->resetStats();
for (uint8_t i = 0; i < STYLE_COUNT; i++) {
manager_->scanTextLen_[i] = 0;
manager_->scanText_[i][0] = '\0';
}
manager_->scanText_.clear();
manager_->scanText_.reserve(2048); // Pre-allocate to avoid heap fragmentation from repeated concat
memset(manager_->scanStyleCounts_, 0, sizeof(manager_->scanStyleCounts_));
manager_->scanFontId_ = -1;
manager_->scanRecorded_ = false;
}
void FontCacheManager::PrewarmScope::endScanAndPrewarm() {
manager_->scanMode_ = ScanMode::None;
if (!manager_->scanRecorded_) return; // nothing recorded (also the "already ran" no-op)
if (manager_->scanText_.empty()) return;
// Prewarm each style separately, against only the text drawn in that style, so a
// secondary style's page-slot buffer stays as small as its actual usage. A single
// per-style bit mask routes prewarmCache() to exactly that style on both the SD
// and compressed-font paths.
for (uint8_t i = 0; i < STYLE_COUNT; i++) {
if (manager_->scanTextLen_[i] == 0) continue;
const unsigned long tStyle = millis();
manager_->prewarmCache(manager_->scanFontId_, manager_->scanText_[i], static_cast<uint8_t>(1u << i));
// Per-style prewarm cost + text volume: on a heap-pressured build (BLE resident)
// a cold chapter-opening page can touch several styles, each paying SD reads for
// bitmaps + kern/ligature tables. This line attributes a slow page to the styles
// and byte volume actually loaded, so the wall-clock spike is not a guess.
LOG_DBG("FCM", "prewarm style%u: %lums (%u text bytes)", i, millis() - tStyle,
(unsigned)manager_->scanTextLen_[i]);
manager_->scanTextLen_[i] = 0;
manager_->scanText_[i][0] = '\0';
// Build style bitmask from all styles that appeared during the scan
uint8_t styleMask = 0;
for (uint8_t i = 0; i < 4; i++) {
if (manager_->scanStyleCounts_[i] > 0) styleMask |= (1 << i);
}
manager_->scanFontId_ = -1;
manager_->scanRecorded_ = false; // makes the dtor's second call a no-op
if (styleMask == 0) styleMask = 1; // default to regular
manager_->prewarmCache(manager_->scanFontId_, manager_->scanText_.c_str(), styleMask);
// Free scan string memory
manager_->scanText_.clear();
manager_->scanText_.shrink_to_fit();
}
FontCacheManager::PrewarmScope::~PrewarmScope() {
+3 -18
View File
@@ -2,9 +2,9 @@
#include <EpdFontFamily.h>
#include <cstddef>
#include <cstdint>
#include <map>
#include <string>
class FontDecompressor;
class SdCardFont;
@@ -51,22 +51,7 @@ class FontCacheManager {
enum class ScanMode : uint8_t { None, Scanning };
ScanMode scanMode_ = ScanMode::None;
// Per-style scan buffers (index = base Style: 0=Regular, 1=Bold, 2=Italic,
// 3=Bold-Italic). Each page render prewarms every style ONLY against the text
// actually drawn in that style, not the whole page. Prewarming a secondary style
// over the entire page produced a full-page-sized FontDecompressor page-slot
// buffer per style; with the BLE stack resident the later styles' malloc failed,
// dropping bold/italic onto the hot-group fallback (which also OOM'd) so those
// glyphs rendered blank. Bucketing by style keeps each secondary buffer as small
// as the bold/italic text present, so every style reliably gets its slot.
static constexpr size_t SCAN_TEXT_CAPACITY = 2048;
static constexpr uint8_t STYLE_COUNT = 4;
char scanText_[STYLE_COUNT][SCAN_TEXT_CAPACITY] = {};
size_t scanTextLen_[STYLE_COUNT] = {};
// Font id recorded during the scan pass. SD-card font ids are negative
// (std::hash-derived), so "nothing recorded" CANNOT be sentinelled as < 0 --
// that would swallow every SD font and skip its prewarm entirely. Track
// whether anything was recorded with an explicit flag instead.
std::string scanText_;
uint32_t scanStyleCounts_[4] = {};
int scanFontId_ = -1;
bool scanRecorded_ = false;
};
-3
View File
@@ -81,9 +81,6 @@ const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const Ep
if (sdFont->isOverflowGlyph(glyph)) {
return sdFont->getOverflowBitmap(glyph); // may be nullptr for zero-width glyphs
}
// Prewarmed SD glyph: the mini bitmap is chunked (non-contiguous), so there is
// no fontData->bitmap base to index. Resolve via the per-style chunk table.
return sdFont->miniGlyphBitmap(fontData->glyphMissCtx, glyph->dataOffset);
}
return &fontData->bitmap[glyph->dataOffset];
}
-19
View File
@@ -324,25 +324,6 @@ STR_HW_BACK_LABEL: "Back (1st button)"
STR_HW_CONFIRM_LABEL: "Confirm (2nd button)"
STR_HW_LEFT_LABEL: "Left (3rd button)"
STR_HW_RIGHT_LABEL: "Right (4th button)"
STR_BLUETOOTH: "Bluetooth"
STR_TOGGLE_BLUETOOTH: "Toggle Bluetooth"
STR_BT_SCAN_PAIR: "Scan & Pair"
STR_BT_NO_DEVICES: "No devices found"
STR_BT_FREE_HINT1: "Set Free2/3 to Reader Mode and"
STR_BT_FREE_HINT2: "Volume Function to pair"
STR_BT_DISCONNECT: "Disconnect"
STR_BT_CONNECTED_TO: "Connected: %s"
STR_BT_NOT_CONNECTED: "Not connected"
STR_BT_PAIRED_DEVICES: "Paired Devices"
STR_BT_NO_PAIRED: "No paired devices"
STR_BT_MAP_BUTTONS: "Map Remote Buttons"
STR_BT_PRESS_REMOTE: "Press a button on your remote"
STR_BT_CONNECTING_POPUP: "BT Connecting..."
STR_BT_PAUSED_LOW_MEM_POPUP: "BT paused (low memory)"
STR_STATE_PAUSED: "PAUSED"
STR_BT_PAGE_FORWARD: "Page Forward"
STR_BT_PAGE_BACK: "Page Back"
STR_BT_FORGET_PROMPT: "Hold Confirm to forget"
STR_GO_TO_PERCENT: "Go to %"
STR_GO_HOME_BUTTON: "Go Home"
STR_SYNC_PROGRESS: "Sync Progress"
+6 -84
View File
@@ -60,16 +60,6 @@ build_flags =
-Wno-bidi-chars
-Wl,--wrap=panic_print_backtrace,--wrap=panic_abort,--wrap=bootloader_common_check_efuse_blk_validity
-fno-exceptions
# FreeInk panel profiles: compile both X3 (792x528/UC8253) and X4 (800x480/SSD1677);
# the firmware picks the active one at runtime via HalDisplay setDisplayX3().
-DFREEINK_DEVICE_X3=1
-DFREEINK_DEVICE_X4=1
# BLE HID page-turner host (BleKeyboardHost). NimBLE role/bond config is baked into
# the prebuilt arduino-esp32 framework's sdkconfig.h, so we don't redefine it here
# (doing so only warns and has no effect). The host only compiles when
# FREEINK_CAP_BLE_HID_HOST is set on the env (below); with the capability off,
# BleKeyboardHost links stubs and pulls in zero NimBLE code.
-DFREEINK_BLE_HID_SHOW_UNNAMED_DEVICES=0
build_unflags =
-std=gnu++11
@@ -80,50 +70,18 @@ board_build.flash_mode = dio
board_build.flash_size = 16MB
board_build.partitions = partitions.csv
; Shrink the NimBLE footprint for a 1-connection HID host moving 3-6 byte reports.
; Field-measured: begin() costs ~52 KB with these trims vs ~68 KB with the prebuilt
; framework defaults — and that 15 KB is the difference between the stack landing
; above the reader's render shed floor (stable coexistence) and below it (a
; guaranteed shed/restart flap). Rebuilds the Arduino core libs on first build
; (slower once, cached after; needs the CMake pin in platformio.local.ini on macOS).
custom_sdkconfig =
; NimBLE-Arduino supplies the host stack; ESP-IDF supplies only the controller.
; Enabling IDF's NimBLE host too produces duplicate npl_freertos_* symbols.
CONFIG_BT_ENABLED=y
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=n
CONFIG_BT_CONTROLLER_ONLY=y
CONFIG_BT_CONTROLLER_ENABLED=y
CONFIG_BT_NIMBLE_ROLE_PERIPHERAL=n
CONFIG_BT_NIMBLE_ROLE_BROADCASTER=n
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
CONFIG_BT_NIMBLE_MAX_CCCDS=2
CONFIG_BT_NIMBLE_ATT_PREFERRED_MTU=23
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=6
CONFIG_BT_NIMBLE_MSYS_2_BLOCK_COUNT=6 ; was 24 x 320 B
CONFIG_BT_NIMBLE_ACL_BUF_COUNT=6 ; was 24 x 255 B
CONFIG_BT_NIMBLE_HCI_EVT_HI_BUF_COUNT=12 ; was 30 x 70 B; only scan bursts need many
; IDF 5.5 sizes the HCI transport pools under TRANSPORT_* names; pin both spellings.
CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=6
CONFIG_BT_NIMBLE_TRANSPORT_EVT_COUNT=12
CONFIG_BT_NIMBLE_ATT_MAX_PREP_ENTRIES=4 ; was 64; a HID host never does prepared writes
CONFIG_BT_CTRL_BLE_MAX_ACT=3 ; was 6; need conn + scan + initiate only
CONFIG_BT_CTRL_BLE_ADV_REPORT_FLOW_CTRL_NUM=50 ; was 100; pairing-time scan only
CONFIG_BT_CTRL_ADV_DUP_FILT_MAX=10 ; was 30
; BLE Mesh defaults ON for mesh-capable SoCs, but this firmware is a HID host,
; not a mesh node. Worse, IDF's esp_ble_mesh guards BLE_MESH_ADV_TASK_CORE
; behind CONFIG_BT_BLUEDROID_ENABLED / CONFIG_BT_NIMBLE_ENABLED (mesh/kernel.h);
; in our controller-only build (both IDF host stacks off, host is
; NimBLE-Arduino) that macro is undefined and adv_common.c fails to compile.
; Disable mesh outright: fixes the build and drops the whole unused component.
CONFIG_BLE_MESH=n
; MEMFIX-PORT: custom_sdkconfig heap reclamation (~32-37 KB). Rebuilds the
; Arduino core libs on first build (slower once, cached after; needs the CMake
; pin in platformio.local.ini on macOS).
;
; If an interrupted rebuild fails with "multiple definition of 'app_main'"
; (stale generated scaffold), clean it up with:
; rm -rf .dummy CMakeLists.txt sdkconfig.default sdkconfig.defaults .pio/build/default
; Do NOT use `git clean -fdX` — it deletes platformio.local.ini.
custom_sdkconfig =
; Task stack right-sizing from measured high-water marks (heap block map +
; per-task stack audit, July 2026): esp_timer used ~0.8 KB of 8 KB across
; every capture incl. BLE sessions; the FreeRTOS timer service used ~0.5 KB
; every capture; the FreeRTOS timer service used ~0.5 KB
; of 4 KB. Neither runs TLS or app code. ~7 KB back to the heap.
CONFIG_ESP_TIMER_TASK_STACK_SIZE=4096
CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=2560
@@ -163,7 +121,6 @@ extra_scripts =
pre:scripts/gen_i18n.py
pre:scripts/git_branch.py
pre:scripts/patch_jpegdec.py
pre:scripts/patch_bt_mem.py
post:scripts/register_unit_tests_target.py
; Libraries
@@ -172,7 +129,6 @@ lib_deps =
InputManager=symlink://freeink-sdk/libs/hardware/InputManager
EInkDisplay=symlink://freeink-sdk/libs/display/FreeInkDisplay
SDCardManager=symlink://freeink-sdk/libs/hardware/SDCardManager
; FreeInk HAL support libs the above depend on (BoardConfig pin maps, etc.).
BoardConfig=symlink://freeink-sdk/libs/hardware/BoardConfig
XteinkDetect=symlink://freeink-sdk/libs/hardware/XteinkDetect
PowerManager=symlink://freeink-sdk/libs/hardware/PowerManager
@@ -188,24 +144,11 @@ lib_deps =
links2004/WebSockets @ 2.7.3
wolfssl/Arduino-wolfSSL @ 5.7.2
; Ignore Arduino's legacy BLE wrapper. The pioarduino core builder preserves
; ESP-IDF's `bt` controller because NimBLE-Arduino is present in lib_deps.
lib_ignore =
BLE
; BLE HID host libraries are declared per-env, not in [base].lib_deps, on purpose:
; a developer's platformio.local.ini replaces [base].lib_deps wholesale (to repoint
; the FreeInk SDK symlinks at a local checkout), which silently dropped these and
; broke the build with "BleKeyboardHost.h: No such file". Env-level lib_deps are NOT
; overridden by local.ini, so declaring them here makes them stick. BleKeyboardHost
; is needed by EVERY env (src/BleInput compiles unconditionally against its stubs);
; NimBLE-Arduino is added only where FREEINK_CAP_BLE_HID_HOST is set.
[env:default]
extends = base
lib_deps =
${base.lib_deps}
BleKeyboardHost=symlink://freeink-sdk/libs/network/BleKeyboardHost
h2zero/NimBLE-Arduino @ ^2.3.8
build_flags =
${base.build_flags}
; CROSSPOINT_VERSION is set by scripts/git_branch.py (includes branch + short SHA)
@@ -213,17 +156,10 @@ build_flags =
-DFREEINK_DEVICE_X3=1
-DENABLE_SERIAL_LOG
-DLOG_LEVEL=2 ; Set log level to debug for development builds
-DFREEINK_CAP_BLE_HID_HOST=1 ; BLE HID page-turner host (NimBLE)
-DFREEINK_BLE_HID_SCAN_DEBUG=1 ; verbose BLE scan lifecycle/advertisement logs for bring-up
-DFREEINK_BLE_HID_REPORT_DEBUG=1 ; raw HID report hex dumps + report-map hints (bring-up)
[env:gh_release]
extends = base
lib_deps =
${base.lib_deps}
BleKeyboardHost=symlink://freeink-sdk/libs/network/BleKeyboardHost
h2zero/NimBLE-Arduino @ ^2.3.8
build_flags =
${base.build_flags}
-DFREEINK_DEVICE_X4=1
@@ -231,14 +167,9 @@ build_flags =
-DCROSSPOINT_VERSION=\"${crosspoint.version}\"
-DENABLE_SERIAL_LOG
-DLOG_LEVEL=1 ; Set log level to info for release builds
-DFREEINK_CAP_BLE_HID_HOST=1 ; BLE HID page-turner host (NimBLE)
[env:gh_release_rc]
extends = base
lib_deps =
${base.lib_deps}
BleKeyboardHost=symlink://freeink-sdk/libs/network/BleKeyboardHost
h2zero/NimBLE-Arduino @ ^2.3.8
build_flags =
${base.build_flags}
-DFREEINK_DEVICE_X4=1
@@ -246,14 +177,9 @@ build_flags =
-DCROSSPOINT_VERSION=\"${crosspoint.version}-rc+${sysenv.CROSSPOINT_RC_HASH}\"
-DENABLE_SERIAL_LOG
-DLOG_LEVEL=1 ; Set log level to info for release candidate builds
-DFREEINK_CAP_BLE_HID_HOST=1 ; BLE HID page-turner host (NimBLE)
[env:slim]
extends = base
; Cap off: BleKeyboardHost links stubs (src/BleInput still compiles), no NimBLE.
lib_deps =
${base.lib_deps}
BleKeyboardHost=symlink://freeink-sdk/libs/network/BleKeyboardHost
build_flags =
${base.build_flags}
-DFREEINK_DEVICE_X4=1
@@ -271,10 +197,6 @@ build_flags =
extends = base
board = esp32-s3-devkitc1-n16r8
board_build.mcu = esp32s3
; Cap off: BleKeyboardHost links stubs (src/BleInput still compiles), no NimBLE.
lib_deps =
${base.lib_deps}
BleKeyboardHost=symlink://freeink-sdk/libs/network/BleKeyboardHost
build_flags =
${base.build_flags}
-DFREEINK_DEVICE_STICKY=1
-95
View File
@@ -1,95 +0,0 @@
"""
PlatformIO pre-build script: make the arduino-esp32 core define _btLibraryInUse
in a BLE-controller-only build.
The core header cores/esp32/esp32-hal-bt-mem.h emits a constructor
(_setBtLibraryInUse) that references `_btLibraryInUse` whenever BLE hardware is
present, but the core only DEFINES that symbol (in esp32-hal-bt.c) when an IDF
host stack -- CONFIG_BLUEDROID_ENABLED or CONFIG_NIMBLE_ENABLED -- is enabled.
CrossPoint runs the ESP-IDF BLE *controller* only, with NimBLE-Arduino supplying
the host (both IDF host stacks are disabled in custom_sdkconfig to avoid duplicate
npl_freertos_* symbols). In that configuration the core never defines the symbol,
so NimBLE's constructor link-fails with "undefined reference to `_btLibraryInUse'"
-- and it fails in links that application source cannot reach (the final firmware
link and the arduino-lib-builder core-rebuild's dummy firmware).
Fix: turn the header's `extern bool _btLibraryInUse;` DECLARATION into a weak
DEFINITION. Every translation unit that includes the header (NimBLE) then provides
the symbol itself; the linker merges the duplicate weak defs, and the core's own
strong definition still wins whenever a host stack is actually enabled. It must
live in the header, not our src, because only the header is seen by NimBLE's TU in
every link.
Idempotent text edit. Patching bumps the header mtime, so NimBLE recompiles
against it on the next build.
"""
Import("env") # noqa: F821 (SCons-injected global)
import glob
import os
EXTERN_DECL = "extern bool _btLibraryInUse;"
MARKER = "patch_bt_mem.py"
WEAK_DEF = (
"__attribute__((weak)) bool _btLibraryInUse = false; /* CrossPoint " + MARKER + " */"
)
def _find_header(env):
rel = os.path.join("cores", "esp32", "esp32-hal-bt-mem.h")
candidates = []
# 1) Ask the platform for the framework package dir (works when the name matches).
try:
fw = env.PioPlatform().get_package_dir("framework-arduinoespressif32") # noqa: F821
if fw:
candidates.append(os.path.join(fw, rel))
except Exception:
pass
# 2) Glob the packages dir(s) -- robust to the exact package name/version.
pkg_dirs = []
for key in ("PROJECT_PACKAGES_DIR", "PROJECT_CORE_DIR"):
try:
d = env.subst("$%s" % key) # noqa: F821
if d:
pkg_dirs.append(d)
except Exception:
pass
pkg_dirs.append(os.path.expanduser(os.path.join("~", ".platformio", "packages")))
for base in pkg_dirs:
pattern = os.path.join(base, "**", "framework-arduinoespressif32*", rel)
candidates.extend(glob.glob(pattern, recursive=True))
for c in candidates:
if os.path.isfile(c):
return c
return None
def patch_bt_mem(env):
header = _find_header(env)
if not header:
print("patch_bt_mem: esp32-hal-bt-mem.h not found; skipping (controller-only "
"link may fail on _btLibraryInUse)")
return
with open(header, "r") as f:
text = f.read()
if MARKER in text:
print("patch_bt_mem: already patched (%s)" % header)
return
if EXTERN_DECL not in text:
print("patch_bt_mem: '%s' not found in %s; NOT patching (upstream header "
"changed -- update this script)" % (EXTERN_DECL, header))
return
text = text.replace(EXTERN_DECL, WEAK_DEF, 1)
with open(header, "w") as f:
f.write(text)
print("patch_bt_mem: patched %s (_btLibraryInUse now weak-defined)" % header)
patch_bt_mem(env) # noqa: F821
-121
View File
@@ -1,121 +0,0 @@
#include "BleInput.h"
#include <GfxRenderer.h>
#include <HalPowerManager.h>
#include <I18n.h>
#include <cstdio>
#include <cstring>
#include "MappedInputManager.h"
#include "components/UITheme.h"
namespace bleinput {
namespace {
volatile bool g_startInProgress = false;
}
// NimBLE controller init/deinit hang (interrupt WDT) if run at the 10 MHz low-power
// frequency, so force normal CPU speed around both. Centralized here so every caller
// (boot restore, settings toggle, reader toggle, sleep) is covered automatically.
bool ensureStarted() {
g_startInProgress = true;
HalPowerManager::Lock powerLock;
const bool ok = BleHid.begin(kHostName);
g_startInProgress = false;
return ok;
}
bool startInProgress() { return g_startInProgress; }
// Full teardown (NimBLE deinit), not just a link drop, so the BLE stack's RAM is
// returned to the heap — otherwise memory-hungry work like EPUB inflate can't
// allocate even after the user turns Bluetooth off.
void stop() {
HalPowerManager::Lock powerLock;
BleHid.end();
}
bool encodeKey(const freeink::KeyEvent& ev, uint8_t& kind, uint8_t& value) {
if (ev.special != freeink::SpecialKey::None) {
kind = 0;
value = static_cast<uint8_t>(ev.special);
return true;
}
if (ev.keycode != 0) {
kind = 1;
value = ev.keycode;
return true;
}
return false;
}
namespace {
const char* specialName(uint8_t value) {
switch (static_cast<freeink::SpecialKey>(value)) {
case freeink::SpecialKey::Enter:
return "Enter";
case freeink::SpecialKey::Backspace:
return "Backspace";
case freeink::SpecialKey::Tab:
return "Tab";
case freeink::SpecialKey::Escape:
return "Escape";
case freeink::SpecialKey::Delete:
return "Delete";
case freeink::SpecialKey::Left:
return "Left";
case freeink::SpecialKey::Right:
return "Right";
case freeink::SpecialKey::Up:
return "Up";
case freeink::SpecialKey::Down:
return "Down";
case freeink::SpecialKey::Home:
return "Home";
case freeink::SpecialKey::End:
return "End";
case freeink::SpecialKey::PageUp:
return "Page Up";
case freeink::SpecialKey::PageDown:
return "Page Down";
default:
return nullptr;
}
}
} // namespace
void showConnectingUntilLinked(const GfxRenderer& renderer, const MappedInputManager& input) {
if (!BleHid.isRunning() || BleHid.isConnected()) return;
// drawPopup refreshes the panel itself, so draw once and let e-ink hold it while we
// pump the host. Holds until the remote links, the user presses a button to bail, or
// a generous timeout (a remote that slept after a disconnect needs a button to wake).
GUI.drawPopup(renderer, tr(STR_BT_CONNECTING_POPUP));
const unsigned long deadline = millis() + 10000;
while (!BleHid.isConnected() && millis() < deadline) {
BleHid.poll();
input.update();
if (input.wasAnyPressed()) break;
delay(50);
}
// Note: the caller must redraw to clear the popup. For grayscale reader pages the
// caller should also request a ghost-cleanup (HALF) refresh first — a plain fast/
// partial refresh ghosts badly over the BW popup (see Activity::requestGhostCleanup).
}
void describeKey(uint8_t kind, uint8_t value, char* out, size_t outLen) {
if (!out || outLen == 0) return;
if (kind == 0) {
const char* name = specialName(value);
if (name) {
strncpy(out, name, outLen - 1);
out[outLen - 1] = '\0';
return;
}
}
// Printable ASCII usage handled as a generic key code; show the raw value.
snprintf(out, outLen, "Key 0x%02X", static_cast<unsigned>(value));
}
} // namespace bleinput
-68
View File
@@ -1,68 +0,0 @@
#pragma once
// CrossPoint <-> FreeInk BLE HID host glue.
//
// Thin, capability-safe helpers around freeink::BleKeyboardHost (the `BleHid`
// singleton). When FREEINK_CAP_BLE_HID_HOST is compiled out the SDK links stubs,
// so every call here is still valid and simply no-ops / returns false — callers
// need no #ifdefs.
//
// The (kind, value) pair produced by encodeKey() is the stable identity stored in
// CrossPointSettings::bleKeyMap. Page-turner remotes emit "special" keys
// (PageUp/PageDown/arrows); plain keyboards emit usage codes. We deliberately
// ignore modifiers and the printable char for matching (page turners don't use
// modifiers), keeping the persisted entry a trivial two-byte comparison.
#include <BleKeyboardHost.h>
#include <cstdint>
class GfxRenderer;
class MappedInputManager;
namespace bleinput {
// Advertised central name shown to peripherals during pairing.
inline constexpr const char* kHostName = "CrossPoint";
// Heap floor for starting the NimBLE stack (measured begin() cost: ~52-57 KB).
// This must leave the reader usable AFTER the stack is resident, not merely fit the
// stack itself. Framebuffer lending covers a section build's one big inflate window,
// but NOT its storm of small allocations (CSS/sscanf/strings) which draw from the
// general heap; at 56 KB the stack started with ~2 KB general heap left and the next
// build abort()ed inside sscanf (We_baseline from a cleared cache, BLE connected).
// 80 KB is reachable at steady-state reading (~84 KB idle with an SD font + section)
// and leaves ~26 KB general heap after startup; the build pre-flight
// (BUILD_MIN_FREE_HEAP) frees BLE and retries when a build still needs more, and the
// render shed (RENDER_MIN_FREE_HEAP) backstops tight renders.
inline constexpr size_t kStartMinFreeHeap = 80 * 1024;
// Lower floor for the Bluetooth settings screen, where the user has explicitly asked
// for BLE right now (scanning/pairing is dead without the stack). No page renders or
// section builds run there, so the reader-sized reserve above doesn't apply — only
// NimBLE's own ~57 KB plus working margin.
inline constexpr size_t kStartMinFreeHeapExplicit = 70 * 1024;
// Start the BLE HID host (idempotent). Returns false if BLE is compiled out or
// NimBLE init failed. Safe to call repeatedly.
bool ensureStarted();
bool startInProgress();
// Drop the active link (e.g. before deep sleep or when the user disables BT).
void stop();
// Encode a decoded key event into the stable (kind, value) identity used by the
// settings map. kind: 0 = SpecialKey, 1 = HID usage. Returns false when the event
// carries no usable identity (no special key and no usage code).
bool encodeKey(const freeink::KeyEvent& ev, uint8_t& kind, uint8_t& value);
// Human-readable name for a stored (kind, value) identity, for the mapping UI.
// Writes a null-terminated string into out (e.g. "Page Down", "Key 0x4B").
void describeKey(uint8_t kind, uint8_t value, char* out, size_t outLen);
// Draw a "BT Connecting..." popup and pump the BLE host until the bonded remote
// links, the user presses a button to dismiss, or a timeout. No-op if BLE isn't
// running or is already connected. The caller must redraw afterward to clear it.
void showConnectingUntilLinked(const GfxRenderer& renderer, const MappedInputManager& input);
} // namespace bleinput
-19
View File
@@ -1,19 +0,0 @@
// Weak fallback definition of `_btLibraryInUse` for the BLE-controller-only build.
//
// The arduino-esp32 core header cores/esp32/esp32-hal-bt-mem.h emits a global
// constructor (`_setBtLibraryInUse`) that references `_btLibraryInUse` whenever BLE
// hardware is present, but the core only DEFINES that symbol (in esp32-hal-bt.c)
// when an IDF host stack — CONFIG_BLUEDROID_ENABLED or CONFIG_NIMBLE_ENABLED — is
// enabled. CrossPoint runs the ESP-IDF BLE *controller* only, with NimBLE-Arduino
// supplying the host (both IDF host stacks are disabled in custom_sdkconfig), so the
// core never defines it and both the core lib and NimBLEDevice.cpp.o fail the final
// firmware link with "undefined reference to `_btLibraryInUse'".
//
// scripts/patch_bt_mem.py turns the header's declaration into a weak definition, but
// that framework file gets reset by package reinstalls and the custom_sdkconfig core
// rebuild, so the patch does not always survive to compile time. This weak definition
// lives in our own always-linked source and resolves the reference in firmware.elf
// regardless of the header's state. It is `weak`, so if a host stack is ever enabled
// (giving the core its own strong definition), that strong symbol wins with no clash;
// and it coexists with the header's weak def when the patch did apply.
extern "C" __attribute__((weak)) bool _btLibraryInUse = false;
Loaded 20 of 53 files, more files were not shown because too many files have changed in this diff. Show more