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