2026-01-27 18:50:15 +01:00
|
|
|
#pragma once
|
|
|
|
|
|
|
|
|
|
#include <Arduino.h>
|
|
|
|
|
#include <InputManager.h>
|
|
|
|
|
|
|
|
|
|
// Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults)
|
|
|
|
|
#define EPD_SCLK 8 // SPI Clock
|
|
|
|
|
#define EPD_MOSI 10 // SPI MOSI (Master Out Slave In)
|
|
|
|
|
#define EPD_CS 21 // Chip Select
|
|
|
|
|
#define EPD_DC 4 // Data/Command
|
|
|
|
|
#define EPD_RST 5 // Reset
|
|
|
|
|
#define EPD_BUSY 6 // Busy
|
|
|
|
|
|
|
|
|
|
#define SPI_MISO 7 // SPI MISO, shared between SD card and display (Master In Slave Out)
|
|
|
|
|
|
|
|
|
|
#define BAT_GPIO0 0 // Battery voltage
|
|
|
|
|
|
|
|
|
|
#define UART0_RXD 20 // Used for USB connection detection
|
|
|
|
|
|
2026-04-04 09:25:43 -06:00
|
|
|
// Xteink X3 Hardware
|
|
|
|
|
#define X3_I2C_SDA 20
|
|
|
|
|
#define X3_I2C_SCL 0
|
|
|
|
|
#define X3_I2C_FREQ 400000
|
|
|
|
|
|
|
|
|
|
// TI BQ27220 Fuel gauge I2C
|
|
|
|
|
#define I2C_ADDR_BQ27220 0x55 // Fuel gauge I2C address
|
|
|
|
|
#define BQ27220_SOC_REG 0x2C // StateOfCharge() command code (%)
|
|
|
|
|
#define BQ27220_CUR_REG 0x0C // Current() command code (signed mA)
|
|
|
|
|
#define BQ27220_VOLT_REG 0x08 // Voltage() command code (mV)
|
|
|
|
|
|
|
|
|
|
// Analog DS3231 RTC I2C
|
|
|
|
|
#define I2C_ADDR_DS3231 0x68 // RTC I2C address
|
|
|
|
|
#define DS3231_SEC_REG 0x00 // Seconds command code (BCD)
|
|
|
|
|
|
|
|
|
|
// QST QMI8658 IMU I2C
|
|
|
|
|
#define I2C_ADDR_QMI8658 0x6B // IMU I2C address
|
|
|
|
|
#define I2C_ADDR_QMI8658_ALT 0x6A // IMU I2C fallback address
|
|
|
|
|
#define QMI8658_WHO_AM_I_REG 0x00 // WHO_AM_I command code
|
|
|
|
|
#define QMI8658_WHO_AM_I_VALUE 0x05 // WHO_AM_I expected value
|
|
|
|
|
|
2026-01-27 18:50:15 +01:00
|
|
|
class HalGPIO {
|
|
|
|
|
#if CROSSPOINT_EMULATED == 0
|
|
|
|
|
InputManager inputMgr;
|
|
|
|
|
#endif
|
|
|
|
|
|
2026-03-20 04:33:29 +01:00
|
|
|
bool lastUsbConnected = false;
|
|
|
|
|
bool usbStateChanged = false;
|
2026-08-12 20:57:45 -04:00
|
|
|
unsigned long usbLastPollMs = 0;
|
|
|
|
|
bool usbElectricalConnected = false; // last result of the per-device electrical/charge check
|
|
|
|
|
|
|
|
|
|
// X3 USB detection is a BQ27220 I2C read (~0.3-1 ms of awake CPU per call);
|
|
|
|
|
// polled every loop it costs a few percent of the light-sleep idle floor for
|
|
|
|
|
// nothing. At >=1 s intervals the energy cost is unmeasurable (~µC/s), so 1 s
|
|
|
|
|
// is chosen for prompt plug/unplug UX (battery icon, light-sleep USB guard /
|
|
|
|
|
// CDC recovery). X4 detection is a single digitalRead and stays per-loop.
|
|
|
|
|
static constexpr unsigned long USB_POLL_X3_MS = 1000;
|
|
|
|
|
|
|
|
|
|
// USB-Serial-JTAG SOF activity, sampled by update(): the host sends a SOF
|
|
|
|
|
// frame every 1 ms while the bus is enumerated, so a frame index that moved
|
|
|
|
|
// between two samples means a live host link. Catches what the charge-based
|
|
|
|
|
// X3 check misses: a data-only cable, and any cable once the battery is full
|
|
|
|
|
// (charge current ~0). Samples must be >SOF_SAMPLE_MS apart — update() can be
|
|
|
|
|
// called back-to-back (inner input loops), and adjacent reads would compare
|
|
|
|
|
// equal and flicker the verdict.
|
|
|
|
|
uint16_t lastSofFrameIndex = 0;
|
|
|
|
|
unsigned long sofLastSampleMs = 0;
|
|
|
|
|
bool usbSofActive = false;
|
|
|
|
|
static constexpr unsigned long SOF_SAMPLE_MS = 10;
|
|
|
|
|
|
|
|
|
|
// Per-device electrical/charge-inference USB check (fresh read; X3 = BQ27220
|
|
|
|
|
// charge current over I2C, X4 = VBUS-driven level on GPIO20).
|
|
|
|
|
bool isUsbElectricalConnected() const;
|
|
|
|
|
|
|
|
|
|
// Shared body of update()/pollUsbState(): SOF sampling + throttled electrical
|
|
|
|
|
// check + combined-verdict edge tracking.
|
|
|
|
|
void updateUsbState(unsigned long now);
|
2026-03-20 04:33:29 +01:00
|
|
|
|
2026-04-04 09:25:43 -06:00
|
|
|
public:
|
|
|
|
|
enum class DeviceType : uint8_t { X4, X3 };
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
DeviceType _deviceType = DeviceType::X4;
|
|
|
|
|
|
2026-01-27 18:50:15 +01:00
|
|
|
public:
|
|
|
|
|
HalGPIO() = default;
|
|
|
|
|
|
2026-04-04 09:25:43 -06:00
|
|
|
// Inline device type helpers for cleaner downstream checks
|
|
|
|
|
inline bool deviceIsX3() const { return _deviceType == DeviceType::X3; }
|
|
|
|
|
inline bool deviceIsX4() const { return _deviceType == DeviceType::X4; }
|
2026-07-20 16:31:07 -04:00
|
|
|
bool isXteinkDevice() const;
|
2026-04-04 09:25:43 -06:00
|
|
|
|
2026-08-12 13:40:09 -04:00
|
|
|
// True when the board's page buttons sit on the left/right screen edges
|
|
|
|
|
// (X3, X4 Pro) rather than an off-screen vertical rocker. Drives side-hint
|
|
|
|
|
// placement and the flipped large-step direction in selection activities.
|
|
|
|
|
// Keyed off the active BoardConfig profile, not the X3/X4 runtime detection.
|
|
|
|
|
bool hasEdgeSideButtons() const;
|
|
|
|
|
|
2026-01-27 18:50:15 +01:00
|
|
|
// Start button GPIO and setup SPI for screen and SD card
|
|
|
|
|
void begin();
|
|
|
|
|
|
|
|
|
|
// Button input methods
|
|
|
|
|
void update();
|
|
|
|
|
bool isPressed(uint8_t buttonIndex) const;
|
|
|
|
|
bool wasPressed(uint8_t buttonIndex) const;
|
|
|
|
|
bool wasAnyPressed() const;
|
|
|
|
|
bool wasReleased(uint8_t buttonIndex) const;
|
|
|
|
|
bool wasAnyReleased() const;
|
2026-08-12 20:57:45 -04:00
|
|
|
// True while a raw button-state change is still inside the debounce window.
|
|
|
|
|
// The idle loop polls fast while this is set so the confirming sample lands
|
|
|
|
|
// ~10 ms after the first; at the 50 ms light-sleep cadence a short tap can
|
|
|
|
|
// otherwise appear in a single sample and never commit (dropped press).
|
|
|
|
|
bool isDebouncePending() const;
|
2026-01-27 18:50:15 +01:00
|
|
|
unsigned long getHeldTime() const;
|
2026-05-16 04:28:52 +02:00
|
|
|
unsigned long getPowerButtonHeldTime() const;
|
2026-07-20 16:31:07 -04:00
|
|
|
bool hasTouch() const;
|
|
|
|
|
bool wasTouchTap(float& nx, float& ny) const;
|
|
|
|
|
bool wasTouchDown(float& nx, float& ny) const;
|
2026-08-12 13:40:09 -04:00
|
|
|
// Raw release edge, reported even when the contact was not a tap (swipe end,
|
|
|
|
|
// drag-off). Snapshot builders forward it so interaction routing can clear
|
|
|
|
|
// pressed state.
|
|
|
|
|
bool wasTouchReleased() const;
|
2026-07-20 16:31:07 -04:00
|
|
|
bool isTouchTapCandidate(float& nx, float& ny, unsigned long& heldMs) const;
|
|
|
|
|
bool isTouchHeldAt(float& nx, float& ny) const;
|
2026-08-12 13:40:09 -04:00
|
|
|
// One-shot long-press, fired by the SDK classifier while the finger is still
|
|
|
|
|
// down (stationary contact held past its threshold). Position = touch-down
|
|
|
|
|
// point. Callers that act on it should suppressTouchContact() so the lift
|
|
|
|
|
// cannot also tap.
|
|
|
|
|
bool wasTouchLongPress(float& nx, float& ny) const;
|
|
|
|
|
// Ignore the remainder of the current contact (its continued hold and its
|
|
|
|
|
// release edge). Self-clears once the contact ends.
|
|
|
|
|
void suppressTouchContact();
|
2026-07-20 16:31:07 -04:00
|
|
|
unsigned long lastTouchHeldMs() const;
|
|
|
|
|
bool wasSwipe(float& nxStart, float& nyStart, float& nxEnd, float& nyEnd) const;
|
|
|
|
|
bool wasTouchActivity() const;
|
|
|
|
|
void setSharedConfirmPowerShortPressEmitsPower(bool enabled);
|
2026-01-27 18:50:15 +01:00
|
|
|
|
2026-04-04 09:25:43 -06:00
|
|
|
// Verify power button was held long enough after wakeup.
|
2026-07-15 17:48:08 +03:00
|
|
|
// Returns true if verification succeeded, false if device should return to sleep.
|
2026-04-04 09:25:43 -06:00
|
|
|
// Should only be called when wakeup reason is PowerButton.
|
2026-07-15 17:48:08 +03:00
|
|
|
bool verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPressAllowed);
|
2026-04-04 09:25:43 -06:00
|
|
|
|
2026-01-27 18:50:15 +01:00
|
|
|
// Check if USB is connected
|
|
|
|
|
bool isUsbConnected() const;
|
|
|
|
|
|
2026-08-12 20:57:45 -04:00
|
|
|
// Sample USB state without a full input update. Called during setup() BEFORE
|
|
|
|
|
// the first e-ink refresh: the boot paint happens before loop() ever runs
|
|
|
|
|
// update(), so without this the light-sleep slice guards see the unsampled
|
|
|
|
|
// default ("no USB") and sleeping through the boot refresh kills a live CDC
|
|
|
|
|
// link (charge-based X3 detection also reads false whenever the battery is
|
|
|
|
|
// full). Two calls >=SOF_SAMPLE_MS apart establish the SOF verdict; the
|
|
|
|
|
// method itself waits out the floor if called too soon after the last sample.
|
|
|
|
|
void pollUsbState();
|
|
|
|
|
|
|
|
|
|
// USB state as sampled by the last update() call.
|
|
|
|
|
// Prefer this in per-loop polling: isUsbConnected() performs a fresh I2C read on X3.
|
|
|
|
|
bool isUsbConnectedCached() const { return lastUsbConnected; }
|
|
|
|
|
|
2026-03-20 04:33:29 +01:00
|
|
|
// Returns true once per edge (plug or unplug) since the last update()
|
|
|
|
|
bool wasUsbStateChanged() const;
|
|
|
|
|
|
2026-02-01 16:19:33 +05:00
|
|
|
enum class WakeupReason { PowerButton, AfterFlash, AfterUSBPower, Other };
|
|
|
|
|
|
|
|
|
|
WakeupReason getWakeupReason() const;
|
2026-01-27 18:50:15 +01:00
|
|
|
|
|
|
|
|
// Button indices
|
|
|
|
|
static constexpr uint8_t BTN_BACK = 0;
|
|
|
|
|
static constexpr uint8_t BTN_CONFIRM = 1;
|
|
|
|
|
static constexpr uint8_t BTN_LEFT = 2;
|
|
|
|
|
static constexpr uint8_t BTN_RIGHT = 3;
|
|
|
|
|
static constexpr uint8_t BTN_UP = 4;
|
|
|
|
|
static constexpr uint8_t BTN_DOWN = 5;
|
|
|
|
|
static constexpr uint8_t BTN_POWER = 6;
|
|
|
|
|
};
|
2026-03-20 04:33:29 +01:00
|
|
|
|
2026-04-04 09:25:43 -06:00
|
|
|
extern HalGPIO gpio;
|