feat: Initial support for the x3 (#875)
## Summary Adds Xteink X3 hardware support to CrossPoint Reader. The X3 uses the same SSD1677 e-ink controller as the X4 but with a different panel (792x528 vs 800x480), different button layout, and an I2C fuel gauge (BQ27220) instead of ADC-based battery reading. All X3-specific behavior is gated by runtime device detection — X4 behavior is unchanged. Depends on community-sdk X3 support: open-x4-epaper/community-sdk#19 (merged). ## Changes ### HAL Layer **HalGPIO** (`lib/hal/HalGPIO.cpp/.h`) - I2C-based device fingerprinting at boot: probes for BQ27220 fuel gauge, DS3231 RTC, and QMI8658 IMU to distinguish X3 from X4 - Detection result cached in NVS for fast subsequent boots - Exposes `deviceIsX3()` / `deviceIsX4()` helpers used throughout the codebase - X3 button mapping (7 GPIOs vs X4's layout) - USB connection detection and wake classification for X3 **HalDisplay** (`lib/hal/HalDisplay.cpp/.h`) - Calls `einkDisplay.setDisplayX3()` before init when X3 is detected - Requests display resync after power button / flash wake events - Runtime display dimension accessors (`getDisplayWidth()`, `getDisplayHeight()`, `getBufferSize()`) - Exposed as global `display` instance for use by image converters **HalPowerManager** (`lib/hal/HalPowerManager.cpp/.h`) - X3 battery reading via I2C fuel gauge (BQ27220 at 0x55, SOC register) - X3 power button uses GPIO hold for deep sleep ### Display & Rendering **GfxRenderer** (`lib/GfxRenderer/GfxRenderer.cpp/.h`) - Buffer size and display dimensions are now runtime values (not compile-time constants) to support both panel sizes - X3 anti-aliasing tuning: only the darker grayscale level is applied to avoid washed-out text on the X3 panel. X4 retains both levels via `deviceIsX4()` gate **Image Converters** (`lib/JpegToBmpConverter`, `lib/PngToBmpConverter`) - Cover image prescale target uses runtime display dimensions from HAL instead of hardcoded 800x480 ### UI Themes **BaseTheme / LyraTheme** (`src/components/themes/`) - X3 button position mapping for the different physical layout - Adjusted UI element positioning for 792x528 viewport ### Boot & Init **main.cpp** - X3 hardware detection logging - Adjusted init sequence for X3 (no `HalSystem::begin()` dependency on X3 path) **HomeActivity** - Uses runtime `renderer.getBufferSize()` instead of static `GfxRenderer::getBufferSize()` FYI I did not add support for the gyro page turner. That can be it's own PR.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
#include "GfxRenderer.h"
|
||||
|
||||
#include <FontDecompressor.h>
|
||||
#include <HalGPIO.h>
|
||||
#include <Logging.h>
|
||||
#include <Utf8.h>
|
||||
|
||||
@@ -28,6 +29,11 @@ void GfxRenderer::begin() {
|
||||
LOG_ERR("GFX", "!! No framebuffer");
|
||||
assert(false);
|
||||
}
|
||||
panelWidth = display.getDisplayWidth();
|
||||
panelHeight = display.getDisplayHeight();
|
||||
panelWidthBytes = display.getDisplayWidthBytes();
|
||||
frameBufferSize = display.getBufferSize();
|
||||
bwBufferChunks.assign((frameBufferSize + BW_BUFFER_CHUNK_SIZE - 1) / BW_BUFFER_CHUNK_SIZE, nullptr);
|
||||
}
|
||||
|
||||
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap.insert({fontId, font}); }
|
||||
@@ -35,25 +41,25 @@ void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap.ins
|
||||
// Translate logical (x,y) coordinates to physical panel coordinates based on current orientation
|
||||
// This should always be inlined for better performance
|
||||
static inline void rotateCoordinates(const GfxRenderer::Orientation orientation, const int x, const int y, int* phyX,
|
||||
int* phyY) {
|
||||
int* phyY, const uint16_t panelWidth, const uint16_t panelHeight) {
|
||||
switch (orientation) {
|
||||
case GfxRenderer::Portrait: {
|
||||
// Logical portrait (480x800) → panel (800x480)
|
||||
// Rotation: 90 degrees clockwise
|
||||
*phyX = y;
|
||||
*phyY = HalDisplay::DISPLAY_HEIGHT - 1 - x;
|
||||
*phyY = panelHeight - 1 - x;
|
||||
break;
|
||||
}
|
||||
case GfxRenderer::LandscapeClockwise: {
|
||||
// Logical landscape (800x480) rotated 180 degrees (swap top/bottom and left/right)
|
||||
*phyX = HalDisplay::DISPLAY_WIDTH - 1 - x;
|
||||
*phyY = HalDisplay::DISPLAY_HEIGHT - 1 - y;
|
||||
*phyX = panelWidth - 1 - x;
|
||||
*phyY = panelHeight - 1 - y;
|
||||
break;
|
||||
}
|
||||
case GfxRenderer::PortraitInverted: {
|
||||
// Logical portrait (480x800) → panel (800x480)
|
||||
// Rotation: 90 degrees counter-clockwise
|
||||
*phyX = HalDisplay::DISPLAY_WIDTH - 1 - y;
|
||||
*phyX = panelWidth - 1 - y;
|
||||
*phyY = x;
|
||||
break;
|
||||
}
|
||||
@@ -125,8 +131,9 @@ static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode
|
||||
if (renderMode == GfxRenderer::BW && bmpVal < 3) {
|
||||
// Black (also paints over the grays in BW mode)
|
||||
renderer.drawPixel(screenX, screenY, pixelState);
|
||||
} else if (renderMode == GfxRenderer::GRAYSCALE_MSB && (bmpVal == 1 || bmpVal == 2)) {
|
||||
} else if (renderMode == GfxRenderer::GRAYSCALE_MSB && (bmpVal == 1 || (gpio.deviceIsX4() && bmpVal == 2))) {
|
||||
// Light gray (also mark the MSB if it's going to be a dark gray too)
|
||||
// X3 AA tuning: keep only the darker antialias level to avoid washed text
|
||||
// We have to flag pixels in reverse for the gray buffers, as 0 leave alone, 1 update
|
||||
renderer.drawPixel(screenX, screenY, false);
|
||||
} else if (renderMode == GfxRenderer::GRAYSCALE_LSB && bmpVal == 1) {
|
||||
@@ -168,16 +175,16 @@ void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
|
||||
int phyY = 0;
|
||||
|
||||
// Note: this call should be inlined for better performance
|
||||
rotateCoordinates(orientation, x, y, &phyX, &phyY);
|
||||
rotateCoordinates(orientation, x, y, &phyX, &phyY, panelWidth, panelHeight);
|
||||
|
||||
// Bounds checking against physical panel dimensions
|
||||
if (phyX < 0 || phyX >= HalDisplay::DISPLAY_WIDTH || phyY < 0 || phyY >= HalDisplay::DISPLAY_HEIGHT) {
|
||||
// Bounds checking against runtime panel dimensions
|
||||
if (phyX < 0 || phyX >= panelWidth || phyY < 0 || phyY >= panelHeight) {
|
||||
LOG_ERR("GFX", "!! Outside range (%d, %d) -> (%d, %d)", x, y, phyX, phyY);
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate byte position and bit position
|
||||
const uint16_t byteIndex = phyY * HalDisplay::DISPLAY_WIDTH_BYTES + (phyX / 8);
|
||||
const uint32_t byteIndex = static_cast<uint32_t>(phyY) * panelWidthBytes + (phyX / 8);
|
||||
const uint8_t bitPosition = 7 - (phyX % 8); // MSB first
|
||||
|
||||
if (state) {
|
||||
@@ -556,7 +563,7 @@ void GfxRenderer::fillRoundedRect(const int x, const int y, const int width, con
|
||||
void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, const int width, const int height) const {
|
||||
int rotatedX = 0;
|
||||
int rotatedY = 0;
|
||||
rotateCoordinates(orientation, x, y, &rotatedX, &rotatedY);
|
||||
rotateCoordinates(orientation, x, y, &rotatedX, &rotatedY, panelWidth, panelHeight);
|
||||
// Rotate origin corner
|
||||
switch (orientation) {
|
||||
case Portrait:
|
||||
@@ -596,12 +603,24 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
|
||||
LOG_DBG("GFX", "Cropping %dx%d by %dx%d pix, is %s", bitmap.getWidth(), bitmap.getHeight(), cropPixX, cropPixY,
|
||||
bitmap.isTopDown() ? "top-down" : "bottom-up");
|
||||
|
||||
if (maxWidth > 0 && (1.0f - cropX) * bitmap.getWidth() > maxWidth) {
|
||||
scale = static_cast<float>(maxWidth) / static_cast<float>((1.0f - cropX) * bitmap.getWidth());
|
||||
isScaled = true;
|
||||
const float croppedWidth = (1.0f - cropX) * static_cast<float>(bitmap.getWidth());
|
||||
const float croppedHeight = (1.0f - cropY) * static_cast<float>(bitmap.getHeight());
|
||||
bool hasTargetBounds = false;
|
||||
float fitScale = 1.0f;
|
||||
|
||||
if (maxWidth > 0 && croppedWidth > 0.0f) {
|
||||
fitScale = static_cast<float>(maxWidth) / croppedWidth;
|
||||
hasTargetBounds = true;
|
||||
}
|
||||
if (maxHeight > 0 && (1.0f - cropY) * bitmap.getHeight() > maxHeight) {
|
||||
scale = std::min(scale, static_cast<float>(maxHeight) / static_cast<float>((1.0f - cropY) * bitmap.getHeight()));
|
||||
|
||||
if (maxHeight > 0 && croppedHeight > 0.0f) {
|
||||
const float heightScale = static_cast<float>(maxHeight) / croppedHeight;
|
||||
fitScale = hasTargetBounds ? std::min(fitScale, heightScale) : heightScale;
|
||||
hasTargetBounds = true;
|
||||
}
|
||||
|
||||
if (hasTargetBounds && fitScale < 1.0f) {
|
||||
scale = fitScale;
|
||||
isScaled = true;
|
||||
}
|
||||
LOG_DBG("GFX", "Scaling by %f - %s", scale, isScaled ? "scaled" : "not scaled");
|
||||
@@ -664,7 +683,7 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
|
||||
|
||||
if (renderMode == BW && val < 3) {
|
||||
drawPixel(screenX, screenY);
|
||||
} else if (renderMode == GRAYSCALE_MSB && (val == 1 || val == 2)) {
|
||||
} else if (renderMode == GRAYSCALE_MSB && (val == 1 || (gpio.deviceIsX4() && val == 2))) {
|
||||
drawPixel(screenX, screenY, false);
|
||||
} else if (renderMode == GRAYSCALE_LSB && val == 1) {
|
||||
drawPixel(screenX, screenY, false);
|
||||
@@ -822,7 +841,7 @@ void GfxRenderer::clearScreen(const uint8_t color) const {
|
||||
}
|
||||
|
||||
void GfxRenderer::invertScreen() const {
|
||||
for (int i = 0; i < HalDisplay::BUFFER_SIZE; i++) {
|
||||
for (uint32_t i = 0; i < frameBufferSize; i++) {
|
||||
frameBuffer[i] = ~frameBuffer[i];
|
||||
}
|
||||
}
|
||||
@@ -923,13 +942,13 @@ int GfxRenderer::getScreenWidth() const {
|
||||
case Portrait:
|
||||
case PortraitInverted:
|
||||
// 480px wide in portrait logical coordinates
|
||||
return HalDisplay::DISPLAY_HEIGHT;
|
||||
return panelHeight;
|
||||
case LandscapeClockwise:
|
||||
case LandscapeCounterClockwise:
|
||||
// 800px wide in landscape logical coordinates
|
||||
return HalDisplay::DISPLAY_WIDTH;
|
||||
return panelWidth;
|
||||
}
|
||||
return HalDisplay::DISPLAY_HEIGHT;
|
||||
return panelHeight;
|
||||
}
|
||||
|
||||
int GfxRenderer::getScreenHeight() const {
|
||||
@@ -937,13 +956,13 @@ int GfxRenderer::getScreenHeight() const {
|
||||
case Portrait:
|
||||
case PortraitInverted:
|
||||
// 800px tall in portrait logical coordinates
|
||||
return HalDisplay::DISPLAY_WIDTH;
|
||||
return panelWidth;
|
||||
case LandscapeClockwise:
|
||||
case LandscapeCounterClockwise:
|
||||
// 480px tall in landscape logical coordinates
|
||||
return HalDisplay::DISPLAY_HEIGHT;
|
||||
return panelHeight;
|
||||
}
|
||||
return HalDisplay::DISPLAY_WIDTH;
|
||||
return panelWidth;
|
||||
}
|
||||
|
||||
int GfxRenderer::getSpaceWidth(const int fontId, const EpdFontFamily::Style style) const {
|
||||
@@ -1095,7 +1114,7 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
|
||||
|
||||
uint8_t* GfxRenderer::getFrameBuffer() const { return frameBuffer; }
|
||||
|
||||
size_t GfxRenderer::getBufferSize() { return HalDisplay::BUFFER_SIZE; }
|
||||
size_t GfxRenderer::getBufferSize() const { return frameBufferSize; }
|
||||
|
||||
// unused
|
||||
// void GfxRenderer::grayscaleRevert() const { display.grayscaleRevert(); }
|
||||
@@ -1123,7 +1142,7 @@ void GfxRenderer::freeBwBufferChunks() {
|
||||
*/
|
||||
bool GfxRenderer::storeBwBuffer() {
|
||||
// Allocate and copy each chunk
|
||||
for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) {
|
||||
for (size_t i = 0; i < bwBufferChunks.size(); i++) {
|
||||
// Check if any chunks are already allocated
|
||||
if (bwBufferChunks[i]) {
|
||||
LOG_ERR("GFX", "!! BW buffer chunk %zu already stored - this is likely a bug, freeing chunk", i);
|
||||
@@ -1132,19 +1151,20 @@ bool GfxRenderer::storeBwBuffer() {
|
||||
}
|
||||
|
||||
const size_t offset = i * BW_BUFFER_CHUNK_SIZE;
|
||||
bwBufferChunks[i] = static_cast<uint8_t*>(malloc(BW_BUFFER_CHUNK_SIZE));
|
||||
const size_t chunkSize = std::min(BW_BUFFER_CHUNK_SIZE, static_cast<size_t>(frameBufferSize - offset));
|
||||
bwBufferChunks[i] = static_cast<uint8_t*>(malloc(chunkSize));
|
||||
|
||||
if (!bwBufferChunks[i]) {
|
||||
LOG_ERR("GFX", "!! Failed to allocate BW buffer chunk %zu (%zu bytes)", i, BW_BUFFER_CHUNK_SIZE);
|
||||
LOG_ERR("GFX", "!! Failed to allocate BW buffer chunk %zu (%zu bytes)", i, chunkSize);
|
||||
// Free previously allocated chunks
|
||||
freeBwBufferChunks();
|
||||
return false;
|
||||
}
|
||||
|
||||
memcpy(bwBufferChunks[i], frameBuffer + offset, BW_BUFFER_CHUNK_SIZE);
|
||||
memcpy(bwBufferChunks[i], frameBuffer + offset, chunkSize);
|
||||
}
|
||||
|
||||
LOG_DBG("GFX", "Stored BW buffer in %zu chunks (%zu bytes each)", BW_BUFFER_NUM_CHUNKS, BW_BUFFER_CHUNK_SIZE);
|
||||
LOG_DBG("GFX", "Stored BW buffer in %zu chunks (%zu bytes each)", bwBufferChunks.size(), BW_BUFFER_CHUNK_SIZE);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1168,9 +1188,10 @@ void GfxRenderer::restoreBwBuffer() {
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) {
|
||||
for (size_t i = 0; i < bwBufferChunks.size(); i++) {
|
||||
const size_t offset = i * BW_BUFFER_CHUNK_SIZE;
|
||||
memcpy(frameBuffer + offset, bwBufferChunks[i], BW_BUFFER_CHUNK_SIZE);
|
||||
const size_t chunkSize = std::min(BW_BUFFER_CHUNK_SIZE, static_cast<size_t>(frameBufferSize - offset));
|
||||
memcpy(frameBuffer + offset, bwBufferChunks[i], chunkSize);
|
||||
}
|
||||
|
||||
display.cleanupGrayscaleBuffers(frameBuffer);
|
||||
|
||||
Reference in New Issue
Block a user