feat: add Xteink X3 hardware support
Adds HAL-level support for the Xteink X3 (SSD1677 controller, 792x528 display). Includes device detection, display initialization, button mapping, power management, and theme adjustments for the X3 form factor.
This commit is contained in:
@@ -37,4 +37,4 @@ class ImageToFramebufferDecoder {
|
|||||||
|
|
||||||
bool validateImageDimensions(int width, int height, const std::string& format);
|
bool validateImageDimensions(int width, int height, const std::string& format);
|
||||||
void warnUnsupportedFeature(const std::string& feature, const std::string& imagePath);
|
void warnUnsupportedFeature(const std::string& feature, const std::string& imagePath);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "GfxRenderer.h"
|
#include "GfxRenderer.h"
|
||||||
|
|
||||||
#include <FontDecompressor.h>
|
#include <FontDecompressor.h>
|
||||||
|
#include <HalGPIO.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
#include <Utf8.h>
|
#include <Utf8.h>
|
||||||
|
|
||||||
@@ -28,6 +29,11 @@ void GfxRenderer::begin() {
|
|||||||
LOG_ERR("GFX", "!! No framebuffer");
|
LOG_ERR("GFX", "!! No framebuffer");
|
||||||
assert(false);
|
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}); }
|
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
|
// Translate logical (x,y) coordinates to physical panel coordinates based on current orientation
|
||||||
// This should always be inlined for better performance
|
// This should always be inlined for better performance
|
||||||
static inline void rotateCoordinates(const GfxRenderer::Orientation orientation, const int x, const int y, int* phyX,
|
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) {
|
switch (orientation) {
|
||||||
case GfxRenderer::Portrait: {
|
case GfxRenderer::Portrait: {
|
||||||
// Logical portrait (480x800) → panel (800x480)
|
// Logical portrait (480x800) → panel (800x480)
|
||||||
// Rotation: 90 degrees clockwise
|
// Rotation: 90 degrees clockwise
|
||||||
*phyX = y;
|
*phyX = y;
|
||||||
*phyY = HalDisplay::DISPLAY_HEIGHT - 1 - x;
|
*phyY = panelHeight - 1 - x;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GfxRenderer::LandscapeClockwise: {
|
case GfxRenderer::LandscapeClockwise: {
|
||||||
// Logical landscape (800x480) rotated 180 degrees (swap top/bottom and left/right)
|
// Logical landscape (800x480) rotated 180 degrees (swap top/bottom and left/right)
|
||||||
*phyX = HalDisplay::DISPLAY_WIDTH - 1 - x;
|
*phyX = panelWidth - 1 - x;
|
||||||
*phyY = HalDisplay::DISPLAY_HEIGHT - 1 - y;
|
*phyY = panelHeight - 1 - y;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GfxRenderer::PortraitInverted: {
|
case GfxRenderer::PortraitInverted: {
|
||||||
// Logical portrait (480x800) → panel (800x480)
|
// Logical portrait (480x800) → panel (800x480)
|
||||||
// Rotation: 90 degrees counter-clockwise
|
// Rotation: 90 degrees counter-clockwise
|
||||||
*phyX = HalDisplay::DISPLAY_WIDTH - 1 - y;
|
*phyX = panelWidth - 1 - y;
|
||||||
*phyY = x;
|
*phyY = x;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -125,8 +131,9 @@ static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode
|
|||||||
if (renderMode == GfxRenderer::BW && bmpVal < 3) {
|
if (renderMode == GfxRenderer::BW && bmpVal < 3) {
|
||||||
// Black (also paints over the grays in BW mode)
|
// Black (also paints over the grays in BW mode)
|
||||||
renderer.drawPixel(screenX, screenY, pixelState);
|
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)
|
// 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
|
// We have to flag pixels in reverse for the gray buffers, as 0 leave alone, 1 update
|
||||||
renderer.drawPixel(screenX, screenY, false);
|
renderer.drawPixel(screenX, screenY, false);
|
||||||
} else if (renderMode == GfxRenderer::GRAYSCALE_LSB && bmpVal == 1) {
|
} 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;
|
int phyY = 0;
|
||||||
|
|
||||||
// Note: this call should be inlined for better performance
|
// 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
|
// Bounds checking against runtime panel dimensions
|
||||||
if (phyX < 0 || phyX >= HalDisplay::DISPLAY_WIDTH || phyY < 0 || phyY >= HalDisplay::DISPLAY_HEIGHT) {
|
if (phyX < 0 || phyX >= panelWidth || phyY < 0 || phyY >= panelHeight) {
|
||||||
LOG_ERR("GFX", "!! Outside range (%d, %d) -> (%d, %d)", x, y, phyX, phyY);
|
LOG_ERR("GFX", "!! Outside range (%d, %d) -> (%d, %d)", x, y, phyX, phyY);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate byte position and bit position
|
// 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
|
const uint8_t bitPosition = 7 - (phyX % 8); // MSB first
|
||||||
|
|
||||||
if (state) {
|
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 {
|
void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, const int width, const int height) const {
|
||||||
int rotatedX = 0;
|
int rotatedX = 0;
|
||||||
int rotatedY = 0;
|
int rotatedY = 0;
|
||||||
rotateCoordinates(orientation, x, y, &rotatedX, &rotatedY);
|
rotateCoordinates(orientation, x, y, &rotatedX, &rotatedY, panelWidth, panelHeight);
|
||||||
// Rotate origin corner
|
// Rotate origin corner
|
||||||
switch (orientation) {
|
switch (orientation) {
|
||||||
case Portrait:
|
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,
|
LOG_DBG("GFX", "Cropping %dx%d by %dx%d pix, is %s", bitmap.getWidth(), bitmap.getHeight(), cropPixX, cropPixY,
|
||||||
bitmap.isTopDown() ? "top-down" : "bottom-up");
|
bitmap.isTopDown() ? "top-down" : "bottom-up");
|
||||||
|
|
||||||
if (maxWidth > 0 && (1.0f - cropX) * bitmap.getWidth() > maxWidth) {
|
const float croppedWidth = (1.0f - cropX) * static_cast<float>(bitmap.getWidth());
|
||||||
scale = static_cast<float>(maxWidth) / static_cast<float>((1.0f - cropX) * bitmap.getWidth());
|
const float croppedHeight = (1.0f - cropY) * static_cast<float>(bitmap.getHeight());
|
||||||
isScaled = true;
|
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;
|
isScaled = true;
|
||||||
}
|
}
|
||||||
LOG_DBG("GFX", "Scaling by %f - %s", scale, isScaled ? "scaled" : "not scaled");
|
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) {
|
if (renderMode == BW && val < 3) {
|
||||||
drawPixel(screenX, screenY);
|
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);
|
drawPixel(screenX, screenY, false);
|
||||||
} else if (renderMode == GRAYSCALE_LSB && val == 1) {
|
} else if (renderMode == GRAYSCALE_LSB && val == 1) {
|
||||||
drawPixel(screenX, screenY, false);
|
drawPixel(screenX, screenY, false);
|
||||||
@@ -822,7 +841,7 @@ void GfxRenderer::clearScreen(const uint8_t color) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GfxRenderer::invertScreen() 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];
|
frameBuffer[i] = ~frameBuffer[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -923,13 +942,13 @@ int GfxRenderer::getScreenWidth() const {
|
|||||||
case Portrait:
|
case Portrait:
|
||||||
case PortraitInverted:
|
case PortraitInverted:
|
||||||
// 480px wide in portrait logical coordinates
|
// 480px wide in portrait logical coordinates
|
||||||
return HalDisplay::DISPLAY_HEIGHT;
|
return panelHeight;
|
||||||
case LandscapeClockwise:
|
case LandscapeClockwise:
|
||||||
case LandscapeCounterClockwise:
|
case LandscapeCounterClockwise:
|
||||||
// 800px wide in landscape logical coordinates
|
// 800px wide in landscape logical coordinates
|
||||||
return HalDisplay::DISPLAY_WIDTH;
|
return panelWidth;
|
||||||
}
|
}
|
||||||
return HalDisplay::DISPLAY_HEIGHT;
|
return panelHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
int GfxRenderer::getScreenHeight() const {
|
int GfxRenderer::getScreenHeight() const {
|
||||||
@@ -937,13 +956,13 @@ int GfxRenderer::getScreenHeight() const {
|
|||||||
case Portrait:
|
case Portrait:
|
||||||
case PortraitInverted:
|
case PortraitInverted:
|
||||||
// 800px tall in portrait logical coordinates
|
// 800px tall in portrait logical coordinates
|
||||||
return HalDisplay::DISPLAY_WIDTH;
|
return panelWidth;
|
||||||
case LandscapeClockwise:
|
case LandscapeClockwise:
|
||||||
case LandscapeCounterClockwise:
|
case LandscapeCounterClockwise:
|
||||||
// 480px tall in landscape logical coordinates
|
// 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 {
|
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; }
|
uint8_t* GfxRenderer::getFrameBuffer() const { return frameBuffer; }
|
||||||
|
|
||||||
size_t GfxRenderer::getBufferSize() { return HalDisplay::BUFFER_SIZE; }
|
size_t GfxRenderer::getBufferSize() const { return frameBufferSize; }
|
||||||
|
|
||||||
// unused
|
// unused
|
||||||
// void GfxRenderer::grayscaleRevert() const { display.grayscaleRevert(); }
|
// void GfxRenderer::grayscaleRevert() const { display.grayscaleRevert(); }
|
||||||
@@ -1123,7 +1142,7 @@ void GfxRenderer::freeBwBufferChunks() {
|
|||||||
*/
|
*/
|
||||||
bool GfxRenderer::storeBwBuffer() {
|
bool GfxRenderer::storeBwBuffer() {
|
||||||
// Allocate and copy each chunk
|
// 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
|
// Check if any chunks are already allocated
|
||||||
if (bwBufferChunks[i]) {
|
if (bwBufferChunks[i]) {
|
||||||
LOG_ERR("GFX", "!! BW buffer chunk %zu already stored - this is likely a bug, freeing chunk", 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;
|
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]) {
|
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
|
// Free previously allocated chunks
|
||||||
freeBwBufferChunks();
|
freeBwBufferChunks();
|
||||||
return false;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1168,9 +1188,10 @@ void GfxRenderer::restoreBwBuffer() {
|
|||||||
return;
|
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;
|
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);
|
display.cleanupGrayscaleBuffers(frameBuffer);
|
||||||
|
|||||||
@@ -30,16 +30,17 @@ class GfxRenderer {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
static constexpr size_t BW_BUFFER_CHUNK_SIZE = 8000; // 8KB chunks to allow for non-contiguous memory
|
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;
|
HalDisplay& display;
|
||||||
RenderMode renderMode;
|
RenderMode renderMode;
|
||||||
Orientation orientation;
|
Orientation orientation;
|
||||||
bool fadingFix;
|
bool fadingFix;
|
||||||
uint8_t* frameBuffer = nullptr;
|
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<uint8_t*> bwBufferChunks;
|
||||||
std::map<int, EpdFontFamily> fontMap;
|
std::map<int, EpdFontFamily> fontMap;
|
||||||
|
|
||||||
// Mutable because drawText() is const but needs to delegate scan-mode
|
// Mutable because drawText() is const but needs to delegate scan-mode
|
||||||
@@ -155,5 +156,5 @@ class GfxRenderer {
|
|||||||
|
|
||||||
// Low level functions
|
// Low level functions
|
||||||
uint8_t* getFrameBuffer() const;
|
uint8_t* getFrameBuffer() const;
|
||||||
static size_t getBufferSize();
|
size_t getBufferSize() const;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "JpegToBmpConverter.h"
|
#include "JpegToBmpConverter.h"
|
||||||
|
|
||||||
|
#include <HalDisplay.h>
|
||||||
#include <HalStorage.h>
|
#include <HalStorage.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
#include <picojpeg.h>
|
#include <picojpeg.h>
|
||||||
@@ -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_FLOYD_STEINBERG = false; // Floyd-Steinberg error diffusion (can cause "worm" artifacts)
|
||||||
constexpr bool USE_NOISE_DITHERING = false; // Hash-based noise dithering (good for downsampling)
|
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)
|
// 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 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)
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
inline void write16(Print& out, const uint16_t value) {
|
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)
|
// Core function: Convert JPEG file to 2-bit BMP (uses default target size)
|
||||||
bool JpegToBmpConverter::jpegFileToBmpStream(FsFile& jpegFile, Print& bmpOut, bool crop) {
|
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)
|
// Convert with custom target size (for thumbnails, 2-bit)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "PngToBmpConverter.h"
|
#include "PngToBmpConverter.h"
|
||||||
|
|
||||||
|
#include <HalDisplay.h>
|
||||||
#include <HalStorage.h>
|
#include <HalStorage.h>
|
||||||
#include <InflateReader.h>
|
#include <InflateReader.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
@@ -16,8 +17,6 @@ constexpr bool USE_8BIT_OUTPUT = false;
|
|||||||
constexpr bool USE_ATKINSON = true;
|
constexpr bool USE_ATKINSON = true;
|
||||||
constexpr bool USE_FLOYD_STEINBERG = false;
|
constexpr bool USE_FLOYD_STEINBERG = false;
|
||||||
constexpr bool USE_PRESCALE = true;
|
constexpr bool USE_PRESCALE = true;
|
||||||
constexpr int TARGET_MAX_WIDTH = 480;
|
|
||||||
constexpr int TARGET_MAX_HEIGHT = 800;
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// BMP writing helpers (same as JpegToBmpConverter)
|
// 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) {
|
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,
|
bool PngToBmpConverter::pngFileToBmpStreamWithSize(FsFile& pngFile, Print& bmpOut, int targetMaxWidth,
|
||||||
|
|||||||
+34
-1
@@ -1,13 +1,30 @@
|
|||||||
#include <HalDisplay.h>
|
#include <HalDisplay.h>
|
||||||
#include <HalGPIO.h>
|
#include <HalGPIO.h>
|
||||||
|
|
||||||
|
// Global HalDisplay instance
|
||||||
|
HalDisplay display;
|
||||||
|
|
||||||
#define SD_SPI_MISO 7
|
#define SD_SPI_MISO 7
|
||||||
|
|
||||||
HalDisplay::HalDisplay() : einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY) {}
|
HalDisplay::HalDisplay() : einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY) {}
|
||||||
|
|
||||||
HalDisplay::~HalDisplay() {}
|
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); }
|
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) {
|
void HalDisplay::displayBuffer(HalDisplay::RefreshMode mode, bool turnOffScreen) {
|
||||||
|
if (gpio.deviceIsX3() && mode == RefreshMode::HALF_REFRESH) {
|
||||||
|
einkDisplay.requestResync(1);
|
||||||
|
}
|
||||||
|
|
||||||
einkDisplay.displayBuffer(convertRefreshMode(mode), turnOffScreen);
|
einkDisplay.displayBuffer(convertRefreshMode(mode), turnOffScreen);
|
||||||
}
|
}
|
||||||
|
|
||||||
void HalDisplay::refreshDisplay(HalDisplay::RefreshMode mode, bool turnOffScreen) {
|
void HalDisplay::refreshDisplay(HalDisplay::RefreshMode mode, bool turnOffScreen) {
|
||||||
|
if (gpio.deviceIsX3() && mode == RefreshMode::HALF_REFRESH) {
|
||||||
|
einkDisplay.requestResync(1);
|
||||||
|
}
|
||||||
|
|
||||||
einkDisplay.refreshDisplay(convertRefreshMode(mode), turnOffScreen);
|
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::cleanupGrayscaleBuffers(const uint8_t* bwBuffer) { einkDisplay.cleanupGrayscaleBuffers(bwBuffer); }
|
||||||
|
|
||||||
void HalDisplay::displayGrayBuffer(bool turnOffScreen) { einkDisplay.displayGrayBuffer(turnOffScreen); }
|
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(); }
|
||||||
|
|||||||
@@ -49,6 +49,14 @@ class HalDisplay {
|
|||||||
|
|
||||||
void displayGrayBuffer(bool turnOffScreen = false);
|
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:
|
private:
|
||||||
EInkDisplay einkDisplay;
|
EInkDisplay einkDisplay;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
extern HalDisplay display;
|
||||||
|
|||||||
+255
-3
@@ -1,10 +1,205 @@
|
|||||||
#include <HalGPIO.h>
|
#include <HalGPIO.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <Preferences.h>
|
||||||
#include <SPI.h>
|
#include <SPI.h>
|
||||||
|
#include <Wire.h>
|
||||||
|
#include <esp_sleep.h>
|
||||||
|
|
||||||
|
// 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<uint8_t>(bq27220) + static_cast<uint8_t>(ds3231) + static_cast<uint8_t>(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<uint8_t>(1), static_cast<uint8_t>(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<uint8_t>(2), static_cast<uint8_t>(true)) < 2) {
|
||||||
|
while (Wire.available()) {
|
||||||
|
Wire.read();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const uint8_t lo = Wire.read();
|
||||||
|
const uint8_t hi = Wire.read();
|
||||||
|
*outValue = (static_cast<uint16_t>(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<int16_t>(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<uint8_t>(defaultValue));
|
||||||
|
prefs.end();
|
||||||
|
if (raw > static_cast<uint8_t>(NvsDeviceValue::X3)) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
return static_cast<NvsDeviceValue>(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
void writeNvsDeviceValue(const char* key, NvsDeviceValue value) {
|
||||||
|
Preferences prefs;
|
||||||
|
if (!prefs.begin(HW_NAMESPACE, false)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
prefs.putUChar(key, static_cast<uint8_t>(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() {
|
void HalGPIO::begin() {
|
||||||
inputMgr.begin();
|
inputMgr.begin();
|
||||||
SPI.begin(EPD_SCLK, SPI_MISO, EPD_MOSI, EPD_CS);
|
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() {
|
void HalGPIO::update() {
|
||||||
@@ -28,16 +223,73 @@ bool HalGPIO::wasAnyReleased() const { return inputMgr.wasAnyReleased(); }
|
|||||||
|
|
||||||
unsigned long HalGPIO::getHeldTime() const { return inputMgr.getHeldTime(); }
|
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 {
|
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
|
// U0RXD/GPIO20 reads HIGH when USB is connected
|
||||||
return digitalRead(UART0_RXD) == HIGH;
|
return digitalRead(UART0_RXD) == HIGH;
|
||||||
}
|
}
|
||||||
|
|
||||||
HalGPIO::WakeupReason HalGPIO::getWakeupReason() const {
|
HalGPIO::WakeupReason HalGPIO::getWakeupReason() const {
|
||||||
const bool usbConnected = isUsbConnected();
|
|
||||||
const auto wakeupCause = esp_sleep_get_wakeup_cause();
|
const auto wakeupCause = esp_sleep_get_wakeup_cause();
|
||||||
const auto resetReason = esp_reset_reason();
|
const auto resetReason = esp_reset_reason();
|
||||||
|
|
||||||
|
const bool usbConnected = isUsbConnected();
|
||||||
|
|
||||||
if ((wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED && resetReason == ESP_RST_POWERON && !usbConnected) ||
|
if ((wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED && resetReason == ESP_RST_POWERON && !usbConnected) ||
|
||||||
(wakeupCause == ESP_SLEEP_WAKEUP_GPIO && resetReason == ESP_RST_DEEPSLEEP && usbConnected)) {
|
(wakeupCause == ESP_SLEEP_WAKEUP_GPIO && resetReason == ESP_RST_DEEPSLEEP && usbConnected)) {
|
||||||
return WakeupReason::PowerButton;
|
return WakeupReason::PowerButton;
|
||||||
@@ -49,4 +301,4 @@ HalGPIO::WakeupReason HalGPIO::getWakeupReason() const {
|
|||||||
return WakeupReason::AfterUSBPower;
|
return WakeupReason::AfterUSBPower;
|
||||||
}
|
}
|
||||||
return WakeupReason::Other;
|
return WakeupReason::Other;
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-2
@@ -1,7 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <BatteryMonitor.h>
|
|
||||||
#include <InputManager.h>
|
#include <InputManager.h>
|
||||||
|
|
||||||
// Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults)
|
// Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults)
|
||||||
@@ -18,6 +17,27 @@
|
|||||||
|
|
||||||
#define UART0_RXD 20 // Used for USB connection detection
|
#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 {
|
class HalGPIO {
|
||||||
#if CROSSPOINT_EMULATED == 0
|
#if CROSSPOINT_EMULATED == 0
|
||||||
InputManager inputMgr;
|
InputManager inputMgr;
|
||||||
@@ -26,9 +46,19 @@ class HalGPIO {
|
|||||||
bool lastUsbConnected = false;
|
bool lastUsbConnected = false;
|
||||||
bool usbStateChanged = false;
|
bool usbStateChanged = false;
|
||||||
|
|
||||||
|
public:
|
||||||
|
enum class DeviceType : uint8_t { X4, X3 };
|
||||||
|
|
||||||
|
private:
|
||||||
|
DeviceType _deviceType = DeviceType::X4;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
HalGPIO() = default;
|
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
|
// Start button GPIO and setup SPI for screen and SD card
|
||||||
void begin();
|
void begin();
|
||||||
|
|
||||||
@@ -41,6 +71,14 @@ class HalGPIO {
|
|||||||
bool wasAnyReleased() const;
|
bool wasAnyReleased() const;
|
||||||
unsigned long getHeldTime() 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
|
// Check if USB is connected
|
||||||
bool isUsbConnected() const;
|
bool isUsbConnected() const;
|
||||||
|
|
||||||
@@ -61,4 +99,4 @@ class HalGPIO {
|
|||||||
static constexpr uint8_t BTN_POWER = 6;
|
static constexpr uint8_t BTN_POWER = 6;
|
||||||
};
|
};
|
||||||
|
|
||||||
extern HalGPIO gpio; // Singleton
|
extern HalGPIO gpio;
|
||||||
|
|||||||
@@ -11,7 +11,15 @@
|
|||||||
HalPowerManager powerManager; // Singleton instance
|
HalPowerManager powerManager; // Singleton instance
|
||||||
|
|
||||||
void HalPowerManager::begin() {
|
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();
|
normalFreq = getCpuFrequencyMhz();
|
||||||
modeMutex = xSemaphoreCreateMutex();
|
modeMutex = xSemaphoreCreateMutex();
|
||||||
assert(modeMutex != nullptr);
|
assert(modeMutex != nullptr);
|
||||||
@@ -78,8 +86,35 @@ void HalPowerManager::startDeepSleep(HalGPIO& gpio) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint16_t HalPowerManager::getBatteryPercentage() 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);
|
static const BatteryMonitor battery = BatteryMonitor(BAT_GPIO0);
|
||||||
return battery.readPercentage();
|
_batteryCachedPercent = battery.readPercentage();
|
||||||
|
return _batteryCachedPercent;
|
||||||
}
|
}
|
||||||
|
|
||||||
HalPowerManager::Lock::Lock() {
|
HalPowerManager::Lock::Lock() {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
#include <BatteryMonitor.h>
|
||||||
#include <InputManager.h>
|
#include <InputManager.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
|
#include <Wire.h>
|
||||||
#include <freertos/semphr.h>
|
#include <freertos/semphr.h>
|
||||||
|
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
@@ -16,6 +18,11 @@ class HalPowerManager {
|
|||||||
int normalFreq = 0; // MHz
|
int normalFreq = 0; // MHz
|
||||||
bool isLowPower = false;
|
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 };
|
enum LockMode { None, NormalSpeed };
|
||||||
LockMode currentLockMode = None;
|
LockMode currentLockMode = None;
|
||||||
SemaphoreHandle_t modeMutex = nullptr; // Protect access to currentLockMode
|
SemaphoreHandle_t modeMutex = nullptr; // Protect access to currentLockMode
|
||||||
@@ -23,6 +30,7 @@ class HalPowerManager {
|
|||||||
public:
|
public:
|
||||||
static constexpr int LOW_POWER_FREQ = 10; // MHz
|
static constexpr int LOW_POWER_FREQ = 10; // MHz
|
||||||
static constexpr unsigned long IDLE_POWER_SAVING_MS = 3000; // ms
|
static constexpr unsigned long IDLE_POWER_SAVING_MS = 3000; // ms
|
||||||
|
static constexpr unsigned long BATTERY_POLL_MS = 1500; // ms
|
||||||
|
|
||||||
void begin();
|
void begin();
|
||||||
|
|
||||||
|
|||||||
+1
-1
Submodule open-x4-sdk updated: 9f76376a5c...157d724d7a
@@ -138,7 +138,7 @@ bool HomeActivity::storeCoverBuffer() {
|
|||||||
// Free any existing buffer first
|
// Free any existing buffer first
|
||||||
freeCoverBuffer();
|
freeCoverBuffer();
|
||||||
|
|
||||||
const size_t bufferSize = GfxRenderer::getBufferSize();
|
const size_t bufferSize = renderer.getBufferSize();
|
||||||
coverBuffer = static_cast<uint8_t*>(malloc(bufferSize));
|
coverBuffer = static_cast<uint8_t*>(malloc(bufferSize));
|
||||||
if (!coverBuffer) {
|
if (!coverBuffer) {
|
||||||
return false;
|
return false;
|
||||||
@@ -158,7 +158,7 @@ bool HomeActivity::restoreCoverBuffer() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const size_t bufferSize = GfxRenderer::getBufferSize();
|
const size_t bufferSize = renderer.getBufferSize();
|
||||||
memcpy(frameBuffer, coverBuffer, bufferSize);
|
memcpy(frameBuffer, coverBuffer, bufferSize);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,7 +140,10 @@ void BaseTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1, const c
|
|||||||
constexpr int buttonHeight = BaseMetrics::values.buttonHintsHeight;
|
constexpr int buttonHeight = BaseMetrics::values.buttonHintsHeight;
|
||||||
constexpr int buttonY = BaseMetrics::values.buttonHintsHeight; // Distance from bottom
|
constexpr int buttonY = BaseMetrics::values.buttonHintsHeight; // Distance from bottom
|
||||||
constexpr int textYOffset = 7; // Distance from top of button to text baseline
|
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};
|
const char* labels[] = {btn1, btn2, btn3, btn4};
|
||||||
|
|
||||||
for (int i = 0; i < 4; i++) {
|
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();
|
const int screenWidth = renderer.getScreenWidth();
|
||||||
constexpr int buttonWidth = BaseMetrics::values.sideButtonHintsWidth; // Width on screen (height when rotated)
|
constexpr int buttonWidth = BaseMetrics::values.sideButtonHintsWidth; // Width on screen (height when rotated)
|
||||||
constexpr int buttonHeight = 80; // Height on screen (width when rotated)
|
constexpr int buttonHeight = 80; // Height on screen (width when rotated)
|
||||||
constexpr int buttonX = 4; // Distance from right edge
|
constexpr int buttonMargin = 4;
|
||||||
// Position for the button group - buttons share a border so they're adjacent
|
|
||||||
constexpr int topButtonY = 345; // Top button position
|
|
||||||
|
|
||||||
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
|
if (topBtn != nullptr && topBtn[0] != '\0') {
|
||||||
const int x = screenWidth - buttonX - buttonWidth;
|
const int leftX = buttonMargin;
|
||||||
|
renderer.drawRect(leftX, x3ButtonY, buttonWidth, buttonHeight);
|
||||||
// Draw top button outline (3 sides, bottom open)
|
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, topBtn);
|
||||||
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]);
|
|
||||||
const int textHeight = renderer.getTextHeight(SMALL_FONT_ID);
|
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
|
if (bottomBtn != nullptr && bottomBtn[0] != '\0') {
|
||||||
const int textX = x + (buttonWidth - textHeight) / 2;
|
const int rightX = screenWidth - buttonMargin - buttonWidth;
|
||||||
const int textY = y + (buttonHeight + textWidth) / 2;
|
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 buttonHeight = LyraMetrics::values.buttonHintsHeight;
|
||||||
constexpr int buttonY = LyraMetrics::values.buttonHintsHeight; // Distance from bottom
|
constexpr int buttonY = LyraMetrics::values.buttonHintsHeight; // Distance from bottom
|
||||||
constexpr int textYOffset = 7; // Distance from top of button to text baseline
|
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};
|
const char* labels[] = {btn1, btn2, btn3, btn4};
|
||||||
|
|
||||||
for (int i = 0; i < 4; i++) {
|
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();
|
const int screenWidth = renderer.getScreenWidth();
|
||||||
constexpr int buttonWidth = LyraMetrics::values.sideButtonHintsWidth; // Width on screen (height when rotated)
|
constexpr int buttonWidth = LyraMetrics::values.sideButtonHintsWidth; // Width on screen (height when rotated)
|
||||||
constexpr int buttonHeight = 78; // Height on screen (width 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
|
if (topBtn != nullptr && topBtn[0] != '\0') {
|
||||||
const int x = screenWidth - buttonWidth;
|
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 (bottomBtn != nullptr && bottomBtn[0] != '\0') {
|
||||||
if (topBtn != nullptr && topBtn[0] != '\0') {
|
const int rightX = screenWidth - buttonWidth;
|
||||||
renderer.drawRoundedRect(x, topHintButtonY, buttonWidth, buttonHeight, 1, cornerRadius, true, false, true, false,
|
renderer.drawRoundedRect(rightX, x3ButtonY, buttonWidth, buttonHeight, 1, cornerRadius, true, false, true, false,
|
||||||
true);
|
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 (topBtn != nullptr && topBtn[0] != '\0') {
|
||||||
if (bottomBtn != nullptr && bottomBtn[0] != '\0') {
|
renderer.drawRoundedRect(x, topHintButtonY, buttonWidth, buttonHeight, 1, cornerRadius, true, false, true, false,
|
||||||
renderer.drawRoundedRect(x, topHintButtonY + buttonHeight + 5, buttonWidth, buttonHeight, 1, cornerRadius, true,
|
true);
|
||||||
false, true, false, true);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Draw text for each button
|
if (bottomBtn != nullptr && bottomBtn[0] != '\0') {
|
||||||
for (int i = 0; i < 2; i++) {
|
renderer.drawRoundedRect(x, topHintButtonY + buttonHeight + 5, buttonWidth, buttonHeight, 1, cornerRadius, true,
|
||||||
if (labels[i] != nullptr && labels[i][0] != '\0') {
|
false, true, false, true);
|
||||||
const int y = topHintButtonY + (i * buttonHeight + 5);
|
}
|
||||||
|
|
||||||
// Draw rotated text centered in the button
|
for (int i = 0; i < 2; i++) {
|
||||||
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, labels[i]);
|
if (labels[i] != nullptr && labels[i][0] != '\0') {
|
||||||
|
const int y = topHintButtonY + (i * buttonHeight) + 5;
|
||||||
renderer.drawTextRotated90CW(SMALL_FONT_ID, x, y + (buttonHeight + textWidth) / 2, labels[i]);
|
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, labels[i]);
|
||||||
|
renderer.drawTextRotated90CW(SMALL_FONT_ID, x, y + (buttonHeight + textWidth) / 2, labels[i]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-13
@@ -27,8 +27,6 @@
|
|||||||
#include "util/ButtonNavigator.h"
|
#include "util/ButtonNavigator.h"
|
||||||
#include "util/ScreenshotUtil.h"
|
#include "util/ScreenshotUtil.h"
|
||||||
|
|
||||||
HalDisplay display;
|
|
||||||
HalGPIO gpio;
|
|
||||||
MappedInputManager mappedInputManager(gpio);
|
MappedInputManager mappedInputManager(gpio);
|
||||||
GfxRenderer renderer(display);
|
GfxRenderer renderer(display);
|
||||||
ActivityManager activityManager(renderer, mappedInputManager);
|
ActivityManager activityManager(renderer, mappedInputManager);
|
||||||
@@ -171,7 +169,6 @@ void verifyPowerButtonDuration() {
|
|||||||
powerManager.startDeepSleep(gpio);
|
powerManager.startDeepSleep(gpio);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void waitForPowerRelease() {
|
void waitForPowerRelease() {
|
||||||
gpio.update();
|
gpio.update();
|
||||||
while (gpio.isPressed(HalGPIO::BTN_POWER)) {
|
while (gpio.isPressed(HalGPIO::BTN_POWER)) {
|
||||||
@@ -189,7 +186,6 @@ void enterDeepSleep() {
|
|||||||
activityManager.goToSleep();
|
activityManager.goToSleep();
|
||||||
|
|
||||||
display.deepSleep();
|
display.deepSleep();
|
||||||
LOG_DBG("MAIN", "Power button press calibration value: %lu ms", t2 - t1);
|
|
||||||
LOG_DBG("MAIN", "Entering deep sleep");
|
LOG_DBG("MAIN", "Entering deep sleep");
|
||||||
|
|
||||||
powerManager.startDeepSleep(gpio);
|
powerManager.startDeepSleep(gpio);
|
||||||
@@ -235,15 +231,17 @@ void setup() {
|
|||||||
gpio.begin();
|
gpio.begin();
|
||||||
powerManager.begin();
|
powerManager.begin();
|
||||||
|
|
||||||
// Only start serial if USB connected
|
#ifdef ENABLE_SERIAL_LOG
|
||||||
if (gpio.isUsbConnected()) {
|
if (gpio.isUsbConnected()) {
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
// Wait up to 3 seconds for Serial to be ready to catch early logs
|
const unsigned long start = millis();
|
||||||
unsigned long start = millis();
|
while (!Serial && (millis() - start) < 500) {
|
||||||
while (!Serial && (millis() - start) < 3000) {
|
|
||||||
delay(10);
|
delay(10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
LOG_INF("MAIN", "Hardware detect: %s", gpio.deviceIsX3() ? "X3" : "X4");
|
||||||
|
|
||||||
// SD Card Initialization
|
// SD Card Initialization
|
||||||
// We need 6 open files concurrently when parsing a new chapter
|
// We need 6 open files concurrently when parsing a new chapter
|
||||||
@@ -263,11 +261,12 @@ void setup() {
|
|||||||
UITheme::getInstance().reload();
|
UITheme::getInstance().reload();
|
||||||
ButtonNavigator::setMappedInputManager(mappedInputManager);
|
ButtonNavigator::setMappedInputManager(mappedInputManager);
|
||||||
|
|
||||||
switch (gpio.getWakeupReason()) {
|
const auto wakeupReason = gpio.getWakeupReason();
|
||||||
|
switch (wakeupReason) {
|
||||||
case HalGPIO::WakeupReason::PowerButton:
|
case HalGPIO::WakeupReason::PowerButton:
|
||||||
// For normal wakeups, verify power button press duration
|
|
||||||
LOG_DBG("MAIN", "Verifying power button press duration");
|
LOG_DBG("MAIN", "Verifying power button press duration");
|
||||||
verifyPowerButtonDuration();
|
gpio.verifyPowerButtonWakeup(SETTINGS.getPowerButtonDuration(),
|
||||||
|
SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::SLEEP);
|
||||||
break;
|
break;
|
||||||
case HalGPIO::WakeupReason::AfterUSBPower:
|
case HalGPIO::WakeupReason::AfterUSBPower:
|
||||||
// If USB power caused a cold boot, go back to sleep
|
// If USB power caused a cold boot, go back to sleep
|
||||||
@@ -332,9 +331,10 @@ void loop() {
|
|||||||
String cmd = line.substring(4);
|
String cmd = line.substring(4);
|
||||||
cmd.trim();
|
cmd.trim();
|
||||||
if (cmd == "SCREENSHOT") {
|
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();
|
uint8_t* buf = display.getFrameBuffer();
|
||||||
logSerial.write(buf, HalDisplay::BUFFER_SIZE);
|
logSerial.write(buf, bufferSize);
|
||||||
logSerial.printf("SCREENSHOT_END\n");
|
logSerial.printf("SCREENSHOT_END\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user