Something went wrong. Try again.
A fork of https://github.com/crosspoint-reader/crosspoint-reader
Something went wrong. Try again.
12 kB · 303 lines
C++
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304#include <BatteryMonitor.h>#include <HalGPIO.h>#include <Logging.h>#include <PowerManager.h>#include <Preferences.h>#include <SPI.h>#include <Wire.h>#include <XteinkDetect.h>#include <esp_sleep.h>
// Global HalGPIO instanceHalGPIO gpio;
namespace X3GPIO {
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;}
} // namespace X3GPIO
namespace {constexpr char HW_NAMESPACE[] = "cphw";constexpr char NVS_KEY_DEV_OVERRIDE[] = "dev_ovr"; // 0=auto, 1=x4, 2=x3constexpr 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: use FreeInk's canonical two-pass X3 fingerprint and persist // only confirmed results. Inconclusive probes deliberately remain uncached. uint8_t score1 = 0; uint8_t score2 = 0; const freeink::XteinkVerdict verdict = freeink::detectXteinkVerdict(&score1, &score2); LOG_INF("HW", "Xteink probe scores: pass1=%u pass2=%u verdict=%u", score1, score2, static_cast<unsigned>(verdict));
if (verdict == freeink::XteinkVerdict::X3Confirmed) { writeNvsDeviceValue(NVS_KEY_DEV_CACHED, NvsDeviceValue::X3); return HalGPIO::DeviceType::X3; }
if (verdict == freeink::XteinkVerdict::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() {#if FREEINK_MCU_C3 _deviceType = detectDeviceTypeWithFingerprint(); BoardConfig::selectDevice(deviceIsX3() ? BoardConfig::Board::XteinkX3 : BoardConfig::Board::XteinkX4);
// Resolve the per-batch controller before SPI owns the display pins. FreeInk // checks the OEM hw_calib/screenType value first, then falls back to its // two-pass display-bus probe. X3's facade keys panel selection off the sibling // board profile, so preserve a detected UC8279 through setDisplayX3(). freeink::applyXteinkDisplayController(); if (deviceIsX3() && BoardConfig::ACTIVE.displayController == BoardConfig::DisplayController::UC8279) { BoardConfig::selectDevice(BoardConfig::Board::XteinkX3Uc8279); }
SPI.begin(EPD_SCLK, SPI_MISO, EPD_MOSI, EPD_CS);
if (deviceIsX4()) { pinMode(BAT_GPIO0, INPUT); pinMode(UART0_RXD, INPUT); }#else _deviceType = DeviceType::X4;#endif inputMgr.begin();}
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(); }
bool HalGPIO::rawInputActive() { if (inputMgr.isPowerButtonPhysicallyPressed()) return true; InputManager::ButtonAdcSample g1{}, g2{}; inputMgr.readButtonAdc(g1, g2); // The Xteink ladder idles at the ADC full-scale rail (~4095); every button band sits below 3900. constexpr int kIdleRailMin = 4000; return (g1.raw >= 0 && g1.raw < kIdleRailMin) || (g2.raw >= 0 && g2.raw < kIdleRailMin);}
unsigned long HalGPIO::getHeldTime() const { return inputMgr.getHeldTime(); }
unsigned long HalGPIO::getPowerButtonHeldTime() const { return inputMgr.getPowerButtonHeldTime(); }
bool HalGPIO::hasTouch() const { return inputMgr.hasTouch(); }
bool HalGPIO::hasHomeKey() const { return BoardConfig::hasHomeKey(); }
bool HalGPIO::wasHomeKeyPressed() const { return inputMgr.wasHomeKeyPressed(); }
bool HalGPIO::wasHomeKeyTapped() const { return inputMgr.wasHomeKeyTapped(); }
bool HalGPIO::wasHomeKeyLongPressed() const { return inputMgr.wasHomeKeyLongPressed(); }
bool HalGPIO::wasTouchTap(float& nx, float& ny) const { return inputMgr.wasTouchTap(nx, ny); }
bool HalGPIO::wasTouchDown(float& nx, float& ny) const { return inputMgr.wasTouchPressedAt(nx, ny); }
bool HalGPIO::wasTouchReleased() const { return inputMgr.wasTouchReleased(); }
bool HalGPIO::isTouchTapCandidate(float& nx, float& ny, unsigned long& heldMs) const { return inputMgr.isTouchTapCandidate(nx, ny, heldMs);}
bool HalGPIO::isTouchHeldAt(float& nx, float& ny) const { return inputMgr.isTouchHeldAt(nx, ny); }
bool HalGPIO::wasTouchLongPress(float& nx, float& ny) const { return inputMgr.wasTouchLongPress(nx, ny); }
void HalGPIO::suppressTouchContact() { inputMgr.suppressTouchContact(); }
unsigned long HalGPIO::lastTouchHeldMs() const { return inputMgr.lastTouchHeldMs(); }
bool HalGPIO::wasSwipe(float& nxStart, float& nyStart, float& nxEnd, float& nyEnd) const { return inputMgr.wasSwipe(nxStart, nyStart, nxEnd, nyEnd);}
bool HalGPIO::wasTouchActivity() const { return inputMgr.wasTouchActivity(); }
void HalGPIO::setSharedConfirmPowerShortPressEmitsPower(const bool enabled) { InputManager::setSharedConfirmPowerShortPressEmitsPower(enabled);}
bool HalGPIO::hasEdgeSideButtons() const { return BoardConfig::ACTIVE.board == BoardConfig::Board::XteinkX3 || BoardConfig::ACTIVE.board == BoardConfig::Board::XteinkX3Uc8279 || BoardConfig::ACTIVE.board == BoardConfig::Board::XteinkX4Pro || BoardConfig::ACTIVE.board == BoardConfig::Board::XteinkX4Classic;}
bool HalGPIO::isXteinkDevice() const { return BoardConfig::ACTIVE.board == BoardConfig::Board::XteinkX3 || BoardConfig::ACTIVE.board == BoardConfig::Board::XteinkX3Uc8279 || BoardConfig::ACTIVE.board == BoardConfig::Board::XteinkX4;}
bool HalGPIO::verifyPowerButtonWakeup() { // M5Paper v1.1: the classic ESP32's reset-to-setup() latency exceeds a normal // wheel click, so a click wake is always released before this samples and // verification would re-sleep on every wake. Its wheel has hard external // pull-ups, so the ghost-wake debounce this implements is not needed. if (BoardConfig::isPaperMono() || BoardConfig::isM5PaperV11() || BoardConfig::ACTIVE.input.power < 0) { return true; }
constexpr unsigned long POWER_WAKE_STABILITY_MS = 10; const bool heldAtFirstSample = inputMgr.isPowerButtonPhysicallyPressed(); const unsigned long sampleStart = millis(); inputMgr.update(); while (millis() - sampleStart < POWER_WAKE_STABILITY_MS || inputMgr.isDebouncePending()) { delay(1); inputMgr.update(); } return heldAtFirstSample && inputMgr.isPowerButtonPhysicallyPressed();}
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; } if (BoardConfig::ACTIVE.usbDetect >= 0) { return digitalRead(BoardConfig::ACTIVE.usbDetect) == HIGH; } // No digital USB-detect line (e.g. Sticky, whose PWR_IN_VOLT is an analog // divider): infer external power from charging state instead. BatteryMonitor // picks the board's best source — charger IC status, gauge Current() sign, or // a /STAT pin — and reports false on boards with no battery telemetry at all. // Caveat: charge termination at 100% reads as "not connected". static const BatteryMonitor battery; return battery.isCharging();}
bool HalGPIO::coldBootImpliesPowerButton() const { // Xteink-style power topology: the power button energizes the rail until // firmware latches it, so a no-USB POWERON can only be a still-held button // boot, and plugging USB into an off device should charge-sleep, not boot. // Everything else boots on any cold boot: boards with no USB detection at // all (M5Paper v1.1, PaperColor, Murphy, de-link) would misread USB and // post-flash boots as battery button boots, and STAT-only boards like the // EEGO A4 misread them the same way once the charger terminates at 100% // (STAT inactive reads as "no USB"). return isXteinkDevice() || BoardConfig::isPaperMono() || BoardConfig::isSticky();}
HalGPIO::WakeupReason HalGPIO::getWakeupReason() const { const auto wakeupCause = esp_sleep_get_wakeup_cause(); const auto resetReason = esp_reset_reason();
const bool usbConnected = isUsbConnected();
if (resetReason == ESP_RST_DEEPSLEEP && (wakeupCause == ESP_SLEEP_WAKEUP_GPIO || wakeupCause == ESP_SLEEP_WAKEUP_EXT1)) { return WakeupReason::PowerButton; } if (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED && resetReason == ESP_RST_POWERON && !usbConnected && coldBootImpliesPowerButton()) { 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;}