#include #include #include #include #include #include // 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(bq27220) + static_cast(ds3231) + static_cast(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(1), static_cast(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(2), static_cast(true)) < 2) { while (Wire.available()) { Wire.read(); } return false; } const uint8_t lo = Wire.read(); const uint8_t hi = Wire.read(); *outValue = (static_cast(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(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(defaultValue)); prefs.end(); if (raw > static_cast(NvsDeviceValue::X3)) { return defaultValue; } return static_cast(raw); } void writeNvsDeviceValue(const char* key, NvsDeviceValue value) { Preferences prefs; if (!prefs.begin(HW_NAMESPACE, false)) { return; } prefs.putUChar(key, static_cast(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::waitForStablePowerRelease() { // Wait until the raw power-button pin reads HIGH (released) for RELEASE_STABLE_MS // consecutive milliseconds. The InputManager debounce (5 ms) is too short for // mechanical switch bounce which can last 10-50 ms, so we bypass it entirely here. constexpr unsigned long RELEASE_STABLE_MS = 200; const unsigned long waitStart = millis(); unsigned long stableStart = 0; while (true) { if (digitalRead(InputManager::POWER_BUTTON_PIN) == HIGH) { if (stableStart == 0) stableStart = millis(); if (millis() - stableStart >= RELEASE_STABLE_MS) break; } else { stableStart = 0; } delay(10); } LOG_DBG("GPIO", "Power button stable-released after %lu ms", millis() - waitStart); } void HalGPIO::startDeepSleep() { LOG_DBG("GPIO", "startDeepSleep: waiting for power button release (isPressed=%d, rawPin=%d)", inputMgr.isPressed(BTN_POWER), digitalRead(InputManager::POWER_BUTTON_PIN) == LOW); waitForStablePowerRelease(); // 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); LOG_DBG("GPIO", "startDeepSleep: entering deep sleep now"); // Enter Deep Sleep esp_deep_sleep_start(); } void HalGPIO::verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPressAllowed) { // The wakeup reason was already confirmed as a power button press before this is called, // so we know a real press occurred. When short presses are allowed, nothing more to verify. if (shortPressAllowed) { LOG_DBG("GPIO", "verifyPowerButtonWakeup: shortPressAllowed, skipping hold verification"); return; } // Calibrate: subtract boot time already elapsed, assuming button held since boot. // Never collapse to less than BOUNCE_TOLERANCE_MS so the hold loop always has time to // sample the button and detect a release (early release = unintentional tap). constexpr unsigned long BOUNCE_TOLERANCE_MS = 100; const uint16_t calibration = millis(); const uint16_t calibratedDuration = (calibration < requiredDurationMs) ? (requiredDurationMs - calibration) : BOUNCE_TOLERANCE_MS; LOG_DBG("GPIO", "verifyPowerButtonWakeup: requiredMs=%u, calibration=%u, calibratedMs=%u", requiredDurationMs, calibration, calibratedDuration); const auto start = millis(); inputMgr.update(); // inputMgr.isPressed() may take up to ~500ms to return correct state after boot while (!inputMgr.isPressed(BTN_POWER) && millis() - start < 1000) { delay(10); inputMgr.update(); } LOG_DBG("GPIO", "verifyPowerButtonWakeup: initial detect took %lu ms, isPressed=%d, rawPin=%d", millis() - start, inputMgr.isPressed(BTN_POWER), digitalRead(InputManager::POWER_BUTTON_PIN) == LOW); if (inputMgr.isPressed(BTN_POWER)) { // Monitor the hold for calibratedDuration, tolerating brief bounces up to BOUNCE_TOLERANCE_MS. // Early release beyond the bounce window means an unintentional tap — go back to sleep. unsigned long lastSeenPressed = millis(); const auto holdStart = millis(); unsigned long bounceCount = 0; while (millis() - holdStart < calibratedDuration) { delay(10); inputMgr.update(); if (inputMgr.isPressed(BTN_POWER)) { if (millis() - lastSeenPressed > 20) { bounceCount++; } lastSeenPressed = millis(); } else if (millis() - lastSeenPressed >= BOUNCE_TOLERANCE_MS) { LOG_DBG("GPIO", "verifyPowerButtonWakeup: released early after %lu ms (bounces=%lu), going to sleep", millis() - holdStart, bounceCount); startDeepSleep(); } } LOG_DBG("GPIO", "verifyPowerButtonWakeup: hold verified after %lu ms (bounces=%lu), proceeding with boot", millis() - holdStart, bounceCount); } else { LOG_DBG("GPIO", "verifyPowerButtonWakeup: button not pressed after 1s wait, going to sleep"); startDeepSleep(); } } bool HalGPIO::isUsbConnected() const { if (deviceIsX3()) { // X3: GPIO20 is repurposed as I2C SDA, so the X4 pin-level USB detect is // unusable here — the I2C pull-ups would always report HIGH. Probe the // BQ27220 fuel gauge instead. Using just Current() mis-reports "not // connected" when the battery is full (current ~= 0 mA); combine it with // the Flags() DSG bit so we report true whenever the charger is present // (DSG=0 means charging or fully charged, not discharging). for (uint8_t attempt = 0; attempt < 2; ++attempt) { uint16_t flags = 0; if (X3GPIO::readI2CReg16LE(I2C_ADDR_BQ27220, BQ27220_FLAGS_REG, &flags)) { if ((flags & BQ27220_FLAG_DSG) == 0) { return true; } int16_t currentMa = 0; if (X3GPIO::readBQ27220CurrentMA(¤tMa) && currentMa > 0) { return true; } return false; } delay(2); } return false; } // X4: 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(); // X3: USB alone cannot cold-boot the MCU — the battery-latch MOSFET must be // closed by a physical power-button press. So any POWERON reset on X3 is a // button press, regardless of whether USB happens to be plugged in as well. // Short-circuit before isUsbConnected() to skip the BQ27220 I2C probe/retry. if (deviceIsX3() && wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED && resetReason == ESP_RST_POWERON) { return WakeupReason::PowerButton; } const bool usbConnected = isUsbConnected(); LOG_DBG("GPIO", "getWakeupReason: wakeupCause=%d, resetReason=%d, usbConnected=%d", static_cast(wakeupCause), static_cast(resetReason), usbConnected); 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; }