mirror of
https://github.com/crosspoint-reader/crosspoint-reader.git
synced 2026-04-29 10:26:52 -07:00
Initial support for the x3
This commit is contained in:
@@ -9,3 +9,4 @@ build
|
||||
**/__pycache__/
|
||||
/compile_commands.json
|
||||
/.cache
|
||||
notes.md
|
||||
|
||||
@@ -8,6 +8,11 @@ void GfxRenderer::begin() {
|
||||
Serial.printf("[%lu] [GFX] !! No framebuffer\n", millis());
|
||||
assert(false);
|
||||
}
|
||||
panelWidth = display.getDisplayWidth();
|
||||
panelHeight = display.getDisplayHeight();
|
||||
panelWidthBytes = display.getDisplayWidthBytes();
|
||||
frameBufferSize = display.getBufferSize();
|
||||
bwBufferChunks.assign((frameBufferSize + BW_BUFFER_CHUNK_SIZE - 1) / BW_BUFFER_CHUNK_SIZE, nullptr);
|
||||
}
|
||||
|
||||
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap.insert({fontId, font}); }
|
||||
@@ -15,25 +20,25 @@ void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap.ins
|
||||
// Translate logical (x,y) coordinates to physical panel coordinates based on current orientation
|
||||
// This should always be inlined for better performance
|
||||
static inline void rotateCoordinates(const GfxRenderer::Orientation orientation, const int x, const int y, int* phyX,
|
||||
int* phyY) {
|
||||
int* phyY, const uint16_t panelWidth, const uint16_t panelHeight) {
|
||||
switch (orientation) {
|
||||
case GfxRenderer::Portrait: {
|
||||
// Logical portrait (480x800) → panel (800x480)
|
||||
// Rotation: 90 degrees clockwise
|
||||
*phyX = y;
|
||||
*phyY = HalDisplay::DISPLAY_HEIGHT - 1 - x;
|
||||
*phyY = panelHeight - 1 - x;
|
||||
break;
|
||||
}
|
||||
case GfxRenderer::LandscapeClockwise: {
|
||||
// Logical landscape (800x480) rotated 180 degrees (swap top/bottom and left/right)
|
||||
*phyX = HalDisplay::DISPLAY_WIDTH - 1 - x;
|
||||
*phyY = HalDisplay::DISPLAY_HEIGHT - 1 - y;
|
||||
*phyX = panelWidth - 1 - x;
|
||||
*phyY = panelHeight - 1 - y;
|
||||
break;
|
||||
}
|
||||
case GfxRenderer::PortraitInverted: {
|
||||
// Logical portrait (480x800) → panel (800x480)
|
||||
// Rotation: 90 degrees counter-clockwise
|
||||
*phyX = HalDisplay::DISPLAY_WIDTH - 1 - y;
|
||||
*phyX = panelWidth - 1 - y;
|
||||
*phyY = x;
|
||||
break;
|
||||
}
|
||||
@@ -53,16 +58,16 @@ void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
|
||||
int phyY = 0;
|
||||
|
||||
// Note: this call should be inlined for better performance
|
||||
rotateCoordinates(orientation, x, y, &phyX, &phyY);
|
||||
rotateCoordinates(orientation, x, y, &phyX, &phyY, panelWidth, panelHeight);
|
||||
|
||||
// Bounds checking against physical panel dimensions
|
||||
if (phyX < 0 || phyX >= HalDisplay::DISPLAY_WIDTH || phyY < 0 || phyY >= HalDisplay::DISPLAY_HEIGHT) {
|
||||
if (phyX < 0 || phyX >= panelWidth || phyY < 0 || phyY >= panelHeight) {
|
||||
Serial.printf("[%lu] [GFX] !! Outside range (%d, %d) -> (%d, %d)\n", millis(), x, y, phyX, phyY);
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate byte position and bit position
|
||||
const uint16_t byteIndex = phyY * HalDisplay::DISPLAY_WIDTH_BYTES + (phyX / 8);
|
||||
const uint32_t byteIndex = static_cast<uint32_t>(phyY) * panelWidthBytes + (phyX / 8);
|
||||
const uint8_t bitPosition = 7 - (phyX % 8); // MSB first
|
||||
|
||||
if (state) {
|
||||
@@ -383,7 +388,7 @@ void GfxRenderer::fillRoundedRect(const int x, const int y, const int width, con
|
||||
void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, const int width, const int height) const {
|
||||
int rotatedX = 0;
|
||||
int rotatedY = 0;
|
||||
rotateCoordinates(orientation, x, y, &rotatedX, &rotatedY);
|
||||
rotateCoordinates(orientation, x, y, &rotatedX, &rotatedY, panelWidth, panelHeight);
|
||||
// Rotate origin corner
|
||||
switch (orientation) {
|
||||
case Portrait:
|
||||
@@ -648,7 +653,7 @@ void GfxRenderer::clearScreen(const uint8_t color) const {
|
||||
}
|
||||
|
||||
void GfxRenderer::invertScreen() const {
|
||||
for (int i = 0; i < HalDisplay::BUFFER_SIZE; i++) {
|
||||
for (uint32_t i = 0; i < frameBufferSize; i++) {
|
||||
frameBuffer[i] = ~frameBuffer[i];
|
||||
}
|
||||
}
|
||||
@@ -684,13 +689,13 @@ int GfxRenderer::getScreenWidth() const {
|
||||
case Portrait:
|
||||
case PortraitInverted:
|
||||
// 480px wide in portrait logical coordinates
|
||||
return HalDisplay::DISPLAY_HEIGHT;
|
||||
return panelHeight;
|
||||
case LandscapeClockwise:
|
||||
case LandscapeCounterClockwise:
|
||||
// 800px wide in landscape logical coordinates
|
||||
return HalDisplay::DISPLAY_WIDTH;
|
||||
return panelWidth;
|
||||
}
|
||||
return HalDisplay::DISPLAY_HEIGHT;
|
||||
return panelHeight;
|
||||
}
|
||||
|
||||
int GfxRenderer::getScreenHeight() const {
|
||||
@@ -698,13 +703,13 @@ int GfxRenderer::getScreenHeight() const {
|
||||
case Portrait:
|
||||
case PortraitInverted:
|
||||
// 800px tall in portrait logical coordinates
|
||||
return HalDisplay::DISPLAY_WIDTH;
|
||||
return panelWidth;
|
||||
case LandscapeClockwise:
|
||||
case LandscapeCounterClockwise:
|
||||
// 480px tall in landscape logical coordinates
|
||||
return HalDisplay::DISPLAY_HEIGHT;
|
||||
return panelHeight;
|
||||
}
|
||||
return HalDisplay::DISPLAY_WIDTH;
|
||||
return panelWidth;
|
||||
}
|
||||
|
||||
int GfxRenderer::getSpaceWidth(const int fontId) const {
|
||||
@@ -841,7 +846,7 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
|
||||
|
||||
uint8_t* GfxRenderer::getFrameBuffer() const { return frameBuffer; }
|
||||
|
||||
size_t GfxRenderer::getBufferSize() { return HalDisplay::BUFFER_SIZE; }
|
||||
size_t GfxRenderer::getBufferSize() { return EInkDisplay::MAX_BUFFER_SIZE; }
|
||||
|
||||
// unused
|
||||
// void GfxRenderer::grayscaleRevert() const { display.grayscaleRevert(); }
|
||||
@@ -869,7 +874,7 @@ void GfxRenderer::freeBwBufferChunks() {
|
||||
*/
|
||||
bool GfxRenderer::storeBwBuffer() {
|
||||
// Allocate and copy each chunk
|
||||
for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) {
|
||||
for (size_t i = 0; i < bwBufferChunks.size(); i++) {
|
||||
// Check if any chunks are already allocated
|
||||
if (bwBufferChunks[i]) {
|
||||
Serial.printf("[%lu] [GFX] !! BW buffer chunk %zu already stored - this is likely a bug, freeing chunk\n",
|
||||
@@ -879,20 +884,20 @@ bool GfxRenderer::storeBwBuffer() {
|
||||
}
|
||||
|
||||
const size_t offset = i * BW_BUFFER_CHUNK_SIZE;
|
||||
bwBufferChunks[i] = static_cast<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]) {
|
||||
Serial.printf("[%lu] [GFX] !! Failed to allocate BW buffer chunk %zu (%zu bytes)\n", millis(), i,
|
||||
BW_BUFFER_CHUNK_SIZE);
|
||||
Serial.printf("[%lu] [GFX] !! Failed to allocate BW buffer chunk %zu (%zu bytes)\n", millis(), i, chunkSize);
|
||||
// Free previously allocated chunks
|
||||
freeBwBufferChunks();
|
||||
return false;
|
||||
}
|
||||
|
||||
memcpy(bwBufferChunks[i], frameBuffer + offset, BW_BUFFER_CHUNK_SIZE);
|
||||
memcpy(bwBufferChunks[i], frameBuffer + offset, chunkSize);
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [GFX] Stored BW buffer in %zu chunks (%zu bytes each)\n", millis(), BW_BUFFER_NUM_CHUNKS,
|
||||
Serial.printf("[%lu] [GFX] Stored BW buffer in %zu chunks (%zu bytes each)\n", millis(), bwBufferChunks.size(),
|
||||
BW_BUFFER_CHUNK_SIZE);
|
||||
return true;
|
||||
}
|
||||
@@ -917,7 +922,7 @@ void GfxRenderer::restoreBwBuffer() {
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) {
|
||||
for (size_t i = 0; i < bwBufferChunks.size(); i++) {
|
||||
// Check if chunk is missing
|
||||
if (!bwBufferChunks[i]) {
|
||||
Serial.printf("[%lu] [GFX] !! BW buffer chunks not stored - this is likely a bug\n", millis());
|
||||
@@ -926,7 +931,8 @@ void GfxRenderer::restoreBwBuffer() {
|
||||
}
|
||||
|
||||
const size_t offset = i * BW_BUFFER_CHUNK_SIZE;
|
||||
memcpy(frameBuffer + offset, bwBufferChunks[i], BW_BUFFER_CHUNK_SIZE);
|
||||
const size_t chunkSize = std::min(BW_BUFFER_CHUNK_SIZE, static_cast<size_t>(frameBufferSize - offset));
|
||||
memcpy(frameBuffer + offset, bwBufferChunks[i], chunkSize);
|
||||
}
|
||||
|
||||
display.cleanupGrayscaleBuffers(frameBuffer);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <HalDisplay.h>
|
||||
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
#include "Bitmap.h"
|
||||
|
||||
@@ -25,16 +26,17 @@ class GfxRenderer {
|
||||
|
||||
private:
|
||||
static constexpr size_t BW_BUFFER_CHUNK_SIZE = 8000; // 8KB chunks to allow for non-contiguous memory
|
||||
static constexpr size_t BW_BUFFER_NUM_CHUNKS = HalDisplay::BUFFER_SIZE / BW_BUFFER_CHUNK_SIZE;
|
||||
static_assert(BW_BUFFER_CHUNK_SIZE * BW_BUFFER_NUM_CHUNKS == HalDisplay::BUFFER_SIZE,
|
||||
"BW buffer chunking does not line up with display buffer size");
|
||||
|
||||
HalDisplay& display;
|
||||
RenderMode renderMode;
|
||||
Orientation orientation;
|
||||
bool fadingFix;
|
||||
uint8_t* frameBuffer = nullptr;
|
||||
uint8_t* bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr};
|
||||
uint16_t panelWidth = HalDisplay::DISPLAY_WIDTH;
|
||||
uint16_t panelHeight = HalDisplay::DISPLAY_HEIGHT;
|
||||
uint16_t panelWidthBytes = HalDisplay::DISPLAY_WIDTH_BYTES;
|
||||
uint32_t frameBufferSize = HalDisplay::BUFFER_SIZE;
|
||||
std::vector<uint8_t*> bwBufferChunks;
|
||||
std::map<int, EpdFontFamily> fontMap;
|
||||
void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, const int* y, bool pixelState,
|
||||
EpdFontFamily::Style style) const;
|
||||
|
||||
@@ -9,6 +9,16 @@ HalDisplay::~HalDisplay() {}
|
||||
|
||||
void HalDisplay::begin() { einkDisplay.begin(); }
|
||||
|
||||
void HalDisplay::setDcPin(int8_t pin) { einkDisplay.setDcPin(pin); }
|
||||
|
||||
void HalDisplay::setBusyActiveHigh(bool activeHigh) { einkDisplay.setBusyActiveHigh(activeHigh); }
|
||||
|
||||
void HalDisplay::setBwOnly(bool bwOnly) { einkDisplay.setBwOnly(bwOnly); }
|
||||
|
||||
void HalDisplay::setControllerType(EInkDisplay::ControllerType type) { einkDisplay.setControllerType(type); }
|
||||
|
||||
void HalDisplay::setDisplayDimensions(uint16_t width, uint16_t height) { einkDisplay.setDisplayDimensions(width, height); }
|
||||
|
||||
void HalDisplay::clearScreen(uint8_t color) const { einkDisplay.clearScreen(color); }
|
||||
|
||||
void HalDisplay::drawImage(const uint8_t* imageData, uint16_t x, uint16_t y, uint16_t w, uint16_t h,
|
||||
@@ -51,3 +61,11 @@ void HalDisplay::copyGrayscaleMsbBuffers(const uint8_t* msbBuffer) { einkDisplay
|
||||
void HalDisplay::cleanupGrayscaleBuffers(const uint8_t* bwBuffer) { einkDisplay.cleanupGrayscaleBuffers(bwBuffer); }
|
||||
|
||||
void HalDisplay::displayGrayBuffer(bool turnOffScreen) { einkDisplay.displayGrayBuffer(turnOffScreen); }
|
||||
|
||||
uint16_t HalDisplay::getDisplayWidth() const { return einkDisplay.getDisplayWidth(); }
|
||||
|
||||
uint16_t HalDisplay::getDisplayHeight() const { return einkDisplay.getDisplayHeight(); }
|
||||
|
||||
uint16_t HalDisplay::getDisplayWidthBytes() const { return einkDisplay.getDisplayWidthBytes(); }
|
||||
|
||||
uint32_t HalDisplay::getBufferSize() const { return einkDisplay.getBufferSize(); }
|
||||
|
||||
@@ -20,6 +20,13 @@ class HalDisplay {
|
||||
// Initialize the display hardware and driver
|
||||
void begin();
|
||||
|
||||
// Pre-begin display config passthroughs (used by X3 setup path)
|
||||
void setDcPin(int8_t pin);
|
||||
void setBusyActiveHigh(bool activeHigh);
|
||||
void setBwOnly(bool bwOnly);
|
||||
void setControllerType(EInkDisplay::ControllerType type);
|
||||
void setDisplayDimensions(uint16_t width, uint16_t height);
|
||||
|
||||
// Display dimensions
|
||||
static constexpr uint16_t DISPLAY_WIDTH = EInkDisplay::DISPLAY_WIDTH;
|
||||
static constexpr uint16_t DISPLAY_HEIGHT = EInkDisplay::DISPLAY_HEIGHT;
|
||||
@@ -47,6 +54,12 @@ class HalDisplay {
|
||||
|
||||
void displayGrayBuffer(bool turnOffScreen = false);
|
||||
|
||||
// Runtime geometry passthrough
|
||||
uint16_t getDisplayWidth() const;
|
||||
uint16_t getDisplayHeight() const;
|
||||
uint16_t getDisplayWidthBytes() const;
|
||||
uint32_t getBufferSize() const;
|
||||
|
||||
private:
|
||||
EInkDisplay einkDisplay;
|
||||
};
|
||||
|
||||
+13
-2
@@ -5,7 +5,14 @@
|
||||
void HalGPIO::begin() {
|
||||
inputMgr.begin();
|
||||
SPI.begin(EPD_SCLK, SPI_MISO, EPD_MOSI, EPD_CS);
|
||||
pinMode(BAT_GPIO0, INPUT);
|
||||
|
||||
// X3 boards bias GPIO4 (EPD DC) around ~700 ADC counts at boot in our setup.
|
||||
// X4 boards do not, and use GPIO0 for battery ADC.
|
||||
_detectAdcValue = analogRead(4);
|
||||
_deviceType = (_detectAdcValue > 500 && _detectAdcValue < 1200) ? DeviceType::X3 : DeviceType::X4;
|
||||
_batteryPin = (_deviceType == DeviceType::X3) ? 4 : BAT_GPIO0;
|
||||
|
||||
pinMode(_batteryPin, INPUT);
|
||||
pinMode(UART0_RXD, INPUT);
|
||||
}
|
||||
|
||||
@@ -36,6 +43,10 @@ void HalGPIO::startDeepSleep() {
|
||||
}
|
||||
|
||||
int HalGPIO::getBatteryPercentage() const {
|
||||
if (_deviceType == DeviceType::X3) {
|
||||
// X3 battery telemetry is not on ADC in stock fw; avoid fighting EPD DC on GPIO4.
|
||||
return 0;
|
||||
}
|
||||
static const BatteryMonitor battery = BatteryMonitor(BAT_GPIO0);
|
||||
return battery.readPercentage();
|
||||
}
|
||||
@@ -61,4 +72,4 @@ HalGPIO::WakeupReason HalGPIO::getWakeupReason() const {
|
||||
return WakeupReason::AfterUSBPower;
|
||||
}
|
||||
return WakeupReason::Other;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,14 @@ class HalGPIO {
|
||||
InputManager inputMgr;
|
||||
#endif
|
||||
|
||||
public:
|
||||
enum class DeviceType : uint8_t { X4, X3 };
|
||||
|
||||
private:
|
||||
DeviceType _deviceType = DeviceType::X4;
|
||||
int _detectAdcValue = 0;
|
||||
int _batteryPin = BAT_GPIO0;
|
||||
|
||||
public:
|
||||
HalGPIO() = default;
|
||||
|
||||
@@ -47,6 +55,11 @@ class HalGPIO {
|
||||
// Check if USB is connected
|
||||
bool isUsbConnected() const;
|
||||
|
||||
// Device detection helpers
|
||||
DeviceType getDeviceType() const { return _deviceType; }
|
||||
int getDetectAdcValue() const { return _detectAdcValue; }
|
||||
int getBatteryPin() const { return _batteryPin; }
|
||||
|
||||
enum class WakeupReason { PowerButton, AfterFlash, AfterUSBPower, Other };
|
||||
|
||||
WakeupReason getWakeupReason() const;
|
||||
|
||||
@@ -253,6 +253,13 @@ void onGoHome() {
|
||||
}
|
||||
|
||||
void setupDisplayAndFonts() {
|
||||
if (gpio.getDeviceType() == HalGPIO::DeviceType::X3) {
|
||||
display.setDcPin(4);
|
||||
display.setBusyActiveHigh(false);
|
||||
display.setBwOnly(true);
|
||||
display.setControllerType(EInkDisplay::ControllerType::SSD1677);
|
||||
display.setDisplayDimensions(792, 528);
|
||||
}
|
||||
display.begin();
|
||||
renderer.begin();
|
||||
Serial.printf("[%lu] [ ] Display initialized\n", millis());
|
||||
|
||||
Reference in New Issue
Block a user