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:
@@ -138,7 +138,7 @@ bool HomeActivity::storeCoverBuffer() {
|
||||
// Free any existing buffer first
|
||||
freeCoverBuffer();
|
||||
|
||||
const size_t bufferSize = GfxRenderer::getBufferSize();
|
||||
const size_t bufferSize = renderer.getBufferSize();
|
||||
coverBuffer = static_cast<uint8_t*>(malloc(bufferSize));
|
||||
if (!coverBuffer) {
|
||||
return false;
|
||||
@@ -158,7 +158,7 @@ bool HomeActivity::restoreCoverBuffer() {
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t bufferSize = GfxRenderer::getBufferSize();
|
||||
const size_t bufferSize = renderer.getBufferSize();
|
||||
memcpy(frameBuffer, coverBuffer, bufferSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -140,7 +140,10 @@ void BaseTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1, const c
|
||||
constexpr int buttonHeight = BaseMetrics::values.buttonHintsHeight;
|
||||
constexpr int buttonY = BaseMetrics::values.buttonHintsHeight; // Distance from bottom
|
||||
constexpr int textYOffset = 7; // Distance from top of button to text baseline
|
||||
constexpr int buttonPositions[] = {25, 130, 245, 350};
|
||||
// X3 has wider screen in portrait (528 vs 480), use more spacing
|
||||
constexpr int x4ButtonPositions[] = {25, 130, 245, 350};
|
||||
constexpr int x3ButtonPositions[] = {38, 154, 268, 384};
|
||||
const int* buttonPositions = gpio.deviceIsX3() ? x3ButtonPositions : x4ButtonPositions;
|
||||
const char* labels[] = {btn1, btn2, btn3, btn4};
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
@@ -162,50 +165,63 @@ void BaseTheme::drawSideButtonHints(const GfxRenderer& renderer, const char* top
|
||||
const int screenWidth = renderer.getScreenWidth();
|
||||
constexpr int buttonWidth = BaseMetrics::values.sideButtonHintsWidth; // Width on screen (height when rotated)
|
||||
constexpr int buttonHeight = 80; // Height on screen (width when rotated)
|
||||
constexpr int buttonX = 4; // Distance from right edge
|
||||
// Position for the button group - buttons share a border so they're adjacent
|
||||
constexpr int topButtonY = 345; // Top button position
|
||||
constexpr int buttonMargin = 4;
|
||||
|
||||
const char* labels[] = {topBtn, bottomBtn};
|
||||
if (gpio.deviceIsX3()) {
|
||||
// X3 layout: Up on left side, Down on right side, positioned higher
|
||||
constexpr int x3ButtonY = 155;
|
||||
|
||||
// Draw the shared border for both buttons as one unit
|
||||
const int x = screenWidth - buttonX - buttonWidth;
|
||||
|
||||
// Draw top button outline (3 sides, bottom open)
|
||||
if (topBtn != nullptr && topBtn[0] != '\0') {
|
||||
renderer.drawLine(x, topButtonY, x + buttonWidth - 1, topButtonY); // Top
|
||||
renderer.drawLine(x, topButtonY, x, topButtonY + buttonHeight - 1); // Left
|
||||
renderer.drawLine(x + buttonWidth - 1, topButtonY, x + buttonWidth - 1, topButtonY + buttonHeight - 1); // Right
|
||||
}
|
||||
|
||||
// Draw shared middle border
|
||||
if ((topBtn != nullptr && topBtn[0] != '\0') || (bottomBtn != nullptr && bottomBtn[0] != '\0')) {
|
||||
renderer.drawLine(x, topButtonY + buttonHeight, x + buttonWidth - 1, topButtonY + buttonHeight); // Shared border
|
||||
}
|
||||
|
||||
// Draw bottom button outline (3 sides, top is shared)
|
||||
if (bottomBtn != nullptr && bottomBtn[0] != '\0') {
|
||||
renderer.drawLine(x, topButtonY + buttonHeight, x, topButtonY + 2 * buttonHeight - 1); // Left
|
||||
renderer.drawLine(x + buttonWidth - 1, topButtonY + buttonHeight, x + buttonWidth - 1,
|
||||
topButtonY + 2 * buttonHeight - 1); // Right
|
||||
renderer.drawLine(x, topButtonY + 2 * buttonHeight - 1, x + buttonWidth - 1,
|
||||
topButtonY + 2 * buttonHeight - 1); // Bottom
|
||||
}
|
||||
|
||||
// Draw text for each button
|
||||
for (int i = 0; i < 2; i++) {
|
||||
if (labels[i] != nullptr && labels[i][0] != '\0') {
|
||||
const int y = topButtonY + i * buttonHeight;
|
||||
|
||||
// Draw rotated text centered in the button
|
||||
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, labels[i]);
|
||||
if (topBtn != nullptr && topBtn[0] != '\0') {
|
||||
const int leftX = buttonMargin;
|
||||
renderer.drawRect(leftX, x3ButtonY, buttonWidth, buttonHeight);
|
||||
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, topBtn);
|
||||
const int textHeight = renderer.getTextHeight(SMALL_FONT_ID);
|
||||
const int textX = leftX + (buttonWidth - textHeight) / 2;
|
||||
const int textY = x3ButtonY + (buttonHeight + textWidth) / 2;
|
||||
renderer.drawTextRotated90CW(SMALL_FONT_ID, textX, textY, topBtn);
|
||||
}
|
||||
|
||||
// Center the rotated text in the button
|
||||
const int textX = x + (buttonWidth - textHeight) / 2;
|
||||
const int textY = y + (buttonHeight + textWidth) / 2;
|
||||
if (bottomBtn != nullptr && bottomBtn[0] != '\0') {
|
||||
const int rightX = screenWidth - buttonMargin - buttonWidth;
|
||||
renderer.drawRect(rightX, x3ButtonY, buttonWidth, buttonHeight);
|
||||
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, bottomBtn);
|
||||
const int textHeight = renderer.getTextHeight(SMALL_FONT_ID);
|
||||
const int textX = rightX + (buttonWidth - textHeight) / 2;
|
||||
const int textY = x3ButtonY + (buttonHeight + textWidth) / 2;
|
||||
renderer.drawTextRotated90CW(SMALL_FONT_ID, textX, textY, bottomBtn);
|
||||
}
|
||||
} else {
|
||||
// X4 layout: Both buttons stacked on right side
|
||||
constexpr int topButtonY = 345;
|
||||
const char* labels[] = {topBtn, bottomBtn};
|
||||
const int x = screenWidth - buttonMargin - buttonWidth;
|
||||
|
||||
renderer.drawTextRotated90CW(SMALL_FONT_ID, textX, textY, labels[i]);
|
||||
if (topBtn != nullptr && topBtn[0] != '\0') {
|
||||
renderer.drawLine(x, topButtonY, x + buttonWidth - 1, topButtonY);
|
||||
renderer.drawLine(x, topButtonY, x, topButtonY + buttonHeight - 1);
|
||||
renderer.drawLine(x + buttonWidth - 1, topButtonY, x + buttonWidth - 1, topButtonY + buttonHeight - 1);
|
||||
}
|
||||
|
||||
if ((topBtn != nullptr && topBtn[0] != '\0') || (bottomBtn != nullptr && bottomBtn[0] != '\0')) {
|
||||
renderer.drawLine(x, topButtonY + buttonHeight, x + buttonWidth - 1, topButtonY + buttonHeight);
|
||||
}
|
||||
|
||||
if (bottomBtn != nullptr && bottomBtn[0] != '\0') {
|
||||
renderer.drawLine(x, topButtonY + buttonHeight, x, topButtonY + 2 * buttonHeight - 1);
|
||||
renderer.drawLine(x + buttonWidth - 1, topButtonY + buttonHeight, x + buttonWidth - 1,
|
||||
topButtonY + 2 * buttonHeight - 1);
|
||||
renderer.drawLine(x, topButtonY + 2 * buttonHeight - 1, x + buttonWidth - 1, topButtonY + 2 * buttonHeight - 1);
|
||||
}
|
||||
|
||||
for (int i = 0; i < 2; i++) {
|
||||
if (labels[i] != nullptr && labels[i][0] != '\0') {
|
||||
const int y = topButtonY + i * buttonHeight;
|
||||
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, labels[i]);
|
||||
const int textHeight = renderer.getTextHeight(SMALL_FONT_ID);
|
||||
const int textX = x + (buttonWidth - textHeight) / 2;
|
||||
const int textY = y + (buttonHeight + textWidth) / 2;
|
||||
renderer.drawTextRotated90CW(SMALL_FONT_ID, textX, textY, labels[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,7 +342,10 @@ void LyraTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1, const c
|
||||
constexpr int buttonHeight = LyraMetrics::values.buttonHintsHeight;
|
||||
constexpr int buttonY = LyraMetrics::values.buttonHintsHeight; // Distance from bottom
|
||||
constexpr int textYOffset = 7; // Distance from top of button to text baseline
|
||||
constexpr int buttonPositions[] = {58, 146, 254, 342};
|
||||
// X3 has wider screen in portrait (528 vs 480), use more spacing
|
||||
constexpr int x4ButtonPositions[] = {58, 146, 254, 342};
|
||||
constexpr int x3ButtonPositions[] = {65, 157, 291, 383};
|
||||
const int* buttonPositions = gpio.deviceIsX3() ? x3ButtonPositions : x4ButtonPositions;
|
||||
const char* labels[] = {btn1, btn2, btn3, btn4};
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
@@ -371,34 +374,47 @@ void LyraTheme::drawSideButtonHints(const GfxRenderer& renderer, const char* top
|
||||
const int screenWidth = renderer.getScreenWidth();
|
||||
constexpr int buttonWidth = LyraMetrics::values.sideButtonHintsWidth; // Width on screen (height when rotated)
|
||||
constexpr int buttonHeight = 78; // Height on screen (width when rotated)
|
||||
// Position for the button group - buttons share a border so they're adjacent
|
||||
constexpr int buttonMargin = 0;
|
||||
|
||||
const char* labels[] = {topBtn, bottomBtn};
|
||||
if (gpio.deviceIsX3()) {
|
||||
// X3 layout: Up on left side, Down on right side, positioned higher
|
||||
constexpr int x3ButtonY = 155;
|
||||
|
||||
// Draw the shared border for both buttons as one unit
|
||||
const int x = screenWidth - buttonWidth;
|
||||
if (topBtn != nullptr && topBtn[0] != '\0') {
|
||||
renderer.drawRoundedRect(buttonMargin, x3ButtonY, buttonWidth, buttonHeight, 1, cornerRadius, false, true, false,
|
||||
true, true);
|
||||
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, topBtn);
|
||||
renderer.drawTextRotated90CW(SMALL_FONT_ID, buttonMargin, x3ButtonY + (buttonHeight + textWidth) / 2, topBtn);
|
||||
}
|
||||
|
||||
// Draw top button outline
|
||||
if (topBtn != nullptr && topBtn[0] != '\0') {
|
||||
renderer.drawRoundedRect(x, topHintButtonY, buttonWidth, buttonHeight, 1, cornerRadius, true, false, true, false,
|
||||
true);
|
||||
}
|
||||
if (bottomBtn != nullptr && bottomBtn[0] != '\0') {
|
||||
const int rightX = screenWidth - buttonWidth;
|
||||
renderer.drawRoundedRect(rightX, x3ButtonY, buttonWidth, buttonHeight, 1, cornerRadius, true, false, true, false,
|
||||
true);
|
||||
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, bottomBtn);
|
||||
renderer.drawTextRotated90CW(SMALL_FONT_ID, rightX, x3ButtonY + (buttonHeight + textWidth) / 2, bottomBtn);
|
||||
}
|
||||
} else {
|
||||
// X4 layout: Both buttons stacked on right side
|
||||
const char* labels[] = {topBtn, bottomBtn};
|
||||
const int x = screenWidth - buttonWidth;
|
||||
|
||||
// Draw bottom button outline
|
||||
if (bottomBtn != nullptr && bottomBtn[0] != '\0') {
|
||||
renderer.drawRoundedRect(x, topHintButtonY + buttonHeight + 5, buttonWidth, buttonHeight, 1, cornerRadius, true,
|
||||
false, true, false, true);
|
||||
}
|
||||
if (topBtn != nullptr && topBtn[0] != '\0') {
|
||||
renderer.drawRoundedRect(x, topHintButtonY, buttonWidth, buttonHeight, 1, cornerRadius, true, false, true, false,
|
||||
true);
|
||||
}
|
||||
|
||||
// Draw text for each button
|
||||
for (int i = 0; i < 2; i++) {
|
||||
if (labels[i] != nullptr && labels[i][0] != '\0') {
|
||||
const int y = topHintButtonY + (i * buttonHeight + 5);
|
||||
if (bottomBtn != nullptr && bottomBtn[0] != '\0') {
|
||||
renderer.drawRoundedRect(x, topHintButtonY + buttonHeight + 5, buttonWidth, buttonHeight, 1, cornerRadius, true,
|
||||
false, true, false, true);
|
||||
}
|
||||
|
||||
// Draw rotated text centered in the button
|
||||
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, labels[i]);
|
||||
|
||||
renderer.drawTextRotated90CW(SMALL_FONT_ID, x, y + (buttonHeight + textWidth) / 2, labels[i]);
|
||||
for (int i = 0; i < 2; i++) {
|
||||
if (labels[i] != nullptr && labels[i][0] != '\0') {
|
||||
const int y = topHintButtonY + (i * buttonHeight) + 5;
|
||||
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, labels[i]);
|
||||
renderer.drawTextRotated90CW(SMALL_FONT_ID, x, y + (buttonHeight + textWidth) / 2, labels[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
26
src/main.cpp
26
src/main.cpp
@@ -27,8 +27,6 @@
|
||||
#include "util/ButtonNavigator.h"
|
||||
#include "util/ScreenshotUtil.h"
|
||||
|
||||
HalDisplay display;
|
||||
HalGPIO gpio;
|
||||
MappedInputManager mappedInputManager(gpio);
|
||||
GfxRenderer renderer(display);
|
||||
ActivityManager activityManager(renderer, mappedInputManager);
|
||||
@@ -171,7 +169,6 @@ void verifyPowerButtonDuration() {
|
||||
powerManager.startDeepSleep(gpio);
|
||||
}
|
||||
}
|
||||
|
||||
void waitForPowerRelease() {
|
||||
gpio.update();
|
||||
while (gpio.isPressed(HalGPIO::BTN_POWER)) {
|
||||
@@ -189,7 +186,6 @@ void enterDeepSleep() {
|
||||
activityManager.goToSleep();
|
||||
|
||||
display.deepSleep();
|
||||
LOG_DBG("MAIN", "Power button press calibration value: %lu ms", t2 - t1);
|
||||
LOG_DBG("MAIN", "Entering deep sleep");
|
||||
|
||||
powerManager.startDeepSleep(gpio);
|
||||
@@ -235,15 +231,17 @@ void setup() {
|
||||
gpio.begin();
|
||||
powerManager.begin();
|
||||
|
||||
// Only start serial if USB connected
|
||||
#ifdef ENABLE_SERIAL_LOG
|
||||
if (gpio.isUsbConnected()) {
|
||||
Serial.begin(115200);
|
||||
// Wait up to 3 seconds for Serial to be ready to catch early logs
|
||||
unsigned long start = millis();
|
||||
while (!Serial && (millis() - start) < 3000) {
|
||||
const unsigned long start = millis();
|
||||
while (!Serial && (millis() - start) < 500) {
|
||||
delay(10);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
LOG_INF("MAIN", "Hardware detect: %s", gpio.deviceIsX3() ? "X3" : "X4");
|
||||
|
||||
// SD Card Initialization
|
||||
// We need 6 open files concurrently when parsing a new chapter
|
||||
@@ -263,11 +261,12 @@ void setup() {
|
||||
UITheme::getInstance().reload();
|
||||
ButtonNavigator::setMappedInputManager(mappedInputManager);
|
||||
|
||||
switch (gpio.getWakeupReason()) {
|
||||
const auto wakeupReason = gpio.getWakeupReason();
|
||||
switch (wakeupReason) {
|
||||
case HalGPIO::WakeupReason::PowerButton:
|
||||
// For normal wakeups, verify power button press duration
|
||||
LOG_DBG("MAIN", "Verifying power button press duration");
|
||||
verifyPowerButtonDuration();
|
||||
gpio.verifyPowerButtonWakeup(SETTINGS.getPowerButtonDuration(),
|
||||
SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::SLEEP);
|
||||
break;
|
||||
case HalGPIO::WakeupReason::AfterUSBPower:
|
||||
// If USB power caused a cold boot, go back to sleep
|
||||
@@ -332,9 +331,10 @@ void loop() {
|
||||
String cmd = line.substring(4);
|
||||
cmd.trim();
|
||||
if (cmd == "SCREENSHOT") {
|
||||
logSerial.printf("SCREENSHOT_START:%d\n", HalDisplay::BUFFER_SIZE);
|
||||
const uint32_t bufferSize = display.getBufferSize();
|
||||
logSerial.printf("SCREENSHOT_START:%d\n", bufferSize);
|
||||
uint8_t* buf = display.getFrameBuffer();
|
||||
logSerial.write(buf, HalDisplay::BUFFER_SIZE);
|
||||
logSerial.write(buf, bufferSize);
|
||||
logSerial.printf("SCREENSHOT_END\n");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user