From cbea838f914d89d8423efe4cd374fb38734fda2e Mon Sep 17 00:00:00 2001 From: Justin Mitchell Date: Thu, 12 Feb 2026 16:16:54 -0500 Subject: [PATCH] Initial support for the x3 --- .gitignore | 1 + lib/GfxRenderer/GfxRenderer.cpp | 56 ++++++++++++++++++--------------- lib/GfxRenderer/GfxRenderer.h | 10 +++--- lib/hal/HalDisplay.cpp | 18 +++++++++++ lib/hal/HalDisplay.h | 13 ++++++++ lib/hal/HalGPIO.cpp | 15 +++++++-- lib/hal/HalGPIO.h | 13 ++++++++ src/main.cpp | 7 +++++ 8 files changed, 102 insertions(+), 31 deletions(-) diff --git a/.gitignore b/.gitignore index ec281eb9c..a19a9b74a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ build **/__pycache__/ /compile_commands.json /.cache +notes.md diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index 14024bc4a..62f227563 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -8,6 +8,11 @@ void GfxRenderer::begin() { Serial.printf("[%lu] [GFX] !! No framebuffer\n", millis()); 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}); } @@ -15,25 +20,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; } @@ -53,16 +58,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) { + if (phyX < 0 || phyX >= panelWidth || phyY < 0 || phyY >= panelHeight) { Serial.printf("[%lu] [GFX] !! Outside range (%d, %d) -> (%d, %d)\n", millis(), 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) { @@ -383,7 +388,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: @@ -648,7 +653,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]; } } @@ -684,13 +689,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 { @@ -698,13 +703,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 { @@ -841,7 +846,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() { return EInkDisplay::MAX_BUFFER_SIZE; } // unused // void GfxRenderer::grayscaleRevert() const { display.grayscaleRevert(); } @@ -869,7 +874,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]) { Serial.printf("[%lu] [GFX] !! BW buffer chunk %zu already stored - this is likely a bug, freeing chunk\n", @@ -879,20 +884,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]) { - Serial.printf("[%lu] [GFX] !! Failed to allocate BW buffer chunk %zu (%zu bytes)\n", millis(), i, - BW_BUFFER_CHUNK_SIZE); + Serial.printf("[%lu] [GFX] !! Failed to allocate BW buffer chunk %zu (%zu bytes)\n", millis(), i, chunkSize); // Free previously allocated chunks freeBwBufferChunks(); return false; } - memcpy(bwBufferChunks[i], frameBuffer + offset, BW_BUFFER_CHUNK_SIZE); + memcpy(bwBufferChunks[i], frameBuffer + offset, chunkSize); } - Serial.printf("[%lu] [GFX] Stored BW buffer in %zu chunks (%zu bytes each)\n", millis(), BW_BUFFER_NUM_CHUNKS, + Serial.printf("[%lu] [GFX] Stored BW buffer in %zu chunks (%zu bytes each)\n", millis(), bwBufferChunks.size(), BW_BUFFER_CHUNK_SIZE); return true; } @@ -917,7 +922,7 @@ void GfxRenderer::restoreBwBuffer() { return; } - for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) { + for (size_t i = 0; i < bwBufferChunks.size(); i++) { // Check if chunk is missing if (!bwBufferChunks[i]) { Serial.printf("[%lu] [GFX] !! BW buffer chunks not stored - this is likely a bug\n", millis()); @@ -926,7 +931,8 @@ void GfxRenderer::restoreBwBuffer() { } 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 4540774ee..63cbc36b8 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -4,6 +4,7 @@ #include #include +#include #include "Bitmap.h" @@ -25,16 +26,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; void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, const int* y, bool pixelState, EpdFontFamily::Style style) const; diff --git a/lib/hal/HalDisplay.cpp b/lib/hal/HalDisplay.cpp index 0fafdbb58..0cccc2fc3 100644 --- a/lib/hal/HalDisplay.cpp +++ b/lib/hal/HalDisplay.cpp @@ -9,6 +9,16 @@ HalDisplay::~HalDisplay() {} void HalDisplay::begin() { einkDisplay.begin(); } +void HalDisplay::setDcPin(int8_t pin) { einkDisplay.setDcPin(pin); } + +void HalDisplay::setBusyActiveHigh(bool activeHigh) { einkDisplay.setBusyActiveHigh(activeHigh); } + +void HalDisplay::setBwOnly(bool bwOnly) { einkDisplay.setBwOnly(bwOnly); } + +void HalDisplay::setControllerType(EInkDisplay::ControllerType type) { einkDisplay.setControllerType(type); } + +void HalDisplay::setDisplayDimensions(uint16_t width, uint16_t height) { einkDisplay.setDisplayDimensions(width, height); } + void HalDisplay::clearScreen(uint8_t color) const { einkDisplay.clearScreen(color); } void HalDisplay::drawImage(const uint8_t* imageData, uint16_t x, uint16_t y, uint16_t w, uint16_t h, @@ -51,3 +61,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 238832b0d..325107c57 100644 --- a/lib/hal/HalDisplay.h +++ b/lib/hal/HalDisplay.h @@ -20,6 +20,13 @@ class HalDisplay { // Initialize the display hardware and driver void begin(); + // Pre-begin display config passthroughs (used by X3 setup path) + void setDcPin(int8_t pin); + void setBusyActiveHigh(bool activeHigh); + void setBwOnly(bool bwOnly); + void setControllerType(EInkDisplay::ControllerType type); + void setDisplayDimensions(uint16_t width, uint16_t height); + // Display dimensions static constexpr uint16_t DISPLAY_WIDTH = EInkDisplay::DISPLAY_WIDTH; static constexpr uint16_t DISPLAY_HEIGHT = EInkDisplay::DISPLAY_HEIGHT; @@ -47,6 +54,12 @@ 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; }; diff --git a/lib/hal/HalGPIO.cpp b/lib/hal/HalGPIO.cpp index 89ce13ba9..f01b50875 100644 --- a/lib/hal/HalGPIO.cpp +++ b/lib/hal/HalGPIO.cpp @@ -5,7 +5,14 @@ void HalGPIO::begin() { inputMgr.begin(); SPI.begin(EPD_SCLK, SPI_MISO, EPD_MOSI, EPD_CS); - pinMode(BAT_GPIO0, INPUT); + + // X3 boards bias GPIO4 (EPD DC) around ~700 ADC counts at boot in our setup. + // X4 boards do not, and use GPIO0 for battery ADC. + _detectAdcValue = analogRead(4); + _deviceType = (_detectAdcValue > 500 && _detectAdcValue < 1200) ? DeviceType::X3 : DeviceType::X4; + _batteryPin = (_deviceType == DeviceType::X3) ? 4 : BAT_GPIO0; + + pinMode(_batteryPin, INPUT); pinMode(UART0_RXD, INPUT); } @@ -36,6 +43,10 @@ void HalGPIO::startDeepSleep() { } int HalGPIO::getBatteryPercentage() const { + if (_deviceType == DeviceType::X3) { + // X3 battery telemetry is not on ADC in stock fw; avoid fighting EPD DC on GPIO4. + return 0; + } static const BatteryMonitor battery = BatteryMonitor(BAT_GPIO0); return battery.readPercentage(); } @@ -61,4 +72,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 615a8d63e..75914d268 100644 --- a/lib/hal/HalGPIO.h +++ b/lib/hal/HalGPIO.h @@ -23,6 +23,14 @@ class HalGPIO { InputManager inputMgr; #endif + public: + enum class DeviceType : uint8_t { X4, X3 }; + + private: + DeviceType _deviceType = DeviceType::X4; + int _detectAdcValue = 0; + int _batteryPin = BAT_GPIO0; + public: HalGPIO() = default; @@ -47,6 +55,11 @@ class HalGPIO { // Check if USB is connected bool isUsbConnected() const; + // Device detection helpers + DeviceType getDeviceType() const { return _deviceType; } + int getDetectAdcValue() const { return _detectAdcValue; } + int getBatteryPin() const { return _batteryPin; } + enum class WakeupReason { PowerButton, AfterFlash, AfterUSBPower, Other }; WakeupReason getWakeupReason() const; diff --git a/src/main.cpp b/src/main.cpp index fa782556f..060ed6f51 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -253,6 +253,13 @@ void onGoHome() { } void setupDisplayAndFonts() { + if (gpio.getDeviceType() == HalGPIO::DeviceType::X3) { + display.setDcPin(4); + display.setBusyActiveHigh(false); + display.setBwOnly(true); + display.setControllerType(EInkDisplay::ControllerType::SSD1677); + display.setDisplayDimensions(792, 528); + } display.begin(); renderer.begin(); Serial.printf("[%lu] [ ] Display initialized\n", millis());