Compare commits

..
66 changed files with 533 additions and 1449 deletions

No files matched your search

+66 -49
View File
@@ -18,7 +18,7 @@ jobs:
- uses: actions/setup-python@v6
with:
python-version: '3.13'
python-version: '3.14'
- name: Install clang-format-21
run: |
@@ -42,7 +42,7 @@ jobs:
- uses: actions/setup-python@v6
with:
python-version: '3.13'
python-version: '3.14'
- name: Install uv
uses: astral-sh/setup-uv@v7
@@ -51,15 +51,7 @@ jobs:
enable-cache: false
- name: Install PlatformIO Core
run: |
uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
# platform-espressif32's builder/main.py imports these unconditionally; its own
# penv bootstrap (builder/penv_setup.py) is supposed to install them automatically
# but isn't picking them up reliably in this runner image, so pin them explicitly.
uv pip install --system -U "littlefs-python>=0.16.0" "fatfs-ng>=0.1.14" "pyyaml>=6.0.2" \
"rich-click>=1.8.6" "zopfli>=0.2.2" "intelhex>=2.3.0" "rich>=14.0.0" "urllib3<2" \
"cryptography>=45.0.3" "certifi>=2025.8.3" "ecdsa>=0.19.1" "bitstring>=4.3.1" \
"reedsolo>=1.5.3,<1.8" "esp-idf-size>=2.0.0" "esp-coredump>=1.14.0"
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
- name: Cache PlatformIO packages
uses: actions/cache@v4
@@ -71,24 +63,9 @@ jobs:
- name: Run cppcheck
run: pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
build:
name: Build ${{ matrix.env }}
build-default:
name: Build default
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Artifact names match the release asset names OtaUpdater requests:
# plain firmware.bin for the C3 X4/X3 binary, firmware-<board>.bin for
# the S3 boards.
include:
- env: default
asset: firmware.bin
- env: sticky
asset: firmware-sticky.bin
- env: x4pro
asset: firmware-x4pro.bin
- env: papermono
asset: firmware-papermono.bin
steps:
- uses: actions/checkout@v6
with:
@@ -96,7 +73,7 @@ jobs:
- uses: actions/setup-python@v6
with:
python-version: '3.13'
python-version: '3.14'
- name: Install uv
uses: astral-sh/setup-uv@v7
@@ -105,15 +82,7 @@ jobs:
enable-cache: false
- name: Install PlatformIO Core
run: |
uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
# platform-espressif32's builder/main.py imports these unconditionally; its own
# penv bootstrap (builder/penv_setup.py) is supposed to install them automatically
# but isn't picking them up reliably in this runner image, so pin them explicitly.
uv pip install --system -U "littlefs-python>=0.16.0" "fatfs-ng>=0.1.14" "pyyaml>=6.0.2" \
"rich-click>=1.8.6" "zopfli>=0.2.2" "intelhex>=2.3.0" "rich>=14.0.0" "urllib3<2" \
"cryptography>=45.0.3" "certifi>=2025.8.3" "ecdsa>=0.19.1" "bitstring>=4.3.1" \
"reedsolo>=1.5.3,<1.8" "esp-idf-size>=2.0.0" "esp-coredump>=1.14.0"
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
- name: Cache PlatformIO packages
uses: actions/cache@v4
@@ -122,10 +91,64 @@ jobs:
key: pio-${{ runner.os }}-${{ hashFiles('platformio.ini') }}
restore-keys: pio-${{ runner.os }}-
- name: Build ${{ matrix.env }} firmware
- name: Build default firmware
run: |
set -euo pipefail
pio run -e ${{ matrix.env }} | tee pio.log
pio run -e default | tee pio.log
- name: Extract firmware stats
run: |
set -euo pipefail
ram_line="$(grep -E "RAM:\\s" -m1 pio.log || true)"
flash_line="$(grep -E "Flash:\\s" -m1 pio.log || true)"
echo "ram_line=${ram_line}" >> "$GITHUB_OUTPUT"
echo "flash_line=${flash_line}" >> "$GITHUB_OUTPUT"
{
echo "## Firmware build stats"
if [ -n "$ram_line" ]; then echo "- ${ram_line}"; else echo "- RAM: not found"; fi
if [ -n "$flash_line" ]; then echo "- ${flash_line}"; else echo "- Flash: not found"; fi
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload firmware.bin artifact
uses: actions/upload-artifact@v6
with:
name: firmware.bin
path: .pio/build/default/firmware.bin
if-no-files-found: error
build-sticky:
name: Build sticky
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: actions/setup-python@v6
with:
python-version: '3.14'
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
enable-cache: false
- name: Install PlatformIO Core
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
- name: Cache PlatformIO packages
uses: actions/cache@v4
with:
path: ~/.platformio
key: pio-${{ runner.os }}-${{ hashFiles('platformio.ini') }}
restore-keys: pio-${{ runner.os }}-
- name: Build sticky firmware
run: |
set -euo pipefail
pio run -e sticky | tee pio.log
- name: Extract firmware stats
run: |
@@ -135,18 +158,11 @@ jobs:
echo "ram_line=${ram_line}" >> "$GITHUB_OUTPUT"
echo "flash_line=${flash_line}" >> "$GITHUB_OUTPUT"
{
echo "## ${{ matrix.env }} firmware build stats"
echo "## Sticky firmware build stats"
if [ -n "$ram_line" ]; then echo "- ${ram_line}"; else echo "- RAM: not found"; fi
if [ -n "$flash_line" ]; then echo "- ${flash_line}"; else echo "- Flash: not found"; fi
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload firmware artifact
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.asset }}
path: .pio/build/${{ matrix.env }}/firmware.bin
if-no-files-found: error
unit-tests:
runs-on: ubuntu-latest
steps:
@@ -179,7 +195,8 @@ jobs:
test-status:
name: Test Status
needs:
- build
- build-default
- build-sticky
- clang-format
- cppcheck
- unit-tests
+1 -2
View File
@@ -1,7 +1,6 @@
name: "PR Formatting"
on:
pull_request:
pull_request_target:
types:
- opened
@@ -9,7 +8,7 @@ on:
- edited
permissions:
statuses: write
pull-requests: read
jobs:
title-check:
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
- uses: actions/setup-python@v6
with:
python-version: '3.13'
python-version: '3.14'
- name: Install font tools
run: pip install freetype-py fonttools pyyaml
-83
View File
@@ -1,83 +0,0 @@
name: Compile Release
# Builds every board's release firmware on tag push, with the .bin named
# exactly as OtaUpdater requests it from the GitHub release: plain
# firmware.bin for the C3 X4/X3 binary, firmware-<board>.bin for every other
# board. Attach the artifacts' .bin files to the release as-is.
on:
push:
tags:
- '*'
permissions:
contents: read
jobs:
build-release:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- pio_env: gh_release
asset: firmware.bin
- pio_env: sticky-gh_release
asset: firmware-sticky.bin
- pio_env: x4pro-gh_release
asset: firmware-x4pro.bin
- pio_env: papermono-gh_release
asset: firmware-papermono.bin
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: actions/setup-python@v6
with:
python-version: '3.13'
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
enable-cache: false
- name: Install PlatformIO Core
run: |
uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
# platform-espressif32's builder/main.py imports these unconditionally; its own
# penv bootstrap (builder/penv_setup.py) is supposed to install them automatically
# but isn't picking them up reliably in this runner image, so pin them explicitly.
uv pip install --system -U "littlefs-python>=0.16.0" "fatfs-ng>=0.1.14" "pyyaml>=6.0.2" \
"rich-click>=1.8.6" "zopfli>=0.2.2" "intelhex>=2.3.0" "rich>=14.0.0" "urllib3<2" \
"cryptography>=45.0.3" "certifi>=2025.8.3" "ecdsa>=0.19.1" "bitstring>=4.3.1" \
"reedsolo>=1.5.3,<1.8" "esp-idf-size>=2.0.0" "esp-coredump>=1.14.0"
- name: Cache PlatformIO packages
uses: actions/cache@v4
with:
path: ~/.platformio
key: pio-${{ runner.os }}-${{ hashFiles('platformio.ini') }}
restore-keys: pio-${{ runner.os }}-
- name: Build CrossPoint
run: pio run -e ${{ matrix.pio_env }} -j1
- name: Name release asset
run: |
if [ "${{ matrix.asset }}" != "firmware.bin" ]; then
cp ".pio/build/${{ matrix.pio_env }}/firmware.bin" \
".pio/build/${{ matrix.pio_env }}/${{ matrix.asset }}"
fi
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.asset }}
path: |
.pio/build/${{ matrix.pio_env }}/bootloader.bin
.pio/build/${{ matrix.pio_env }}/${{ matrix.asset }}
.pio/build/${{ matrix.pio_env }}/firmware.elf
.pio/build/${{ matrix.pio_env }}/firmware.map
.pio/build/${{ matrix.pio_env }}/partitions.bin
+2 -10
View File
@@ -14,7 +14,7 @@ jobs:
- uses: actions/setup-python@v6
with:
python-version: '3.13'
python-version: '3.14'
- name: Install uv
uses: astral-sh/setup-uv@v7
@@ -23,15 +23,7 @@ jobs:
enable-cache: false
- name: Install PlatformIO Core
run: |
uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
# platform-espressif32's builder/main.py imports these unconditionally; its own
# penv bootstrap (builder/penv_setup.py) is supposed to install them automatically
# but isn't picking them up reliably in this runner image, so pin them explicitly.
uv pip install --system -U "littlefs-python>=0.16.0" "fatfs-ng>=0.1.14" "pyyaml>=6.0.2" \
"rich-click>=1.8.6" "zopfli>=0.2.2" "intelhex>=2.3.0" "rich>=14.0.0" "urllib3<2" \
"cryptography>=45.0.3" "certifi>=2025.8.3" "ecdsa>=0.19.1" "bitstring>=4.3.1" \
"reedsolo>=1.5.3,<1.8" "esp-idf-size>=2.0.0" "esp-coredump>=1.14.0"
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
- name: Cache PlatformIO packages
uses: actions/cache@v4
+6 -6
View File
@@ -501,7 +501,7 @@ The **Sleep Screen** setting controls what is displayed when the device goes to
| **Custom** | A custom image from the SD card (see below). Falls back to **Dark** if no custom image is found. |
| **Cover** | The cover of the currently open book. Falls back to **Dark** if no book is open. |
| **Cover + Custom** | The cover of the currently open book, shown only while actively reading. Falls back to **Custom** behavior when not reading. |
| **Transparent** | A BMP overlay drawn over the current screen. Supports alpha transparency in 32-bit BGRA images and treats white as transparent in regular BMPs. Falls back to **Dark** if no valid overlay image is found. |
| **Transparent** | A BMP or PNG overlay drawn over the current screen. Supports PNG and 32-bit BGRA alpha transparency, and treats white as transparent in regular BMPs. Falls back to **Dark** if no valid overlay image is found. |
| **None** | A blank screen. |
#### Cover settings
@@ -520,17 +520,17 @@ To use custom sleep images, set the sleep screen mode to **Custom** or **Cover +
#### Transparent overlay images
To use transparent sleep overlays, set the sleep screen mode to **Transparent**, then place BMP files on the SD card:
To use transparent sleep overlays, set the sleep screen mode to **Transparent**, then place BMP or PNG files on the SD card:
- **Multiple Images (recommended):** Create a `.sleep-overlay` directory in the root of the SD card and place any number of valid overlay `.bmp` images inside. One will be randomly selected each time the device sleeps. A directory named `sleep-overlay` is also accepted as a fallback.
- **Single Image:** Place a file named `sleep-overlay.bmp` in the root directory. This takes priority over the `.sleep-overlay`/`sleep-overlay` directories.
- **Multiple Images (recommended):** Create a `.sleep-overlay` directory in the root of the SD card and place any number of valid overlay `.bmp` or `.png` images inside. One will be randomly selected each time the device sleeps. A directory named `sleep-overlay` is also accepted as a fallback.
- **Single Image:** Place `sleep-overlay.bmp` or `sleep-overlay.png` in the root directory. A root BMP takes priority over a root PNG, and both take priority over the `.sleep-overlay`/`sleep-overlay` directories.
Transparent overlay files are intentionally separate from normal sleep images. Regular BMP formats supported by CrossPoint are accepted; white pixels leave the existing screen unchanged. For per-pixel alpha transparency, use a 32-bit BGRA BMP with both visible and non-opaque pixels. Opaque white pixels in these alpha images erase the content behind them.
Transparent overlay files are intentionally separate from normal sleep images. Regular BMP formats supported by CrossPoint are accepted; white pixels leave the existing screen unchanged. For per-pixel alpha transparency, use a PNG with an alpha channel or a 32-bit BGRA BMP with both visible and non-opaque pixels. Opaque white pixels in alpha images erase the content behind them.
> [!TIP]
> For best results:
> - For non-transparent **Custom** mode, use uncompressed BMP files with 24-bit color depth.
> - For **Transparent** mode, use a regular BMP for white-as-transparent artwork or an uncompressed 32-bit BGRA BMP for per-pixel alpha.
> - For **Transparent** mode, use a PNG or uncompressed 32-bit BGRA BMP for per-pixel alpha, or a regular BMP for white-as-transparent artwork.
> - X4: Use a resolution of 480x800 pixels to match the device's screen resolution.
> - X3: Use a resolution of 528x792 pixels to match the device's screen resolution.
+2 -2
View File
@@ -1,6 +1,6 @@
# Touch and UI Development
CrossPoint runs on touch devices (Seeed Sticky, M5Paper, M5Stack PaperMono, LilyGo T5, Xteink X4 Pro) alongside the button-only Xteink X3/X4. Every screen must work with both input styles.
CrossPoint runs on touch devices (Seeed Sticky, M5Paper, LilyGo T5) alongside the button-only Xteink X3/X4. Every screen must work with both input styles.
**There is one supported way to build a new screen: FreeInkUI, hosted through the firmware base classes below.** Touch hit-testing, tap highlighting, long-press, swipe scrolling, and button focus navigation all come from the shared stack; you never hand-roll coordinate math.
@@ -18,7 +18,7 @@ The old bridge helpers (`rowTouch`, `colTouch`, `wasTapInRect`, manual rect `con
| A modal picker or confirm inside a legacy activity | `OptionPopup` (or push `ConfirmationActivity`) | [`OtaUpdateActivity`](../../src/activities/settings/OtaUpdateActivity.cpp) |
| A Yes/No prompt inside a FUI screen | build `fui::optionDialog` into the screen | `WifiSelectionActivity::buildPromptDialog` |
All of these paths route input through the same SDK interaction table, so touch and physical buttons fire the same actions with no per-screen coordinate code.
All four paths route input through the same SDK interaction table, so touch and physical buttons fire the same actions with no per-screen coordinate code.
## The hosting stack
+3 -3
View File
@@ -144,14 +144,14 @@ struct DirectPixelWriter {
// Write a single 2-bit dithered pixel value to the framebuffer.
// Must be called after beginRow() for the current row.
// No bounds checking — caller guarantees coordinates are valid.
inline void writePixel(int logicalX, uint8_t pixelValue) const {
inline void writePixel(int logicalX, uint8_t pixelValue, bool writeWhiteInBw = false) const {
// Determine whether to draw based on render mode
bool draw;
bool state;
switch (mode) {
case GfxRenderer::BW:
draw = (pixelValue < 3);
state = true;
draw = writeWhiteInBw || pixelValue < 3;
state = pixelValue < 3;
break;
case GfxRenderer::GRAYSCALE_MSB:
draw = (pixelValue == 1 || pixelValue == 2);
@@ -18,6 +18,9 @@ struct RenderConfig {
bool useDithering = true;
bool performanceMode = false;
bool useExactDimensions = false; // If true, use maxWidth/maxHeight as exact output size (no recalculation)
float sourceCropX = 0.0f; // Fraction cropped equally from the left and right edges
float sourceCropY = 0.0f; // Fraction cropped equally from the top and bottom edges
bool preserveAlpha = false; // Skip transparent pixels instead of compositing them against white
std::string cachePath; // If non-empty, decoder will write pixel cache to this path
};
@@ -7,6 +7,7 @@
#include <Memory.h>
#include <PNGdec.h>
#include <algorithm>
#include <cstdlib>
#include <memory>
#include <new>
@@ -21,6 +22,7 @@ namespace {
// The draw callback receives this via pDraw->pUser (set by png.decode()).
// The file I/O callbacks receive the HalFile* via pFile->fHandle (set by pngOpen()).
struct PngContext {
PNG* decoder{nullptr};
GfxRenderer* renderer{nullptr};
const RenderConfig* config{nullptr};
int screenWidth{0};
@@ -30,14 +32,18 @@ struct PngContext {
float scale{1.f};
int srcWidth{0};
int srcHeight{0};
int cropLeft{0};
int cropTop{0};
int visibleWidth{0};
int visibleHeight{0};
int dstWidth{0};
int dstHeight{0};
int lastDstY{-1}; // Track last rendered destination Y to avoid duplicates
PixelCache cache;
bool caching{false};
uint8_t* grayLineBuffer{nullptr};
uint8_t* alphaLineBuffer{nullptr};
};
// File I/O callbacks use pFile->fHandle to access the HalFile*,
@@ -131,19 +137,31 @@ uint8_t expandSampleToByte(uint8_t sample, int bitsPerSample) {
return static_cast<uint8_t>((sample * 255U) / maxSample);
}
// Convert entire source line to grayscale with alpha blending to white background.
uint8_t alphaThreshold4x4(const int x, const int y) {
static constexpr uint8_t BAYER_4X4[16] = {0, 128, 32, 160, 192, 64, 224, 96, 48, 176, 16, 144, 240, 112, 208, 80};
return BAYER_4X4[((y & 0x03) << 2) | (x & 0x03)];
}
// Convert an entire source line to grayscale and optionally retain alpha.
// Low-bit-depth grayscale/indexed scanlines are packed most-significant sample first.
// For indexed PNGs with tRNS chunk, alpha values are stored at palette[768] onwards.
// Processing the whole line at once improves cache locality and reduces per-pixel overhead.
void convertLineToGray(const uint8_t* pPixels, uint8_t* grayLine, int width, int pixelType, int bitsPerSample,
uint8_t* palette, int hasAlpha) {
uint8_t* palette, int hasAlpha, uint32_t transparentColor, uint8_t* alphaLine) {
const auto store = [grayLine, alphaLine](const int x, const uint8_t gray, const uint8_t alpha) {
grayLine[x] = alphaLine ? gray : static_cast<uint8_t>((gray * alpha + 255u * (255u - alpha)) / 255u);
if (alphaLine) alphaLine[x] = alpha;
};
switch (pixelType) {
case PNG_PIXEL_GRAYSCALE:
if (bitsPerSample == 8) {
if (bitsPerSample == 8 && !hasAlpha && !alphaLine) {
memcpy(grayLine, pPixels, width);
} else {
for (int x = 0; x < width; x++) {
grayLine[x] = expandSampleToByte(readPackedSample(pPixels, x, bitsPerSample), bitsPerSample);
const uint8_t sample = readPackedSample(pPixels, x, bitsPerSample);
const uint8_t gray = expandSampleToByte(sample, bitsPerSample);
store(x, gray, hasAlpha && sample == static_cast<uint8_t>(transparentColor) ? 0 : 255);
}
}
break;
@@ -151,53 +169,44 @@ void convertLineToGray(const uint8_t* pPixels, uint8_t* grayLine, int width, int
case PNG_PIXEL_TRUECOLOR:
for (int x = 0; x < width; x++) {
const uint8_t* p = &pPixels[x * 3];
grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
const uint8_t gray = static_cast<uint8_t>((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
const uint32_t color = (static_cast<uint32_t>(p[0]) << 16) | (static_cast<uint32_t>(p[1]) << 8) | p[2];
store(x, gray, hasAlpha && color == transparentColor ? 0 : 255);
}
break;
case PNG_PIXEL_INDEXED:
if (palette) {
if (hasAlpha) {
for (int x = 0; x < width; x++) {
uint8_t idx = readPackedSample(pPixels, x, bitsPerSample);
uint8_t* p = &palette[idx * 3];
uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
uint8_t alpha = palette[768 + idx];
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
}
} else {
for (int x = 0; x < width; x++) {
uint8_t idx = readPackedSample(pPixels, x, bitsPerSample);
uint8_t* p = &palette[idx * 3];
grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
}
for (int x = 0; x < width; x++) {
const uint8_t idx = readPackedSample(pPixels, x, bitsPerSample);
const uint8_t* p = &palette[idx * 3];
const uint8_t gray = static_cast<uint8_t>((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
store(x, gray, hasAlpha ? palette[768 + idx] : 255);
}
} else {
for (int x = 0; x < width; x++) {
grayLine[x] = expandSampleToByte(readPackedSample(pPixels, x, bitsPerSample), bitsPerSample);
store(x, expandSampleToByte(readPackedSample(pPixels, x, bitsPerSample), bitsPerSample), 255);
}
}
break;
case PNG_PIXEL_GRAY_ALPHA:
for (int x = 0; x < width; x++) {
uint8_t gray = pPixels[x * 2];
uint8_t alpha = pPixels[x * 2 + 1];
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
store(x, pPixels[x * 2], pPixels[x * 2 + 1]);
}
break;
case PNG_PIXEL_TRUECOLOR_ALPHA:
for (int x = 0; x < width; x++) {
const uint8_t* p = &pPixels[x * 4];
uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
uint8_t alpha = p[3];
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
const uint8_t gray = static_cast<uint8_t>((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
store(x, gray, p[3]);
}
break;
default:
memset(grayLine, 128, width);
if (alphaLine) memset(alphaLine, 255, width);
break;
}
}
@@ -208,25 +217,29 @@ int pngDrawCallback(PNGDRAW* pDraw) {
int srcY = pDraw->y;
int srcWidth = ctx->srcWidth;
if (srcY < ctx->cropTop || srcY >= ctx->cropTop + ctx->visibleHeight) return 1;
const int visibleSrcY = srcY - ctx->cropTop;
// Map source rows with the exact output-height ratio. During downscaling,
// multiple source rows can select the same output row; during upscaling, one
// source row must be repeated across every output row in its range. Emitting
// only the first row of an upscale leaves zero-filled (black) gaps in the
// streamed pixel cache.
int firstDstY = (srcY * ctx->dstHeight) / ctx->srcHeight;
int firstDstY = (visibleSrcY * ctx->dstHeight) / ctx->visibleHeight;
int endDstY = firstDstY + 1;
if (ctx->dstHeight > ctx->srcHeight) {
endDstY = ((srcY + 1) * ctx->dstHeight) / ctx->srcHeight;
if (ctx->dstHeight > ctx->visibleHeight) {
endDstY = ((visibleSrcY + 1) * ctx->dstHeight) / ctx->visibleHeight;
}
if (firstDstY <= ctx->lastDstY) firstDstY = ctx->lastDstY + 1;
if (firstDstY >= endDstY || firstDstY >= ctx->dstHeight) return 1;
if (endDstY > ctx->dstHeight) endDstY = ctx->dstHeight;
// Convert entire source line to grayscale (improves cache locality)
// PNGdec parses tRNS while decoding, after open() returns, so query the
// transparent color here rather than caching its pre-decode value.
const uint32_t transparentColor = ctx->decoder ? ctx->decoder->getTransparentColor() : 0;
convertLineToGray(pDraw->pPixels, ctx->grayLineBuffer, srcWidth, pDraw->iPixelType, pDraw->iBpp, pDraw->pPalette,
pDraw->iHasAlpha);
pDraw->iHasAlpha, transparentColor, ctx->alphaLineBuffer);
// Render scaled rows using Bresenham-style integer stepping (no floating-point division)
int dstWidth = ctx->dstWidth;
@@ -261,27 +274,30 @@ int pngDrawCallback(PNGDRAW* pDraw) {
}
}
int srcX = 0;
int srcX = ctx->cropLeft;
int error = 0;
for (int dstX = 0; dstX < dstWidth; dstX++) {
int outX = outXBase + dstX;
if (outX < screenWidth) {
uint8_t gray = ctx->grayLineBuffer[srcX];
const uint8_t alpha = ctx->alphaLineBuffer ? ctx->alphaLineBuffer[srcX] : 255;
if (alpha >= 8 && alpha > alphaThreshold4x4(outX, outY)) {
uint8_t gray = ctx->grayLineBuffer[srcX];
uint8_t ditheredGray;
if (useDithering) {
ditheredGray = applyBayerDither4Level(gray, outX, outY);
} else {
ditheredGray = gray / 85;
if (ditheredGray > 3) ditheredGray = 3;
uint8_t ditheredGray;
if (useDithering) {
ditheredGray = applyBayerDither4Level(gray, outX, outY);
} else {
ditheredGray = gray / 85;
if (ditheredGray > 3) ditheredGray = 3;
}
pw.writePixel(outX, ditheredGray, ctx->alphaLineBuffer != nullptr);
if (caching) cw.writePixel(outX, ditheredGray);
}
pw.writePixel(outX, ditheredGray);
if (caching) cw.writePixel(outX, ditheredGray);
}
// Bresenham-style stepping: advance srcX based on ratio srcWidth/dstWidth
error += srcWidth;
// Bresenham-style stepping: advance srcX based on ratio visibleWidth/dstWidth
error += ctx->visibleWidth;
while (error >= dstWidth) {
error -= dstWidth;
srcX++;
@@ -340,6 +356,7 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
}
PngContext ctx;
ctx.decoder = png.get();
ctx.renderer = &renderer;
ctx.config = &config;
ctx.screenWidth = renderer.getScreenWidth();
@@ -360,28 +377,36 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
// Calculate output dimensions
ctx.srcWidth = png->getWidth();
ctx.srcHeight = png->getHeight();
ctx.cropLeft = static_cast<int>(ctx.srcWidth * std::clamp(config.sourceCropX, 0.0f, 0.99f) / 2.0f);
ctx.cropTop = static_cast<int>(ctx.srcHeight * std::clamp(config.sourceCropY, 0.0f, 0.99f) / 2.0f);
ctx.visibleWidth = ctx.srcWidth - 2 * ctx.cropLeft;
ctx.visibleHeight = ctx.srcHeight - 2 * ctx.cropTop;
if (ctx.visibleWidth <= 0 || ctx.visibleHeight <= 0) {
LOG_ERR("PNG", "PNG crop leaves no visible pixels");
return false;
}
if (config.useExactDimensions && config.maxWidth > 0 && config.maxHeight > 0) {
// Use exact dimensions as specified (avoids rounding mismatches with pre-calculated sizes)
ctx.dstWidth = config.maxWidth;
ctx.dstHeight = config.maxHeight;
ctx.scale = (float)ctx.dstWidth / ctx.srcWidth;
ctx.scale = (float)ctx.dstWidth / ctx.visibleWidth;
} else {
// Calculate scale factor to fit within maxWidth/maxHeight
float scaleX = (float)config.maxWidth / ctx.srcWidth;
float scaleY = (float)config.maxHeight / ctx.srcHeight;
float scaleX = (float)config.maxWidth / ctx.visibleWidth;
float scaleY = (float)config.maxHeight / ctx.visibleHeight;
ctx.scale = (scaleX < scaleY) ? scaleX : scaleY;
if (ctx.scale > 1.0f) ctx.scale = 1.0f; // Don't upscale
ctx.dstWidth = (int)(ctx.srcWidth * ctx.scale);
ctx.dstHeight = (int)(ctx.srcHeight * ctx.scale);
ctx.dstWidth = (int)(ctx.visibleWidth * ctx.scale);
ctx.dstHeight = (int)(ctx.visibleHeight * ctx.scale);
}
ctx.lastDstY = -1; // Reset row tracking
const int pixelType = png->getPixelType();
const int bitsPerSample = png->getBpp();
LOG_DBG("PNG", "PNG %dx%d -> %dx%d (scale %.2f), type: %d, bpp: %d", ctx.srcWidth, ctx.srcHeight, ctx.dstWidth,
ctx.dstHeight, ctx.scale, pixelType, bitsPerSample);
LOG_DBG("PNG", "PNG %dx%d (visible %dx%d) -> %dx%d (scale %.2f), type: %d, bpp: %d", ctx.srcWidth, ctx.srcHeight,
ctx.visibleWidth, ctx.visibleHeight, ctx.dstWidth, ctx.dstHeight, ctx.scale, pixelType, bitsPerSample);
const int requiredInternal = requiredPngInternalBufferBytes(ctx.srcWidth, pixelType, bitsPerSample);
if (requiredInternal > PNG_MAX_BUFFERED_PIXELS) {
@@ -410,12 +435,18 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
return false;
}
auto grayLineBuffer = makeUniqueNoThrow<uint8_t[]>(grayBufSize);
if (!grayLineBuffer) {
LOG_ERR("PNG", "Failed to allocate gray line buffer");
// tRNS chunks are parsed during decode(), so hasAlpha() is not reliable yet
// for color-key and indexed transparency. Reserve the alpha line whenever
// the caller requests preservation; opaque pixels simply store alpha 255.
const bool retainAlpha = config.preserveAlpha;
const size_t lineBufferBytes = grayBufSize * (retainAlpha ? 2u : 1u);
auto lineBuffers = makeUniqueNoThrow<uint8_t[]>(lineBufferBytes);
if (!lineBuffers) {
LOG_ERR("PNG", "Failed to allocate PNG line buffers (%u bytes)", static_cast<unsigned>(lineBufferBytes));
return false;
}
ctx.grayLineBuffer = grayLineBuffer.get();
ctx.grayLineBuffer = lineBuffers.get();
ctx.alphaLineBuffer = retainAlpha ? ctx.grayLineBuffer + grayBufSize : nullptr;
// Stream the pixel cache to disk. PNGdec delivers source scanlines top to
// bottom and we emit at most one (downscaled) output row per callback, so the
@@ -423,7 +454,7 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
// unlike the old full-image buffer it neither competes with the ~44KB decoder
// nor forces larger images to skip caching - which previously meant a full
// re-decode on every one of an image page's ~14 render passes.
ctx.caching = !config.cachePath.empty();
ctx.caching = !config.preserveAlpha && !config.cachePath.empty();
if (ctx.caching) {
if (!ctx.cache.begin(config.cachePath, ctx.dstWidth, ctx.dstHeight, config.x, config.y, 1)) {
LOG_ERR("PNG", "Failed to start cache stream, continuing without caching");
@@ -436,6 +467,7 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
unsigned long decodeTime = millis() - decodeStart;
ctx.grayLineBuffer = nullptr;
ctx.alphaLineBuffer = nullptr;
if (rc != PNG_SUCCESS) {
LOG_ERR("PNG", "Decode failed: %d", rc);
-2
View File
@@ -2138,8 +2138,6 @@ void GfxRenderer::writeGrayscalePlaneStrip(bool lsbPlane, const uint8_t* scratch
bool GfxRenderer::supportsStripGrayscale() const { return display.supportsStripGrayscale(); }
bool GfxRenderer::combinesGrayscaleBase() const { return display.combinesGrayscaleBase(); }
void GfxRenderer::freeBwBufferChunks() {
for (auto& bwBufferChunk : bwBufferChunks) {
if (bwBufferChunk) {
-3
View File
@@ -293,9 +293,6 @@ class GfxRenderer {
// numRows)), bypassing the framebuffer. supportsStripGrayscale() gates use.
void writeGrayscalePlaneStrip(bool lsbPlane, const uint8_t* scratch, int yStart, int numRows) const;
bool supportsStripGrayscale() const;
// Paper Mono: the base activation is deferred so base + gray planes go out
// as one waveform. Route the base through displayGrayscaleBase() when true.
bool combinesGrayscaleBase() const;
bool storeBwBuffer(); // Returns true if buffer was stored successfully
void restoreBwBuffer(); // Restore and free the stored buffer
void cleanupGrayscaleWithFrameBuffer() const;
-9
View File
@@ -95,7 +95,6 @@ STR_SHORT_PWR_BTN: "Short Power Button Click"
STR_ORIENTATION: "Reading Orientation"
STR_SIDE_BTN_LAYOUT: "Side Button Layout (reader)"
STR_TOUCH_READER_CONTROLS: "Touch Reader Controls"
STR_TAP_FOR_READER_MENU: "Tap For Reader Menu"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orient front buttons"
STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior"
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
@@ -185,7 +184,6 @@ STR_NEXT_PREV: "Next/Prev"
STR_KOSYNC: "KOSync"
STR_BOOKMARK_OPTION: "Bookmark"
STR_DICTIONARY: "Dictionary"
STR_READER_MENU: "Reader Menu"
STR_DISABLED: "Disabled"
STR_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans"
@@ -249,13 +247,6 @@ STR_SHOW: "Show"
STR_HIDE: "Hide"
STR_STATE_ON: "ON"
STR_STATE_OFF: "OFF"
STR_STATE_TAP: "Tap"
STR_STATE_SWIPE: "Swipe"
STR_STATE_INVERTED_TAP: "Inverted Tap"
STR_FRONTLIGHT: "Frontlight"
STR_BRIGHTNESS: "Brightness"
STR_WARMTH: "Warmth"
STR_RESTORE_LIGHT_ON_WAKE: "Restore Light on Wake"
STR_NOT_SET: "Not Set"
STR_DIR_LEFT: "Left"
STR_DIR_RIGHT: "Right"
+1 -6
View File
@@ -16,14 +16,9 @@ void safeCopy(char* dst, size_t dstSize, const char* src, size_t srcLen) {
ReleaseJsonParser::ReleaseJsonParser()
: parser(JsonCallbacks{this, sOnKey, sOnString, sOnNumber, sOnBool, sOnNull, sOnObjectStart, sOnObjectEnd,
sOnArrayStart, sOnArrayEnd}) {
safeCopy(firmwareAssetName, sizeof(firmwareAssetName), "firmware.bin", sizeof("firmware.bin") - 1);
reset();
}
void ReleaseJsonParser::setFirmwareAssetName(const char* name) {
safeCopy(firmwareAssetName, sizeof(firmwareAssetName), name, strlen(name));
}
void ReleaseJsonParser::reset() {
parser.reset();
position = Position::TOP_LEVEL;
@@ -49,7 +44,7 @@ const char* ReleaseJsonParser::getFirmwareUrl() const { return firmwareUrl; }
size_t ReleaseJsonParser::getFirmwareSize() const { return firmwareSize; }
void ReleaseJsonParser::commitAsset() {
if (strcmp(currentAssetName, firmwareAssetName) == 0) {
if (strcmp(currentAssetName, "firmware.bin") == 0) {
memcpy(firmwareUrl, currentAssetUrl, sizeof(firmwareUrl));
firmwareSize = currentAssetSize;
firmwareFound = true;
-7
View File
@@ -15,11 +15,6 @@ class ReleaseJsonParser {
void reset();
void feed(const char* data, size_t len);
// Release-asset filename to match (default "firmware.bin"). Boards with
// their own release binaries pass e.g. "firmware-papermono.bin". Survives
// reset(); truncated silently if longer than the internal buffer.
void setFirmwareAssetName(const char* name);
bool foundTag() const;
bool foundFirmware() const;
const char* getTagName() const;
@@ -70,6 +65,4 @@ class ReleaseJsonParser {
char currentAssetName[32];
char currentAssetUrl[512];
size_t currentAssetSize;
char firmwareAssetName[32];
};
-2
View File
@@ -143,8 +143,6 @@ void HalDisplay::writeGrayscalePlaneStrip(bool lsbPlane, const uint8_t* rows, ui
bool HalDisplay::supportsStripGrayscale() const { return einkDisplay.supportsStripGrayscale(); }
bool HalDisplay::combinesGrayscaleBase() const { return einkDisplay.combinesGrayscaleBase(); }
uint16_t HalDisplay::getDisplayWidth() const { return einkDisplay.getDisplayWidth(); }
uint16_t HalDisplay::getDisplayHeight() const { return einkDisplay.getDisplayHeight(); }
-7
View File
@@ -102,13 +102,6 @@ class HalDisplay {
void writeGrayscalePlaneStrip(bool lsbPlane, const uint8_t* rows, uint16_t yStart, uint16_t numRows);
bool supportsStripGrayscale() const;
// True when displayGrayscaleBase() defers the base activation so the gray
// planes join it in a single waveform (Paper Mono). Callers should then route
// the base of a grayscale page through displayGrayscaleBase() instead of
// displayBuffer(): a separate B/W refresh first makes the gray pass re-drive
// the whole text body (a visible flash).
bool combinesGrayscaleBase() const;
// Runtime geometry passthrough
uint16_t getDisplayWidth() const;
uint16_t getDisplayHeight() const;
-31
View File
@@ -1,31 +0,0 @@
#include "HalFrontlight.h"
#include <Logging.h>
HalFrontlight HalFrontlight::instance;
void HalFrontlight::begin(const uint8_t brightness, const uint8_t warmth, const bool on) {
if (!manager.present()) return;
manager.begin();
lastBrightness = brightness > 100 ? 100 : brightness;
manager.setColorTemperature(warmth > 100 ? 100 : warmth);
lit = on;
manager.setBrightness(lit ? lastBrightness : 0);
LOG_INF("LIGHT", "Frontlight up: %u%% warm=%u%% %s", lastBrightness, manager.colorTemperature(), lit ? "on" : "off");
}
void HalFrontlight::setBrightness(const uint8_t percent) {
lastBrightness = percent > 100 ? 100 : percent;
if (lit) manager.setBrightness(lastBrightness);
}
void HalFrontlight::setWarmth(const uint8_t warmPercent) {
manager.setColorTemperature(warmPercent > 100 ? 100 : warmPercent);
}
void HalFrontlight::setOn(const bool on) {
if (on == lit) return;
lit = on;
manager.setBrightness(lit ? lastBrightness : 0);
}
-36
View File
@@ -1,36 +0,0 @@
#pragma once
#include <FrontlightManager.h>
// Thin firmware HAL over the SDK frontlight manager. It is inert on boards
// without a frontlight, so callers do not need board-specific conditionals.
class HalFrontlight {
public:
static HalFrontlight& getInstance() { return instance; }
void begin(uint8_t brightness, uint8_t warmth, bool on);
bool present() const { return manager.present(); }
bool hasColorTemperature() const { return manager.hasColorTemperature(); }
void setBrightness(uint8_t percent);
void setWarmth(uint8_t warmPercent);
void setOn(bool on);
uint8_t brightness() const { return lastBrightness; }
uint8_t warmth() const { return manager.colorTemperature(); }
bool isOn() const { return lit; }
private:
HalFrontlight() = default;
FrontlightManager manager;
// The SDK represents off as brightness 0. Keep the selected brightness so
// toggling back on restores it.
uint8_t lastBrightness = 60;
bool lit = false;
static HalFrontlight instance;
};
#define Frontlight HalFrontlight::getInstance()
Loaded 20 of 66 files, more files were not shown because too many files have changed in this diff. Show more