## 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.
305 lines
9.3 KiB
C++
305 lines
9.3 KiB
C++
#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);
|
|
|
|
_deviceType = detectDeviceTypeWithFingerprint();
|
|
|
|
if (deviceIsX4()) {
|
|
pinMode(BAT_GPIO0, INPUT);
|
|
pinMode(UART0_RXD, INPUT);
|
|
}
|
|
}
|
|
|
|
void HalGPIO::update() {
|
|
inputMgr.update();
|
|
const bool connected = isUsbConnected();
|
|
usbStateChanged = (connected != lastUsbConnected);
|
|
lastUsbConnected = connected;
|
|
}
|
|
|
|
bool HalGPIO::wasUsbStateChanged() const { return usbStateChanged; }
|
|
|
|
bool HalGPIO::isPressed(uint8_t buttonIndex) const { return inputMgr.isPressed(buttonIndex); }
|
|
|
|
bool HalGPIO::wasPressed(uint8_t buttonIndex) const { return inputMgr.wasPressed(buttonIndex); }
|
|
|
|
bool HalGPIO::wasAnyPressed() const { return inputMgr.wasAnyPressed(); }
|
|
|
|
bool HalGPIO::wasReleased(uint8_t buttonIndex) const { return inputMgr.wasReleased(buttonIndex); }
|
|
|
|
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 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;
|
|
}
|
|
if (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED && resetReason == ESP_RST_UNKNOWN && usbConnected) {
|
|
return WakeupReason::AfterFlash;
|
|
}
|
|
if (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED && resetReason == ESP_RST_POWERON && usbConnected) {
|
|
return WakeupReason::AfterUSBPower;
|
|
}
|
|
return WakeupReason::Other;
|
|
}
|