mirror of
https://github.com/meshtastic/firmware.git
synced 2026-11-07 23:04:49 -05:00
BookReader
This commit is contained in:
1 parent
04cb888511
commit
60f7b8be7e
26 files changed
+10613
-175
No files matched your search
+1
-1
@@ -452,4 +452,4 @@ void setupSDCard()
|
||||
LOG_DEBUG("Total space: %lu MB", (uint32_t)(SD.totalBytes() / (1024 * 1024)));
|
||||
LOG_DEBUG("Used space: %lu MB", (uint32_t)(SD.usedBytes() / (1024 * 1024)));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,16 @@
|
||||
#include "./ED047TC1.h"
|
||||
|
||||
#include "FastEPD.h"
|
||||
#include "UptimeClock.h"
|
||||
#include "configuration.h"
|
||||
#include "mesh/Throttle.h"
|
||||
#if INKHUD_BOOKREADER_GRAYSCALE
|
||||
#include <esp_heap_caps.h>
|
||||
#endif
|
||||
|
||||
extern volatile bool dma_is_done;
|
||||
extern void bbepRowControl(FASTEPDSTATE *state, int type);
|
||||
extern void bbepWriteRow(FASTEPDSTATE *state, uint8_t *data, int length, int rowStep);
|
||||
|
||||
using namespace NicheGraphics::Drivers;
|
||||
|
||||
@@ -123,12 +132,283 @@ int safeEPDiyV7EinkPower(void *pBBEP, int bOn)
|
||||
class SafeFastEPD : public FASTEPD
|
||||
{
|
||||
public:
|
||||
static constexpr unsigned whiteFinishPasses = 6;
|
||||
|
||||
void installSafePowerHandler()
|
||||
{
|
||||
#if defined(T5_S3_EPAPER_PRO_V2)
|
||||
_state.pfnEinkPower = safeEPDiyV7EinkPower;
|
||||
#endif
|
||||
}
|
||||
int partialUpdateWithWhiteFinish(uint32_t whitePixels)
|
||||
{
|
||||
if (dmaFault || _state.mode != BB_MODE_1BPP || _state.native_width != 960 || _state.native_height != 540 ||
|
||||
_state.rotation != 0 || (_state.iFlags & (BB_PANEL_FLAG_MIRROR_X | BB_PANEL_FLAG_MIRROR_Y)) ||
|
||||
_state.panelDef.iLinePadding || !_state.pTemp || !_state.dma_buf)
|
||||
return BBEP_ERROR_BAD_PARAMETER;
|
||||
int rc = FASTEPD::partialUpdate(true, 0, 539);
|
||||
if (rc == BBEP_SUCCESS && whitePixels)
|
||||
rc = finishWhitePixels(_state.pTemp, whiteFinishPasses);
|
||||
if (rc == BBEP_SUCCESS)
|
||||
rc = writePulseFrame(nullptr);
|
||||
const int offRc = einkPower(0);
|
||||
return rc == BBEP_SUCCESS ? offRc : rc;
|
||||
}
|
||||
|
||||
bool hasDmaFault() const { return dmaFault; }
|
||||
|
||||
#if INKHUD_BOOKREADER_GRAYSCALE
|
||||
bool promoteMonochromeBaseline()
|
||||
{
|
||||
if (dmaFault || _state.mode != BB_MODE_1BPP || _state.native_width != 960 || _state.native_height != 540 ||
|
||||
_state.rotation != 0 || (_state.iFlags & (BB_PANEL_FLAG_MIRROR_X | BB_PANEL_FLAG_MIRROR_Y)) || !_state.pCurrent)
|
||||
return false;
|
||||
// Expand backwards; the larger gray plane overlaps the monochrome source.
|
||||
for (size_t i = 960U * 540U / 8U; i-- > 0;) {
|
||||
const uint8_t pixels = _state.pCurrent[i];
|
||||
for (unsigned pair = 0; pair < 4; ++pair)
|
||||
_state.pCurrent[i * 4U + pair] =
|
||||
((pixels & (0x80U >> (pair * 2U))) ? 0xF0U : 0U) | ((pixels & (0x40U >> (pair * 2U))) ? 0x0FU : 0U);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Optimize frame preparation only; panel drive timing stays unchanged.
|
||||
__attribute__((optimize("O3"))) bool preparePanelGrayUpdate(const uint8_t *image, uint16_t width, uint16_t height,
|
||||
uint16_t left, uint16_t top, bool baselineValid,
|
||||
uint32_t &changed)
|
||||
{
|
||||
if (dmaFault || !image || _state.mode != BB_MODE_4BPP || _state.native_width != 960 || _state.native_height != 540 ||
|
||||
_state.rotation != 0 || (_state.iFlags & (BB_PANEL_FLAG_MIRROR_X | BB_PANEL_FLAG_MIRROR_Y)) ||
|
||||
_state.panelDef.iLinePadding || !_state.pCurrent || !_state.dma_buf || (left & 1U) || (width & 1U) ||
|
||||
left + width > 960 || top + height > 540 || !_state.panelDef.pGrayMatrix || _state.panelDef.iMatrixSize != 16U * 8U)
|
||||
return false;
|
||||
if (baselineValid) {
|
||||
uint16_t y = 0;
|
||||
for (; y < height; ++y)
|
||||
if (memcmp(_state.pCurrent + (top + y) * 480U + left / 2U, image + y * (width / 2U), width / 2U) != 0)
|
||||
break;
|
||||
if (y == height) {
|
||||
changed = 0;
|
||||
#if INKHUD_BOOKREADER_PERF_LOG
|
||||
LOG_INFO("[BookReaderDraw] panel prepare skipped identical packed frame");
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!grayLookupReady) {
|
||||
const uint8_t *matrix = _state.panelDef.pGrayMatrix;
|
||||
for (unsigned pass = 0; pass < 8; ++pass)
|
||||
for (unsigned packed = 0; packed < 256; ++packed)
|
||||
grayPairLookup[pass][packed] =
|
||||
(matrix[(packed >> 4U) * 8U + pass] << 2U) | matrix[(packed & 15U) * 8U + pass];
|
||||
grayLookupReady = true;
|
||||
}
|
||||
constexpr size_t planeBytes = 960U * 540U / 4U;
|
||||
if (!grayPulsePlanes)
|
||||
grayPulsePlanes = static_cast<uint8_t *>(heap_caps_malloc(planeBytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
|
||||
if (!grayPulsePlanes)
|
||||
return false;
|
||||
memset(grayPulsePlanes, 0, planeBytes);
|
||||
if (!baselineValid)
|
||||
memset(_state.pCurrent, 0xFF, 960U * 540U / 2U);
|
||||
changed = 0;
|
||||
#if INKHUD_BOOKREADER_PERF_LOG
|
||||
uint32_t inputHash = 2166136261U, targetHash = 2166136261U;
|
||||
uint32_t targetCounts[4] = {}, transitions[16] = {};
|
||||
uint32_t packedCounts[256] = {};
|
||||
#endif
|
||||
|
||||
// Batch external-RAM transfers; compare packed pixels in local row buffers.
|
||||
uint8_t oldRow[480], newRow[480];
|
||||
const size_t rowBytes = width / 2U;
|
||||
for (uint16_t y = 0; y < height; ++y) {
|
||||
uint8_t *current = _state.pCurrent + (top + y) * 480U + left / 2U;
|
||||
const uint8_t *next = image + y * (width / 2U);
|
||||
memcpy(oldRow, current, rowBytes);
|
||||
memcpy(newRow, next, rowBytes);
|
||||
uint8_t *mask = grayPulsePlanes + (top + y) * 240U;
|
||||
uint8_t pendingMask = 0;
|
||||
bool rowChanged = false;
|
||||
for (uint16_t x = 0; x < width; x += 2) {
|
||||
const uint8_t oldPacked = oldRow[x / 2U];
|
||||
const uint8_t input = newRow[x / 2U];
|
||||
const uint8_t newPacked = input;
|
||||
#if INKHUD_BOOKREADER_PERF_LOG
|
||||
inputHash = (inputHash ^ input) * 16777619U;
|
||||
targetHash = (targetHash ^ newPacked) * 16777619U;
|
||||
++packedCounts[newPacked];
|
||||
#endif
|
||||
const unsigned physicalX = left + x;
|
||||
if (!baselineValid || oldPacked != newPacked) {
|
||||
const uint8_t difference = oldPacked ^ newPacked;
|
||||
const bool highChanged = !baselineValid || (difference & 0xF0U);
|
||||
const bool lowChanged = !baselineValid || (difference & 0x0FU);
|
||||
#if INKHUD_BOOKREADER_PERF_LOG
|
||||
if (highChanged)
|
||||
++transitions[(oldPacked >> 6U) * 4U + (newPacked >> 6U)];
|
||||
if (lowChanged)
|
||||
++transitions[((oldPacked & 15U) / 4U) * 4U + (newPacked & 15U) / 4U];
|
||||
#endif
|
||||
const uint8_t pairMask = (highChanged ? 12U : 0U) | (lowChanged ? 3U : 0U);
|
||||
pendingMask |= pairMask << ((physicalX & 2U) ? 0U : 4U);
|
||||
changed += highChanged + lowChanged;
|
||||
rowChanged = true;
|
||||
}
|
||||
if ((physicalX & 2U) || x + 2U == width) {
|
||||
if (pendingMask)
|
||||
mask[physicalX / 4U] = pendingMask;
|
||||
pendingMask = 0;
|
||||
}
|
||||
}
|
||||
if (rowChanged)
|
||||
memcpy(current, newRow, rowBytes);
|
||||
}
|
||||
#if INKHUD_BOOKREADER_PERF_LOG
|
||||
for (unsigned packed = 0; packed < 256U; ++packed) {
|
||||
targetCounts[packed >> 6U] += packedCounts[packed];
|
||||
targetCounts[(packed & 15U) >> 2U] += packedCounts[packed];
|
||||
}
|
||||
LOG_INFO("[BookReaderDraw] panel prepare inputHash=%08lx targetHash=%08lx baseline=%u "
|
||||
"targetBands0-3,4-7,8-11,12-15=%lu,%lu,%lu,%lu",
|
||||
static_cast<unsigned long>(inputHash), static_cast<unsigned long>(targetHash), baselineValid,
|
||||
static_cast<unsigned long>(targetCounts[0]), static_cast<unsigned long>(targetCounts[1]),
|
||||
static_cast<unsigned long>(targetCounts[2]), static_cast<unsigned long>(targetCounts[3]));
|
||||
for (unsigned oldLevel = 0; oldLevel < 4; ++oldLevel)
|
||||
LOG_INFO("[BookReaderDraw] transitions oldBand=%u toBands=%lu,%lu,%lu,%lu", oldLevel,
|
||||
static_cast<unsigned long>(transitions[oldLevel * 4U]),
|
||||
static_cast<unsigned long>(transitions[oldLevel * 4U + 1U]),
|
||||
static_cast<unsigned long>(transitions[oldLevel * 4U + 2U]),
|
||||
static_cast<unsigned long>(transitions[oldLevel * 4U + 3U]));
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
int updatePanelGray()
|
||||
{
|
||||
if (dmaFault || !grayPulsePlanes || !grayLookupReady || !_state.dma_buf)
|
||||
return BBEP_ERROR_BAD_PARAMETER;
|
||||
#if INKHUD_BOOKREADER_PERF_LOG
|
||||
uint32_t passStarts[8] = {};
|
||||
#endif
|
||||
const int powerRc = einkPower(1);
|
||||
if (powerRc != BBEP_SUCCESS) {
|
||||
einkPower(0);
|
||||
return powerRc;
|
||||
}
|
||||
int rc = BBEP_SUCCESS;
|
||||
for (unsigned pass = 0; pass < 8 && rc == BBEP_SUCCESS; ++pass) {
|
||||
#if INKHUD_BOOKREADER_PERF_LOG
|
||||
passStarts[pass] = millis();
|
||||
#endif
|
||||
rc = writePulseFrame(grayPulsePlanes, grayPairLookup[pass]);
|
||||
}
|
||||
#if INKHUD_BOOKREADER_PERF_LOG
|
||||
const uint32_t neutralStart = millis();
|
||||
#endif
|
||||
if (rc == BBEP_SUCCESS)
|
||||
rc = writePulseFrame(nullptr);
|
||||
#if INKHUD_BOOKREADER_PERF_LOG
|
||||
const uint32_t powerOffStart = millis();
|
||||
#endif
|
||||
const int offRc = einkPower(0);
|
||||
#if INKHUD_BOOKREADER_PERF_LOG
|
||||
// Serial output must not stretch the powered waveform between phases.
|
||||
for (unsigned pass = 0; pass < 8; ++pass)
|
||||
LOG_INFO("[BookReaderDraw] panel-native streamed pass=%u at=%lu", pass, static_cast<unsigned long>(passStarts[pass]));
|
||||
LOG_INFO("[BookReaderDraw] neutral at=%lu power-off at=%lu", static_cast<unsigned long>(neutralStart),
|
||||
static_cast<unsigned long>(powerOffStart));
|
||||
LOG_INFO("[BookReaderDraw] panel phase power-off END at=%lu rc=%d powerRc=%d", static_cast<unsigned long>(millis()), rc,
|
||||
offRc);
|
||||
#endif
|
||||
return rc == BBEP_SUCCESS ? offRc : rc;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
private:
|
||||
#if INKHUD_BOOKREADER_GRAYSCALE
|
||||
uint8_t *grayPulsePlanes = nullptr;
|
||||
uint8_t grayPairLookup[8][256] = {};
|
||||
bool grayLookupReady = false;
|
||||
#endif
|
||||
bool dmaFault = false;
|
||||
|
||||
int finishWhitePixels(uint8_t *plane, unsigned passes)
|
||||
{
|
||||
// Keep only the black-to-white pulses prepared by the monochrome partial update.
|
||||
#if INKHUD_BOOKREADER_PERF_LOG
|
||||
const uint32_t prepareStart = millis();
|
||||
uint32_t whitePixels = 0;
|
||||
#endif
|
||||
uint8_t active = 0;
|
||||
for (size_t i = 0; i < 960U * 540U / 4U; ++i) {
|
||||
uint8_t finish = plane[i] & 0xAA;
|
||||
plane[i] = finish;
|
||||
active |= plane[i];
|
||||
#if INKHUD_BOOKREADER_PERF_LOG
|
||||
whitePixels += __builtin_popcount(static_cast<unsigned>(finish));
|
||||
#endif
|
||||
}
|
||||
int rc = BBEP_SUCCESS;
|
||||
#if INKHUD_BOOKREADER_PERF_LOG
|
||||
LOG_INFO("[BookReaderDraw] white-only finish BEGIN at=%lu pixels=%lu passes=%u prepareMs=%lu mode=%d",
|
||||
static_cast<unsigned long>(millis()), static_cast<unsigned long>(whitePixels), active ? passes : 0U,
|
||||
static_cast<unsigned long>(millis() - prepareStart), _state.mode);
|
||||
#endif
|
||||
if (active)
|
||||
for (unsigned pass = 0; pass < passes && rc == BBEP_SUCCESS; ++pass)
|
||||
rc = writePulseFrame(plane);
|
||||
#if INKHUD_BOOKREADER_PERF_LOG
|
||||
LOG_INFO("[BookReaderDraw] white-only finish END rc=%d", rc);
|
||||
#endif
|
||||
return rc;
|
||||
}
|
||||
|
||||
bool waitForDma()
|
||||
{
|
||||
if (dma_is_done)
|
||||
return true;
|
||||
const uint32_t start = Time::getMillis();
|
||||
while (!dma_is_done) {
|
||||
if (Throttle::hasElapsed(start, 250U)) {
|
||||
dmaFault = true;
|
||||
LOG_ERROR("ED047TC1 DMA timeout; display transfers stopped");
|
||||
return false;
|
||||
}
|
||||
delayMicroseconds(1);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int writePulseFrame(const uint8_t *plane, const uint8_t *grayLookup = nullptr)
|
||||
{
|
||||
if (!waitForDma())
|
||||
return BBEP_IO_ERROR;
|
||||
bbepRowControl(&_state, ROW_START);
|
||||
unsigned offset = 0;
|
||||
for (unsigned row = 0; row < 540; ++row) {
|
||||
// Alternate buffers keep the active DMA row untouched.
|
||||
uint8_t *out = _state.dma_buf + offset;
|
||||
if (grayLookup) {
|
||||
const uint8_t *source = _state.pCurrent + row * 480U;
|
||||
const uint8_t *mask = plane + row * 240U;
|
||||
for (unsigned x = 0; x < 240; ++x)
|
||||
out[x] = mask[x] ? ((grayLookup[source[x * 2U]] << 4U) | grayLookup[source[x * 2U + 1U]]) & mask[x] : 0U;
|
||||
} else if (plane)
|
||||
memcpy(out, plane + row * 240U, 240);
|
||||
else
|
||||
memset(out, 0, 240);
|
||||
if (!waitForDma())
|
||||
return BBEP_IO_ERROR;
|
||||
bbepWriteRow(&_state, out, 240, row != 0);
|
||||
offset ^= 240U;
|
||||
}
|
||||
if (!waitForDma())
|
||||
return BBEP_IO_ERROR;
|
||||
delayMicroseconds(230);
|
||||
return BBEP_SUCCESS;
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
@@ -146,9 +426,9 @@ void ED047TC1::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_
|
||||
|
||||
int initRc = BBEP_ERROR_BAD_PARAMETER;
|
||||
#if defined(T5_S3_EPAPER_PRO_V1)
|
||||
initRc = epaper->initPanel(BB_PANEL_LILYGO_T5PRO, 28000000);
|
||||
initRc = epaper->initPanel(BB_PANEL_LILYGO_T5PRO, 20000000);
|
||||
#elif defined(T5_S3_EPAPER_PRO_V2)
|
||||
initRc = epaper->initPanel(BB_PANEL_LILYGO_T5PRO_V2, 28000000);
|
||||
initRc = epaper->initPanel(BB_PANEL_LILYGO_T5PRO_V2, 20000000);
|
||||
// Initialize all PCA9535 port-0 pins as outputs / HIGH
|
||||
for (int i = 0; i < 8; i++) {
|
||||
epaper->ioPinMode(i, OUTPUT);
|
||||
@@ -172,28 +452,42 @@ void ED047TC1::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_
|
||||
const int modeRc = epaper->setMode(BB_MODE_1BPP);
|
||||
if (modeRc != BBEP_SUCCESS) {
|
||||
LOG_WARN("ED047TC1 setMode failed rc=%d", modeRc);
|
||||
return;
|
||||
}
|
||||
|
||||
const int clearRc = epaper->clearWhite();
|
||||
if (clearRc != BBEP_SUCCESS) {
|
||||
LOG_WARN("ED047TC1 clearWhite failed rc=%d", clearRc);
|
||||
}
|
||||
|
||||
const int fullRc = epaper->fullUpdate(true); // Blocking initial clear
|
||||
if (fullRc != BBEP_SUCCESS) {
|
||||
panelReady = true;
|
||||
// FastEPD clearWhite()/backupPlane() overrun the aliased previous plane.
|
||||
epaper->fillScreen(BBEP_WHITE);
|
||||
const int fullRc = epaper->fullUpdate(CLEAR_SLOW, false);
|
||||
monochromeBaselineValid = fullRc == BBEP_SUCCESS;
|
||||
failed = !monochromeBaselineValid;
|
||||
if (failed)
|
||||
LOG_WARN("ED047TC1 initial fullUpdate failed rc=%d", fullRc);
|
||||
}
|
||||
LOG_INFO("ED047TC1 panel profile: clock=20MHz partialPasses=4 fullPasses=5");
|
||||
}
|
||||
|
||||
void ED047TC1::update(uint8_t *imageData, UpdateTypes type)
|
||||
{
|
||||
if (!epaper)
|
||||
if (!panelReady || !imageData)
|
||||
return;
|
||||
#if INKHUD_BOOKREADER_GRAYSCALE
|
||||
const bool leavingGrayscale = epaper->getMode() == BB_MODE_4BPP;
|
||||
grayscaleBaselineValid = false;
|
||||
#endif
|
||||
|
||||
if (epaper->getMode() != BB_MODE_1BPP) {
|
||||
monochromeBaselineValid = false;
|
||||
const int rc = epaper->setMode(BB_MODE_1BPP);
|
||||
if (rc != BBEP_SUCCESS) {
|
||||
failed = true;
|
||||
LOG_WARN("ED047TC1 monochrome setMode failed rc=%d", rc);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// InkHUD renders into a DISPLAY_WIDTH × DISPLAY_HEIGHT safe-area buffer.
|
||||
// We need to place that into the centre of the physical 960×540 FastEPD buffer,
|
||||
// leaving blank margins at every edge to avoid the panel's inactive border.
|
||||
const uint32_t srcRowBytes = (DISPLAY_WIDTH + 7) / 8; // bytes per row in InkHUD buffer (118)
|
||||
const uint32_t srcRowBytes = (DISPLAY_WIDTH + 7) / 8; // bytes per row in InkHUD buffer (116)
|
||||
const uint32_t dstRowBytes = (960 + 7) / 8; // bytes per row in physical buffer (120)
|
||||
const uint32_t dstTotalRows = 540;
|
||||
|
||||
@@ -209,15 +503,90 @@ void ED047TC1::update(uint8_t *imageData, UpdateTypes type)
|
||||
memcpy(dstRow, srcRow, srcRowBytes);
|
||||
}
|
||||
|
||||
if (type == FULL) {
|
||||
epaper->fullUpdate(CLEAR_SLOW, false);
|
||||
epaper->backupPlane(); // Sync pPrevious so next partialUpdate has a correct baseline
|
||||
int rc;
|
||||
if (type == FULL || !monochromeBaselineValid) {
|
||||
#if INKHUD_BOOKREADER_GRAYSCALE
|
||||
rc = epaper->fullUpdate(leavingGrayscale ? CLEAR_WHITE : CLEAR_SLOW, false);
|
||||
#else
|
||||
rc = epaper->fullUpdate(CLEAR_SLOW, false);
|
||||
#endif
|
||||
} else {
|
||||
// FAST: true partial update - compares pCurrent vs pPrevious and only applies
|
||||
// update waveform to rows that changed. partialUpdate() updates pPrevious.
|
||||
epaper->partialUpdate(false, 0, dstTotalRows - 1);
|
||||
const uint32_t startMs = millis();
|
||||
uint32_t blackToWhite = 0, whiteToBlack = 0;
|
||||
const uint8_t *previous = epaper->previousBuffer();
|
||||
for (size_t i = 0; i < dstRowBytes * dstTotalRows; ++i) {
|
||||
blackToWhite += __builtin_popcount(static_cast<unsigned>(cur[i] & ~previous[i]));
|
||||
whiteToBlack += __builtin_popcount(static_cast<unsigned>(~cur[i] & previous[i]));
|
||||
}
|
||||
auto *safeEpaper = static_cast<SafeFastEPD *>(epaper);
|
||||
rc = safeEpaper->partialUpdateWithWhiteFinish(blackToWhite);
|
||||
if (safeEpaper->hasDmaFault())
|
||||
panelReady = false;
|
||||
LOG_DEBUG("[EInkPerf] FAST blackToWhite=%lu whiteToBlack=%lu whiteExtraPasses=%u ms=%lu rc=%d",
|
||||
static_cast<unsigned long>(blackToWhite), static_cast<unsigned long>(whiteToBlack),
|
||||
blackToWhite ? SafeFastEPD::whiteFinishPasses : 0U, static_cast<unsigned long>(millis() - startMs), rc);
|
||||
}
|
||||
monochromeBaselineValid = rc == BBEP_SUCCESS;
|
||||
failed = !monochromeBaselineValid;
|
||||
if (failed)
|
||||
LOG_WARN("ED047TC1 monochrome update failed rc=%d", rc);
|
||||
}
|
||||
|
||||
#if INKHUD_BOOKREADER_GRAYSCALE
|
||||
void ED047TC1::updateGrayscale(uint8_t *imageData)
|
||||
{
|
||||
if (!panelReady || !imageData)
|
||||
return;
|
||||
#if INKHUD_BOOKREADER_PERF_LOG
|
||||
const uint32_t startMs = millis();
|
||||
#endif
|
||||
|
||||
auto *safeEpaper = static_cast<SafeFastEPD *>(epaper);
|
||||
if (epaper->getMode() != BB_MODE_4BPP)
|
||||
grayscaleBaselineValid = monochromeBaselineValid && safeEpaper->promoteMonochromeBaseline();
|
||||
monochromeBaselineValid = false;
|
||||
const int modeRc = epaper->setMode(BB_MODE_4BPP);
|
||||
if (modeRc != BBEP_SUCCESS) {
|
||||
failed = true;
|
||||
grayscaleBaselineValid = false;
|
||||
LOG_WARN("ED047TC1 grayscale setMode failed rc=%d", modeRc);
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t changedPixels = 0;
|
||||
const bool establishBaseline = !grayscaleBaselineValid;
|
||||
if (!safeEpaper->preparePanelGrayUpdate(imageData, DISPLAY_WIDTH, DISPLAY_HEIGHT, H_OFFSET_BYTES * 8U, V_OFFSET_TOP,
|
||||
grayscaleBaselineValid, changedPixels)) {
|
||||
failed = true;
|
||||
LOG_WARN("ED047TC1 panel-native grayscale preparation failed; retaining visible frame");
|
||||
return;
|
||||
}
|
||||
if (!establishBaseline && !changedPixels) {
|
||||
failed = false;
|
||||
#if INKHUD_BOOKREADER_PERF_LOG
|
||||
LOG_INFO("[BookReaderPerf] gray panel skipped unchanged ms=%lu", static_cast<unsigned long>(millis() - startMs));
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
#if INKHUD_BOOKREADER_PERF_LOG
|
||||
LOG_INFO("[BookReaderPerf] gray panel BEGIN clear=none waveform=panel-native shades=16 baseline=%u "
|
||||
"changed=%lu prepareMs=%lu",
|
||||
!establishBaseline, static_cast<unsigned long>(changedPixels), static_cast<unsigned long>(millis() - startMs));
|
||||
const uint32_t driveStartMs = millis();
|
||||
#endif
|
||||
const int rc = safeEpaper->updatePanelGray();
|
||||
if (safeEpaper->hasDmaFault())
|
||||
panelReady = false;
|
||||
#if INKHUD_BOOKREADER_PERF_LOG
|
||||
LOG_INFO("[BookReaderPerf] gray panel END rc=%d driveMs=%lu totalMs=%lu", rc,
|
||||
static_cast<unsigned long>(millis() - driveStartMs), static_cast<unsigned long>(millis() - startMs));
|
||||
#endif
|
||||
failed = rc != BBEP_SUCCESS;
|
||||
grayscaleBaselineValid = !failed;
|
||||
if (failed)
|
||||
LOG_WARN("ED047TC1 grayscale update failed rc=%d", rc);
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // T5_S3_EPAPER_PRO
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
@@ -77,12 +77,21 @@ class ED047TC1 : public EInk
|
||||
// EInk interface - SPI params are not used for this parallel display
|
||||
void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = 0xFF) override;
|
||||
void update(uint8_t *imageData, UpdateTypes type) override;
|
||||
#if INKHUD_BOOKREADER_GRAYSCALE
|
||||
bool supportsGrayscale() const override { return panelReady; }
|
||||
void updateGrayscale(uint8_t *imageData) override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool isUpdateDone() override { return true; } // FastEPD updates are blocking
|
||||
|
||||
private:
|
||||
FASTEPD *epaper = nullptr;
|
||||
bool panelReady = false;
|
||||
bool monochromeBaselineValid = false;
|
||||
#if INKHUD_BOOKREADER_GRAYSCALE
|
||||
bool grayscaleBaselineValid = false;
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
||||
|
||||
@@ -30,9 +30,13 @@ class EInk : private concurrency::OSThread
|
||||
EInk(uint16_t width, uint16_t height, UpdateTypes supported);
|
||||
virtual void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = -1) = 0;
|
||||
virtual void update(uint8_t *imageData, UpdateTypes type) = 0; // Change the display image
|
||||
void await(); // Wait for an in-progress update to complete before proceeding
|
||||
bool supports(UpdateTypes type); // Can display perform a certain update type
|
||||
bool busy() { return updateRunning; } // Display able to update right now?
|
||||
#if INKHUD_BOOKREADER_GRAYSCALE
|
||||
virtual bool supportsGrayscale() const { return false; }
|
||||
virtual void updateGrayscale(uint8_t *imageData) { (void)imageData; }
|
||||
#endif
|
||||
void await(); // Wait for an in-progress update to complete before proceeding
|
||||
bool supports(UpdateTypes type); // Can display perform a certain update type
|
||||
bool busy() { return updateRunning; } // Display able to update right now?
|
||||
|
||||
const uint16_t width; // Public so that NicheGraphics implementations can access. Safe because const.
|
||||
const uint16_t height;
|
||||
@@ -54,4 +58,4 @@ class EInk : private concurrency::OSThread
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
||||
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
@@ -69,7 +69,8 @@ class Applet : public GFX
|
||||
bool wantsToRender(); // Check whether applet wants to render
|
||||
bool wantsToAutoshow(); // Check whether applet wants to become foreground
|
||||
Drivers::EInk::UpdateTypes wantsUpdateType(); // Check which display update type the applet would prefer
|
||||
bool wantsFullRender(); // Check whether applet wants to render over its previous render
|
||||
bool wantsFullRender(); // Check whether applet wants to render over its
|
||||
// previous render
|
||||
void updateDimensions(); // Get current size from tile
|
||||
void resetDrawingSpace(); // Makes sure every render starts with same parameters
|
||||
|
||||
@@ -85,6 +86,9 @@ class Applet : public GFX
|
||||
// Event handlers
|
||||
|
||||
virtual void onRender(bool full) = 0; // For drawing the applet
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
virtual bool keepScreenAwakeDuringRender() const { return false; }
|
||||
#endif
|
||||
virtual void onActivate() {}
|
||||
virtual void onDeactivate() {}
|
||||
virtual void onForeground() {}
|
||||
@@ -104,6 +108,11 @@ class Applet : public GFX
|
||||
virtual void onFreeText(char c) {}
|
||||
virtual void onFreeTextDone() {}
|
||||
virtual void onFreeTextCancel() {}
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
// Visual-frame swipe direction; true prevents the board's app-switch
|
||||
// fallback.
|
||||
virtual bool onTouchSwipe(int8_t, int8_t) { return false; }
|
||||
#endif
|
||||
// Absolute display-space touch point, for touch-friendly UI interactions.
|
||||
// Return true if consumed.
|
||||
virtual bool onTouchPoint(uint16_t x, uint16_t y, bool longPress)
|
||||
@@ -124,7 +133,8 @@ class Applet : public GFX
|
||||
NAV_LEFT = 64, // | no-op | Joystick Left |
|
||||
NAV_RIGHT = 128 // | no-op | Joystick Right |
|
||||
};
|
||||
bool isInputSubscribed(InputMask input); // Check if input should be handled by applet, this should not be overloaded.
|
||||
bool isInputSubscribed(InputMask input); // Check if input should be handled by
|
||||
// applet, this should not be overloaded.
|
||||
|
||||
virtual bool approveNotification(Notification &n); // Allow an applet to veto a notification
|
||||
|
||||
@@ -133,9 +143,11 @@ class Applet : public GFX
|
||||
|
||||
static uint16_t getHeaderHeight(); // How tall the "standard" applet header is
|
||||
|
||||
static AppletFont fontSmall, fontMedium, fontLarge; // The general purpose fonts, used by all applets
|
||||
static AppletFont fontSmall, fontMedium,
|
||||
fontLarge; // The general purpose fonts, used by all applets
|
||||
|
||||
const char *name = nullptr; // Shown in applet selection menu. Also used as an identifier by InkHUD::getSystemApplet
|
||||
const char *name = nullptr; // Shown in applet selection menu. Also used as an
|
||||
// identifier by InkHUD::getSystemApplet
|
||||
|
||||
protected:
|
||||
void drawPixel(int16_t x, int16_t y, uint16_t color) override; // Place a single pixel. All drawing output passes through here
|
||||
@@ -144,17 +156,20 @@ class Applet : public GFX
|
||||
bool full = true); // Ask WindowManager to schedule a display update
|
||||
void requestAutoshow(); // Ask for applet to be moved to foreground
|
||||
|
||||
uint16_t X(float f); // Map applet width, mapped from 0 to 1.0
|
||||
uint16_t Y(float f); // Map applet height, mapped from 0 to 1.0
|
||||
void setCrop(int16_t left, int16_t top, uint16_t width, uint16_t height); // Ignore pixels drawn outside a certain region
|
||||
void resetCrop(); // Removes setCrop()
|
||||
uint16_t X(float f); // Map applet width, mapped from 0 to 1.0
|
||||
uint16_t Y(float f); // Map applet height, mapped from 0 to 1.0
|
||||
void setCrop(int16_t left, int16_t top, uint16_t width,
|
||||
uint16_t height); // Ignore pixels drawn outside a certain region
|
||||
void resetCrop(); // Removes setCrop()
|
||||
|
||||
// User Input Handling
|
||||
|
||||
uint8_t subscribedInputs = 0b00000000; // Maybe uint16_t for futureproofing? other devices may need more inputs
|
||||
uint8_t subscribedInputs = 0b00000000; // Maybe uint16_t for futureproofing?
|
||||
// other devices may need more inputs
|
||||
void setInputsSubscribed(uint8_t input,
|
||||
bool captured); // Set if an input should be handled by applet or not, this should not be
|
||||
// overloaded. Can take multiple inputs at once if you OR/`|` them together
|
||||
bool captured); // Set if an input should be handled by applet or not,
|
||||
// this should not be overloaded. Can take multiple inputs
|
||||
// at once if you OR/`|` them together
|
||||
|
||||
// Text
|
||||
|
||||
@@ -162,26 +177,31 @@ class Applet : public GFX
|
||||
AppletFont getFont();
|
||||
uint16_t getTextWidth(const std::string &text);
|
||||
uint16_t getTextWidth(const char *text);
|
||||
uint32_t getWrappedTextHeight(int16_t left, uint16_t width, const std::string &text); // Result of printWrapped
|
||||
uint32_t getWrappedTextHeight(int16_t left, uint16_t width,
|
||||
const std::string &text); // Result of printWrapped
|
||||
void printAt(int16_t x, int16_t y, const char *text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP);
|
||||
void printAt(int16_t x, int16_t y, const std::string &text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP);
|
||||
void printThick(int16_t xCenter, int16_t yCenter, const std::string &text, uint8_t thicknessX,
|
||||
uint8_t thicknessY); // Faux bold
|
||||
void printWrapped(int16_t left, int16_t top, uint16_t width, const std::string &text); // Per-word line wrapping
|
||||
uint8_t thicknessY); // Faux bold
|
||||
void printWrapped(int16_t left, int16_t top, uint16_t width,
|
||||
const std::string &text); // Per-word line wrapping
|
||||
|
||||
void hatchRegion(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t spacing, Color color); // Fill with sparse lines
|
||||
void drawHeader(const std::string &text); // Draw the standard applet header
|
||||
|
||||
// Meshtastic Logo
|
||||
|
||||
static constexpr float LOGO_ASPECT_RATIO = 1.9; // Width:Height for drawing the Meshtastic logo
|
||||
uint16_t getLogoWidth(uint16_t limitWidth, uint16_t limitHeight); // Size Meshtastic logo to fit within region
|
||||
uint16_t getLogoHeight(uint16_t limitWidth, uint16_t limitHeight); // Size Meshtastic logo to fit within region
|
||||
static constexpr float LOGO_ASPECT_RATIO = 1.9; // Width:Height for drawing the Meshtastic logo
|
||||
uint16_t getLogoWidth(uint16_t limitWidth,
|
||||
uint16_t limitHeight); // Size Meshtastic logo to fit within region
|
||||
uint16_t getLogoHeight(uint16_t limitWidth,
|
||||
uint16_t limitHeight); // Size Meshtastic logo to fit within region
|
||||
void drawLogo(int16_t centerX, int16_t centerY, uint16_t width, uint16_t height,
|
||||
Color color = BLACK); // Draw the Meshtastic logo
|
||||
|
||||
std::string hexifyNodeNum(NodeNum num); // Style as !0123abdc
|
||||
SignalStrength getSignalStrength(float snr, float rssi); // Interpret SNR and RSSI, as an easy to understand value
|
||||
std::string hexifyNodeNum(NodeNum num); // Style as !0123abdc
|
||||
SignalStrength getSignalStrength(float snr,
|
||||
float rssi); // Interpret SNR and RSSI, as an easy to understand value
|
||||
std::string getTimeString(uint32_t epochSeconds); // Human readable
|
||||
std::string getTimeString(); // Current time, human readable
|
||||
std::string localizeDistance(uint32_t meters); // Human readable distance, imperial or metric
|
||||
@@ -196,18 +216,24 @@ class Applet : public GFX
|
||||
Persistence::LatestMessage *latestMessage = nullptr;
|
||||
|
||||
private:
|
||||
Tile *assignedTile = nullptr; // Rendered pixels are fed into a Tile object, which translates them, then passes to WM
|
||||
Tile *assignedTile = nullptr; // Rendered pixels are fed into a Tile object,
|
||||
// which translates them, then passes to WM
|
||||
bool active = false; // Has the user enabled this applet (at run-time)?
|
||||
bool foreground = false; // Is the applet currently drawn on a tile?
|
||||
|
||||
bool wantRender = false; // In some situations, checked by WindowManager when updating, to skip unneeded redrawing.
|
||||
bool wantAutoshow = false; // Does the applet have new data it would like to display in foreground?
|
||||
bool wantRender = false; // In some situations, checked by WindowManager when
|
||||
// updating, to skip unneeded redrawing.
|
||||
bool wantAutoshow = false; // Does the applet have new data it would like to
|
||||
// display in foreground?
|
||||
NicheGraphics::Drivers::EInk::UpdateTypes wantUpdateType =
|
||||
NicheGraphics::Drivers::EInk::UpdateTypes::UNSPECIFIED; // Which update method we'd prefer when redrawing the display
|
||||
NicheGraphics::Drivers::EInk::UpdateTypes::UNSPECIFIED; // Which update method we'd prefer when redrawing the
|
||||
// display
|
||||
bool wantFullRender = true; // Render with a fresh canvas
|
||||
|
||||
using GFX::setFont; // Make sure derived classes use AppletFont instead of AdafruitGFX fonts directly
|
||||
using GFX::setRotation; // Block setRotation calls. Rotation is handled globally by WindowManager.
|
||||
using GFX::setFont; // Make sure derived classes use AppletFont instead of
|
||||
// AdafruitGFX fonts directly
|
||||
using GFX::setRotation; // Block setRotation calls. Rotation is handled
|
||||
// globally by WindowManager.
|
||||
|
||||
AppletFont currentFont; // As passed to setFont
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,317 @@
|
||||
#if defined(MESHTASTIC_INCLUDE_INKHUD) && defined(MESHTASTIC_INCLUDE_INKHUD_BOOKREADER)
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "graphics/niche/InkHUD/Applet.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
#include <esp_heap_caps.h>
|
||||
#endif
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
template <typename T> struct ReaderTextAllocator : std::allocator<T> {
|
||||
using value_type = T;
|
||||
template <typename U> struct rebind {
|
||||
using other = ReaderTextAllocator<U>;
|
||||
};
|
||||
ReaderTextAllocator() = default;
|
||||
template <typename U> ReaderTextAllocator(const ReaderTextAllocator<U> &) {}
|
||||
|
||||
T *allocate(size_t count)
|
||||
{
|
||||
#ifdef ARCH_ESP32
|
||||
if (count > SIZE_MAX / sizeof(T))
|
||||
std::abort();
|
||||
void *memory = heap_caps_malloc(count * sizeof(T), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
if (!memory)
|
||||
memory = heap_caps_malloc(count * sizeof(T), MALLOC_CAP_8BIT);
|
||||
if (!memory)
|
||||
std::abort();
|
||||
return static_cast<T *>(memory);
|
||||
#else
|
||||
return std::allocator<T>::allocate(count);
|
||||
#endif
|
||||
}
|
||||
|
||||
void deallocate(T *memory, size_t count)
|
||||
{
|
||||
#ifdef ARCH_ESP32
|
||||
(void)count;
|
||||
heap_caps_free(memory);
|
||||
#else
|
||||
std::allocator<T>::deallocate(memory, count);
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
class BookReaderApplet : public Applet, private concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
enum class Mode : uint8_t {
|
||||
BROWSER = 0,
|
||||
READER = 1,
|
||||
CHAPTERS = 2,
|
||||
OPENING = 3,
|
||||
MENU = 4,
|
||||
};
|
||||
|
||||
enum class BookFormat : uint8_t {
|
||||
PLAIN_TEXT = 0,
|
||||
HTML_LIKE = 1,
|
||||
RTF = 2,
|
||||
EPUB_CONTAINER = 3,
|
||||
};
|
||||
|
||||
struct BrowserEntry {
|
||||
std::string name;
|
||||
std::string path;
|
||||
bool isDir = false;
|
||||
uint32_t size = 0;
|
||||
BookFormat format = BookFormat::PLAIN_TEXT;
|
||||
};
|
||||
struct PrefetchEntry {
|
||||
int32_t partIndex = -1;
|
||||
std::string path;
|
||||
std::string text;
|
||||
bool truncated = false;
|
||||
|
||||
void clear()
|
||||
{
|
||||
partIndex = -1;
|
||||
path.clear();
|
||||
text.clear();
|
||||
truncated = false;
|
||||
}
|
||||
};
|
||||
struct ContentsEntry {
|
||||
std::string title;
|
||||
std::string path;
|
||||
std::string fragment;
|
||||
uint8_t depth = 0;
|
||||
int32_t part = -1;
|
||||
int32_t line = -1;
|
||||
size_t offset = std::string::npos;
|
||||
};
|
||||
struct DeferredPartLoad {
|
||||
int32_t partIndex = -1;
|
||||
|
||||
void clear() { partIndex = -1; }
|
||||
};
|
||||
BookReaderApplet();
|
||||
|
||||
void onActivate() override;
|
||||
void onDeactivate() override;
|
||||
void onBackground() override;
|
||||
void onShutdown() override;
|
||||
void onRender(bool full) override;
|
||||
bool keepScreenAwakeDuringRender() const override { return true; }
|
||||
static void setDisplayRefreshActive(bool active);
|
||||
|
||||
void onButtonShortPress() override;
|
||||
void onButtonLongPress() override;
|
||||
void onExitShort() override;
|
||||
void onNavUp() override;
|
||||
void onNavDown() override;
|
||||
void onNavLeft() override;
|
||||
void onNavRight() override;
|
||||
bool onTouchPoint(uint16_t x, uint16_t y, bool longPress) override;
|
||||
bool onTouchSwipe(int8_t dx, int8_t dy) override;
|
||||
|
||||
private:
|
||||
int32_t runOnce() override;
|
||||
void ensurePageIndex();
|
||||
void resetPageIndex();
|
||||
void recordCurrentPartPages();
|
||||
void installContents(const std::vector<ContentsEntry> &entries);
|
||||
void resolveContentsOffsets(const std::string &path, const std::string &raw, std::string &text,
|
||||
bool *incompleteTable = nullptr);
|
||||
void updateContentsLines(int32_t part);
|
||||
std::string readingProgress() const;
|
||||
std::string readingContext() const;
|
||||
void renderReadingProgress();
|
||||
uint64_t pageIndexFingerprint() const;
|
||||
std::string pageIndexLabel() const;
|
||||
std::vector<ContentsEntry> contents;
|
||||
std::vector<int32_t> partPageCounts;
|
||||
uint16_t pageIndexWidth = 0, pageIndexHeight = 0;
|
||||
size_t pageIndexNext = 0;
|
||||
std::string pageIndexText;
|
||||
size_t pageIndexImageOffset = 0;
|
||||
bool pageIndexSaved = false;
|
||||
struct PageIndexCache {
|
||||
static constexpr uint32_t VERSION = 13;
|
||||
uint32_t version = VERSION;
|
||||
uint64_t fingerprint = 0;
|
||||
uint16_t width = 0, height = 0, count = 0;
|
||||
uint8_t textSize = 0;
|
||||
std::array<uint16_t, 256> pages{};
|
||||
};
|
||||
Mode mode = Mode::BROWSER;
|
||||
bool initialized = false;
|
||||
bool initialLoadArmed = false;
|
||||
Mode menuReturnMode = Mode::BROWSER;
|
||||
uint8_t menuSelection = 0;
|
||||
struct ReadingPosition {
|
||||
uint32_t version = 1;
|
||||
char path[256] = {};
|
||||
int32_t part = -1;
|
||||
int32_t line = 0;
|
||||
int32_t bookmarkPart = -1;
|
||||
int32_t bookmarkLine = -1;
|
||||
uint8_t textSize = 0;
|
||||
uint8_t bookmarkTextSize = 0;
|
||||
} readingPosition;
|
||||
|
||||
std::string currentDir;
|
||||
std::vector<BrowserEntry> entries;
|
||||
int32_t selectedIndex = 0;
|
||||
int32_t firstVisibleIndex = 0;
|
||||
bool browserCursorShown = true;
|
||||
bool hideTouchBrowserSelectionHighlight = false;
|
||||
int32_t pendingOpenIndex = -1;
|
||||
int32_t pendingChapterIndex = -1;
|
||||
int pendingPageDelta = 0;
|
||||
bool openingSplashRendered = false;
|
||||
|
||||
std::string openFilePath;
|
||||
std::string bookPath;
|
||||
std::string epubCoverImagePath;
|
||||
bool useEpubCoverForFirstPart = false;
|
||||
bool pageNavigation = false;
|
||||
bool openFileTruncated = false;
|
||||
bool fileLoaded = false;
|
||||
std::string displayText;
|
||||
// Retained lines must not consume the internal RAM needed by filesystem/RTOS locks.
|
||||
using WrappedLine = std::basic_string<char, std::char_traits<char>, ReaderTextAllocator<char>>;
|
||||
std::vector<WrappedLine, ReaderTextAllocator<WrappedLine>> wrappedLines;
|
||||
std::vector<uint8_t, ReaderTextAllocator<uint8_t>> wrappedLineStyles;
|
||||
std::vector<size_t> chapterOffsets;
|
||||
std::vector<int32_t> chapterStartLines;
|
||||
std::vector<BrowserEntry> pagedParts;
|
||||
int32_t pagedPartIndex = -1;
|
||||
bool pagedBookMode = false;
|
||||
PrefetchEntry prefetchedNextPart;
|
||||
DeferredPartLoad deferredPartLoad;
|
||||
bool deferredNextPartPrefetch = false;
|
||||
size_t prefetchImageIndex = 0;
|
||||
bool backgroundReaderWorkPending = false;
|
||||
bool backgroundReaderWorkArmed = false;
|
||||
bool deferredWrapExpansion = false;
|
||||
size_t deferredWrapImageOffset = 0;
|
||||
uint32_t lastReaderInputMs = 0;
|
||||
uint32_t lastReaderRenderMs = 0;
|
||||
int32_t chapterPickerSelected = 0;
|
||||
int32_t chapterPickerFirstVisible = 0;
|
||||
bool chapterCursorShown = true;
|
||||
bool hideTouchChapterSelectionHighlight = false;
|
||||
bool deferInlineImagesUntilNextRender = false;
|
||||
std::string imageViewKey;
|
||||
std::string expandedImageLine;
|
||||
bool expandedImageLoading = false;
|
||||
struct InlineImageTarget {
|
||||
std::string line;
|
||||
int16_t left, top;
|
||||
uint16_t width, height;
|
||||
};
|
||||
std::vector<InlineImageTarget> inlineImageTargets;
|
||||
uint16_t imageZoomPermille = 1000;
|
||||
bool imageInteractionPending = false;
|
||||
bool imagePanOnlyRender = false;
|
||||
bool imageViewportFilled = false;
|
||||
int16_t imagePanX = 0;
|
||||
int16_t imagePanY = 0;
|
||||
int32_t scrollLine = 0;
|
||||
int32_t linesPerPage = 1;
|
||||
uint16_t wrapWidth = 0;
|
||||
|
||||
std::string status;
|
||||
bool wrapDirty = true;
|
||||
|
||||
bool ensureInitialDirectory();
|
||||
void resetSessionState();
|
||||
bool ensureSDReady();
|
||||
bool loadDirectory(const std::string &dir);
|
||||
bool openSelectedEntry();
|
||||
bool loadFile(const BrowserEntry &entry);
|
||||
bool loadEpubContainerAsBook(const BrowserEntry &entry);
|
||||
bool loadPartText(const BrowserEntry &part, size_t maxBytes, std::string &out, bool &truncatedOut,
|
||||
bool *needsExpansion = nullptr);
|
||||
bool loadPagedPart(int32_t partIndex, bool jumpToEndPage, size_t readLimitOverride = 0U, bool allowDeferredExpand = false);
|
||||
void prefetchNextPartIfPossible();
|
||||
void clearBackgroundReaderState();
|
||||
void updateBackgroundReaderPending();
|
||||
void queueDeferredPartExpansion(int32_t partIndex);
|
||||
void queueDeferredWrapExpansion();
|
||||
void queueDeferredNextPartPrefetch();
|
||||
bool runBackgroundReaderPass();
|
||||
void beginOpenSelectedEntry();
|
||||
void moveSelection(int delta);
|
||||
void scrollTouchList(int direction);
|
||||
void onNavVertical(int direction);
|
||||
void onNavHorizontal(int direction);
|
||||
uint16_t libraryRowHeight() const;
|
||||
void renderListScrollbar(int32_t count, int32_t first, int32_t rows, uint16_t top, uint16_t areaHeight);
|
||||
void goToParentDirectory();
|
||||
static void ensureSelectionVisible(int32_t selected, int32_t &first, int32_t rowsVisible);
|
||||
|
||||
void rewrapIfNeeded(uint16_t contentWidth, uint16_t contentHeight, bool allowPartialWrap = false);
|
||||
void wrapDisplayText(uint16_t contentWidth, size_t maxLines = 0U);
|
||||
void scrollByPages(int deltaPages, bool showLoading = true);
|
||||
void resetImageView();
|
||||
void closeExpandedImage();
|
||||
bool currentImagePageLine(std::string *lineOut = nullptr) const;
|
||||
bool syncImageViewToCurrentPage();
|
||||
bool cycleImageZoom(bool zoomOut);
|
||||
bool panImageView(int16_t dx, int16_t dy);
|
||||
void queueImageInteractionUpdate(bool panOnly = false);
|
||||
void openReaderMenu();
|
||||
void selectMenuItem(uint8_t item);
|
||||
uint8_t readerMenuItemCount() const;
|
||||
void renderReaderMenu();
|
||||
void renderReaderHeader(const char *rightLabel);
|
||||
void renderImageToolbar();
|
||||
AppletFont readingFont();
|
||||
void saveReadingPosition();
|
||||
void loadReadingPosition();
|
||||
void restoreLocation(int32_t part, int32_t line);
|
||||
int32_t estimateImageLineSpan(const std::string &imagePath, uint16_t contentWidth, uint16_t lineHeight) const;
|
||||
bool drawInlineImage(const std::string &imagePath, int16_t left, int16_t top, uint16_t maxWidth, uint16_t maxHeight,
|
||||
uint16_t zoomPermille = 1000, int16_t panX = 0, int16_t panY = 0,
|
||||
std::vector<uint8_t> *monoBitmap = nullptr, bool useRenderedCache = false, bool preloadOnly = false,
|
||||
bool inlineRaster = false, int16_t clipBottom = -1, bool *prefetchInterrupted = nullptr,
|
||||
uint16_t *bitmapWidth = nullptr, uint16_t *bitmapHeight = nullptr);
|
||||
bool prefetchDecodedImages(const std::string &text);
|
||||
bool drawInlineImageSequence(const std::vector<std::string> &imagePaths, int16_t left, int16_t top, uint16_t maxWidth,
|
||||
uint16_t maxHeight, uint16_t zoomPermille = 1000, int16_t panX = 0, int16_t panY = 0,
|
||||
bool useRenderedCache = false);
|
||||
bool jumpToChapter(int32_t chapterIndex);
|
||||
void beginJumpToChapter(int32_t chapterIndex);
|
||||
int32_t getCurrentChapterIndex() const;
|
||||
bool openChapterPicker();
|
||||
void closeChapterPicker();
|
||||
|
||||
void renderBrowser();
|
||||
void renderOpening(bool full);
|
||||
void renderLoading(int16_t left, int16_t top, uint16_t areaWidth, uint16_t areaHeight, bool overlay = true);
|
||||
void renderReader(bool completeImages = false, bool imageOnly = false);
|
||||
bool pageImagesNeedLoading(int32_t firstLine, int32_t count);
|
||||
uint16_t alignedTextOffset(const std::string &line, uint16_t offset, uint8_t alignment);
|
||||
void renderStyledText(int16_t x, int16_t y, std::string_view text, uint8_t &style, int16_t underlineY = -1);
|
||||
void renderChapterPicker();
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
struct ReaderToolbar {
|
||||
static constexpr uint16_t height = 128;
|
||||
static constexpr uint8_t count = 5;
|
||||
static constexpr uint16_t progressBottomInset = 8;
|
||||
static uint16_t progressFooterHeight(uint16_t lineHeight) { return lineHeight + 2U * progressBottomInset; }
|
||||
|
||||
static uint16_t inset(uint16_t width) { return std::min<uint16_t>(24, width / 12); }
|
||||
static uint16_t boundary(uint16_t width, uint8_t index)
|
||||
{
|
||||
return inset(width) + static_cast<uint32_t>(width - 2 * inset(width)) * index / count;
|
||||
}
|
||||
static uint8_t hit(uint16_t width, uint16_t x)
|
||||
{
|
||||
for (uint8_t i = 1; i < count; ++i) {
|
||||
if (x < boundary(width, i))
|
||||
return i - 1;
|
||||
}
|
||||
return count - 1;
|
||||
}
|
||||
};
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
inline int32_t readerPageCount(int32_t lines, int32_t step)
|
||||
{
|
||||
return 1 + (std::max<int32_t>(1, lines) - 1) / std::max<int32_t>(1, step);
|
||||
}
|
||||
inline int32_t readerLastPageStart(int32_t lines, int32_t step)
|
||||
{
|
||||
return (readerPageCount(lines, step) - 1) * std::max<int32_t>(1, step);
|
||||
}
|
||||
inline uint32_t readerPagePrefix(const std::vector<int32_t> &counts, size_t end)
|
||||
{
|
||||
uint32_t pages = 0;
|
||||
if (end > counts.size())
|
||||
return 0;
|
||||
for (size_t i = 0; i < end; ++i) {
|
||||
if (counts[i] <= 0)
|
||||
return 0;
|
||||
pages += counts[i];
|
||||
}
|
||||
return pages;
|
||||
}
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
@@ -0,0 +1,239 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
constexpr const char *BULLET_MARKER = "[[_BR_BULLET_]]";
|
||||
constexpr const char *FIRST_LINE_MARKER = "[[_BR_FIRST_]]";
|
||||
constexpr const char *TABLE_START = "[[_BR_TABLE_]]";
|
||||
constexpr const char *TABLE_END = "[[_BR_TABLE_END_]]";
|
||||
constexpr const char *TABLE_ROW = "[[_BR_ROW_]]";
|
||||
constexpr const char *TABLE_CELL = "[[_BR_CELL_]]";
|
||||
constexpr const char *TABLE_LINE = "[[_BR_GRID_]]";
|
||||
|
||||
inline size_t readerTagEnd(std::string_view text, size_t pos)
|
||||
{
|
||||
if (pos >= text.size())
|
||||
return std::string_view::npos;
|
||||
const size_t start = pos && text[pos - 1] == '<' ? pos - 1 : pos;
|
||||
for (const auto &block : {std::pair{"<!--", "-->"}, std::pair{"<![CDATA[", "]]>"}}) {
|
||||
if (text.compare(start, std::strlen(block.first), block.first) == 0) {
|
||||
const size_t end = text.find(block.second, start + std::strlen(block.first));
|
||||
return end == std::string_view::npos ? end : end + 2;
|
||||
}
|
||||
}
|
||||
char quote = 0;
|
||||
for (; pos < text.size(); ++pos) {
|
||||
const char c = text[pos];
|
||||
if (quote) {
|
||||
if (c == quote)
|
||||
quote = 0;
|
||||
} else if (c == '\'' || c == '"') {
|
||||
quote = c;
|
||||
} else if (c == '>') {
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
return std::string_view::npos;
|
||||
}
|
||||
|
||||
inline std::string readerXmlAttribute(std::string_view tag, std::string_view name)
|
||||
{
|
||||
auto space = [](char c) { return c == ' ' || c == '\t' || c == '\r' || c == '\n'; };
|
||||
auto lower = [](char c) { return c >= 'A' && c <= 'Z' ? c + ('a' - 'A') : c; };
|
||||
size_t pos = 0;
|
||||
while (pos < tag.size()) {
|
||||
if (space(tag[pos]) || tag[pos] == '<' || tag[pos] == '/' || tag[pos] == '?') {
|
||||
++pos;
|
||||
continue;
|
||||
}
|
||||
if (tag[pos] == '>')
|
||||
break;
|
||||
const size_t start = pos;
|
||||
while (pos < tag.size() && !space(tag[pos]) && tag[pos] != '=' && tag[pos] != '/' && tag[pos] != '>' &&
|
||||
tag[pos] != '\'' && tag[pos] != '"')
|
||||
++pos;
|
||||
const std::string_view key = tag.substr(start, pos - start);
|
||||
while (pos < tag.size() && space(tag[pos]))
|
||||
++pos;
|
||||
if (pos == tag.size())
|
||||
break;
|
||||
if (tag[pos] != '=') {
|
||||
if (tag[pos] == '\'' || tag[pos] == '"') {
|
||||
const size_t end = tag.find(tag[pos], pos + 1);
|
||||
pos = end == std::string_view::npos ? tag.size() : end + 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
++pos;
|
||||
while (pos < tag.size() && space(tag[pos]))
|
||||
++pos;
|
||||
if (pos == tag.size())
|
||||
break;
|
||||
const char quote = tag[pos] == '\'' || tag[pos] == '"' ? tag[pos++] : 0;
|
||||
const size_t valueStart = pos;
|
||||
while (pos < tag.size() && (quote ? tag[pos] != quote : !space(tag[pos]) && tag[pos] != '>'))
|
||||
++pos;
|
||||
if (quote && pos == tag.size())
|
||||
return {};
|
||||
const std::string_view value = tag.substr(valueStart, pos - valueStart);
|
||||
if (quote)
|
||||
++pos;
|
||||
bool matches = !name.empty() && key.size() == name.size();
|
||||
for (size_t i = 0; matches && i < key.size(); ++i)
|
||||
matches = lower(key[i]) == lower(name[i]);
|
||||
if (!matches)
|
||||
continue;
|
||||
|
||||
std::string out;
|
||||
out.reserve(value.size());
|
||||
for (size_t i = 0; i < value.size();) {
|
||||
if (value[i] != '&') {
|
||||
out.push_back(value[i++]);
|
||||
continue;
|
||||
}
|
||||
const size_t end = value.find_first_of(";&", i + 1);
|
||||
if (end == std::string_view::npos) {
|
||||
out.append(value.substr(i));
|
||||
break;
|
||||
}
|
||||
if (value[end] == '&') {
|
||||
out.append(value.substr(i, end - i));
|
||||
i = end;
|
||||
continue;
|
||||
}
|
||||
const std::string_view ref = value.substr(i + 1, end - i - 1);
|
||||
uint32_t cp = ref == "amp" ? '&'
|
||||
: ref == "lt" ? '<'
|
||||
: ref == "gt" ? '>'
|
||||
: ref == "quot" ? '"'
|
||||
: ref == "apos" ? '\''
|
||||
: ref == "nbsp" ? 0xA0
|
||||
: ref == "hellip" ? 0x2026
|
||||
: 0;
|
||||
if (!ref.empty() && ref[0] == '#') {
|
||||
const bool hex = ref.size() > 1 && ref[1] == 'x';
|
||||
size_t digit = hex ? 2 : 1;
|
||||
bool valid = digit < ref.size();
|
||||
const uint32_t base = hex ? 16 : 10;
|
||||
for (; valid && digit < ref.size(); ++digit) {
|
||||
const char c = ref[digit];
|
||||
const uint32_t n = c >= '0' && c <= '9' ? c - '0'
|
||||
: c >= 'a' && c <= 'f' ? c - 'a' + 10
|
||||
: c >= 'A' && c <= 'F' ? c - 'A' + 10
|
||||
: 16;
|
||||
valid = n < base && cp <= (0x10FFFFU - n) / base;
|
||||
if (valid)
|
||||
cp = cp * base + n;
|
||||
}
|
||||
if (!valid)
|
||||
cp = 0;
|
||||
}
|
||||
const bool valid = cp == 9 || cp == 10 || cp == 13 || (cp >= 0x20 && cp <= 0xD7FF) ||
|
||||
(cp >= 0xE000 && cp <= 0xFFFD) || (cp >= 0x10000 && cp <= 0x10FFFF);
|
||||
if (!valid) {
|
||||
out.append(value.substr(i, end - i + 1));
|
||||
} else if (cp < 0x80) {
|
||||
out.push_back(static_cast<char>(cp));
|
||||
} else {
|
||||
const unsigned count = cp < 0x800 ? 2 : cp < 0x10000 ? 3 : 4;
|
||||
out.push_back(static_cast<char>((count == 2 ? 0xC0 : count == 3 ? 0xE0 : 0xF0) | (cp >> (6 * (count - 1)))));
|
||||
for (unsigned shift = 6 * (count - 1); shift; shift -= 6)
|
||||
out.push_back(static_cast<char>(0x80 | ((cp >> (shift - 6)) & 0x3F)));
|
||||
}
|
||||
i = end + 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
inline std::vector<std::string> readerTableCells(const std::string &row)
|
||||
{
|
||||
std::vector<std::string> cells;
|
||||
size_t start = row.find(TABLE_CELL);
|
||||
while (start != std::string::npos) {
|
||||
start += std::strlen(TABLE_CELL);
|
||||
const size_t end = row.find(TABLE_CELL, start);
|
||||
cells.push_back(row.substr(start, end == std::string::npos ? end : end - start));
|
||||
start = end;
|
||||
}
|
||||
return cells;
|
||||
}
|
||||
|
||||
inline size_t readerBulletLengthAt(const std::string &text, size_t pos)
|
||||
{
|
||||
if (pos >= text.size())
|
||||
return 0;
|
||||
if (text.size() - pos >= 3) {
|
||||
const auto a = static_cast<uint8_t>(text[pos]);
|
||||
const auto b = static_cast<uint8_t>(text[pos + 1]);
|
||||
const auto c = static_cast<uint8_t>(text[pos + 2]);
|
||||
if (a == 0xE2U && ((b == 0x80U && (c == 0xA2U || c == 0xA3U)) || (b == 0x97U && (c == 0x8FU || c == 0xA6U))))
|
||||
return 3;
|
||||
}
|
||||
if (text[pos] != '&')
|
||||
return 0;
|
||||
for (const char *entity : {"•", "•", "•", "•"}) {
|
||||
const size_t length = std::strlen(entity);
|
||||
if (text.compare(pos, length, entity) == 0)
|
||||
return length;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
inline bool readerParagraphStartsWithBullet(const std::string &text, size_t pos)
|
||||
{
|
||||
while (pos < text.size()) {
|
||||
if (text[pos] == '<') {
|
||||
const size_t end = readerTagEnd(text, pos + 1);
|
||||
if (end == std::string::npos || text[pos + 1] == '/')
|
||||
return false;
|
||||
pos = end + 1;
|
||||
} else if (text[pos] == ' ' || text[pos] == '\t' || text[pos] == '\r' || text[pos] == '\n') {
|
||||
++pos;
|
||||
} else {
|
||||
return readerBulletLengthAt(text, pos) != 0;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
inline size_t readerSpaceLengthAt(const std::string &text, size_t pos)
|
||||
{
|
||||
if (pos >= text.size())
|
||||
return 0;
|
||||
const size_t remaining = text.size() - pos;
|
||||
if (remaining >= 2 && static_cast<uint8_t>(text[pos]) == 0xC2U && static_cast<uint8_t>(text[pos + 1]) == 0xA0U)
|
||||
return 2;
|
||||
if (remaining < 3)
|
||||
return 0;
|
||||
const auto a = static_cast<uint8_t>(text[pos]);
|
||||
const auto b = static_cast<uint8_t>(text[pos + 1]);
|
||||
const auto c = static_cast<uint8_t>(text[pos + 2]);
|
||||
if ((a == 0xE2U && b == 0x80U && ((c >= 0x80U && c <= 0x8AU) || c == 0xAFU)) || (a == 0xE2U && b == 0x81U && c == 0x9FU) ||
|
||||
(a == 0xE3U && b == 0x80U && c == 0x80U))
|
||||
return 3;
|
||||
return 0;
|
||||
}
|
||||
|
||||
inline void appendHtmlBlockBreak(std::string &text, bool compact)
|
||||
{
|
||||
while (!text.empty() && text.back() == ' ')
|
||||
text.pop_back();
|
||||
if (text.empty())
|
||||
return;
|
||||
const bool endsWithBreak = text.back() == '\n';
|
||||
if (compact && endsWithBreak)
|
||||
return;
|
||||
if (endsWithBreak && text.size() > 1 && text[text.size() - 2] == '\n')
|
||||
return;
|
||||
text.push_back('\n');
|
||||
}
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
@@ -0,0 +1,361 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#if INKHUD_BOOKREADER_GRAYSCALE && defined(ARCH_ESP32) && defined(__GNUC__)
|
||||
// Pixel conversion favors speed; restore the firmware's size optimization after these helpers.
|
||||
#pragma GCC push_options
|
||||
#pragma GCC optimize("O3")
|
||||
#endif
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
inline size_t readerBitmapStride(uint16_t width)
|
||||
{
|
||||
#if INKHUD_BOOKREADER_GRAYSCALE
|
||||
return (static_cast<size_t>(width) + 1U) / 2U;
|
||||
#else
|
||||
return (static_cast<size_t>(width) + 7U) / 8U;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if INKHUD_BOOKREADER_GRAYSCALE
|
||||
// Reader caches store ink coverage: zero is white, fifteen is black.
|
||||
inline constexpr uint8_t readerInkFromLuminance(unsigned luminance)
|
||||
{
|
||||
// Blend linear and quadratic luminance to deepen midtones without clipping the endpoints.
|
||||
const unsigned adjusted = (luminance * (1275U + 3U * luminance) + 1020U) / 2040U;
|
||||
return 15U - (adjusted * 15U + 127U) / 255U;
|
||||
}
|
||||
|
||||
inline constexpr auto readerInkLookup = [] {
|
||||
std::array<uint8_t, 256> ink{};
|
||||
for (unsigned luminance = 0; luminance < ink.size(); ++luminance)
|
||||
ink[luminance] = readerInkFromLuminance(luminance);
|
||||
return ink;
|
||||
}();
|
||||
|
||||
// Common scaling footprints contain at most sixteen source pixels.
|
||||
inline constexpr auto readerAverageInkLookup = [] {
|
||||
std::array<std::array<uint8_t, 241>, 16> averages{};
|
||||
for (unsigned samples = 1; samples <= averages.size(); ++samples)
|
||||
for (unsigned sum = 0; sum <= samples * 15U; ++sum)
|
||||
averages[samples - 1U][sum] = (sum + samples / 2U) / samples;
|
||||
return averages;
|
||||
}();
|
||||
|
||||
inline uint8_t readerGrayAt(const uint8_t *data, size_t stride, uint16_t x, uint16_t y)
|
||||
{
|
||||
return (data[y * stride + x / 2U] >> ((x & 1U) ? 0U : 4U)) & 15U;
|
||||
}
|
||||
|
||||
inline unsigned readerShortGraySpanSum(const uint8_t *row, uint16_t left, uint16_t span)
|
||||
{
|
||||
const uint8_t pair = row[left / 2U];
|
||||
unsigned ink = (pair >> ((left & 1U) ? 0U : 4U)) & 15U;
|
||||
if (span >= 2U)
|
||||
ink += (left & 1U) ? row[left / 2U + 1U] >> 4U : pair & 15U;
|
||||
if (span == 3U)
|
||||
ink += (row[left / 2U + 1U] >> ((left & 1U) ? 0U : 4U)) & 15U;
|
||||
return ink;
|
||||
}
|
||||
|
||||
inline void setReaderGray(uint8_t *data, size_t stride, uint16_t x, uint16_t y, uint8_t ink)
|
||||
{
|
||||
uint8_t &byte = data[y * stride + x / 2U];
|
||||
const unsigned shift = (x & 1U) ? 0U : 4U;
|
||||
byte = (byte & ~(15U << shift)) | ((ink & 15U) << shift);
|
||||
}
|
||||
#endif
|
||||
|
||||
inline bool rasterizeInlineRgb565Block(const uint16_t *pixels, int x, int y, int width, int height, int pixelStride, uint16_t sw,
|
||||
uint16_t sh, uint16_t dw, uint16_t dh, std::vector<uint8_t> &destination)
|
||||
{
|
||||
const size_t stride = readerBitmapStride(dw);
|
||||
if (!pixels || x < 0 || y < 0 || width <= 0 || height <= 0 || pixelStride < width || !sw || !sh || !dw || !dh ||
|
||||
destination.size() < stride * dh)
|
||||
return false;
|
||||
if (x >= sw || y >= sh)
|
||||
return true;
|
||||
const uint32_t xEnd = static_cast<uint32_t>(x) + std::min<int>(width, sw - x);
|
||||
const uint32_t yEnd = static_cast<uint32_t>(y) + std::min<int>(height, sh - y);
|
||||
const uint32_t left = (static_cast<uint32_t>(x) * dw + sw - 1U) / sw;
|
||||
const uint32_t top = (static_cast<uint32_t>(y) * dh + sh - 1U) / sh;
|
||||
const uint32_t right = (xEnd * dw + sw - 1U) / sw;
|
||||
const uint32_t bottom = (yEnd * dh + sh - 1U) / sh;
|
||||
#if !INKHUD_BOOKREADER_GRAYSCALE
|
||||
// Dither after scaling, so gray detail survives reduction.
|
||||
constexpr uint8_t bayer[] = {0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5};
|
||||
#endif
|
||||
for (uint32_t dy = top; dy < bottom; ++dy) {
|
||||
const uint16_t *row = pixels + (dy * sh / dh - y) * pixelStride;
|
||||
#if !INKHUD_BOOKREADER_GRAYSCALE
|
||||
uint8_t *out = destination.data() + dy * stride;
|
||||
#endif
|
||||
for (uint32_t dx = left; dx < right; ++dx) {
|
||||
const uint16_t rgb = row[dx * sw / dw - x];
|
||||
#if INKHUD_BOOKREADER_GRAYSCALE
|
||||
const unsigned r = (rgb >> 11) & 31U, g = (rgb >> 5) & 63U, b = rgb & 31U;
|
||||
const unsigned luminance =
|
||||
(((r << 3U) | (r >> 2U)) * 38U + ((g << 2U) | (g >> 4U)) * 75U + ((b << 3U) | (b >> 2U)) * 15U + 64U) >> 7U;
|
||||
setReaderGray(destination.data(), stride, dx, dy, readerInkLookup[luminance]);
|
||||
#else
|
||||
const unsigned luminance = (((rgb >> 11) & 31U) * 38U + ((rgb >> 5) & 63U) * 75U + (rgb & 31U) * 15U) / 25U;
|
||||
const unsigned threshold = bayer[(dy & 3U) * 4U + (dx & 3U)] * 16U + 8U;
|
||||
const uint8_t mask = 0x80U >> (dx & 7U);
|
||||
if (luminance < threshold)
|
||||
out[dx / 8U] |= mask;
|
||||
else
|
||||
out[dx / 8U] &= static_cast<uint8_t>(~mask);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool packGraySourceBlock(const uint8_t *pixels, int x, int y, int width, int height, int pixelStride, uint16_t sourceWidth,
|
||||
uint16_t sourceHeight, std::vector<uint8_t> &source)
|
||||
{
|
||||
const size_t stride = readerBitmapStride(sourceWidth);
|
||||
if (!pixels || width <= 0 || height <= 0 || pixelStride < width || source.size() < stride * sourceHeight)
|
||||
return false;
|
||||
#if INKHUD_BOOKREADER_GRAYSCALE
|
||||
const auto inkAt = [](uint8_t gray) { return readerInkLookup[gray]; };
|
||||
const int firstX = std::max(0, -x);
|
||||
const int endX = std::min(width, static_cast<int>(sourceWidth) - x);
|
||||
if (firstX >= endX)
|
||||
return true;
|
||||
for (int sy = std::max(0, -y); sy < height && y + sy < sourceHeight; ++sy) {
|
||||
uint8_t *out = source.data() + static_cast<size_t>(y + sy) * stride + (x + firstX) / 2U;
|
||||
const uint8_t *in = pixels + static_cast<size_t>(sy) * pixelStride + firstX;
|
||||
int remaining = endX - firstX;
|
||||
if ((x + firstX) & 1U) {
|
||||
*out = (*out & 0xF0U) | inkAt(*in++);
|
||||
++out;
|
||||
--remaining;
|
||||
}
|
||||
for (; remaining >= 2; remaining -= 2, in += 2)
|
||||
*out++ = (inkAt(in[0]) << 4U) | inkAt(in[1]);
|
||||
if (remaining)
|
||||
*out = (*out & 0x0FU) | (inkAt(*in) << 4U);
|
||||
}
|
||||
return true;
|
||||
#else
|
||||
if (x >= 0 && y >= 0 && (x & 7) == 0 && x + width <= sourceWidth && y + height <= sourceHeight) {
|
||||
const size_t firstByte = static_cast<size_t>(x) / 8U;
|
||||
for (int sy = 0; sy < height; ++sy) {
|
||||
uint8_t *out = source.data() + static_cast<size_t>(y + sy) * stride + firstByte;
|
||||
const uint8_t *in = pixels + static_cast<size_t>(sy) * pixelStride;
|
||||
int sx = 0;
|
||||
for (; sx + 8 <= width; sx += 8) {
|
||||
out[sx / 8] = static_cast<uint8_t>((in[sx] < 132 ? 0x80U : 0U) | (in[sx + 1] < 132 ? 0x40U : 0U) |
|
||||
(in[sx + 2] < 132 ? 0x20U : 0U) | (in[sx + 3] < 132 ? 0x10U : 0U) |
|
||||
(in[sx + 4] < 132 ? 0x08U : 0U) | (in[sx + 5] < 132 ? 0x04U : 0U) |
|
||||
(in[sx + 6] < 132 ? 0x02U : 0U) | (in[sx + 7] < 132 ? 0x01U : 0U));
|
||||
}
|
||||
for (; sx < width; ++sx) {
|
||||
const uint8_t mask = 0x80U >> (sx & 7);
|
||||
if (in[sx] < 132)
|
||||
out[sx / 8] |= mask;
|
||||
else
|
||||
out[sx / 8] &= static_cast<uint8_t>(~mask);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
for (int sy = 0; sy < height && y + sy < sourceHeight; ++sy) {
|
||||
if (y + sy < 0)
|
||||
continue;
|
||||
uint8_t *row = source.data() + (y + sy) * stride;
|
||||
for (int sx = 0; sx < width && x + sx < sourceWidth; ++sx) {
|
||||
if (x + sx < 0)
|
||||
continue;
|
||||
const uint8_t mask = 0x80U >> ((x + sx) & 7);
|
||||
// Match the RGB565 threshold for grayscale samples.
|
||||
if (pixels[sy * pixelStride + sx] < 132)
|
||||
row[(x + sx) / 8] |= mask;
|
||||
else
|
||||
row[(x + sx) / 8] &= static_cast<uint8_t>(~mask);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline bool scaleSourceBitmap(const std::vector<uint8_t> &source, uint16_t sw, uint16_t sh, uint16_t dw, uint16_t dh,
|
||||
std::vector<uint8_t> &destination)
|
||||
{
|
||||
const size_t sourceStride = readerBitmapStride(sw);
|
||||
if (!sw || !sh || !dw || !dh || source.size() < sourceStride * sh || &source == &destination)
|
||||
return false;
|
||||
if (sw == dw && sh == dh) {
|
||||
destination.assign(source.begin(), source.begin() + sourceStride * sh);
|
||||
return true;
|
||||
}
|
||||
const size_t stride = readerBitmapStride(dw);
|
||||
destination.assign(stride * dh, 0);
|
||||
#if INKHUD_BOOKREADER_GRAYSCALE
|
||||
std::vector<uint16_t> columns(static_cast<size_t>(dw) + 1U);
|
||||
for (uint32_t x = 0; x <= dw; ++x)
|
||||
columns[x] = x * sw / dw;
|
||||
std::vector<uint16_t> spans(dw);
|
||||
for (uint32_t x = 0; x < dw; ++x)
|
||||
spans[x] = std::max<uint16_t>(1U, columns[x + 1U] - columns[x]);
|
||||
std::vector<uint32_t> sums(dw);
|
||||
uint32_t previousTop = UINT32_MAX, previousBottom = UINT32_MAX;
|
||||
for (uint32_t y = 0; y < dh; ++y) {
|
||||
const uint32_t top = y * sh / dh;
|
||||
const uint32_t bottom = std::max(top + 1U, (y + 1U) * sh / dh);
|
||||
if (y && top == previousTop && bottom == previousBottom) {
|
||||
std::copy_n(destination.data() + (y - 1U) * stride, stride, destination.data() + y * stride);
|
||||
continue;
|
||||
}
|
||||
previousTop = top;
|
||||
previousBottom = bottom;
|
||||
if (sw <= 2U * dw && bottom == top + 1U) {
|
||||
uint8_t *out = destination.data() + y * stride;
|
||||
const auto ink = [&](uint32_t x) {
|
||||
const uint8_t first = readerGrayAt(source.data(), sourceStride, columns[x], top);
|
||||
if (spans[x] == 1U)
|
||||
return first;
|
||||
const uint8_t second = readerGrayAt(source.data(), sourceStride, columns[x] + 1U, top);
|
||||
return static_cast<uint8_t>((first + second + 1U) >> 1U);
|
||||
};
|
||||
uint32_t x = 0;
|
||||
for (; x + 1U < dw; x += 2U)
|
||||
out[x / 2U] = (ink(x) << 4U) | ink(x + 1U);
|
||||
if (x < dw)
|
||||
out[x / 2U] = ink(x) << 4U;
|
||||
continue;
|
||||
}
|
||||
if (sw <= 2U * dw && bottom == top + 2U) {
|
||||
const uint8_t *firstRow = source.data() + top * sourceStride;
|
||||
const uint8_t *secondRow = firstRow + sourceStride;
|
||||
uint8_t *out = destination.data() + y * stride;
|
||||
const auto ink = [&](uint32_t x) {
|
||||
const uint32_t left = columns[x];
|
||||
const uint8_t first = firstRow[left / 2U], second = secondRow[left / 2U];
|
||||
const unsigned shift = (left & 1U) ? 0U : 4U;
|
||||
unsigned sum = ((first >> shift) & 15U) + ((second >> shift) & 15U);
|
||||
if (spans[x] == 1U)
|
||||
return (sum + 1U) >> 1U;
|
||||
sum += (left & 1U) ? (firstRow[left / 2U + 1U] >> 4U) + (secondRow[left / 2U + 1U] >> 4U)
|
||||
: (first & 15U) + (second & 15U);
|
||||
return (sum + 2U) >> 2U;
|
||||
};
|
||||
uint32_t x = 0;
|
||||
for (; x + 1U < dw; x += 2U)
|
||||
out[x / 2U] = (ink(x) << 4U) | ink(x + 1U);
|
||||
if (x < dw)
|
||||
out[x / 2U] = ink(x) << 4U;
|
||||
continue;
|
||||
}
|
||||
if (sw <= 3U * dw && bottom <= top + 3U) {
|
||||
const uint8_t *firstRow = source.data() + top * sourceStride;
|
||||
const unsigned rows = bottom - top;
|
||||
uint8_t *out = destination.data() + y * stride;
|
||||
const auto ink = [&](uint32_t x) {
|
||||
const uint16_t left = columns[x], span = spans[x];
|
||||
unsigned sum = readerShortGraySpanSum(firstRow, left, span);
|
||||
if (rows >= 2U)
|
||||
sum += readerShortGraySpanSum(firstRow + sourceStride, left, span);
|
||||
if (rows == 3U)
|
||||
sum += readerShortGraySpanSum(firstRow + 2U * sourceStride, left, span);
|
||||
return readerAverageInkLookup[span * rows - 1U][sum];
|
||||
};
|
||||
uint32_t x = 0;
|
||||
for (; x + 1U < dw; x += 2U)
|
||||
out[x / 2U] = (ink(x) << 4U) | ink(x + 1U);
|
||||
if (x < dw)
|
||||
out[x / 2U] = ink(x) << 4U;
|
||||
continue;
|
||||
}
|
||||
std::fill(sums.begin(), sums.end(), 0U);
|
||||
// Read each source row sequentially instead of revisiting it for every destination pixel.
|
||||
for (uint32_t sy = top; sy < bottom; ++sy) {
|
||||
const uint8_t *row = source.data() + sy * sourceStride;
|
||||
for (uint32_t x = 0; x < dw; ++x) {
|
||||
const uint32_t left = columns[x];
|
||||
if (spans[x] <= 3U) {
|
||||
sums[x] += readerShortGraySpanSum(row, left, spans[x]);
|
||||
continue;
|
||||
}
|
||||
const uint32_t right = left + spans[x];
|
||||
uint32_t ink = 0;
|
||||
uint32_t sx = left;
|
||||
if (sx & 1U) {
|
||||
ink += row[sx / 2U] & 15U;
|
||||
++sx;
|
||||
}
|
||||
for (; sx + 1U < right; sx += 2U) {
|
||||
const uint8_t pair = row[sx / 2U];
|
||||
ink += (pair >> 4U) + (pair & 15U);
|
||||
}
|
||||
if (sx < right)
|
||||
ink += row[sx / 2U] >> 4U;
|
||||
sums[x] += ink;
|
||||
}
|
||||
}
|
||||
uint8_t *out = destination.data() + y * stride;
|
||||
const uint32_t rows = bottom - top;
|
||||
const auto average = [&](uint32_t x) {
|
||||
const uint32_t samples = spans[x] * rows;
|
||||
return samples <= readerAverageInkLookup.size() ? static_cast<uint32_t>(readerAverageInkLookup[samples - 1U][sums[x]])
|
||||
: (sums[x] + samples / 2U) / samples;
|
||||
};
|
||||
uint32_t x = 0;
|
||||
for (; x + 1U < dw; x += 2U)
|
||||
out[x / 2U] = (average(x) << 4U) | average(x + 1U);
|
||||
if (x < dw)
|
||||
out[x / 2U] = average(x) << 4U;
|
||||
}
|
||||
return true;
|
||||
#else
|
||||
if (dw < sw || dh < sh) {
|
||||
// Preserve source-pixel coverage when reducing fine lines and screentones.
|
||||
constexpr uint8_t bayer[] = {0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5};
|
||||
for (uint32_t y = 0; y < dh; ++y) {
|
||||
const uint32_t top = y * sh / dh;
|
||||
const uint32_t bottom = std::max(top + 1U, (y + 1U) * sh / dh);
|
||||
uint8_t *out = destination.data() + y * stride;
|
||||
for (uint32_t x = 0; x < dw; ++x) {
|
||||
const uint32_t left = x * sw / dw;
|
||||
const uint32_t right = std::max(left + 1U, (x + 1U) * sw / dw);
|
||||
uint32_t black = 0;
|
||||
for (uint32_t sy = top; sy < bottom; ++sy) {
|
||||
const uint8_t *row = source.data() + sy * sourceStride;
|
||||
for (uint32_t sx = left; sx < right; ++sx)
|
||||
black += (row[sx / 8U] >> (7U - (sx & 7U))) & 1U;
|
||||
}
|
||||
const uint32_t samples = (right - left) * (bottom - top);
|
||||
const uint32_t threshold = bayer[(y & 3U) * 4U + (x & 3U)] * 2U + 1U;
|
||||
if (static_cast<uint64_t>(black) * 32U > static_cast<uint64_t>(samples) * threshold)
|
||||
out[x / 8U] |= 0x80U >> (x & 7U);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
std::vector<uint16_t> columns(dw);
|
||||
for (uint32_t x = 0; x < dw; ++x)
|
||||
columns[x] = x * sw / dw;
|
||||
for (uint32_t y = 0; y < dh; ++y) {
|
||||
const uint8_t *row = source.data() + (y * sh / dh) * sourceStride;
|
||||
uint8_t *out = destination.data() + y * stride;
|
||||
for (uint32_t x = 0; x < dw; ++x) {
|
||||
const uint16_t sx = columns[x];
|
||||
if (row[sx / 8] & (0x80U >> (sx & 7)))
|
||||
out[x / 8] |= 0x80U >> (x & 7);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#if INKHUD_BOOKREADER_GRAYSCALE && defined(ARCH_ESP32) && defined(__GNUC__)
|
||||
#pragma GCC pop_options
|
||||
#endif
|
||||
@@ -157,7 +157,13 @@ void InkHUD::Events::onExitShort()
|
||||
|
||||
// Touch-capable InkHUD nodes use EXIT/HOME as a quick app switcher launcher.
|
||||
if (!dismissedExt && inkhud->hasTouchEnabledProvider()) {
|
||||
inkhud->openAppSwitcher();
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
Applet *userConsumer = inkhud->getActiveApplet();
|
||||
if (userConsumer && userConsumer->isInputSubscribed(Applet::EXIT_SHORT))
|
||||
userConsumer->onExitShort();
|
||||
else
|
||||
#endif
|
||||
inkhud->openAppSwitcher();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -553,4 +553,19 @@ void InkHUD::InkHUD::drawPixel(int16_t x, int16_t y, Color c)
|
||||
renderer->handlePixel(x, y, c);
|
||||
}
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
#if INKHUD_BOOKREADER_GRAYSCALE
|
||||
void InkHUD::InkHUD::writeGrayBitmap(int16_t screenX, int16_t screenY, uint16_t w, uint16_t h, const uint8_t *bitmap,
|
||||
uint16_t bitmapStride, uint16_t sourceX, uint16_t sourceY)
|
||||
{
|
||||
renderer->writeGrayBitmap(screenX, screenY, w, h, bitmap, bitmapStride, sourceX, sourceY);
|
||||
}
|
||||
#endif
|
||||
void InkHUD::InkHUD::writeBitmap(int16_t screenX, int16_t screenY, uint16_t w, uint16_t h, const uint8_t *bitmap,
|
||||
uint16_t bitmapStride, uint16_t sourceX, uint16_t sourceY)
|
||||
{
|
||||
renderer->writeBitmap(screenX, screenY, w, h, bitmap, bitmapStride, sourceX, sourceY);
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -134,6 +134,17 @@ class InkHUD
|
||||
// Pass drawing output to Renderer
|
||||
void drawPixel(int16_t x, int16_t y, Color c);
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
// Fast bulk bitmap blit bypasses the per-pixel GFX/Tile call chain (used by BookReader for pan).
|
||||
// screenX/screenY are post-tile-offset screen coordinates.
|
||||
void writeBitmap(int16_t screenX, int16_t screenY, uint16_t w, uint16_t h, const uint8_t *bitmap, uint16_t bitmapStride,
|
||||
uint16_t sourceX = 0, uint16_t sourceY = 0);
|
||||
#if INKHUD_BOOKREADER_GRAYSCALE
|
||||
void writeGrayBitmap(int16_t screenX, int16_t screenY, uint16_t w, uint16_t h, const uint8_t *bitmap, uint16_t bitmapStride,
|
||||
uint16_t sourceX = 0, uint16_t sourceY = 0);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// Shared data which persists between boots
|
||||
Persistence *persistence = nullptr;
|
||||
|
||||
|
||||
@@ -2,7 +2,15 @@
|
||||
|
||||
#include "./Renderer.h"
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
#include "./Applets/User/BookReader/BookReaderApplet.h"
|
||||
#include "PowerFSM.h"
|
||||
#endif
|
||||
#include "main.h"
|
||||
#if INKHUD_BOOKREADER_GRAYSCALE
|
||||
#include <array>
|
||||
#include <esp_heap_caps.h>
|
||||
#endif
|
||||
|
||||
#include "./Applet.h"
|
||||
#include "./SystemApplet.h"
|
||||
@@ -10,6 +18,16 @@
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
#ifndef INKHUD_BOOKREADER_PERF_LOG
|
||||
#define INKHUD_BOOKREADER_PERF_LOG 0
|
||||
#endif
|
||||
|
||||
#if INKHUD_BOOKREADER_PERF_LOG
|
||||
static uint32_t readerRefreshStartMs = 0;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
InkHUD::Renderer::Renderer() : concurrency::OSThread("Renderer")
|
||||
{
|
||||
// Nothing for the timer to do just yet
|
||||
@@ -21,7 +39,8 @@ InkHUD::Renderer::Renderer() : concurrency::OSThread("Renderer")
|
||||
}
|
||||
|
||||
// Connect the (fully set-up) E-Ink driver to InkHUD
|
||||
// Should happen in your variant's nicheGraphics.h file, before InkHUD::begin is called
|
||||
// Should happen in your variant's nicheGraphics.h file, before InkHUD::begin is
|
||||
// called
|
||||
void InkHUD::Renderer::setDriver(Drivers::EInk *driver)
|
||||
{
|
||||
// Make sure not already set
|
||||
@@ -36,7 +55,8 @@ void InkHUD::Renderer::setDriver(Drivers::EInk *driver)
|
||||
|
||||
// Determine the dimensions of the image buffer, in bytes.
|
||||
// Along rows, pixels are stored 8 per byte.
|
||||
// Not all display widths are divisible by 8. Need to make sure bytecount accommodates padding for these.
|
||||
// Not all display widths are divisible by 8. Need to make sure bytecount
|
||||
// accommodates padding for these.
|
||||
imageBufferWidth = ((driver->width - 1) / 8) + 1;
|
||||
imageBufferHeight = driver->height;
|
||||
|
||||
@@ -44,10 +64,11 @@ void InkHUD::Renderer::setDriver(Drivers::EInk *driver)
|
||||
imageBuffer = new uint8_t[imageBufferWidth * imageBufferHeight];
|
||||
}
|
||||
|
||||
// Set the target number of FAST display updates in a row, before a FULL update is used for display health
|
||||
// This value applies only to updates with an UNSPECIFIED update type
|
||||
// If explicitly requested FAST updates exceed this target, the stressMultiplier parameter determines how many
|
||||
// subsequent FULL updates will be performed, in an attempt to restore the display's health
|
||||
// Set the target number of FAST display updates in a row, before a FULL update
|
||||
// is used for display health This value applies only to updates with an
|
||||
// UNSPECIFIED update type If explicitly requested FAST updates exceed this
|
||||
// target, the stressMultiplier parameter determines how many subsequent FULL
|
||||
// updates will be performed, in an attempt to restore the display's health
|
||||
void InkHUD::Renderer::setDisplayResilience(uint8_t fastPerFull, float stressMultiplier)
|
||||
{
|
||||
displayHealth.fastPerFull = fastPerFull;
|
||||
@@ -59,9 +80,354 @@ void InkHUD::Renderer::begin()
|
||||
forceUpdate(Drivers::EInk::UpdateTypes::FULL, true, false);
|
||||
}
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
#if INKHUD_BOOKREADER_GRAYSCALE
|
||||
void InkHUD::Renderer::setGrayPixel(uint16_t x, uint16_t y, uint8_t luminance)
|
||||
{
|
||||
uint8_t &byte = grayBuffer[static_cast<size_t>(y) * ((driver->width + 1U) / 2U) + x / 2U];
|
||||
const unsigned shift = (x & 1U) ? 0U : 4U;
|
||||
const uint8_t previous = (byte >> shift) & 15U;
|
||||
grayPixelCount -= previous != 0U && previous != 15U;
|
||||
grayPixelCount += luminance != 0U && luminance != 15U;
|
||||
byte = (byte & ~(15U << shift)) | (luminance << shift);
|
||||
}
|
||||
|
||||
#if defined(ARCH_ESP32) && defined(__GNUC__)
|
||||
// Favor speed for the packed-pixel blit without changing panel timing.
|
||||
__attribute__((optimize("O3")))
|
||||
#endif
|
||||
void InkHUD::Renderer::writeGrayBitmap(int16_t screenX, int16_t screenY, uint16_t w, uint16_t h, const uint8_t *bitmap,
|
||||
uint16_t bitmapStride, uint16_t sourceX, uint16_t sourceY)
|
||||
{
|
||||
if (!bitmap || !w || !h)
|
||||
return;
|
||||
if (!grayBuffer && driver->supportsGrayscale()) {
|
||||
grayBuffer = static_cast<uint8_t *>(
|
||||
heap_caps_malloc(((driver->width + 1U) / 2U) * driver->height, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
|
||||
if (grayBuffer) {
|
||||
memset(grayBuffer, 0xFF, ((driver->width + 1U) / 2U) * driver->height);
|
||||
for (uint16_t y = 0; y < driver->height; ++y)
|
||||
for (uint16_t x = 0; x < driver->width; ++x)
|
||||
setGrayPixel(x, y, (imageBuffer[y * imageBufferWidth + x / 8U] & (0x80U >> (x & 7U))) ? 15U : 0U);
|
||||
}
|
||||
}
|
||||
constexpr uint8_t bayer[] = {0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5};
|
||||
const int32_t firstX = std::max<int32_t>(0, -static_cast<int32_t>(screenX));
|
||||
const int32_t firstY = std::max<int32_t>(0, -static_cast<int32_t>(screenY));
|
||||
const int32_t endX = std::min<int32_t>(w, static_cast<int32_t>(width()) - screenX);
|
||||
const int32_t endY = std::min<int32_t>(h, static_cast<int32_t>(height()) - screenY);
|
||||
if (firstX >= endX || firstY >= endY)
|
||||
return;
|
||||
const uint16_t monoStride = imageBufferWidth;
|
||||
const uint16_t grayStride = (driver->width + 1U) / 2U;
|
||||
uint8_t *const mono = imageBuffer;
|
||||
uint8_t *const gray = grayBuffer;
|
||||
uint32_t intermediatePixels = grayPixelCount;
|
||||
static constexpr auto intermediateCount = [] {
|
||||
std::array<uint8_t, 256> counts{};
|
||||
for (unsigned pair = 0; pair < counts.size(); ++pair)
|
||||
counts[pair] = ((pair >> 4U) != 0U && (pair >> 4U) != 15U) + ((pair & 15U) != 0U && (pair & 15U) != 15U);
|
||||
return counts;
|
||||
}();
|
||||
#if INKHUD_BOOKREADER_PERF_LOG
|
||||
const uint32_t blitStartMs = millis();
|
||||
uint32_t inkHistogram[16] = {};
|
||||
uint32_t blitHash = 2166136261U;
|
||||
#endif
|
||||
const auto writePixel = [&](uint8_t *monoRow, uint8_t *grayRow, int16_t x, int32_t sx, int32_t sy, uint8_t ink) {
|
||||
#if INKHUD_BOOKREADER_PERF_LOG
|
||||
++inkHistogram[ink];
|
||||
blitHash = (blitHash ^ ink) * 16777619U;
|
||||
#endif
|
||||
if (grayRow) {
|
||||
uint8_t &byte = grayRow[x / 2U];
|
||||
const unsigned shift = (x & 1U) ? 0U : 4U;
|
||||
const uint8_t previous = (byte >> shift) & 15U;
|
||||
intermediatePixels -= previous != 0U && previous != 15U;
|
||||
intermediatePixels += ink != 0U && ink != 15U;
|
||||
byte = (byte & ~(15U << shift)) | ((15U - ink) << shift);
|
||||
} else {
|
||||
const bool black = ink * 32U > (bayer[((screenY + sy) & 3U) * 4U + ((screenX + sx) & 3U)] * 2U + 1U) * 15U;
|
||||
bitWrite(monoRow[x / 8U], 7U - (x & 7U), !black);
|
||||
}
|
||||
};
|
||||
const auto writeGraySpan = [&](uint8_t *row, int16_t x, int step, const uint8_t *source, int count, int sourceStride) {
|
||||
int i = 0;
|
||||
if ((x & 1U) != (step < 0)) {
|
||||
writePixel(nullptr, row, x, 0, 0, source[0]);
|
||||
x += step;
|
||||
++i;
|
||||
}
|
||||
for (; i + 1 < count; i += 2, x += 2 * step) {
|
||||
const uint8_t first = source[i * sourceStride];
|
||||
const uint8_t second = source[(i + 1) * sourceStride];
|
||||
#if INKHUD_BOOKREADER_PERF_LOG
|
||||
++inkHistogram[first];
|
||||
++inkHistogram[second];
|
||||
blitHash = ((blitHash ^ first) * 16777619U ^ second) * 16777619U;
|
||||
#endif
|
||||
uint8_t &byte = row[x / 2U];
|
||||
const uint8_t packedInk = step > 0 ? (first << 4U) | second : (second << 4U) | first;
|
||||
intermediatePixels -= intermediateCount[byte];
|
||||
intermediatePixels += intermediateCount[packedInk];
|
||||
byte = packedInk ^ 0xFFU;
|
||||
}
|
||||
if (i < count)
|
||||
writePixel(nullptr, row, x, 0, 0, source[i * sourceStride]);
|
||||
};
|
||||
// Transpose small SRAM tiles so rotated output does not thrash the PSRAM cache.
|
||||
constexpr int32_t tileSize = 16;
|
||||
uint8_t inks[tileSize][tileSize];
|
||||
const uint8_t rotation = settings->rotation;
|
||||
for (int32_t ty = firstY; ty < endY; ty += tileSize) {
|
||||
const int32_t tileH = std::min(tileSize, endY - ty);
|
||||
for (int32_t tx = firstX; tx < endX; tx += tileSize) {
|
||||
const int32_t tileW = std::min(tileSize, endX - tx);
|
||||
for (int32_t sy = 0; sy < tileH; ++sy) {
|
||||
const uint8_t *srcRow = bitmap + static_cast<size_t>(sourceY + ty + sy) * bitmapStride;
|
||||
uint16_t srcX = sourceX + tx;
|
||||
int32_t sx = 0;
|
||||
if (srcX & 1U) {
|
||||
inks[sy][sx++] = srcRow[srcX++ / 2U] & 15U;
|
||||
}
|
||||
for (; sx + 1 < tileW; sx += 2, srcX += 2) {
|
||||
const uint8_t pair = srcRow[srcX / 2U];
|
||||
inks[sy][sx] = pair >> 4U;
|
||||
inks[sy][sx + 1] = pair & 15U;
|
||||
}
|
||||
if (sx < tileW)
|
||||
inks[sy][sx] = srcRow[srcX / 2U] >> 4U;
|
||||
}
|
||||
if (rotation & 1U) {
|
||||
for (int32_t sx = 0; sx < tileW; ++sx) {
|
||||
int16_t x = screenX + tx + sx;
|
||||
int16_t y = screenY + ty;
|
||||
rotatePixelCoords(&x, &y);
|
||||
uint8_t *const monoRow = mono + static_cast<size_t>(y) * monoStride;
|
||||
uint8_t *const grayRow = gray ? gray + static_cast<size_t>(y) * grayStride : nullptr;
|
||||
if (grayRow) {
|
||||
writeGraySpan(grayRow, x, rotation == 1 ? -1 : 1, reinterpret_cast<const uint8_t *>(inks) + sx, tileH,
|
||||
tileSize);
|
||||
continue;
|
||||
}
|
||||
for (int32_t sy = 0; sy < tileH; ++sy) {
|
||||
writePixel(monoRow, grayRow, x, tx + sx, ty + sy, inks[sy][sx]);
|
||||
x += rotation == 1 ? -1 : 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (int32_t sy = 0; sy < tileH; ++sy) {
|
||||
int16_t x = screenX + tx;
|
||||
int16_t y = screenY + ty + sy;
|
||||
rotatePixelCoords(&x, &y);
|
||||
uint8_t *const monoRow = mono + static_cast<size_t>(y) * monoStride;
|
||||
uint8_t *const grayRow = gray ? gray + static_cast<size_t>(y) * grayStride : nullptr;
|
||||
if (grayRow) {
|
||||
writeGraySpan(grayRow, x, rotation == 0 ? 1 : -1, reinterpret_cast<const uint8_t *>(inks) + sy * tileSize,
|
||||
tileW, 1);
|
||||
continue;
|
||||
}
|
||||
for (int32_t sx = 0; sx < tileW; ++sx) {
|
||||
writePixel(monoRow, grayRow, x, tx + sx, ty + sy, inks[sy][sx]);
|
||||
x += rotation == 0 ? 1 : -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
grayPixelCount = intermediatePixels;
|
||||
if (gray)
|
||||
monoBufferNeedsSync = true;
|
||||
#if INKHUD_BOOKREADER_PERF_LOG
|
||||
LOG_INFO("[BookReaderDraw] blit xy=%d,%d size=%ux%u src=%u,%u "
|
||||
"clip=%ld,%ld-%ld,%ld rotation=%u gray=%u hash=%08lx ms=%lu",
|
||||
screenX, screenY, w, h, sourceX, sourceY, static_cast<long>(firstX), static_cast<long>(firstY),
|
||||
static_cast<long>(endX), static_cast<long>(endY), rotation, gray != nullptr, static_cast<unsigned long>(blitHash),
|
||||
static_cast<unsigned long>(millis() - blitStartMs));
|
||||
LOG_INFO("[BookReaderDraw] image ink bins "
|
||||
"white-to-black=%lu,%lu,%lu,%lu,%lu,%lu,%lu,%lu,%lu,%lu,%lu,%lu,%lu,"
|
||||
"%lu,%lu,%lu",
|
||||
inkHistogram[0], inkHistogram[1], inkHistogram[2], inkHistogram[3], inkHistogram[4], inkHistogram[5],
|
||||
inkHistogram[6], inkHistogram[7], inkHistogram[8], inkHistogram[9], inkHistogram[10], inkHistogram[11],
|
||||
inkHistogram[12], inkHistogram[13], inkHistogram[14], inkHistogram[15]);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
// Fast path for blitting MSB-first 1-bpp bitmaps into imageBuffer.
|
||||
void InkHUD::Renderer::writeBitmap(int16_t screenX, int16_t screenY, uint16_t w, uint16_t h, const uint8_t *bitmap,
|
||||
uint16_t bitmapStride, uint16_t sourceX, uint16_t sourceY)
|
||||
{
|
||||
if (!bitmap || w == 0 || h == 0) {
|
||||
return;
|
||||
}
|
||||
#if INKHUD_BOOKREADER_GRAYSCALE
|
||||
if (grayBuffer) {
|
||||
for (uint16_t y = 0; y < h; ++y)
|
||||
for (uint16_t x = 0; x < w; ++x) {
|
||||
const uint16_t sx = sourceX + x;
|
||||
const bool black = bitmap[(sourceY + y) * bitmapStride + sx / 8U] & (0x80U >> (sx & 7U));
|
||||
if (screenX + x >= 0 && screenY + y >= 0 && screenX + x < width() && screenY + y < height())
|
||||
handlePixel(screenX + x, screenY + y, black ? BLACK : WHITE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
switch (settings->rotation) {
|
||||
case 0: {
|
||||
// No rotation: iterate row-first for sequential imageBuffer access.
|
||||
for (uint16_t sy = 0; sy < h; sy++) {
|
||||
const int16_t bufY = static_cast<int16_t>(screenY + sy);
|
||||
if (bufY < 0 || static_cast<uint16_t>(bufY) >= imageBufferHeight) {
|
||||
continue;
|
||||
}
|
||||
const uint8_t *srcRow = bitmap + static_cast<size_t>(sourceY + sy) * bitmapStride;
|
||||
uint8_t *dstRow = imageBuffer + static_cast<size_t>(bufY) * imageBufferWidth;
|
||||
for (uint16_t sx = 0; sx < w; sx++) {
|
||||
const int16_t bufX = static_cast<int16_t>(screenX + sx);
|
||||
if (bufX < 0 || static_cast<uint16_t>(bufX) >= driver->width) {
|
||||
continue;
|
||||
}
|
||||
const uint16_t srcX = static_cast<uint16_t>(sourceX + sx);
|
||||
const bool black = (srcRow[srcX >> 3] & (static_cast<uint8_t>(0x80U) >> (srcX & 7U))) != 0U;
|
||||
const uint8_t bitMask = static_cast<uint8_t>(0x80U) >> (static_cast<uint16_t>(bufX) & 7U);
|
||||
if (black) {
|
||||
dstRow[static_cast<uint16_t>(bufX) >> 3] &= static_cast<uint8_t>(~bitMask);
|
||||
} else {
|
||||
dstRow[static_cast<uint16_t>(bufX) >> 3] |= bitMask;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
// 90 degrees CW: iterate column-first so each inner loop writes one
|
||||
// physical row.
|
||||
const uint16_t dW = driver->width;
|
||||
for (uint16_t sx = 0; sx < w; sx++) {
|
||||
const int16_t physRow = static_cast<int16_t>(screenX + sx);
|
||||
if (physRow < 0 || static_cast<uint16_t>(physRow) >= imageBufferHeight) {
|
||||
continue;
|
||||
}
|
||||
uint8_t *dstRow = imageBuffer + static_cast<size_t>(physRow) * imageBufferWidth;
|
||||
const uint16_t srcX = static_cast<uint16_t>(sourceX + sx);
|
||||
const uint8_t srcMask = static_cast<uint8_t>(0x80U) >> (srcX & 7U);
|
||||
const uint16_t srcByteOff = srcX >> 3;
|
||||
for (uint16_t sy = 0; sy < h; sy++) {
|
||||
const int32_t physColI =
|
||||
static_cast<int32_t>(dW - 1U) - (static_cast<int32_t>(screenY) + static_cast<int32_t>(sy));
|
||||
if (physColI < 0 || static_cast<uint32_t>(physColI) >= driver->width) {
|
||||
continue;
|
||||
}
|
||||
const uint16_t physCol = static_cast<uint16_t>(physColI);
|
||||
const bool black = (bitmap[static_cast<size_t>(sourceY + sy) * bitmapStride + srcByteOff] & srcMask) != 0U;
|
||||
const uint8_t bitMask = static_cast<uint8_t>(0x80U) >> (physCol & 7U);
|
||||
if (black) {
|
||||
dstRow[physCol >> 3] &= static_cast<uint8_t>(~bitMask);
|
||||
} else {
|
||||
dstRow[physCol >> 3] |= bitMask;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
// 180 degrees: iterate row-first for sequential physical row access.
|
||||
const uint32_t physicalWidth = driver->width;
|
||||
for (uint16_t sy = 0; sy < h; sy++) {
|
||||
const int32_t physRowI =
|
||||
static_cast<int32_t>(imageBufferHeight - 1U) - (static_cast<int32_t>(screenY) + static_cast<int32_t>(sy));
|
||||
if (physRowI < 0 || static_cast<uint32_t>(physRowI) >= static_cast<uint32_t>(imageBufferHeight)) {
|
||||
continue;
|
||||
}
|
||||
uint8_t *dstRow = imageBuffer + static_cast<size_t>(physRowI) * imageBufferWidth;
|
||||
const uint8_t *srcRow = bitmap + static_cast<size_t>(sourceY + sy) * bitmapStride;
|
||||
for (uint16_t sx = 0; sx < w; sx++) {
|
||||
const int32_t physColI =
|
||||
static_cast<int32_t>(physicalWidth - 1U) - (static_cast<int32_t>(screenX) + static_cast<int32_t>(sx));
|
||||
if (physColI < 0 || static_cast<uint32_t>(physColI) >= physicalWidth) {
|
||||
continue;
|
||||
}
|
||||
const uint16_t physCol = static_cast<uint16_t>(physColI);
|
||||
const uint16_t srcX = static_cast<uint16_t>(sourceX + sx);
|
||||
const bool black = (srcRow[srcX >> 3] & (static_cast<uint8_t>(0x80U) >> (srcX & 7U))) != 0U;
|
||||
const uint8_t bitMask = static_cast<uint8_t>(0x80U) >> (physCol & 7U);
|
||||
if (black) {
|
||||
dstRow[physCol >> 3] &= static_cast<uint8_t>(~bitMask);
|
||||
} else {
|
||||
dstRow[physCol >> 3] |= bitMask;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
// 270 degrees CW: iterate column-first so each inner loop writes one
|
||||
// physical row.
|
||||
for (uint16_t sx = 0; sx < w; sx++) {
|
||||
const int32_t physRowI =
|
||||
static_cast<int32_t>(imageBufferHeight - 1U) - (static_cast<int32_t>(screenX) + static_cast<int32_t>(sx));
|
||||
if (physRowI < 0 || static_cast<uint32_t>(physRowI) >= static_cast<uint32_t>(imageBufferHeight)) {
|
||||
continue;
|
||||
}
|
||||
const uint16_t physRow = static_cast<uint16_t>(physRowI);
|
||||
uint8_t *dstRow = imageBuffer + static_cast<size_t>(physRow) * imageBufferWidth;
|
||||
const uint16_t srcX = static_cast<uint16_t>(sourceX + sx);
|
||||
const uint8_t srcMask = static_cast<uint8_t>(0x80U) >> (srcX & 7U);
|
||||
const uint16_t srcByteOff = srcX >> 3;
|
||||
for (uint16_t sy = 0; sy < h;) {
|
||||
const int16_t physCol = static_cast<int16_t>(screenY + sy);
|
||||
if (physCol < 0 || static_cast<uint16_t>(physCol) >= driver->width) {
|
||||
++sy;
|
||||
continue;
|
||||
}
|
||||
// Transpose eight source rows into one destination byte without
|
||||
// changing any pixels.
|
||||
if ((physCol & 7) == 0 && static_cast<uint32_t>(sy) + 8U <= h &&
|
||||
static_cast<uint32_t>(physCol) + 8U <= driver->width) {
|
||||
uint8_t blackBits = 0;
|
||||
for (uint16_t row = 0; row < 8U; ++row) {
|
||||
if (bitmap[static_cast<size_t>(sourceY + sy + row) * bitmapStride + srcByteOff] & srcMask)
|
||||
blackBits |= static_cast<uint8_t>(0x80U >> row);
|
||||
}
|
||||
dstRow[static_cast<uint16_t>(physCol) >> 3] = static_cast<uint8_t>(~blackBits);
|
||||
sy += 8U;
|
||||
continue;
|
||||
}
|
||||
const bool black = (bitmap[static_cast<size_t>(sourceY + sy) * bitmapStride + srcByteOff] & srcMask) != 0U;
|
||||
const uint8_t bitMask = static_cast<uint8_t>(0x80U) >> (static_cast<uint16_t>(physCol) & 7U);
|
||||
if (black) {
|
||||
dstRow[physCol >> 3] &= static_cast<uint8_t>(~bitMask);
|
||||
} else {
|
||||
dstRow[physCol >> 3] |= bitMask;
|
||||
}
|
||||
++sy;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// Unexpected rotation: fall back to per-pixel handlePixel.
|
||||
for (uint16_t sy = 0; sy < h; sy++) {
|
||||
const uint8_t *srcRow = bitmap + static_cast<size_t>(sourceY + sy) * bitmapStride;
|
||||
for (uint16_t sx = 0; sx < w; sx++) {
|
||||
const uint16_t srcX = static_cast<uint16_t>(sourceX + sx);
|
||||
const bool black = (srcRow[srcX >> 3] & (static_cast<uint8_t>(0x80U) >> (srcX & 7U))) != 0U;
|
||||
handlePixel(static_cast<int16_t>(screenX + sx), static_cast<int16_t>(screenY + sy), black ? BLACK : WHITE);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
// Set a flag, which will be picked up by runOnce, ASAP.
|
||||
// Quite likely, multiple applets will all want to respond to one event (Observable, etc)
|
||||
// Each affected applet can independently call requestUpdate(), and all share the one opportunity to render, at next runOnce
|
||||
// Quite likely, multiple applets will all want to respond to one event
|
||||
// (Observable, etc) Each affected applet can independently call
|
||||
// requestUpdate(), and all share the one opportunity to render, at next runOnce
|
||||
void InkHUD::Renderer::requestUpdate(bool all)
|
||||
{
|
||||
requested = true;
|
||||
@@ -74,12 +440,12 @@ void InkHUD::Renderer::requestUpdate(bool all)
|
||||
runASAP = true;
|
||||
}
|
||||
|
||||
// requestUpdate will not actually update if no requests were made by applets which are actually visible
|
||||
// This can occur, because applets requestUpdate even from the background,
|
||||
// in case the user's autoshow settings permit them to be moved to foreground.
|
||||
// Sometimes, however, we will want to trigger a display update manually, in the absence of any sort of applet event
|
||||
// Display health, for example.
|
||||
// In these situations, we use forceUpdate
|
||||
// requestUpdate will not actually update if no requests were made by applets
|
||||
// which are actually visible This can occur, because applets requestUpdate even
|
||||
// from the background, in case the user's autoshow settings permit them to be
|
||||
// moved to foreground. Sometimes, however, we will want to trigger a display
|
||||
// update manually, in the absence of any sort of applet event Display health,
|
||||
// for example. In these situations, we use forceUpdate
|
||||
void InkHUD::Renderer::forceUpdate(Drivers::EInk::UpdateTypes type, bool all, bool async)
|
||||
{
|
||||
requested = true;
|
||||
@@ -87,17 +453,19 @@ void InkHUD::Renderer::forceUpdate(Drivers::EInk::UpdateTypes type, bool all, bo
|
||||
renderAll |= all;
|
||||
displayHealth.forceUpdateType(type);
|
||||
|
||||
// Normally, we need to start the timer, in case the display is busy and we briefly defer the update
|
||||
// Normally, we need to start the timer, in case the display is busy and we
|
||||
// briefly defer the update
|
||||
if (async) {
|
||||
// We will run the thread as soon as we loop(),
|
||||
// after all Applets have had a chance to observe whatever event set this off
|
||||
// after all Applets have had a chance to observe whatever event set this
|
||||
// off
|
||||
OSThread::setIntervalFromNow(0);
|
||||
OSThread::enabled = true;
|
||||
runASAP = true;
|
||||
}
|
||||
|
||||
// If the update is *not* asynchronous, we begin the render process directly here
|
||||
// so that it can block code flow while running
|
||||
// If the update is *not* asynchronous, we begin the render process directly
|
||||
// here so that it can block code flow while running
|
||||
else
|
||||
render(false);
|
||||
}
|
||||
@@ -112,15 +480,21 @@ void InkHUD::Renderer::awaitUpdate()
|
||||
}
|
||||
|
||||
// Set a ready-to-draw pixel into the image buffer
|
||||
// All rotations / translations have already taken place: this buffer data is formatted ready for the driver
|
||||
// All rotations / translations have already taken place: this buffer data is
|
||||
// formatted ready for the driver
|
||||
void InkHUD::Renderer::handlePixel(int16_t x, int16_t y, Color c)
|
||||
{
|
||||
rotatePixelCoords(&x, &y);
|
||||
|
||||
uint32_t byteNum = (y * imageBufferWidth) + (x / 8); // X data is 8 pixels per byte
|
||||
uint8_t bitNum = 7 - (x % 8); // Invert order: leftmost bit (most significant) is leftmost pixel of byte.
|
||||
uint8_t bitNum = 7 - (x % 8); // Invert order: leftmost bit (most significant)
|
||||
// is leftmost pixel of byte.
|
||||
|
||||
bitWrite(imageBuffer[byteNum], bitNum, c);
|
||||
#if INKHUD_BOOKREADER_GRAYSCALE
|
||||
if (grayBuffer)
|
||||
setGrayPixel(x, y, c == WHITE ? 15U : 0U);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Width of the display, relative to rotation
|
||||
@@ -142,10 +516,15 @@ uint16_t InkHUD::Renderer::height()
|
||||
}
|
||||
|
||||
// Runs at regular intervals
|
||||
// - postponing render: until next loop(), allowing all applets to be notified of some Mesh event before render
|
||||
// - postponing render: until next loop(), allowing all applets to be notified
|
||||
// of some Mesh event before render
|
||||
// - queuing another render: while one is already is progress
|
||||
int32_t InkHUD::Renderer::runOnce()
|
||||
{
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
maintainRenderActivity();
|
||||
#endif
|
||||
|
||||
// If an applet asked to render, and hardware is able, lets try now
|
||||
if (requested && !driver->busy()) {
|
||||
render();
|
||||
@@ -153,16 +532,38 @@ int32_t InkHUD::Renderer::runOnce()
|
||||
|
||||
// If our render() call failed, try again shortly
|
||||
// otherwise, stop our thread until next update due
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
if (requested || keepAwakeUntilUpdateComplete)
|
||||
#else
|
||||
if (requested)
|
||||
#endif
|
||||
return 250UL;
|
||||
else
|
||||
return OSThread::disable();
|
||||
}
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
void InkHUD::Renderer::maintainRenderActivity()
|
||||
{
|
||||
if (keepAwakeUntilUpdateComplete) {
|
||||
// Give the reader a full idle timeout after the physical refresh finishes.
|
||||
powerFSM.trigger(EVENT_INPUT);
|
||||
keepAwakeUntilUpdateComplete = driver->busy();
|
||||
BookReaderApplet::setDisplayRefreshActive(keepAwakeUntilUpdateComplete);
|
||||
#if INKHUD_BOOKREADER_PERF_LOG
|
||||
if (!keepAwakeUntilUpdateComplete)
|
||||
LOG_INFO("[BookReaderPerf] refresh END visible=1 ms=%lu timeoutRestarted=1",
|
||||
static_cast<unsigned long>(millis() - readerRefreshStartMs));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Applies the system-wide rotation to pixel positions
|
||||
// This step is applied to image data which has already been translated by a Tile object
|
||||
// This is the final step before the pixel is placed into the image buffer
|
||||
// No return: values of the *x and *y parameters are modified by the method
|
||||
// This step is applied to image data which has already been translated by a
|
||||
// Tile object This is the final step before the pixel is placed into the image
|
||||
// buffer No return: values of the *x and *y parameters are modified by the
|
||||
// method
|
||||
void InkHUD::Renderer::rotatePixelCoords(int16_t *x, int16_t *y)
|
||||
{
|
||||
// Apply a global rotation to pixel locations
|
||||
@@ -190,8 +591,8 @@ void InkHUD::Renderer::rotatePixelCoords(int16_t *x, int16_t *y)
|
||||
*y = y1;
|
||||
}
|
||||
|
||||
// Make an attempt to gather image data from some / all applets, and update the display
|
||||
// Might not be possible right now, if update already is progress.
|
||||
// Make an attempt to gather image data from some / all applets, and update the
|
||||
// display Might not be possible right now, if update already is progress.
|
||||
void InkHUD::Renderer::render(bool async)
|
||||
{
|
||||
// Make sure the display is ready for a new update
|
||||
@@ -204,16 +605,24 @@ void InkHUD::Renderer::render(bool async)
|
||||
driver->await();
|
||||
}
|
||||
|
||||
// Determine if a system applet has requested exclusive rights to request an update,
|
||||
// or exclusive rights to render
|
||||
// Determine if a system applet has requested exclusive rights to request an
|
||||
// update, or exclusive rights to render
|
||||
checkLocks();
|
||||
|
||||
// (Potentially) change applet to display new info,
|
||||
// then check if this newly displayed applet makes a pending notification redundant
|
||||
// then check if this newly displayed applet makes a pending notification
|
||||
// redundant
|
||||
inkhud->autoshow();
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
// Consume this request before drawing; an applet may queue its next loading
|
||||
// pass in onRender().
|
||||
requested = false;
|
||||
#endif
|
||||
|
||||
// If an update is justified.
|
||||
// We don't know this until after autoshow has run, as new applets may now be in foreground
|
||||
// We don't know this until after autoshow has run, as new applets may now be
|
||||
// in foreground
|
||||
if (shouldUpdate()) {
|
||||
|
||||
// Decide which technique the display will use to change image
|
||||
@@ -226,41 +635,140 @@ void InkHUD::Renderer::render(bool async)
|
||||
renderUserApplets();
|
||||
renderPlaceholders();
|
||||
renderSystemApplets();
|
||||
#if INKHUD_BOOKREADER_GRAYSCALE
|
||||
bool readerVisible = false;
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
if (!lockRendering)
|
||||
for (Applet *applet : inkhud->userApplets)
|
||||
if (applet && applet->isActive() && applet->isForeground() && applet->keepScreenAwakeDuringRender()) {
|
||||
readerVisible = true;
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
const bool useGrayscale = grayBuffer && (grayPixelCount || readerVisible);
|
||||
// Grayscale frames do not consume the monochrome buffer; rebuild it only on a monochrome transition.
|
||||
if (!useGrayscale && grayBuffer && monoBufferNeedsSync) {
|
||||
[[maybe_unused]] const uint32_t syncStartMs = millis();
|
||||
const size_t grayStride = (driver->width + 1U) / 2U;
|
||||
for (uint16_t y = 0; y < driver->height; ++y) {
|
||||
const uint8_t *row = grayBuffer + static_cast<size_t>(y) * grayStride;
|
||||
uint8_t *out = imageBuffer + static_cast<size_t>(y) * imageBufferWidth;
|
||||
for (uint16_t bx = 0; bx < imageBufferWidth; ++bx) {
|
||||
uint8_t white = 0xFFU;
|
||||
for (uint16_t bit = 0; bit < 8U && bx * 8U + bit < driver->width; ++bit) {
|
||||
const uint16_t x = bx * 8U + bit;
|
||||
if (((row[x / 2U] >> ((x & 1U) ? 0U : 4U)) & 15U) == 0U)
|
||||
white &= ~(0x80U >> bit);
|
||||
}
|
||||
out[bx] = white;
|
||||
}
|
||||
}
|
||||
monoBufferNeedsSync = false;
|
||||
#if INKHUD_BOOKREADER_PERF_LOG
|
||||
LOG_INFO("[BookReaderPerf] monochrome buffer restored ms=%lu", static_cast<unsigned long>(millis() - syncStartMs));
|
||||
#endif
|
||||
}
|
||||
if (useGrayscale && updateType != Drivers::EInk::UpdateTypes::FULL) {
|
||||
displayHealth.forceUpdateType(Drivers::EInk::UpdateTypes::FULL);
|
||||
updateType = displayHealth.decideUpdateType();
|
||||
}
|
||||
#endif
|
||||
|
||||
// Invert Buffer if set by user
|
||||
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) {
|
||||
for (size_t i = 0; i < imageBufferWidth * imageBufferHeight; ++i) {
|
||||
imageBuffer[i] = ~imageBuffer[i];
|
||||
}
|
||||
#if INKHUD_BOOKREADER_GRAYSCALE
|
||||
if (useGrayscale)
|
||||
for (size_t i = 0; i < ((driver->width + 1U) / 2U) * driver->height; ++i)
|
||||
grayBuffer[i] = ~grayBuffer[i];
|
||||
#endif
|
||||
}
|
||||
|
||||
// Tell display to begin process of drawing new image
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
if (keepAwakeUntilUpdateComplete)
|
||||
BookReaderApplet::setDisplayRefreshActive(true);
|
||||
#endif
|
||||
LOG_INFO("Updating display");
|
||||
driver->update(imageBuffer, updateType);
|
||||
#if INKHUD_BOOKREADER_GRAYSCALE && INKHUD_BOOKREADER_PERF_LOG
|
||||
static uint32_t frameId = 0;
|
||||
LOG_INFO("[BookReaderDraw] submit frame=%lu type=%u gray=%u grayPixels=%lu "
|
||||
"reader=%u renderAll=%u locked=%u inverted=%u",
|
||||
static_cast<unsigned long>(++frameId), static_cast<unsigned>(updateType), useGrayscale,
|
||||
static_cast<unsigned long>(grayPixelCount), readerVisible, renderAll, lockRendering,
|
||||
config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED);
|
||||
#endif
|
||||
#if defined(MESHTASTIC_INCLUDE_INKHUD_BOOKREADER) && INKHUD_BOOKREADER_PERF_LOG
|
||||
if (keepAwakeUntilUpdateComplete) {
|
||||
readerRefreshStartMs = millis();
|
||||
LOG_INFO("[BookReaderPerf] refresh BEGIN type=%u async=%u", static_cast<unsigned>(updateType),
|
||||
static_cast<unsigned>(async));
|
||||
}
|
||||
#endif
|
||||
#if INKHUD_BOOKREADER_GRAYSCALE
|
||||
if (useGrayscale)
|
||||
driver->updateGrayscale(grayBuffer);
|
||||
else
|
||||
#endif
|
||||
driver->update(imageBuffer, updateType);
|
||||
|
||||
#if INKHUD_BOOKREADER_GRAYSCALE
|
||||
// Retained overlay renders must start from the original, uninverted pixels.
|
||||
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) {
|
||||
driver->await();
|
||||
for (size_t i = 0; i < imageBufferWidth * imageBufferHeight; ++i)
|
||||
imageBuffer[i] = ~imageBuffer[i];
|
||||
if (useGrayscale)
|
||||
for (size_t i = 0; i < ((driver->width + 1U) / 2U) * driver->height; ++i)
|
||||
grayBuffer[i] = ~grayBuffer[i];
|
||||
}
|
||||
#endif
|
||||
|
||||
// If not async, wait here until the update is complete
|
||||
if (!async)
|
||||
driver->await();
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
maintainRenderActivity();
|
||||
if (keepAwakeUntilUpdateComplete) {
|
||||
OSThread::enabled = true;
|
||||
OSThread::setIntervalFromNow(250UL);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Our part is done now.
|
||||
// If update is async, the display hardware is still performing the update process,
|
||||
// but that's all handled by NicheGraphics::Drivers::EInk
|
||||
// If update is async, the display hardware is still performing the update
|
||||
// process, but that's all handled by NicheGraphics::Drivers::EInk
|
||||
|
||||
// Tidy up, ready for a new request
|
||||
#ifndef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
requested = false;
|
||||
#endif
|
||||
forced = false;
|
||||
renderAll = false;
|
||||
}
|
||||
|
||||
// Manually fill the image buffer with WHITE
|
||||
// Clears any old drawing
|
||||
// Note: benchmarking revealed that this is *much* faster than setting pixels individually
|
||||
// So much so that it's more efficient to re-render all applets,
|
||||
// rather than rendering selectively, and manually blanking a portion of the display
|
||||
// Note: benchmarking revealed that this is *much* faster than setting pixels
|
||||
// individually So much so that it's more efficient to re-render all applets,
|
||||
// rather than rendering selectively, and manually blanking a portion of the
|
||||
// display
|
||||
void InkHUD::Renderer::clearBuffer()
|
||||
{
|
||||
#if INKHUD_BOOKREADER_GRAYSCALE && INKHUD_BOOKREADER_PERF_LOG
|
||||
LOG_INFO("[BookReaderDraw] clear framebuffer gray=%u", grayBuffer != nullptr);
|
||||
#endif
|
||||
memset(imageBuffer, 0xFF, imageBufferHeight * imageBufferWidth);
|
||||
#if INKHUD_BOOKREADER_GRAYSCALE
|
||||
if (grayBuffer)
|
||||
memset(grayBuffer, 0xFF, ((driver->width + 1U) / 2U) * driver->height);
|
||||
grayPixelCount = 0;
|
||||
monoBufferNeedsSync = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Manually clear the pixels below a tile
|
||||
@@ -309,6 +817,39 @@ void InkHUD::Renderer::clearTile(Tile *t)
|
||||
xEnd = driver->width;
|
||||
if (yEnd > driver->height)
|
||||
yEnd = driver->height;
|
||||
#if INKHUD_BOOKREADER_GRAYSCALE && INKHUD_BOOKREADER_PERF_LOG
|
||||
LOG_INFO("[BookReaderDraw] clear tile xy=%u,%u-%u,%u gray=%u", xStart, yStart, xEnd, yEnd, grayBuffer != nullptr);
|
||||
#endif
|
||||
#if INKHUD_BOOKREADER_GRAYSCALE
|
||||
if (grayBuffer) {
|
||||
if (xStart == 0 && yStart == 0 && xEnd == driver->width && yEnd == driver->height) {
|
||||
memset(grayBuffer, 0xFF, ((driver->width + 1U) / 2U) * driver->height);
|
||||
grayPixelCount = 0;
|
||||
} else {
|
||||
const size_t stride = (driver->width + 1U) / 2U;
|
||||
const size_t firstByte = (xStart + 1U) / 2U;
|
||||
const size_t endByte = xEnd / 2U;
|
||||
uint32_t clearedGrayPixels = 0;
|
||||
for (uint16_t y = yStart; y < yEnd; ++y) {
|
||||
if ((xStart & 1U) && xEnd > xStart)
|
||||
setGrayPixel(xStart, y, 15U);
|
||||
if ((xEnd & 1U) && xEnd > xStart)
|
||||
setGrayPixel(xEnd - 1U, y, 15U);
|
||||
uint8_t *row = grayBuffer + static_cast<size_t>(y) * stride;
|
||||
for (size_t x = firstByte; x < endByte; ++x) {
|
||||
const uint8_t pair = row[x];
|
||||
const uint8_t high = pair >> 4U;
|
||||
const uint8_t low = pair & 15U;
|
||||
clearedGrayPixels += high != 0U && high != 15U;
|
||||
clearedGrayPixels += low != 0U && low != 15U;
|
||||
}
|
||||
if (endByte > firstByte)
|
||||
memset(row + firstByte, 0xFF, endByte - firstByte);
|
||||
}
|
||||
grayPixelCount -= clearedGrayPixels;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Clear the pixels
|
||||
if (xStart == 0 && xEnd == driver->width) { // full width box is easier to clear
|
||||
@@ -385,9 +926,10 @@ bool InkHUD::Renderer::shouldUpdate()
|
||||
return should;
|
||||
}
|
||||
|
||||
// Determine which type of E-Ink update the display will perform, to change the image.
|
||||
// Considers the needs of the various applets, then weighs against display health.
|
||||
// An update type specified by forceUpdate will be granted with no further questioning.
|
||||
// Determine which type of E-Ink update the display will perform, to change the
|
||||
// image. Considers the needs of the various applets, then weighs against
|
||||
// display health. An update type specified by forceUpdate will be granted with
|
||||
// no further questioning.
|
||||
Drivers::EInk::UpdateTypes InkHUD::Renderer::decideUpdateType()
|
||||
{
|
||||
// Ask applets which update type they would prefer
|
||||
@@ -411,10 +953,12 @@ Drivers::EInk::UpdateTypes InkHUD::Renderer::decideUpdateType()
|
||||
}
|
||||
|
||||
// Run the drawing operations of any user applets which are currently displayed
|
||||
// Pixel output is placed into the framebuffer, ready for handoff to the EInk driver
|
||||
// Pixel output is placed into the framebuffer, ready for handoff to the EInk
|
||||
// driver
|
||||
void InkHUD::Renderer::renderUserApplets()
|
||||
{
|
||||
// Don't render user applets if a system applet has demanded the whole display to itself
|
||||
// Don't render user applets if a system applet has demanded the whole display
|
||||
// to itself
|
||||
if (lockRendering)
|
||||
return;
|
||||
|
||||
@@ -429,6 +973,12 @@ void InkHUD::Renderer::renderUserApplets()
|
||||
|
||||
uint32_t start = millis();
|
||||
bool full = ua->wantsFullRender() || renderAll;
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
if (ua->keepScreenAwakeDuringRender()) {
|
||||
keepAwakeUntilUpdateComplete = true;
|
||||
powerFSM.trigger(EVENT_INPUT);
|
||||
}
|
||||
#endif
|
||||
ua->render(full); // Draw!
|
||||
uint32_t stop = millis();
|
||||
LOG_DEBUG("%s took %dms to render", ua->name, stop - start);
|
||||
@@ -436,8 +986,9 @@ void InkHUD::Renderer::renderUserApplets()
|
||||
}
|
||||
}
|
||||
|
||||
// Run the drawing operations of any system applets which are currently displayed
|
||||
// Pixel output is placed into the framebuffer, ready for handoff to the EInk driver
|
||||
// Run the drawing operations of any system applets which are currently
|
||||
// displayed Pixel output is placed into the framebuffer, ready for handoff to
|
||||
// the EInk driver
|
||||
void InkHUD::Renderer::renderSystemApplets()
|
||||
{
|
||||
SystemApplet *battery = inkhud->getSystemApplet("BatteryIcon");
|
||||
@@ -473,8 +1024,8 @@ void InkHUD::Renderer::renderSystemApplets()
|
||||
// uint32_t start = millis();
|
||||
bool full = sa->wantsFullRender() || renderAll;
|
||||
sa->render(full); // Draw!
|
||||
// uint32_t stop = millis();
|
||||
// LOG_DEBUG("%s took %dms to render", sa->name, stop - start);
|
||||
// uint32_t stop = millis();
|
||||
// LOG_DEBUG("%s took %dms to render", sa->name, stop - start);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,7 +1034,8 @@ void InkHUD::Renderer::renderSystemApplets()
|
||||
// In this case, we will fill the empty space with diagonal lines.
|
||||
void InkHUD::Renderer::renderPlaceholders()
|
||||
{
|
||||
// Don't fill empty space with placeholders if a system applet wants exclusive use of the display
|
||||
// Don't fill empty space with placeholders if a system applet wants exclusive
|
||||
// use of the display
|
||||
if (lockRendering)
|
||||
return;
|
||||
|
||||
@@ -509,4 +1061,4 @@ void InkHUD::Renderer::renderPlaceholders()
|
||||
// LOG_DEBUG("Placeholders took %dms to render", stop - start);
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
@@ -47,6 +47,16 @@ class Renderer : protected concurrency::OSThread
|
||||
// Receives pixel output from an applet (via a tile, which translates the coordinates)
|
||||
void handlePixel(int16_t x, int16_t y, Color c);
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
// Fast path for writing MSB-first 1-bpp bitmaps in the same coordinate space as handlePixel().
|
||||
void writeBitmap(int16_t screenX, int16_t screenY, uint16_t w, uint16_t h, const uint8_t *bitmap, uint16_t bitmapStride,
|
||||
uint16_t sourceX = 0, uint16_t sourceY = 0);
|
||||
#if INKHUD_BOOKREADER_GRAYSCALE
|
||||
void writeGrayBitmap(int16_t screenX, int16_t screenY, uint16_t w, uint16_t h, const uint8_t *bitmap, uint16_t bitmapStride,
|
||||
uint16_t sourceX = 0, uint16_t sourceY = 0);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// Size of display, in context of current rotation
|
||||
|
||||
uint16_t width();
|
||||
@@ -61,6 +71,9 @@ class Renderer : protected concurrency::OSThread
|
||||
|
||||
// Execute the render process now, then hand off to driver for display update
|
||||
void render(bool async = true);
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
void maintainRenderActivity();
|
||||
#endif
|
||||
|
||||
// Steps of the rendering process
|
||||
|
||||
@@ -80,6 +93,12 @@ class Renderer : protected concurrency::OSThread
|
||||
uint16_t imageBufferHeight = 0;
|
||||
uint16_t imageBufferWidth = 0;
|
||||
uint32_t imageBufferSize = 0; // Bytes
|
||||
#if INKHUD_BOOKREADER_GRAYSCALE
|
||||
uint8_t *grayBuffer = nullptr;
|
||||
uint32_t grayPixelCount = 0;
|
||||
bool monoBufferNeedsSync = false;
|
||||
void setGrayPixel(uint16_t x, uint16_t y, uint8_t luminance);
|
||||
#endif
|
||||
|
||||
SystemApplet *lockRendering = nullptr; // Render this applet *only*
|
||||
SystemApplet *lockRequests = nullptr; // Honor update requests from this applet *only*
|
||||
@@ -87,6 +106,9 @@ class Renderer : protected concurrency::OSThread
|
||||
bool requested = false;
|
||||
bool forced = false;
|
||||
bool renderAll = false;
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
bool keepAwakeUntilUpdateComplete = false;
|
||||
#endif
|
||||
|
||||
// For convenience
|
||||
InkHUD *inkhud = nullptr;
|
||||
|
||||
@@ -20,6 +20,7 @@ A haphazard collection of notes which _might_ be helpful for developers.
|
||||
- [NicheGraphics Framework](#nichegraphics-framework)
|
||||
- [NicheGraphics E-Ink Drivers](#nichegraphics-e-ink-drivers)
|
||||
- [InkHUD Applets](#inkhud-applets)
|
||||
- [Book Reader Applet](#book-reader-applet)
|
||||
- [Adding a Variant](#adding-a-variant)
|
||||
- [platformio.ini](#platformioini)
|
||||
- [nicheGraphics.h](#nichegraphicsh)
|
||||
@@ -250,6 +251,101 @@ In a variant's `nicheGraphics.h`:
|
||||
|
||||
You will need to add these lines to any variants which will use your applet.
|
||||
|
||||
#### Book Reader Applet
|
||||
|
||||
`BookReaderApplet` provides an SD-card library, EPUB reader, contents picker, image viewer, and reader menu. Its implementation is in `src/graphics/niche/InkHUD/Applets/User/BookReader`.
|
||||
|
||||
##### Books and Layout
|
||||
|
||||
Place normal EPUB files in `/books` on the SD card. Subfolders are browsable. The library accepts `.epub`, `.kepub`, and `.epu` filenames containing EPUB archives; it does not open generic ZIP files or treat extracted EPUB folders as books. Standalone text, HTML, RTF, PDF, and comic archive files are not library entries.
|
||||
|
||||
The library lists folders before books and sorts names case-insensitively. Book rows wrap long titles and show the format and file size.
|
||||
|
||||
The reader uses the EPUB package's spine for reading order and its NCX or navigation document for contents entries. Contents links can target anchors within a chapter. Text is reflowed for the current viewport rather than preserving the publisher's page boundaries.
|
||||
|
||||
Supported content includes headings, paragraphs, bold and italic text, lists, basic tables, and JPEG/PNG images. A subset of inline and linked CSS controls alignment, indentation, spacing, and visibility. This is not a browser layout engine; arbitrary CSS, scripting, embedded fonts, and encrypted books are not supported.
|
||||
|
||||
Mixed text/image pages keep images together when paginating. An image that cannot fit in the remaining space moves to the following page; large images are fitted to the available image area. Image-only EPUB parts use a fitted image view with page and zoom controls. Consecutive images in an image-only part can form a combined image sequence.
|
||||
|
||||
##### Touch Controls
|
||||
|
||||
Touch-friendly layout is selected when the variant registers a touch-enabled provider with InkHUD.
|
||||
|
||||
| View | Control | Action |
|
||||
| ---- | ------- | ------ |
|
||||
| Library | Tap a book or folder | Open the entry |
|
||||
| Library | Back | Go to the parent folder |
|
||||
| Library / contents | Swipe up or down | Move through the list by a screenful |
|
||||
| Contents | Tap an entry | Open its chapter or anchor |
|
||||
| Contents | Back | Return to the reader |
|
||||
| Reader | Menu | Open the reader menu |
|
||||
| Reader | Contents | Open the book's contents picker, when available |
|
||||
| Reader at 100% | Tap the left / right third | Previous / next page; tapping an inline image opens that image instead |
|
||||
| Reader at 100% | Swipe left or up | Next page |
|
||||
| Reader at 100% | Swipe right or down | Previous page |
|
||||
| Mixed page | Tap an inline image | Expand it into the image viewer |
|
||||
| Expanded image | Back | Close the image viewer and return to the text page |
|
||||
| Image toolbar | `<` / `>` | Previous / next page |
|
||||
| Image toolbar | `-` / `+` | Step down / up through zoom levels |
|
||||
| Image toolbar | Percentage | Reset zoom to 100% and recenter |
|
||||
| Image above 100% | Swipe | Pan the image; use toolbar arrows to change pages |
|
||||
| Reader menu | Tap an item | Perform the selected action |
|
||||
| Reader menu | Swipe | Move the selected menu item |
|
||||
|
||||
Image zoom levels are **100%, 120%, 150%, 175%, 260%, and 360%**. Here, 100% means fit-to-view, not one source pixel per display pixel. Panning is clamped to the image bounds. Pinch-to-zoom is not implemented.
|
||||
|
||||
The reader consumes touch events, including border taps, so they do not fall through to physical-button applet switching. Hardware button mappings remain variant-specific. On the T5 S3 Pro, the side key's short press toggles touch input, and holding it toggles the backlight; these are board controls, not reader gestures.
|
||||
|
||||
For non-touch layouts, navigation up/down moves the library or contents selection and turns reader pages. Right opens the selected entry; left returns to the parent folder or closes contents. In the reader, left/right turn pages. A short or long user-button press opens the selected library/contents entry or advances the reader page. In the reader menu, a short press cycles the selection; right or long press activates it, and left closes the menu.
|
||||
|
||||
##### Menu and Persistence
|
||||
|
||||
The reader menu provides Resume, Library, Contents, Bookmark this page, and Exit to applets. Go to bookmark and Remove bookmark appear when a bookmark exists. There is one explicit bookmark per book, separate from the saved reading position.
|
||||
|
||||
Reading positions and bookmarks are saved through InkHUD persistence using a per-book key. Reopening a book restores its saved part and wrapped-line position. The page-count cache is separate and is validated against the book and layout. Total page count can display `?` while background pagination is incomplete. Saved locations use wrapped lines, so changing the layout can affect the restored passage.
|
||||
|
||||
##### Variant Setup
|
||||
|
||||
The variant must provide a working SD filesystem and include both `MESHTASTIC_INCLUDE_INKHUD` and `MESHTASTIC_INCLUDE_INKHUD_BOOKREADER`. The reader's source and header are guarded by these feature definitions; an additional reader-specific source-filter exclusion/reinclusion is unnecessary.
|
||||
|
||||
Include and register the applet in the variant's `nicheGraphics.h`:
|
||||
|
||||
```cpp
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
#include "graphics/niche/InkHUD/Applets/User/BookReader/BookReaderApplet.h"
|
||||
#endif
|
||||
|
||||
// Inside setupNicheGraphics(), after creating the InkHUD instance:
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
inkhud->addApplet("Book Reader", new InkHUD::BookReaderApplet, true, false);
|
||||
#endif
|
||||
```
|
||||
|
||||
| Build definition | Purpose |
|
||||
| ---------------- | ------- |
|
||||
| `INKHUD_BOOKREADER_ENABLE_INLINE_IMAGES=1` | Enable JPEG/PNG image rendering; provide the required decoder dependencies |
|
||||
| `INKHUD_BOOKREADER_USE_JPEGDEC=1` | Use JPEGDEC instead of the ESP32 ROM JPEG decoder |
|
||||
| `INKHUD_BOOKREADER_GRAYSCALE=1` | Enable the grayscale image path; requires compatible renderer and display-driver support |
|
||||
| `INKHUD_BOOKREADER_PERF_LOG=1` | Enable detailed reader loading, parsing, caching, and rendering logs |
|
||||
|
||||
`variants/esp32s3/t5s3_epaper/platformio.ini` contains the current image-enabled configuration and JPEGDEC/PNGdec dependencies. Grayscale support is a driver capability, not a guarantee that every InkHUD display supports the same shades or refresh behavior.
|
||||
|
||||
Resource limits have overridable defaults in `BookReaderApplet.cpp`. Set overrides in the relevant variant, not as universal device limits. The `INKHUD_BOOKREADER_MAX_*` definitions bound text, metadata, archive directories, and individual archive/image entries; `INKHUD_BOOKREADER_CSS_MAX_*` bounds stylesheet work; `INKHUD_BOOKREADER_PREFETCH_MAX_*` bounds speculative text loading. Image sizing, JPEG scaling, and initial partial chapter reads have their own overrides. Oversized or unsupported content may be truncated or unavailable.
|
||||
|
||||
##### Loading and Implementation
|
||||
|
||||
Opening a book or an uncached chapter/page uses a centered book icon and loading label. Chapter reads, wrapping, pagination, and image prefetch are deferred where possible so the reader can show content before preparing the entire book. A cached transition need not show a loading indicator. E-paper refresh time is separate from CPU rendering time and depends on the display driver.
|
||||
|
||||
| Component | Responsibility |
|
||||
| --------- | -------------- |
|
||||
| `BookReaderApplet.cpp` / `.h` | Library, EPUB access, parsing, views, input, persistence, and background work |
|
||||
| `ReaderText.h` | Shared text markers, quote-aware tag scanning, and XML attribute decoding |
|
||||
| `ReaderPagination.h` | Page boundaries and page-count helpers |
|
||||
| `ReaderControls.h` | Shared image-toolbar dimensions and hit testing |
|
||||
| `SourceBitmap.h` | Image rasterization, scaling, and grayscale/monochrome bitmap helpers |
|
||||
|
||||
Decoded-source and rendered-image caches reduce repeat decoding during page changes, zooming, and panning. Background prefetch protects sources referenced by the current page and yields to interactive/display work. Cache residency and resource limits affect timing; compare the same pages and distinguish first-load from cached operations when profiling.
|
||||
|
||||
#### Applet Bases
|
||||
|
||||
If you need to create several similar applets, it might make sense to create a reusable base class. Several of these already exist in `src/graphics/niche/InkHUD/Applets/Bases`, but use these with caution, as they may be modified in future.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "TouchScreenBase.h"
|
||||
#include "main.h"
|
||||
#include "mesh/Throttle.h"
|
||||
#include <algorithm>
|
||||
|
||||
#if defined(RAK14014) && !defined(MESHTASTIC_EXCLUDE_CANNEDMESSAGES)
|
||||
#include "modules/CannedMessageModule.h"
|
||||
@@ -84,9 +85,21 @@ int32_t TouchScreenBase::runOnce()
|
||||
this->setInterval(TOUCH_POLL_INTERVAL_IDLE);
|
||||
const bool fastTapMode = fastTapModeEnabled();
|
||||
const bool allowLongPress = longPressEnabled();
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
const bool swipeFriendlyMode = swipeFriendlyModeEnabled();
|
||||
const uint16_t thresholdX = swipeFriendlyMode ? std::min<uint16_t>(20, TOUCH_THRESHOLD_X) : TOUCH_THRESHOLD_X;
|
||||
const uint16_t thresholdY = swipeFriendlyMode ? std::min<uint16_t>(20, TOUCH_THRESHOLD_Y) : TOUCH_THRESHOLD_Y;
|
||||
#else
|
||||
constexpr bool swipeFriendlyMode = false;
|
||||
constexpr uint16_t thresholdX = TOUCH_THRESHOLD_X, thresholdY = TOUCH_THRESHOLD_Y;
|
||||
#endif
|
||||
|
||||
// process touch events
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
int16_t x = _last_x, y = _last_y;
|
||||
#else
|
||||
int16_t x, y;
|
||||
#endif
|
||||
bool touched = getTouch(x, y);
|
||||
if (x < 0 || y < 0) // T-deck can emit phantom touch events with a negative value when turning off the screen
|
||||
touched = false;
|
||||
@@ -95,6 +108,16 @@ int32_t TouchScreenBase::runOnce()
|
||||
this->setInterval(fastTapMode ? TOUCH_POLL_INTERVAL_ACTIVE_FAST : TOUCH_POLL_INTERVAL_ACTIVE);
|
||||
_last_x = x;
|
||||
_last_y = y;
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
if (_touchedOld && swipeFriendlyMode) {
|
||||
const int32_t dx = x - _first_x, dy = y - _first_y;
|
||||
const int32_t peakDx = _peak_x - _first_x, peakDy = _peak_y - _first_y;
|
||||
if (dx * dx + dy * dy > peakDx * peakDx + peakDy * peakDy) {
|
||||
_peak_x = x;
|
||||
_peak_y = y;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
} else if (_touchedOld && ((uint32_t)millis() - _lastTouchSeenMs) < TOUCH_RELEASE_GRACE_MS) {
|
||||
// Treat brief no-touch samples as continuous touch to preserve long-press detection.
|
||||
touched = true;
|
||||
@@ -107,6 +130,10 @@ int32_t TouchScreenBase::runOnce()
|
||||
_longPressSuppressed = false;
|
||||
_first_x = x;
|
||||
_first_y = y;
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
_peak_x = x;
|
||||
_peak_y = y;
|
||||
#endif
|
||||
} else {
|
||||
_state = TOUCH_EVENT_CLEARED;
|
||||
uint32_t duration = nowMs - _pressStartMs;
|
||||
@@ -117,11 +144,17 @@ int32_t TouchScreenBase::runOnce()
|
||||
// compute distance
|
||||
int16_t dx = x - _first_x;
|
||||
int16_t dy = y - _first_y;
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
if (swipeFriendlyMode) {
|
||||
dx = _peak_x - _first_x;
|
||||
dy = _peak_y - _first_y;
|
||||
}
|
||||
#endif
|
||||
uint16_t adx = abs(dx);
|
||||
uint16_t ady = abs(dy);
|
||||
|
||||
// swipe horizontal
|
||||
if (adx > ady && adx > TOUCH_THRESHOLD_X) {
|
||||
if (adx > ady && adx > thresholdX) {
|
||||
if (0 > dx) { // swipe right to left
|
||||
e.touchEvent = static_cast<char>(TOUCH_ACTION_LEFT);
|
||||
LOG_DEBUG("action SWIPE: right to left");
|
||||
@@ -131,7 +164,7 @@ int32_t TouchScreenBase::runOnce()
|
||||
}
|
||||
}
|
||||
// swipe vertical
|
||||
else if (ady > adx && ady > TOUCH_THRESHOLD_Y) {
|
||||
else if ((ady > adx || (swipeFriendlyMode && ady == adx)) && ady > thresholdY) {
|
||||
if (0 > dy) { // swipe bottom to top
|
||||
e.touchEvent = static_cast<char>(TOUCH_ACTION_UP);
|
||||
LOG_DEBUG("action SWIPE: bottom to top");
|
||||
@@ -221,3 +254,10 @@ bool TouchScreenBase::longPressEnabled() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
bool TouchScreenBase::swipeFriendlyModeEnabled() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
@@ -17,6 +17,9 @@ class TouchScreenBase : public Observable<const InputEvent *>, public concurrenc
|
||||
public:
|
||||
explicit TouchScreenBase(const char *name, uint16_t width, uint16_t height);
|
||||
void init(bool hasTouch);
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
bool isTouchActive() const { return _touchedOld; }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
enum TouchScreenBaseStateType { TOUCH_EVENT_OCCURRED, TOUCH_EVENT_CLEARED };
|
||||
@@ -37,6 +40,9 @@ class TouchScreenBase : public Observable<const InputEvent *>, public concurrenc
|
||||
virtual void onEvent(const TouchEvent &event) = 0;
|
||||
virtual bool fastTapModeEnabled() const;
|
||||
virtual bool longPressEnabled() const;
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
virtual bool swipeFriendlyModeEnabled() const;
|
||||
#endif
|
||||
|
||||
volatile TouchScreenBaseStateType _state = TOUCH_EVENT_CLEARED;
|
||||
volatile TouchScreenBaseEventType _action = TOUCH_ACTION_NONE;
|
||||
@@ -50,7 +56,10 @@ class TouchScreenBase : public Observable<const InputEvent *>, public concurrenc
|
||||
bool _touchedOld = false; // previous touch state
|
||||
int16_t _first_x, _last_x; // horizontal swipe direction
|
||||
int16_t _first_y, _last_y; // vertical swipe direction
|
||||
uint32_t _pressStartMs; // when the current touch began; read via Throttle::hasElapsed()
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
int16_t _peak_x = 0, _peak_y = 0;
|
||||
#endif
|
||||
uint32_t _pressStartMs; // when the current touch began; read via Throttle::hasElapsed()
|
||||
|
||||
// LONG_PRESS repeat suppression while one touch is held: the bool is the armed flag, the
|
||||
// deadline is read only while it is set. No value of the deadline can mean "unarmed".
|
||||
|
||||
@@ -129,9 +129,30 @@ bool TouchScreenImpl1::fastTapModeEnabled() const
|
||||
|
||||
bool TouchScreenImpl1::longPressEnabled() const
|
||||
{
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
return !fastTapModeEnabled() && !swipeFriendlyModeEnabled();
|
||||
#else
|
||||
return !fastTapModeEnabled();
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
bool TouchScreenImpl1::swipeFriendlyModeEnabled() const
|
||||
{
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
auto *inkhud = NicheGraphics::InkHUD::InkHUD::getInstance();
|
||||
if (inkhud) {
|
||||
for (auto *sa : inkhud->systemApplets)
|
||||
if (sa && sa->handleInput)
|
||||
return false;
|
||||
auto *applet = inkhud->getActiveApplet();
|
||||
return applet && applet->isForeground() && applet->name && strcmp(applet->name, "Book Reader") == 0;
|
||||
}
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief forward touchscreen event
|
||||
*
|
||||
|
||||
@@ -12,6 +12,9 @@ class TouchScreenImpl1 : public TouchScreenBase
|
||||
virtual void onEvent(const TouchEvent &event);
|
||||
bool fastTapModeEnabled() const override;
|
||||
bool longPressEnabled() const override;
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
bool swipeFriendlyModeEnabled() const override;
|
||||
#endif
|
||||
|
||||
// Attach/detach a hardware interrupt on the touch IRQ pin (SCREEN_TOUCH_INT) so a new touch
|
||||
// wakes the polling thread immediately. No-op on boards without a usable touch interrupt line.
|
||||
|
||||
@@ -13,9 +13,11 @@
|
||||
#include "mesh/Throttle.h"
|
||||
#include "sleep.h"
|
||||
#include "touch/TouchDrvGT911.hpp"
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
#include "graphics/niche/InkHUD/Applet.h"
|
||||
#include "graphics/niche/InkHUD/InkHUD.h"
|
||||
#include "graphics/niche/InkHUD/Persistence.h"
|
||||
#include "graphics/niche/InkHUD/SystemApplet.h"
|
||||
@@ -23,17 +25,20 @@
|
||||
// Bridges touch events from TouchScreenImpl1 directly into InkHUD,
|
||||
// bypassing the InputBroker (which is excluded in InkHUD builds).
|
||||
// Routing mirrors the mini-epaper-s3 two-way rocker pattern:
|
||||
// - Nav left/right: prevApplet/nextApplet when idle, navUp/Down when a system applet has focus (e.g. menu)
|
||||
// - Nav left/right: prevApplet/nextApplet when idle, navUp/Down when a system
|
||||
// applet has focus (e.g. menu)
|
||||
// - Nav up/down: navUp/navDown always (menu scroll)
|
||||
// - Tap/long-press: direct touch point dispatch (with fallback to short/long button semantics)
|
||||
// - Tap/long-press: direct touch point dispatch (with fallback to short/long
|
||||
// button semantics)
|
||||
class TouchInkHUDBridge : public Observer<const InputEvent *>
|
||||
{
|
||||
int onNotify(const InputEvent *e) override
|
||||
{
|
||||
auto *inkhud = NicheGraphics::InkHUD::InkHUD::getInstance();
|
||||
|
||||
// Keep alignment in sync with the current rotation so that visual-frame gestures
|
||||
// always pass through nav functions without remapping: (rotation + alignment) % 4 == 0.
|
||||
// Keep alignment in sync with the current rotation so that visual-frame
|
||||
// gestures always pass through nav functions without remapping: (rotation +
|
||||
// alignment) % 4 == 0.
|
||||
inkhud->persistence->settings.joystick.alignment = (4 - inkhud->persistence->settings.rotation) % 4;
|
||||
|
||||
// Check whether a system applet (e.g. menu) is currently handling input
|
||||
@@ -45,6 +50,17 @@ class TouchInkHUDBridge : public Observer<const InputEvent *>
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
if (!systemHandlingInput) {
|
||||
auto *applet = inkhud->getActiveApplet();
|
||||
const int8_t dx = (e->inputEvent == INPUT_BROKER_RIGHT) - (e->inputEvent == INPUT_BROKER_LEFT);
|
||||
const int8_t dy = (e->inputEvent == INPUT_BROKER_DOWN) - (e->inputEvent == INPUT_BROKER_UP);
|
||||
if (applet && (dx || dy) && applet->onTouchSwipe(dx, dy)) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
switch (e->inputEvent) {
|
||||
case INPUT_BROKER_USER_PRESS:
|
||||
inkhud->touchTap(e->touchX, e->touchY);
|
||||
@@ -102,8 +118,9 @@ volatile bool touchControllerReady = false;
|
||||
volatile bool touchLightSleepActive = false;
|
||||
volatile bool touchNeedsWake = false;
|
||||
volatile bool touchIndicatorRefreshPending = false;
|
||||
// When the light-sleep resume happened, not when the block expires: an interval bounds a missed
|
||||
// 0-check by the settle time, where a stored deadline would block for up to half a wrap cycle.
|
||||
// When the light-sleep resume happened, not when the block expires: an interval
|
||||
// bounds a missed 0-check by the settle time, where a stored deadline would
|
||||
// block for up to half a wrap cycle.
|
||||
constexpr uint32_t TOUCH_RESUME_BLOCK_MS = 150;
|
||||
volatile uint32_t touchResumeAtMs = 0;
|
||||
volatile uint32_t touchStateEpoch = 1;
|
||||
@@ -116,7 +133,8 @@ void showTouchIndicator(const char *text)
|
||||
{
|
||||
#if HAS_SCREEN
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
// InkHUD builds render a dedicated bottom-edge "TOUCH OFF" overlay instead of popup banners.
|
||||
// InkHUD builds render a dedicated bottom-edge "TOUCH OFF" overlay instead of
|
||||
// popup banners.
|
||||
(void)text;
|
||||
return;
|
||||
#else
|
||||
@@ -154,23 +172,11 @@ bool readPca9535Port1(uint8_t *value)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool isPca9535SideKeyPressed()
|
||||
{
|
||||
uint8_t port1 = 0xFF;
|
||||
if (!readPca9535Port1(&port1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (port1 & BOARD_PCA9535_BUTTON_MASK) == 0;
|
||||
}
|
||||
|
||||
class SideKeyInterruptThread : public concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
SideKeyInterruptThread() : concurrency::OSThread("t5s3SideKeyInt", SAMPLE_MS)
|
||||
SideKeyInterruptThread() : concurrency::OSThread("t5s3SideKeyInt", IDLE_POLL_MS)
|
||||
{
|
||||
// Do not run unless an edge arrives.
|
||||
OSThread::disable();
|
||||
instance = this;
|
||||
#ifdef ARCH_ESP32
|
||||
lsObserver.observe(¬ifyLightSleep);
|
||||
@@ -187,47 +193,57 @@ class SideKeyInterruptThread : public concurrency::OSThread
|
||||
protected:
|
||||
int32_t runOnce() override
|
||||
{
|
||||
const uint32_t now = millis();
|
||||
const uint32_t now = Time::getMillis();
|
||||
|
||||
// 0 means the device has never light-slept, so no block is armed - test it first.
|
||||
// 0 means the device has never light-slept, so no block is armed - test it
|
||||
// first.
|
||||
if (touchResumeAtMs != 0 && Throttle::isWithinTimespanMs(touchResumeAtMs, TOUCH_RESUME_BLOCK_MS)) {
|
||||
resetStateAndStop();
|
||||
return OSThread::disable();
|
||||
resetState();
|
||||
return IDLE_POLL_MS;
|
||||
}
|
||||
|
||||
if (touchLightSleepActive) {
|
||||
resetStateAndStop();
|
||||
return OSThread::disable();
|
||||
resetState();
|
||||
return IDLE_POLL_MS;
|
||||
}
|
||||
|
||||
// Ignore side-key handling while BOOT/user button is held.
|
||||
if (digitalRead(BUTTON_PIN) == LOW) {
|
||||
resetStateAndStop();
|
||||
return OSThread::disable();
|
||||
resetState();
|
||||
return IDLE_POLL_MS;
|
||||
}
|
||||
|
||||
if (state == State::REST) {
|
||||
// Recover a latched expander interrupt even if its edge was missed.
|
||||
if (digitalRead(BOARD_PCA9535_INT) != LOW)
|
||||
return IDLE_POLL_MS;
|
||||
irqAtMs = now;
|
||||
state = State::IRQ_PENDING;
|
||||
}
|
||||
if (state == State::IRQ_PENDING && !Throttle::hasElapsed(irqAtMs, DEBOUNCE_MS))
|
||||
return SAMPLE_MS;
|
||||
|
||||
uint8_t port1 = 0xFF;
|
||||
if (!readPca9535Port1(&port1))
|
||||
return SAMPLE_MS;
|
||||
const bool pressed = (port1 & BOARD_PCA9535_BUTTON_MASK) == 0;
|
||||
|
||||
switch (state) {
|
||||
case State::IRQ_PENDING:
|
||||
// Initial debounce after expander interrupt edge.
|
||||
if ((uint32_t)(now - irqAtMs) < DEBOUNCE_MS) {
|
||||
return SAMPLE_MS;
|
||||
if (!pressed) {
|
||||
resetState();
|
||||
return IDLE_POLL_MS;
|
||||
}
|
||||
|
||||
if (isPca9535SideKeyPressed()) {
|
||||
state = State::PRESSED;
|
||||
pressStartMs = now;
|
||||
return SAMPLE_MS;
|
||||
}
|
||||
|
||||
// Spurious/cleared edge.
|
||||
resetStateAndStop();
|
||||
return OSThread::disable();
|
||||
state = State::PRESSED;
|
||||
pressStartMs = irqAtMs;
|
||||
[[fallthrough]];
|
||||
|
||||
case State::PRESSED: {
|
||||
if (isPca9535SideKeyPressed()) {
|
||||
// Fire long-press action as soon as threshold is reached, without waiting for release.
|
||||
if (!longPressFired && (uint32_t)(now - pressStartMs) >= LONG_PRESS_MIN_MS &&
|
||||
(uint32_t)(now - lastActionMs) >= ACTION_COOLDOWN_MS) {
|
||||
if (pressed) {
|
||||
// Fire long-press action as soon as threshold is reached, without
|
||||
// waiting for release.
|
||||
if (!longPressFired && Throttle::hasElapsed(pressStartMs, LONG_PRESS_MIN_MS) &&
|
||||
Throttle::hasElapsed(lastActionMs, ACTION_COOLDOWN_MS)) {
|
||||
t5BacklightToggleUser();
|
||||
longPressFired = true;
|
||||
lastActionMs = now;
|
||||
@@ -235,10 +251,12 @@ class SideKeyInterruptThread : public concurrency::OSThread
|
||||
return SAMPLE_MS;
|
||||
}
|
||||
|
||||
// Released: if long-press already fired, do nothing. Otherwise classify short press.
|
||||
// Released: if long-press already fired, do nothing. Otherwise classify
|
||||
// short press.
|
||||
const uint32_t heldMs = now - pressStartMs;
|
||||
if (!longPressFired && heldMs >= SHORT_PRESS_MIN_MS && (uint32_t)(now - lastActionMs) >= ACTION_COOLDOWN_MS) {
|
||||
// If timeout forced touch/backlight off, short-press acts as a wake action first.
|
||||
if (!longPressFired && heldMs >= SHORT_PRESS_MIN_MS && Throttle::hasElapsed(lastActionMs, ACTION_COOLDOWN_MS)) {
|
||||
// If timeout forced touch/backlight off, short-press acts as a wake
|
||||
// action first.
|
||||
if (t5TouchIsForcedByTimeout()) {
|
||||
t5TouchHandleUserInput();
|
||||
t5BacklightHandleUserInput();
|
||||
@@ -248,13 +266,13 @@ class SideKeyInterruptThread : public concurrency::OSThread
|
||||
lastActionMs = now;
|
||||
}
|
||||
|
||||
resetStateAndStop();
|
||||
return OSThread::disable();
|
||||
resetState();
|
||||
return IDLE_POLL_MS;
|
||||
}
|
||||
|
||||
case State::REST:
|
||||
default:
|
||||
return OSThread::disable();
|
||||
return IDLE_POLL_MS;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,6 +284,7 @@ class SideKeyInterruptThread : public concurrency::OSThread
|
||||
};
|
||||
|
||||
static constexpr uint32_t SAMPLE_MS = 15;
|
||||
static constexpr uint32_t IDLE_POLL_MS = 100;
|
||||
static constexpr uint32_t DEBOUNCE_MS = 25;
|
||||
static constexpr uint32_t SHORT_PRESS_MIN_MS = 30;
|
||||
static constexpr uint32_t LONG_PRESS_MIN_MS = 450;
|
||||
@@ -295,25 +314,16 @@ class SideKeyInterruptThread : public concurrency::OSThread
|
||||
|
||||
state = State::IRQ_PENDING;
|
||||
irqAtMs = millis();
|
||||
startThread();
|
||||
OSThread::setIntervalFromNow(0);
|
||||
runASAP = true;
|
||||
BaseType_t higherWake = 0;
|
||||
concurrency::mainDelay.interruptFromISR(&higherWake);
|
||||
}
|
||||
|
||||
void startThread()
|
||||
{
|
||||
if (!OSThread::enabled) {
|
||||
OSThread::setIntervalFromNow(0);
|
||||
OSThread::enabled = true;
|
||||
runASAP = true;
|
||||
}
|
||||
}
|
||||
|
||||
void resetStateAndStop()
|
||||
void resetState()
|
||||
{
|
||||
state = State::REST;
|
||||
longPressFired = false;
|
||||
if (OSThread::enabled) {
|
||||
OSThread::disable();
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
@@ -324,7 +334,7 @@ class SideKeyInterruptThread : public concurrency::OSThread
|
||||
// If INT is left asserted low, light sleep exits immediately.
|
||||
uint8_t ignored = 0xFF;
|
||||
(void)readPca9535Port1(&ignored);
|
||||
resetStateAndStop();
|
||||
resetState();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -375,7 +385,8 @@ void refreshTouchIndicatorInInkHUD(bool async = true)
|
||||
touchStatus->bringToForeground();
|
||||
}
|
||||
|
||||
// Re-render all applets so touch-status visibility changes are immediately reflected.
|
||||
// Re-render all applets so touch-status visibility changes are immediately
|
||||
// reflected.
|
||||
inkhud->forceUpdate(NicheGraphics::Drivers::EInk::UpdateTypes::FAST, true, async);
|
||||
}
|
||||
#endif
|
||||
@@ -434,7 +445,8 @@ void t5TouchSetForcedByTimeout(bool forced)
|
||||
|
||||
if (forced) {
|
||||
// Timeout only gates touch input in software. Avoid GT911 I2C here because
|
||||
// PowerFSM same-state transitions can fire from phone contact and screen timeout.
|
||||
// PowerFSM same-state transitions can fire from phone contact and screen
|
||||
// timeout.
|
||||
touchNeedsWake = false;
|
||||
} else if (touchInputEnabled && touchControllerReady && !touchLightSleepActive) {
|
||||
touchNeedsWake = false;
|
||||
@@ -512,7 +524,8 @@ void toggleTouchInputEnabled()
|
||||
}
|
||||
|
||||
// Commands the GT911 into standby before the Wire bus is torn down.
|
||||
// notifyDeepSleep fires before Wire.end() in doDeepSleep(), so I2C is still available here.
|
||||
// notifyDeepSleep fires before Wire.end() in doDeepSleep(), so I2C is still
|
||||
// available here.
|
||||
struct TouchDeepSleepObserver {
|
||||
int onDeepSleep(void *)
|
||||
{
|
||||
@@ -528,7 +541,8 @@ struct TouchLightSleepObserver {
|
||||
{
|
||||
touchLightSleepActive = true;
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
// Render touch-off overlay before sleeping so user sees touch is unavailable.
|
||||
// Render touch-off overlay before sleeping so user sees touch is
|
||||
// unavailable.
|
||||
touchIndicatorRefreshPending = true;
|
||||
refreshTouchIndicatorInInkHUD(false);
|
||||
touchIndicatorRefreshPending = false;
|
||||
@@ -585,7 +599,8 @@ bool readTouch(int16_t *x, int16_t *y)
|
||||
suppressFromMs = 0;
|
||||
}
|
||||
|
||||
// Let buses and peripherals settle briefly after light-sleep wake. 0 means no wake yet.
|
||||
// Let buses and peripherals settle briefly after light-sleep wake. 0 means no
|
||||
// wake yet.
|
||||
if (touchResumeAtMs != 0 && Throttle::isWithinTimespanMs(touchResumeAtMs, TOUCH_RESUME_BLOCK_MS)) {
|
||||
return false;
|
||||
}
|
||||
@@ -607,21 +622,23 @@ bool readTouch(int16_t *x, int16_t *y)
|
||||
return false;
|
||||
}
|
||||
|
||||
// After a recovery pulse, emit a brief "released" window so gesture state can reset.
|
||||
// After a recovery pulse, emit a brief "released" window so gesture state can
|
||||
// reset.
|
||||
if (suppressFromMs != 0 && Throttle::isWithinTimespanMs(suppressFromMs, TOUCH_WAKE_SUPPRESS_MS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
if (!digitalRead(GT911_PIN_INT)) {
|
||||
// 0.4.x deprecates getPoint() in favour of getTouchPoints(); only the first touch is used here.
|
||||
const TouchPoints &points = touch.getTouchPoints();
|
||||
if (points.getPointCount()) {
|
||||
const int16_t raw_x = static_cast<int16_t>(points.getPoint(0).x);
|
||||
const int16_t raw_y = static_cast<int16_t>(points.getPoint(0).y);
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
// Transform raw GT911 axes to visual-frame coordinates for the current display rotation.
|
||||
// rotation=3 is the physical identity (device's default orientation).
|
||||
// Transform raw GT911 axes to visual-frame coordinates for the current
|
||||
// display rotation. rotation=3 is the physical identity (device's default
|
||||
// orientation).
|
||||
switch (NicheGraphics::InkHUD::InkHUD::getInstance()->persistence->settings.rotation) {
|
||||
default:
|
||||
case 3:
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#if defined(MESHTASTIC_INCLUDE_INKHUD_BOOKREADER) && CONFIG_IDF_TARGET_ESP32S3
|
||||
#define dsps_fft2r_fc32_aes3_enabled 1
|
||||
#define dsps_fft2r_sc16_aes3_enabled 1
|
||||
#endif
|
||||
|
||||
#ifndef dsps_fft2r_fc32_aes3_enabled
|
||||
#define dsps_fft2r_fc32_aes3_enabled 0
|
||||
#endif
|
||||
|
||||
#ifndef dsps_fft2r_sc16_aes3_enabled
|
||||
#define dsps_fft2r_sc16_aes3_enabled 0
|
||||
#endif
|
||||
@@ -25,6 +25,9 @@ This is driven via the FastEPD library through the NicheGraphics ED047TC1 driver
|
||||
|
||||
// Applets
|
||||
#include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h"
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
#include "graphics/niche/InkHUD/Applets/User/BookReader/BookReaderApplet.h"
|
||||
#endif
|
||||
#include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h"
|
||||
#include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h"
|
||||
#include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h"
|
||||
@@ -88,6 +91,9 @@ void setupNicheGraphics()
|
||||
inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet, true, false); // Activated, not autoshown
|
||||
inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0
|
||||
inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet, false, false); // Not Active, not autoshown
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
inkhud->addApplet("Book Reader", new InkHUD::BookReaderApplet, true, false); // Activated, not autoshown
|
||||
#endif
|
||||
|
||||
// Enable reusable InkHUD touch status indicator for this touch-capable board.
|
||||
inkhud->setTouchEnabledProvider(isTouchInputEnabled);
|
||||
|
||||
@@ -55,7 +55,26 @@ custom_meshtastic_has_ink_hud = true
|
||||
build_flags =
|
||||
${t5s3_epaper_base.build_flags}
|
||||
${inkhud.build_flags}
|
||||
-D MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
-D INKHUD_BOOKREADER_GRAYSCALE=1
|
||||
-D SDCARD_USE_SPI1
|
||||
-D INKHUD_BOOKREADER_ENABLE_INLINE_IMAGES=1
|
||||
; Board-specific resource limits; other reader variants can set their own.
|
||||
-D INKHUD_BOOKREADER_MAX_FILE_BYTES=196608U
|
||||
-D INKHUD_BOOKREADER_MAX_HTML_FILE_BYTES=524288U
|
||||
-D INKHUD_BOOKREADER_MAX_OPF_PARSE_BYTES=131072U
|
||||
-D INKHUD_BOOKREADER_MAX_TOC_PARSE_BYTES=131072U
|
||||
-D INKHUD_BOOKREADER_CSS_MAX_INLINE_TOTAL_BYTES=49152U
|
||||
-D INKHUD_BOOKREADER_CSS_MAX_EXTERNAL_FILE_BYTES=32768U
|
||||
-D INKHUD_BOOKREADER_CSS_MAX_EXTERNAL_TOTAL_BYTES=65536U
|
||||
-D INKHUD_BOOKREADER_CSS_MAX_SOURCE_TEXT_BYTES=98304U
|
||||
-D INKHUD_BOOKREADER_PREFETCH_MAX_FILE_BYTES=65536U
|
||||
-D INKHUD_BOOKREADER_PREFETCH_MAX_TEXT_BYTES=98304U
|
||||
-D INKHUD_BOOKREADER_MAX_EPUB_CENTRAL_DIR_BYTES=524288U
|
||||
-D INKHUD_BOOKREADER_MAX_EPUB_UNCOMPRESSED_ENTRY_BYTES=2097152U
|
||||
-D INKHUD_BOOKREADER_MAX_EPUB_INLINE_IMAGE_BYTES=2097152U
|
||||
-D INKHUD_BOOKREADER_INITIAL_PARTIAL_BYTES=16384U
|
||||
-D INKHUD_BOOKREADER_USE_JPEGDEC=1
|
||||
-D T5_S3_EPAPER_PRO_V2
|
||||
build_src_filter =
|
||||
${t5s3_epaper_base.build_src_filter}
|
||||
@@ -63,6 +82,9 @@ build_src_filter =
|
||||
lib_deps =
|
||||
${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX
|
||||
${t5s3_epaper_base.lib_deps}
|
||||
# EPUB inline image rendering for Book Reader (scoped to this env only)
|
||||
https://github.com/bitbank2/JPEGDEC/archive/refs/tags/1.8.4.zip
|
||||
https://github.com/bitbank2/PNGdec/archive/refs/tags/1.1.6.zip
|
||||
|
||||
|
||||
[env:t5s3-epaper-v1] ; H752
|
||||
|
||||
@@ -80,6 +80,10 @@ void t5SetHomeCapButtonEventsEnabled(bool enabled);
|
||||
#define BOARD_PCA9535_BUTTON_MASK 0x04
|
||||
#endif
|
||||
// SD card
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
||||
// The reader uses the radio's SPI instance so SD and LoRa share its lock.
|
||||
#undef SDCARD_USE_SPI1
|
||||
#endif
|
||||
#define HAS_SDCARD
|
||||
#define SDCARD_CS SPI_CS
|
||||
#define SD_SPI_FREQUENCY 75000000U
|
||||
|
||||
Reference in new issue
Block a user