diff --git a/lib/Epub/Epub/converters/ImageToFramebufferDecoder.h b/lib/Epub/Epub/converters/ImageToFramebufferDecoder.h index e10d2f30..afb5c3bd 100644 --- a/lib/Epub/Epub/converters/ImageToFramebufferDecoder.h +++ b/lib/Epub/Epub/converters/ImageToFramebufferDecoder.h @@ -37,4 +37,4 @@ class ImageToFramebufferDecoder { bool validateImageDimensions(int width, int height, const std::string& format); void warnUnsupportedFeature(const std::string& feature, const std::string& imagePath); -}; \ No newline at end of file +}; diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index 7caba7c4..50aa2db1 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -1,6 +1,7 @@ #include "GfxRenderer.h" #include +#include #include #include @@ -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(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(maxWidth) / static_cast((1.0f - cropX) * bitmap.getWidth()); - isScaled = true; + const float croppedWidth = (1.0f - cropX) * static_cast(bitmap.getWidth()); + const float croppedHeight = (1.0f - cropY) * static_cast(bitmap.getHeight()); + bool hasTargetBounds = false; + float fitScale = 1.0f; + + if (maxWidth > 0 && croppedWidth > 0.0f) { + fitScale = static_cast(maxWidth) / croppedWidth; + hasTargetBounds = true; } - if (maxHeight > 0 && (1.0f - cropY) * bitmap.getHeight() > maxHeight) { - scale = std::min(scale, static_cast(maxHeight) / static_cast((1.0f - cropY) * bitmap.getHeight())); + + if (maxHeight > 0 && croppedHeight > 0.0f) { + const float heightScale = static_cast(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(malloc(BW_BUFFER_CHUNK_SIZE)); + const size_t chunkSize = std::min(BW_BUFFER_CHUNK_SIZE, static_cast(frameBufferSize - offset)); + bwBufferChunks[i] = static_cast(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(frameBufferSize - offset)); + memcpy(frameBuffer + offset, bwBufferChunks[i], chunkSize); } display.cleanupGrayscaleBuffers(frameBuffer); diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index a6861a9e..01556522 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -30,16 +30,17 @@ class GfxRenderer { private: static constexpr size_t BW_BUFFER_CHUNK_SIZE = 8000; // 8KB chunks to allow for non-contiguous memory - static constexpr size_t BW_BUFFER_NUM_CHUNKS = HalDisplay::BUFFER_SIZE / BW_BUFFER_CHUNK_SIZE; - static_assert(BW_BUFFER_CHUNK_SIZE * BW_BUFFER_NUM_CHUNKS == HalDisplay::BUFFER_SIZE, - "BW buffer chunking does not line up with display buffer size"); HalDisplay& display; RenderMode renderMode; Orientation orientation; bool fadingFix; uint8_t* frameBuffer = nullptr; - uint8_t* bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr}; + uint16_t panelWidth = HalDisplay::DISPLAY_WIDTH; + uint16_t panelHeight = HalDisplay::DISPLAY_HEIGHT; + uint16_t panelWidthBytes = HalDisplay::DISPLAY_WIDTH_BYTES; + uint32_t frameBufferSize = HalDisplay::BUFFER_SIZE; + std::vector bwBufferChunks; std::map fontMap; // Mutable because drawText() is const but needs to delegate scan-mode @@ -155,5 +156,5 @@ class GfxRenderer { // Low level functions uint8_t* getFrameBuffer() const; - static size_t getBufferSize(); + size_t getBufferSize() const; }; diff --git a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp index bdc368ab..4b87d632 100644 --- a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp +++ b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp @@ -1,5 +1,6 @@ #include "JpegToBmpConverter.h" +#include #include #include #include @@ -26,9 +27,7 @@ constexpr bool USE_ATKINSON = true; // Atkinson dithering (cleaner than constexpr bool USE_FLOYD_STEINBERG = false; // Floyd-Steinberg error diffusion (can cause "worm" artifacts) constexpr bool USE_NOISE_DITHERING = false; // Hash-based noise dithering (good for downsampling) // Pre-resize to target display size (CRITICAL: avoids dithering artifacts from post-downsampling) -constexpr bool USE_PRESCALE = true; // true: scale image to target size before dithering -constexpr int TARGET_MAX_WIDTH = 480; // Max width for cover images (portrait display width) -constexpr int TARGET_MAX_HEIGHT = 800; // Max height for cover images (portrait display height) +constexpr bool USE_PRESCALE = true; // true: scale image to target size before dithering // ============================================================================ inline void write16(Print& out, const uint16_t value) { @@ -559,7 +558,10 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm // Core function: Convert JPEG file to 2-bit BMP (uses default target size) bool JpegToBmpConverter::jpegFileToBmpStream(FsFile& jpegFile, Print& bmpOut, bool crop) { - return jpegFileToBmpStreamInternal(jpegFile, bmpOut, TARGET_MAX_WIDTH, TARGET_MAX_HEIGHT, false, crop); + // Use runtime display dimensions (swapped for portrait cover sizing) + const int targetWidth = display.getDisplayHeight(); + const int targetHeight = display.getDisplayWidth(); + return jpegFileToBmpStreamInternal(jpegFile, bmpOut, targetWidth, targetHeight, false, crop); } // Convert with custom target size (for thumbnails, 2-bit) diff --git a/lib/PngToBmpConverter/PngToBmpConverter.cpp b/lib/PngToBmpConverter/PngToBmpConverter.cpp index 875b46bd..5a8c4752 100644 --- a/lib/PngToBmpConverter/PngToBmpConverter.cpp +++ b/lib/PngToBmpConverter/PngToBmpConverter.cpp @@ -1,5 +1,6 @@ #include "PngToBmpConverter.h" +#include #include #include #include @@ -16,8 +17,6 @@ constexpr bool USE_8BIT_OUTPUT = false; constexpr bool USE_ATKINSON = true; constexpr bool USE_FLOYD_STEINBERG = false; constexpr bool USE_PRESCALE = true; -constexpr int TARGET_MAX_WIDTH = 480; -constexpr int TARGET_MAX_HEIGHT = 800; // ============================================================================ // BMP writing helpers (same as JpegToBmpConverter) @@ -822,7 +821,10 @@ bool PngToBmpConverter::pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOu } bool PngToBmpConverter::pngFileToBmpStream(FsFile& pngFile, Print& bmpOut, bool crop) { - return pngFileToBmpStreamInternal(pngFile, bmpOut, TARGET_MAX_WIDTH, TARGET_MAX_HEIGHT, false, crop); + // Use runtime display dimensions (swapped for portrait cover sizing) + const int targetWidth = display.getDisplayHeight(); + const int targetHeight = display.getDisplayWidth(); + return pngFileToBmpStreamInternal(pngFile, bmpOut, targetWidth, targetHeight, false, crop); } bool PngToBmpConverter::pngFileToBmpStreamWithSize(FsFile& pngFile, Print& bmpOut, int targetMaxWidth, diff --git a/lib/hal/HalDisplay.cpp b/lib/hal/HalDisplay.cpp index 240c8925..665df535 100644 --- a/lib/hal/HalDisplay.cpp +++ b/lib/hal/HalDisplay.cpp @@ -1,13 +1,30 @@ #include #include +// Global HalDisplay instance +HalDisplay display; + #define SD_SPI_MISO 7 HalDisplay::HalDisplay() : einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY) {} HalDisplay::~HalDisplay() {} -void HalDisplay::begin() { einkDisplay.begin(); } +void HalDisplay::begin() { + // Set X3-specific panel mode before initializing. + if (gpio.deviceIsX3()) { + einkDisplay.setDisplayX3(); + } + + einkDisplay.begin(); + + // Request resync after specific wakeup events to ensure clean display state + const auto wakeupReason = gpio.getWakeupReason(); + if (wakeupReason == HalGPIO::WakeupReason::PowerButton || wakeupReason == HalGPIO::WakeupReason::AfterFlash || + wakeupReason == HalGPIO::WakeupReason::Other) { + einkDisplay.requestResync(); + } +} void HalDisplay::clearScreen(uint8_t color) const { einkDisplay.clearScreen(color); } @@ -34,10 +51,18 @@ EInkDisplay::RefreshMode convertRefreshMode(HalDisplay::RefreshMode mode) { } void HalDisplay::displayBuffer(HalDisplay::RefreshMode mode, bool turnOffScreen) { + if (gpio.deviceIsX3() && mode == RefreshMode::HALF_REFRESH) { + einkDisplay.requestResync(1); + } + einkDisplay.displayBuffer(convertRefreshMode(mode), turnOffScreen); } void HalDisplay::refreshDisplay(HalDisplay::RefreshMode mode, bool turnOffScreen) { + if (gpio.deviceIsX3() && mode == RefreshMode::HALF_REFRESH) { + einkDisplay.requestResync(1); + } + einkDisplay.refreshDisplay(convertRefreshMode(mode), turnOffScreen); } @@ -56,3 +81,11 @@ void HalDisplay::copyGrayscaleMsbBuffers(const uint8_t* msbBuffer) { einkDisplay void HalDisplay::cleanupGrayscaleBuffers(const uint8_t* bwBuffer) { einkDisplay.cleanupGrayscaleBuffers(bwBuffer); } void HalDisplay::displayGrayBuffer(bool turnOffScreen) { einkDisplay.displayGrayBuffer(turnOffScreen); } + +uint16_t HalDisplay::getDisplayWidth() const { return einkDisplay.getDisplayWidth(); } + +uint16_t HalDisplay::getDisplayHeight() const { return einkDisplay.getDisplayHeight(); } + +uint16_t HalDisplay::getDisplayWidthBytes() const { return einkDisplay.getDisplayWidthBytes(); } + +uint32_t HalDisplay::getBufferSize() const { return einkDisplay.getBufferSize(); } diff --git a/lib/hal/HalDisplay.h b/lib/hal/HalDisplay.h index 25e34423..a0a7f920 100644 --- a/lib/hal/HalDisplay.h +++ b/lib/hal/HalDisplay.h @@ -49,6 +49,14 @@ class HalDisplay { void displayGrayBuffer(bool turnOffScreen = false); + // Runtime geometry passthrough + uint16_t getDisplayWidth() const; + uint16_t getDisplayHeight() const; + uint16_t getDisplayWidthBytes() const; + uint32_t getBufferSize() const; + private: EInkDisplay einkDisplay; }; + +extern HalDisplay display; diff --git a/lib/hal/HalGPIO.cpp b/lib/hal/HalGPIO.cpp index ec0176a2..59b6eb72 100644 --- a/lib/hal/HalGPIO.cpp +++ b/lib/hal/HalGPIO.cpp @@ -1,10 +1,205 @@ #include +#include +#include #include +#include +#include + +// Global HalGPIO instance +HalGPIO gpio; + +namespace X3GPIO { + +struct X3ProbeResult { + bool bq27220 = false; + bool ds3231 = false; + bool qmi8658 = false; + + uint8_t score() const { + return static_cast(bq27220) + static_cast(ds3231) + static_cast(qmi8658); + } +}; + +bool readI2CReg8(uint8_t addr, uint8_t reg, uint8_t* outValue) { + Wire.beginTransmission(addr); + Wire.write(reg); + if (Wire.endTransmission(false) != 0) { + return false; + } + if (Wire.requestFrom(addr, static_cast(1), static_cast(true)) < 1) { + return false; + } + *outValue = Wire.read(); + return true; +} + +bool readI2CReg16LE(uint8_t addr, uint8_t reg, uint16_t* outValue) { + Wire.beginTransmission(addr); + Wire.write(reg); + if (Wire.endTransmission(false) != 0) { + return false; + } + if (Wire.requestFrom(addr, static_cast(2), static_cast(true)) < 2) { + while (Wire.available()) { + Wire.read(); + } + return false; + } + const uint8_t lo = Wire.read(); + const uint8_t hi = Wire.read(); + *outValue = (static_cast(hi) << 8) | lo; + return true; +} + +bool readBQ27220CurrentMA(int16_t* outCurrent) { + uint16_t raw = 0; + if (!readI2CReg16LE(I2C_ADDR_BQ27220, BQ27220_CUR_REG, &raw)) { + return false; + } + *outCurrent = static_cast(raw); + return true; +} + +bool probeBQ27220Signature() { + uint16_t soc = 0; + uint16_t voltageMv = 0; + if (!readI2CReg16LE(I2C_ADDR_BQ27220, BQ27220_SOC_REG, &soc)) { + return false; + } + if (soc > 100) { + return false; + } + if (!readI2CReg16LE(I2C_ADDR_BQ27220, BQ27220_VOLT_REG, &voltageMv)) { + return false; + } + return voltageMv >= 2500 && voltageMv <= 5000; +} + +bool probeDS3231Signature() { + uint8_t sec = 0; + if (!readI2CReg8(I2C_ADDR_DS3231, DS3231_SEC_REG, &sec)) { + return false; + } + const uint8_t tensDigit = (sec >> 4) & 0x07; + const uint8_t onesDigit = sec & 0x0F; + + return tensDigit <= 5 && onesDigit <= 9; +} + +bool probeQMI8658Signature() { + uint8_t whoami = 0; + if (readI2CReg8(I2C_ADDR_QMI8658, QMI8658_WHO_AM_I_REG, &whoami) && whoami == QMI8658_WHO_AM_I_VALUE) { + return true; + } + if (readI2CReg8(I2C_ADDR_QMI8658_ALT, QMI8658_WHO_AM_I_REG, &whoami) && whoami == QMI8658_WHO_AM_I_VALUE) { + return true; + } + return false; +} + +X3ProbeResult runX3ProbePass() { + X3ProbeResult result; + Wire.begin(X3_I2C_SDA, X3_I2C_SCL, X3_I2C_FREQ); + Wire.setTimeOut(6); + + result.bq27220 = probeBQ27220Signature(); + result.ds3231 = probeDS3231Signature(); + result.qmi8658 = probeQMI8658Signature(); + + Wire.end(); + pinMode(20, INPUT); + pinMode(0, INPUT); + return result; +} + +} // namespace X3GPIO + +namespace { +constexpr char HW_NAMESPACE[] = "cphw"; +constexpr char NVS_KEY_DEV_OVERRIDE[] = "dev_ovr"; // 0=auto, 1=x4, 2=x3 +constexpr char NVS_KEY_DEV_CACHED[] = "dev_det"; // 0=unknown, 1=x4, 2=x3 + +enum class NvsDeviceValue : uint8_t { Unknown = 0, X4 = 1, X3 = 2 }; + +NvsDeviceValue readNvsDeviceValue(const char* key, NvsDeviceValue defaultValue) { + Preferences prefs; + if (!prefs.begin(HW_NAMESPACE, true)) { + return defaultValue; + } + const uint8_t raw = prefs.getUChar(key, static_cast(defaultValue)); + prefs.end(); + if (raw > static_cast(NvsDeviceValue::X3)) { + return defaultValue; + } + return static_cast(raw); +} + +void writeNvsDeviceValue(const char* key, NvsDeviceValue value) { + Preferences prefs; + if (!prefs.begin(HW_NAMESPACE, false)) { + return; + } + prefs.putUChar(key, static_cast(value)); + prefs.end(); +} + +HalGPIO::DeviceType nvsToDeviceType(NvsDeviceValue value) { + return value == NvsDeviceValue::X3 ? HalGPIO::DeviceType::X3 : HalGPIO::DeviceType::X4; +} + +HalGPIO::DeviceType detectDeviceTypeWithFingerprint() { + // Explicit override for recovery/support: + // 0 = auto, 1 = force X4, 2 = force X3 + const NvsDeviceValue overrideValue = readNvsDeviceValue(NVS_KEY_DEV_OVERRIDE, NvsDeviceValue::Unknown); + if (overrideValue == NvsDeviceValue::X3 || overrideValue == NvsDeviceValue::X4) { + LOG_INF("HW", "Device override active: %s", overrideValue == NvsDeviceValue::X3 ? "X3" : "X4"); + return nvsToDeviceType(overrideValue); + } + + const NvsDeviceValue cachedValue = readNvsDeviceValue(NVS_KEY_DEV_CACHED, NvsDeviceValue::Unknown); + if (cachedValue == NvsDeviceValue::X3 || cachedValue == NvsDeviceValue::X4) { + LOG_INF("HW", "Using cached device type: %s", cachedValue == NvsDeviceValue::X3 ? "X3" : "X4"); + return nvsToDeviceType(cachedValue); + } + + // No cache yet: run active X3 fingerprint probe and persist result. + const X3GPIO::X3ProbeResult pass1 = X3GPIO::runX3ProbePass(); + delay(2); + const X3GPIO::X3ProbeResult pass2 = X3GPIO::runX3ProbePass(); + + const uint8_t score1 = pass1.score(); + const uint8_t score2 = pass2.score(); + LOG_INF("HW", "X3 probe scores: pass1=%u(bq=%d rtc=%d imu=%d) pass2=%u(bq=%d rtc=%d imu=%d)", score1, pass1.bq27220, + pass1.ds3231, pass1.qmi8658, score2, pass2.bq27220, pass2.ds3231, pass2.qmi8658); + const bool x3Confirmed = (score1 >= 2) && (score2 >= 2); + const bool x4Confirmed = (score1 == 0) && (score2 == 0); + + if (x3Confirmed) { + writeNvsDeviceValue(NVS_KEY_DEV_CACHED, NvsDeviceValue::X3); + return HalGPIO::DeviceType::X3; + } + + if (x4Confirmed) { + writeNvsDeviceValue(NVS_KEY_DEV_CACHED, NvsDeviceValue::X4); + return HalGPIO::DeviceType::X4; + } + + // Conservative fallback for first boot with inconclusive probes. + return HalGPIO::DeviceType::X4; +} + +} // namespace void HalGPIO::begin() { inputMgr.begin(); SPI.begin(EPD_SCLK, SPI_MISO, EPD_MOSI, EPD_CS); - pinMode(UART0_RXD, INPUT); + + _deviceType = detectDeviceTypeWithFingerprint(); + + if (deviceIsX4()) { + pinMode(BAT_GPIO0, INPUT); + pinMode(UART0_RXD, INPUT); + } } void HalGPIO::update() { @@ -28,16 +223,73 @@ bool HalGPIO::wasAnyReleased() const { return inputMgr.wasAnyReleased(); } unsigned long HalGPIO::getHeldTime() const { return inputMgr.getHeldTime(); } +void HalGPIO::startDeepSleep() { + // Ensure that the power button has been released to avoid immediately turning back on if you're holding it + while (inputMgr.isPressed(BTN_POWER)) { + delay(50); + inputMgr.update(); + } + // Arm the wakeup trigger *after* the button is released + esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW); + // Enter Deep Sleep + esp_deep_sleep_start(); +} + +void HalGPIO::verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPressAllowed) { + if (shortPressAllowed) { + // Fast path - no duration check needed + return; + } + // TODO: Intermittent edge case remains: a single tap followed by another single tap + // can still power on the device. Tighten wake debounce/state handling here. + + // Calibrate: subtract boot time already elapsed, assuming button held since boot + const uint16_t calibration = millis(); + const uint16_t calibratedDuration = (calibration < requiredDurationMs) ? (requiredDurationMs - calibration) : 1; + + const auto start = millis(); + inputMgr.update(); + // inputMgr.isPressed() may take up to ~500ms to return correct state + while (!inputMgr.isPressed(BTN_POWER) && millis() - start < 1000) { + delay(10); + inputMgr.update(); + } + if (inputMgr.isPressed(BTN_POWER)) { + do { + delay(10); + inputMgr.update(); + } while (inputMgr.isPressed(BTN_POWER) && inputMgr.getHeldTime() < calibratedDuration); + if (inputMgr.getHeldTime() < calibratedDuration) { + startDeepSleep(); + } + } else { + startDeepSleep(); + } +} + bool HalGPIO::isUsbConnected() const { + if (deviceIsX3()) { + // X3: infer USB/charging via BQ27220 Current() register (0x0C, signed mA). + // Positive current means charging. + for (uint8_t attempt = 0; attempt < 2; ++attempt) { + int16_t currentMa = 0; + if (X3GPIO::readBQ27220CurrentMA(¤tMa)) { + return currentMa > 0; + } + delay(2); + } + return false; + } // U0RXD/GPIO20 reads HIGH when USB is connected return digitalRead(UART0_RXD) == HIGH; } HalGPIO::WakeupReason HalGPIO::getWakeupReason() const { - const bool usbConnected = isUsbConnected(); const auto wakeupCause = esp_sleep_get_wakeup_cause(); const auto resetReason = esp_reset_reason(); + const bool usbConnected = isUsbConnected(); + if ((wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED && resetReason == ESP_RST_POWERON && !usbConnected) || (wakeupCause == ESP_SLEEP_WAKEUP_GPIO && resetReason == ESP_RST_DEEPSLEEP && usbConnected)) { return WakeupReason::PowerButton; @@ -49,4 +301,4 @@ HalGPIO::WakeupReason HalGPIO::getWakeupReason() const { return WakeupReason::AfterUSBPower; } return WakeupReason::Other; -} \ No newline at end of file +} diff --git a/lib/hal/HalGPIO.h b/lib/hal/HalGPIO.h index a283ed60..6337cf9e 100644 --- a/lib/hal/HalGPIO.h +++ b/lib/hal/HalGPIO.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include // Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults) @@ -18,6 +17,27 @@ #define UART0_RXD 20 // Used for USB connection detection +// Xteink X3 Hardware +#define X3_I2C_SDA 20 +#define X3_I2C_SCL 0 +#define X3_I2C_FREQ 400000 + +// TI BQ27220 Fuel gauge I2C +#define I2C_ADDR_BQ27220 0x55 // Fuel gauge I2C address +#define BQ27220_SOC_REG 0x2C // StateOfCharge() command code (%) +#define BQ27220_CUR_REG 0x0C // Current() command code (signed mA) +#define BQ27220_VOLT_REG 0x08 // Voltage() command code (mV) + +// Analog DS3231 RTC I2C +#define I2C_ADDR_DS3231 0x68 // RTC I2C address +#define DS3231_SEC_REG 0x00 // Seconds command code (BCD) + +// QST QMI8658 IMU I2C +#define I2C_ADDR_QMI8658 0x6B // IMU I2C address +#define I2C_ADDR_QMI8658_ALT 0x6A // IMU I2C fallback address +#define QMI8658_WHO_AM_I_REG 0x00 // WHO_AM_I command code +#define QMI8658_WHO_AM_I_VALUE 0x05 // WHO_AM_I expected value + class HalGPIO { #if CROSSPOINT_EMULATED == 0 InputManager inputMgr; @@ -26,9 +46,19 @@ class HalGPIO { bool lastUsbConnected = false; bool usbStateChanged = false; + public: + enum class DeviceType : uint8_t { X4, X3 }; + + private: + DeviceType _deviceType = DeviceType::X4; + public: HalGPIO() = default; + // Inline device type helpers for cleaner downstream checks + inline bool deviceIsX3() const { return _deviceType == DeviceType::X3; } + inline bool deviceIsX4() const { return _deviceType == DeviceType::X4; } + // Start button GPIO and setup SPI for screen and SD card void begin(); @@ -41,6 +71,14 @@ class HalGPIO { bool wasAnyReleased() const; unsigned long getHeldTime() const; + // Setup wake up GPIO and enter deep sleep + void startDeepSleep(); + + // Verify power button was held long enough after wakeup. + // If verification fails, enters deep sleep and does not return. + // Should only be called when wakeup reason is PowerButton. + void verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPressAllowed); + // Check if USB is connected bool isUsbConnected() const; @@ -61,4 +99,4 @@ class HalGPIO { static constexpr uint8_t BTN_POWER = 6; }; -extern HalGPIO gpio; // Singleton +extern HalGPIO gpio; diff --git a/lib/hal/HalPowerManager.cpp b/lib/hal/HalPowerManager.cpp index c25b1a28..a40c81ed 100644 --- a/lib/hal/HalPowerManager.cpp +++ b/lib/hal/HalPowerManager.cpp @@ -11,7 +11,15 @@ HalPowerManager powerManager; // Singleton instance void HalPowerManager::begin() { - pinMode(BAT_GPIO0, INPUT); + if (gpio.deviceIsX3()) { + // X3 uses an I2C fuel gauge for battery monitoring. + // I2C init must come AFTER gpio.begin() so early hardware detection/probes are finished. + Wire.begin(X3_I2C_SDA, X3_I2C_SCL, X3_I2C_FREQ); + Wire.setTimeOut(4); + _batteryUseI2C = true; + } else { + pinMode(BAT_GPIO0, INPUT); + } normalFreq = getCpuFrequencyMhz(); modeMutex = xSemaphoreCreateMutex(); assert(modeMutex != nullptr); @@ -78,8 +86,35 @@ void HalPowerManager::startDeepSleep(HalGPIO& gpio) const { } uint16_t HalPowerManager::getBatteryPercentage() const { + if (_batteryUseI2C) { + const unsigned long now = millis(); + if (_batteryLastPollMs != 0 && (now - _batteryLastPollMs) < BATTERY_POLL_MS) { + return _batteryCachedPercent; + } + + // Read SOC directly from I2C fuel gauge (16-bit LE register). + // On I2C error, keep last known value to avoid UI jitter/slowdowns. + Wire.beginTransmission(I2C_ADDR_BQ27220); + Wire.write(BQ27220_SOC_REG); + if (Wire.endTransmission(false) != 0) { + _batteryLastPollMs = now; + return _batteryCachedPercent; + } + Wire.requestFrom(I2C_ADDR_BQ27220, (uint8_t)2); + if (Wire.available() < 2) { + _batteryLastPollMs = now; + return _batteryCachedPercent; + } + const uint8_t lo = Wire.read(); + const uint8_t hi = Wire.read(); + const uint16_t soc = (hi << 8) | lo; + _batteryCachedPercent = soc > 100 ? 100 : soc; + _batteryLastPollMs = now; + return _batteryCachedPercent; + } static const BatteryMonitor battery = BatteryMonitor(BAT_GPIO0); - return battery.readPercentage(); + _batteryCachedPercent = battery.readPercentage(); + return _batteryCachedPercent; } HalPowerManager::Lock::Lock() { diff --git a/lib/hal/HalPowerManager.h b/lib/hal/HalPowerManager.h index 74cff1bf..bf78623c 100644 --- a/lib/hal/HalPowerManager.h +++ b/lib/hal/HalPowerManager.h @@ -1,8 +1,10 @@ #pragma once #include +#include #include #include +#include #include #include @@ -16,6 +18,11 @@ class HalPowerManager { int normalFreq = 0; // MHz bool isLowPower = false; + // I2C fuel gauge configuration for X3 battery monitoring + bool _batteryUseI2C = false; // True if using I2C fuel gauge (X3), false for ADC (X4) + mutable int _batteryCachedPercent = 0; // Last read battery percentage (0-100) + mutable unsigned long _batteryLastPollMs = 0; // Timestamp of last battery read in milliseconds + enum LockMode { None, NormalSpeed }; LockMode currentLockMode = None; SemaphoreHandle_t modeMutex = nullptr; // Protect access to currentLockMode @@ -23,6 +30,7 @@ class HalPowerManager { public: static constexpr int LOW_POWER_FREQ = 10; // MHz static constexpr unsigned long IDLE_POWER_SAVING_MS = 3000; // ms + static constexpr unsigned long BATTERY_POLL_MS = 1500; // ms void begin(); diff --git a/open-x4-sdk b/open-x4-sdk index 9f76376a..157d724d 160000 --- a/open-x4-sdk +++ b/open-x4-sdk @@ -1 +1 @@ -Subproject commit 9f76376a5cc7894cff9ca87bbdd34dab715d8a59 +Subproject commit 157d724d7a7389d49fe108e6dd5da2455a5340ba diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index 810cb50b..3cc72fb8 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -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(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; } diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index 9c563eb1..055b37a7 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -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]); + } } } } diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index 58dabeab..0eee84a2 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -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]); + } } } } diff --git a/src/main.cpp b/src/main.cpp index 75bf69c5..5ba58cbc 100644 --- a/src/main.cpp +++ b/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"); } }