Touch held time was being lost when gestures were detected because the gesture detection happens before the touch release event. Now remembers the held time at the moment of gesture detection and returns it within a 250ms window, allowing UI elements to properly respond to long-press gestures. Also refactors deep sleep code to use PowerManager methods.
357 lines
11 KiB
C++
357 lines
11 KiB
C++
#include <HalGPIO.h>
|
|
#include <Logging.h>
|
|
#include <PowerManager.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();
|
|
#if FREEINK_MCU_C3
|
|
// Claim the shared SPI bus with the X4/X3 display+SD pins. These EPD_* pin
|
|
// macros are hardcoded C3/Xteink values, so this pre-claim is only valid on the
|
|
// C3 family. On other boards (M5Paper's IT8951, Sticky's SSD1677, ...) the SDK
|
|
// driver and SDCardManager bring up SPI from BoardConfig::ACTIVE pins; pre-
|
|
// claiming here would stick (SPIClass::begin early-returns once the bus is
|
|
// started) and leave the display/SD on the wrong pins.
|
|
SPI.begin(EPD_SCLK, SPI_MISO, EPD_MOSI, EPD_CS);
|
|
#endif
|
|
|
|
#if FREEINK_MCU_C3
|
|
_deviceType = detectDeviceTypeWithFingerprint();
|
|
|
|
if (deviceIsX4()) {
|
|
pinMode(BAT_GPIO0, INPUT);
|
|
pinMode(UART0_RXD, INPUT);
|
|
}
|
|
#else
|
|
// Non-C3 boards (S3/ESP32) are single-device builds; their pins, panel, and
|
|
// battery backend all come from BoardConfig::ACTIVE. The X3/X4 fingerprint probe
|
|
// targets Xteink C3 hardware and drives Wire on X3_I2C_SDA/SCL (GPIO20/0) — which
|
|
// collides with this board's own I2C (e.g. Sticky's BQ27220 gauge shares GPIO0)
|
|
// and reconfigures strapping pins. Skip it; _deviceType stays X4 ("not X3"),
|
|
// the correct non-X3 branch for every deviceIsX3() consumer.
|
|
_deviceType = DeviceType::X4;
|
|
#endif
|
|
}
|
|
|
|
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(); }
|
|
|
|
unsigned long HalGPIO::getPowerButtonHeldTime() const { return inputMgr.getPowerButtonHeldTime(); }
|
|
|
|
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::isTouchTapCandidate(float& nx, float& ny, unsigned long& heldMs) const {
|
|
return inputMgr.isTouchTapCandidate(nx, ny, heldMs);
|
|
}
|
|
|
|
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::hasTouch() const { return inputMgr.hasTouch(); }
|
|
|
|
bool HalGPIO::wasTouchActivity() const { return inputMgr.wasTouchActivity(); }
|
|
|
|
bool HalGPIO::isXteinkDevice() const {
|
|
const auto board = BoardConfig::ACTIVE.board;
|
|
return board == BoardConfig::Board::XteinkX3 || board == BoardConfig::Board::XteinkX4;
|
|
}
|
|
|
|
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();
|
|
}
|
|
freeink::PowerManager::armPowerButtonWakeup();
|
|
freeink::PowerManager::deepSleep();
|
|
}
|
|
|
|
void HalGPIO::verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPressAllowed) {
|
|
if (BoardConfig::ACTIVE.input.power < 0) {
|
|
// No readable power-button input pin: can't verify a hold, so don't sleep.
|
|
return;
|
|
}
|
|
#if defined(FREEINK_DEVICE_M5PAPER) && FREEINK_DEVICE_M5PAPER
|
|
// M5Paper: power-on is a hardware latch and the "power button" is the rotary
|
|
// push (G38), shared with Confirm. A USB/flash cold boot is indistinguishable
|
|
// from an intentional power-on hold here, so skip the X4-style anti-accidental-
|
|
// wake check and always boot. G38 still serves as the deep-sleep wake source.
|
|
return;
|
|
#endif
|
|
if (shortPressAllowed) {
|
|
// Fast path - no duration check needed
|
|
return;
|
|
}
|
|
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)) {
|
|
const auto holdStart = millis();
|
|
do {
|
|
delay(10);
|
|
inputMgr.update();
|
|
} while (inputMgr.isPressed(BTN_POWER) && millis() - holdStart < requiredDurationMs);
|
|
if (millis() - holdStart < requiredDurationMs) {
|
|
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;
|
|
}
|
|
if (BoardConfig::ACTIVE.usbDetect < 0) {
|
|
return false;
|
|
}
|
|
return digitalRead(BoardConfig::ACTIVE.usbDetect) == 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 (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) {
|
|
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;
|
|
}
|