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:
@@ -138,7 +138,7 @@ bool HomeActivity::storeCoverBuffer() {
|
||||
// Free any existing buffer first
|
||||
freeCoverBuffer();
|
||||
|
||||
const size_t bufferSize = GfxRenderer::getBufferSize();
|
||||
const size_t bufferSize = renderer.getBufferSize();
|
||||
coverBuffer = static_cast<uint8_t*>(malloc(bufferSize));
|
||||
if (!coverBuffer) {
|
||||
return false;
|
||||
@@ -158,7 +158,7 @@ bool HomeActivity::restoreCoverBuffer() {
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t bufferSize = GfxRenderer::getBufferSize();
|
||||
const size_t bufferSize = renderer.getBufferSize();
|
||||
memcpy(frameBuffer, coverBuffer, bufferSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -140,7 +140,10 @@ void BaseTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1, const c
|
||||
constexpr int buttonHeight = BaseMetrics::values.buttonHintsHeight;
|
||||
constexpr int buttonY = BaseMetrics::values.buttonHintsHeight; // Distance from bottom
|
||||
constexpr int textYOffset = 7; // Distance from top of button to text baseline
|
||||
constexpr int buttonPositions[] = {25, 130, 245, 350};
|
||||
// X3 has wider screen in portrait (528 vs 480), use more spacing
|
||||
constexpr int x4ButtonPositions[] = {25, 130, 245, 350};
|
||||
constexpr int x3ButtonPositions[] = {38, 154, 268, 384};
|
||||
const int* buttonPositions = gpio.deviceIsX3() ? x3ButtonPositions : x4ButtonPositions;
|
||||
const char* labels[] = {btn1, btn2, btn3, btn4};
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
@@ -162,50 +165,63 @@ void BaseTheme::drawSideButtonHints(const GfxRenderer& renderer, const char* top
|
||||
const int screenWidth = renderer.getScreenWidth();
|
||||
constexpr int buttonWidth = BaseMetrics::values.sideButtonHintsWidth; // Width on screen (height when rotated)
|
||||
constexpr int buttonHeight = 80; // Height on screen (width when rotated)
|
||||
constexpr int buttonX = 4; // Distance from right edge
|
||||
// Position for the button group - buttons share a border so they're adjacent
|
||||
constexpr int topButtonY = 345; // Top button position
|
||||
constexpr int buttonMargin = 4;
|
||||
|
||||
const char* labels[] = {topBtn, bottomBtn};
|
||||
if (gpio.deviceIsX3()) {
|
||||
// X3 layout: Up on left side, Down on right side, positioned higher
|
||||
constexpr int x3ButtonY = 155;
|
||||
|
||||
// Draw the shared border for both buttons as one unit
|
||||
const int x = screenWidth - buttonX - buttonWidth;
|
||||
|
||||
// Draw top button outline (3 sides, bottom open)
|
||||
if (topBtn != nullptr && topBtn[0] != '\0') {
|
||||
renderer.drawLine(x, topButtonY, x + buttonWidth - 1, topButtonY); // Top
|
||||
renderer.drawLine(x, topButtonY, x, topButtonY + buttonHeight - 1); // Left
|
||||
renderer.drawLine(x + buttonWidth - 1, topButtonY, x + buttonWidth - 1, topButtonY + buttonHeight - 1); // Right
|
||||
}
|
||||
|
||||
// Draw shared middle border
|
||||
if ((topBtn != nullptr && topBtn[0] != '\0') || (bottomBtn != nullptr && bottomBtn[0] != '\0')) {
|
||||
renderer.drawLine(x, topButtonY + buttonHeight, x + buttonWidth - 1, topButtonY + buttonHeight); // Shared border
|
||||
}
|
||||
|
||||
// Draw bottom button outline (3 sides, top is shared)
|
||||
if (bottomBtn != nullptr && bottomBtn[0] != '\0') {
|
||||
renderer.drawLine(x, topButtonY + buttonHeight, x, topButtonY + 2 * buttonHeight - 1); // Left
|
||||
renderer.drawLine(x + buttonWidth - 1, topButtonY + buttonHeight, x + buttonWidth - 1,
|
||||
topButtonY + 2 * buttonHeight - 1); // Right
|
||||
renderer.drawLine(x, topButtonY + 2 * buttonHeight - 1, x + buttonWidth - 1,
|
||||
topButtonY + 2 * buttonHeight - 1); // Bottom
|
||||
}
|
||||
|
||||
// Draw text for each button
|
||||
for (int i = 0; i < 2; i++) {
|
||||
if (labels[i] != nullptr && labels[i][0] != '\0') {
|
||||
const int y = topButtonY + i * buttonHeight;
|
||||
|
||||
// Draw rotated text centered in the button
|
||||
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, labels[i]);
|
||||
if (topBtn != nullptr && topBtn[0] != '\0') {
|
||||
const int leftX = buttonMargin;
|
||||
renderer.drawRect(leftX, x3ButtonY, buttonWidth, buttonHeight);
|
||||
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, topBtn);
|
||||
const int textHeight = renderer.getTextHeight(SMALL_FONT_ID);
|
||||
const int textX = leftX + (buttonWidth - textHeight) / 2;
|
||||
const int textY = x3ButtonY + (buttonHeight + textWidth) / 2;
|
||||
renderer.drawTextRotated90CW(SMALL_FONT_ID, textX, textY, topBtn);
|
||||
}
|
||||
|
||||
// Center the rotated text in the button
|
||||
const int textX = x + (buttonWidth - textHeight) / 2;
|
||||
const int textY = y + (buttonHeight + textWidth) / 2;
|
||||
if (bottomBtn != nullptr && bottomBtn[0] != '\0') {
|
||||
const int rightX = screenWidth - buttonMargin - buttonWidth;
|
||||
renderer.drawRect(rightX, x3ButtonY, buttonWidth, buttonHeight);
|
||||
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, bottomBtn);
|
||||
const int textHeight = renderer.getTextHeight(SMALL_FONT_ID);
|
||||
const int textX = rightX + (buttonWidth - textHeight) / 2;
|
||||
const int textY = x3ButtonY + (buttonHeight + textWidth) / 2;
|
||||
renderer.drawTextRotated90CW(SMALL_FONT_ID, textX, textY, bottomBtn);
|
||||
}
|
||||
} else {
|
||||
// X4 layout: Both buttons stacked on right side
|
||||
constexpr int topButtonY = 345;
|
||||
const char* labels[] = {topBtn, bottomBtn};
|
||||
const int x = screenWidth - buttonMargin - buttonWidth;
|
||||
|
||||
renderer.drawTextRotated90CW(SMALL_FONT_ID, textX, textY, labels[i]);
|
||||
if (topBtn != nullptr && topBtn[0] != '\0') {
|
||||
renderer.drawLine(x, topButtonY, x + buttonWidth - 1, topButtonY);
|
||||
renderer.drawLine(x, topButtonY, x, topButtonY + buttonHeight - 1);
|
||||
renderer.drawLine(x + buttonWidth - 1, topButtonY, x + buttonWidth - 1, topButtonY + buttonHeight - 1);
|
||||
}
|
||||
|
||||
if ((topBtn != nullptr && topBtn[0] != '\0') || (bottomBtn != nullptr && bottomBtn[0] != '\0')) {
|
||||
renderer.drawLine(x, topButtonY + buttonHeight, x + buttonWidth - 1, topButtonY + buttonHeight);
|
||||
}
|
||||
|
||||
if (bottomBtn != nullptr && bottomBtn[0] != '\0') {
|
||||
renderer.drawLine(x, topButtonY + buttonHeight, x, topButtonY + 2 * buttonHeight - 1);
|
||||
renderer.drawLine(x + buttonWidth - 1, topButtonY + buttonHeight, x + buttonWidth - 1,
|
||||
topButtonY + 2 * buttonHeight - 1);
|
||||
renderer.drawLine(x, topButtonY + 2 * buttonHeight - 1, x + buttonWidth - 1, topButtonY + 2 * buttonHeight - 1);
|
||||
}
|
||||
|
||||
for (int i = 0; i < 2; i++) {
|
||||
if (labels[i] != nullptr && labels[i][0] != '\0') {
|
||||
const int y = topButtonY + i * buttonHeight;
|
||||
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, labels[i]);
|
||||
const int textHeight = renderer.getTextHeight(SMALL_FONT_ID);
|
||||
const int textX = x + (buttonWidth - textHeight) / 2;
|
||||
const int textY = y + (buttonHeight + textWidth) / 2;
|
||||
renderer.drawTextRotated90CW(SMALL_FONT_ID, textX, textY, labels[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,7 +342,10 @@ void LyraTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1, const c
|
||||
constexpr int buttonHeight = LyraMetrics::values.buttonHintsHeight;
|
||||
constexpr int buttonY = LyraMetrics::values.buttonHintsHeight; // Distance from bottom
|
||||
constexpr int textYOffset = 7; // Distance from top of button to text baseline
|
||||
constexpr int buttonPositions[] = {58, 146, 254, 342};
|
||||
// X3 has wider screen in portrait (528 vs 480), use more spacing
|
||||
constexpr int x4ButtonPositions[] = {58, 146, 254, 342};
|
||||
constexpr int x3ButtonPositions[] = {65, 157, 291, 383};
|
||||
const int* buttonPositions = gpio.deviceIsX3() ? x3ButtonPositions : x4ButtonPositions;
|
||||
const char* labels[] = {btn1, btn2, btn3, btn4};
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
@@ -371,34 +374,47 @@ void LyraTheme::drawSideButtonHints(const GfxRenderer& renderer, const char* top
|
||||
const int screenWidth = renderer.getScreenWidth();
|
||||
constexpr int buttonWidth = LyraMetrics::values.sideButtonHintsWidth; // Width on screen (height when rotated)
|
||||
constexpr int buttonHeight = 78; // Height on screen (width when rotated)
|
||||
// Position for the button group - buttons share a border so they're adjacent
|
||||
constexpr int buttonMargin = 0;
|
||||
|
||||
const char* labels[] = {topBtn, bottomBtn};
|
||||
if (gpio.deviceIsX3()) {
|
||||
// X3 layout: Up on left side, Down on right side, positioned higher
|
||||
constexpr int x3ButtonY = 155;
|
||||
|
||||
// Draw the shared border for both buttons as one unit
|
||||
const int x = screenWidth - buttonWidth;
|
||||
if (topBtn != nullptr && topBtn[0] != '\0') {
|
||||
renderer.drawRoundedRect(buttonMargin, x3ButtonY, buttonWidth, buttonHeight, 1, cornerRadius, false, true, false,
|
||||
true, true);
|
||||
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, topBtn);
|
||||
renderer.drawTextRotated90CW(SMALL_FONT_ID, buttonMargin, x3ButtonY + (buttonHeight + textWidth) / 2, topBtn);
|
||||
}
|
||||
|
||||
// Draw top button outline
|
||||
if (topBtn != nullptr && topBtn[0] != '\0') {
|
||||
renderer.drawRoundedRect(x, topHintButtonY, buttonWidth, buttonHeight, 1, cornerRadius, true, false, true, false,
|
||||
true);
|
||||
}
|
||||
if (bottomBtn != nullptr && bottomBtn[0] != '\0') {
|
||||
const int rightX = screenWidth - buttonWidth;
|
||||
renderer.drawRoundedRect(rightX, x3ButtonY, buttonWidth, buttonHeight, 1, cornerRadius, true, false, true, false,
|
||||
true);
|
||||
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, bottomBtn);
|
||||
renderer.drawTextRotated90CW(SMALL_FONT_ID, rightX, x3ButtonY + (buttonHeight + textWidth) / 2, bottomBtn);
|
||||
}
|
||||
} else {
|
||||
// X4 layout: Both buttons stacked on right side
|
||||
const char* labels[] = {topBtn, bottomBtn};
|
||||
const int x = screenWidth - buttonWidth;
|
||||
|
||||
// Draw bottom button outline
|
||||
if (bottomBtn != nullptr && bottomBtn[0] != '\0') {
|
||||
renderer.drawRoundedRect(x, topHintButtonY + buttonHeight + 5, buttonWidth, buttonHeight, 1, cornerRadius, true,
|
||||
false, true, false, true);
|
||||
}
|
||||
if (topBtn != nullptr && topBtn[0] != '\0') {
|
||||
renderer.drawRoundedRect(x, topHintButtonY, buttonWidth, buttonHeight, 1, cornerRadius, true, false, true, false,
|
||||
true);
|
||||
}
|
||||
|
||||
// Draw text for each button
|
||||
for (int i = 0; i < 2; i++) {
|
||||
if (labels[i] != nullptr && labels[i][0] != '\0') {
|
||||
const int y = topHintButtonY + (i * buttonHeight + 5);
|
||||
if (bottomBtn != nullptr && bottomBtn[0] != '\0') {
|
||||
renderer.drawRoundedRect(x, topHintButtonY + buttonHeight + 5, buttonWidth, buttonHeight, 1, cornerRadius, true,
|
||||
false, true, false, true);
|
||||
}
|
||||
|
||||
// Draw rotated text centered in the button
|
||||
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, labels[i]);
|
||||
|
||||
renderer.drawTextRotated90CW(SMALL_FONT_ID, x, y + (buttonHeight + textWidth) / 2, labels[i]);
|
||||
for (int i = 0; i < 2; i++) {
|
||||
if (labels[i] != nullptr && labels[i][0] != '\0') {
|
||||
const int y = topHintButtonY + (i * buttonHeight) + 5;
|
||||
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, labels[i]);
|
||||
renderer.drawTextRotated90CW(SMALL_FONT_ID, x, y + (buttonHeight + textWidth) / 2, labels[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-13
@@ -27,8 +27,6 @@
|
||||
#include "util/ButtonNavigator.h"
|
||||
#include "util/ScreenshotUtil.h"
|
||||
|
||||
HalDisplay display;
|
||||
HalGPIO gpio;
|
||||
MappedInputManager mappedInputManager(gpio);
|
||||
GfxRenderer renderer(display);
|
||||
ActivityManager activityManager(renderer, mappedInputManager);
|
||||
@@ -171,7 +169,6 @@ void verifyPowerButtonDuration() {
|
||||
powerManager.startDeepSleep(gpio);
|
||||
}
|
||||
}
|
||||
|
||||
void waitForPowerRelease() {
|
||||
gpio.update();
|
||||
while (gpio.isPressed(HalGPIO::BTN_POWER)) {
|
||||
@@ -189,7 +186,6 @@ void enterDeepSleep() {
|
||||
activityManager.goToSleep();
|
||||
|
||||
display.deepSleep();
|
||||
LOG_DBG("MAIN", "Power button press calibration value: %lu ms", t2 - t1);
|
||||
LOG_DBG("MAIN", "Entering deep sleep");
|
||||
|
||||
powerManager.startDeepSleep(gpio);
|
||||
@@ -235,15 +231,17 @@ void setup() {
|
||||
gpio.begin();
|
||||
powerManager.begin();
|
||||
|
||||
// Only start serial if USB connected
|
||||
#ifdef ENABLE_SERIAL_LOG
|
||||
if (gpio.isUsbConnected()) {
|
||||
Serial.begin(115200);
|
||||
// Wait up to 3 seconds for Serial to be ready to catch early logs
|
||||
unsigned long start = millis();
|
||||
while (!Serial && (millis() - start) < 3000) {
|
||||
const unsigned long start = millis();
|
||||
while (!Serial && (millis() - start) < 500) {
|
||||
delay(10);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
LOG_INF("MAIN", "Hardware detect: %s", gpio.deviceIsX3() ? "X3" : "X4");
|
||||
|
||||
// SD Card Initialization
|
||||
// We need 6 open files concurrently when parsing a new chapter
|
||||
@@ -263,11 +261,12 @@ void setup() {
|
||||
UITheme::getInstance().reload();
|
||||
ButtonNavigator::setMappedInputManager(mappedInputManager);
|
||||
|
||||
switch (gpio.getWakeupReason()) {
|
||||
const auto wakeupReason = gpio.getWakeupReason();
|
||||
switch (wakeupReason) {
|
||||
case HalGPIO::WakeupReason::PowerButton:
|
||||
// For normal wakeups, verify power button press duration
|
||||
LOG_DBG("MAIN", "Verifying power button press duration");
|
||||
verifyPowerButtonDuration();
|
||||
gpio.verifyPowerButtonWakeup(SETTINGS.getPowerButtonDuration(),
|
||||
SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::SLEEP);
|
||||
break;
|
||||
case HalGPIO::WakeupReason::AfterUSBPower:
|
||||
// If USB power caused a cold boot, go back to sleep
|
||||
@@ -332,9 +331,10 @@ void loop() {
|
||||
String cmd = line.substring(4);
|
||||
cmd.trim();
|
||||
if (cmd == "SCREENSHOT") {
|
||||
logSerial.printf("SCREENSHOT_START:%d\n", HalDisplay::BUFFER_SIZE);
|
||||
const uint32_t bufferSize = display.getBufferSize();
|
||||
logSerial.printf("SCREENSHOT_START:%d\n", bufferSize);
|
||||
uint8_t* buf = display.getFrameBuffer();
|
||||
logSerial.write(buf, HalDisplay::BUFFER_SIZE);
|
||||
logSerial.write(buf, bufferSize);
|
||||
logSerial.printf("SCREENSHOT_END\n");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user