12#include "esp_heap_caps.h"
13#include "driver/gpio.h"
14#include "freertos/FreeRTOS.h"
15#include "freertos/idf_additions.h"
16#include "freertos/task.h"
17#include "freertos/semphr.h"
19static const char*
TAG =
"Keypad";
79 return static_cast<uint16_t
>(
KEY_MASK_ALL ^ (1u << bit));
136 bool init()
override;
137 bool start()
override;
138 void stop()
override;
140 const char*
getName()
const override {
return "keypad"; }
146 bool hasKey()
const override;
153 if (enabled) deferSources_ |= source;
154 else deferSources_ &= ~source;
157 keyRepeatInitialMs_ = initial_ms;
158 keyRepeatPeriodMs_ = period_ms;
165 uint16_t readInputs()
const;
166 void bufferAddKey(
Key key);
168 static void taskFunc(
void* arg);
169 static void IRAM_ATTR isrHandler(
void* arg);
179 uint8_t bufferHead_ = 0;
180 uint8_t bufferTail_ = 0;
181 mutable portMUX_TYPE bufferMux_ = portMUX_INITIALIZER_UNLOCKED;
184 SemaphoreHandle_t semaphore_ =
nullptr;
185 TaskHandle_t taskHandle_ =
nullptr;
189 volatile bool inSleepMode_ =
false;
195 bool panicChordFired_ =
false;
196 uint32_t deferSources_ = 0;
197 bool longPressEnabled_ =
false;
202 uint32_t pressStartTime_ = 0;
203 bool longPressFired_ =
false;
206 uint16_t keyRepeatInitialMs_ = 0;
207 uint16_t keyRepeatPeriodMs_ = 0;
208 uint32_t lastRepeatMs_ = 0;
224 LOG_E(
TAG,
"I2C bus not initialized");
231 LOG_E(
TAG,
"Failed to add TCA9535 device");
237 uint8_t allHigh = 0xFF;
238 uint8_t noInvert = 0x00;
239 uint8_t allInputs = 0xFF;
242 if (bus_->writeReg(device_,
REG_OUTPUT_0, &allHigh, 1) != ESP_OK ||
243 bus_->writeReg(device_,
REG_OUTPUT_1, &allHigh, 1) != ESP_OK ||
244 bus_->writeReg(device_,
REG_POLARITY_0, &noInvert, 1) != ESP_OK ||
245 bus_->writeReg(device_,
REG_POLARITY_1, &noInvert, 1) != ESP_OK ||
246 bus_->writeReg(device_,
REG_CONFIG_0, &allInputs, 1) != ESP_OK ||
247 bus_->writeReg(device_,
REG_CONFIG_1, &allInputs, 1) != ESP_OK) {
248 LOG_E(
TAG,
"Failed to configure TCA9535");
254 lastRawState_ = readInputs();
255 LOG_I(
TAG,
"Initial state: 0x%04X", lastRawState_);
258 semaphore_ = xSemaphoreCreateBinary();
260 LOG_E(
TAG,
"Failed to create semaphore");
269 BaseType_t ret = xTaskCreateWithCaps(taskFunc,
"keypad",
TASK_STACK_SIZE,
271 MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
273 LOG_E(
TAG,
"Failed to create task");
274 vSemaphoreDelete(semaphore_);
275 semaphore_ =
nullptr;
281 gpio_config_t io_conf = {};
283 io_conf.mode = GPIO_MODE_INPUT;
284 io_conf.pull_up_en = GPIO_PULLUP_ENABLE;
285 io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
286 io_conf.intr_type = GPIO_INTR_NEGEDGE;
287 gpio_config(&io_conf);
290 esp_err_t err = gpio_install_isr_service(0);
291 if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
292 LOG_E(
TAG,
"Failed to install ISR service: %s", esp_err_to_name(err));
293 vSemaphoreDelete(semaphore_);
294 semaphore_ =
nullptr;
298 gpio_isr_handler_add(
EXP_IRQ_PIN, isrHandler,
this);
331uint16_t TCA9535Keypad::readInputs()
const {
334 uint8_t lo = 0xFF, hi = 0xFF;
338 return (uint16_t)((hi << 8) | lo);
350 uint16_t current = readInputs();
359 return bufferGetKey();
367 portENTER_CRITICAL(&bufferMux_);
368 bool hasKey = bufferHead_ != bufferTail_;
369 portEXIT_CRITICAL(&bufferMux_);
378 uint16_t current = readInputs();
389 longPressEnabled_ = enabled;
390 longPressThresholdMs_ = thresholdMs;
397void TCA9535Keypad::bufferAddKey(
Key key) {
400 portENTER_CRITICAL(&bufferMux_);
402 if (nextHead != bufferTail_) {
403 keyBuffer_[bufferHead_] = key;
404 bufferHead_ = nextHead;
406 portEXIT_CRITICAL(&bufferMux_);
413Key TCA9535Keypad::bufferGetKey() {
414 portENTER_CRITICAL(&bufferMux_);
415 if (bufferHead_ == bufferTail_) {
416 portEXIT_CRITICAL(&bufferMux_);
420 Key key = keyBuffer_[bufferTail_];
422 portEXIT_CRITICAL(&bufferMux_);
430void IRAM_ATTR TCA9535Keypad::isrHandler(
void* arg) {
432 if (self->inSleepMode_)
return;
434 BaseType_t xHigherPriorityTaskWoken = pdFALSE;
435 xSemaphoreGiveFromISR(self->semaphore_, &xHigherPriorityTaskWoken);
436 portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
443void TCA9535Keypad::taskFunc(
void* arg) {
463 uint16_t raw = self->readInputs();
465 if (raw != self->lastRawState_) {
469 uint16_t pressedBits =
static_cast<uint16_t
>((~raw) &
KEY_MASK_ALL);
470 uint8_t pressedCount = __builtin_popcount(pressedBits);
472 if (pressedCount > 1) {
477 self->panicChordFired_ =
true;
478 if (self->panicChordCallback_) self->panicChordCallback_();
481 self->panicChordFired_ =
false;
489 if (self->deferSources_ == 0) {
490 self->bufferAddKey(key);
493 if (self->callback_) {
494 self->callback_(key,
true);
498 if (self->longPressEnabled_) {
499 self->pressedKey_ = key;
500 self->pressStartTime_ = xTaskGetTickCount() * portTICK_PERIOD_MS;
501 self->lastRepeatMs_ = self->pressStartTime_;
502 self->longPressFired_ =
false;
506 if (self->deferSources_ != 0 && !self->longPressFired_ &&
508 self->bufferAddKey(self->pressedKey_);
511 self->callback_(self->pressedKey_,
false);
516 self->lastRawState_ = raw;
523 uint32_t now = xTaskGetTickCount() * portTICK_PERIOD_MS;
524 if (self->keyRepeatPeriodMs_ > 0) {
525 if (now - self->pressStartTime_ >= self->keyRepeatInitialMs_ &&
526 now - self->lastRepeatMs_ >= self->keyRepeatPeriodMs_) {
527 self->lastRepeatMs_ = now;
528 self->bufferAddKey(self->pressedKey_);
530 }
else if (self->longPressEnabled_ && !self->longPressFired_) {
531 if (now - self->pressStartTime_ >= self->longPressThresholdMs_) {
532 self->longPressFired_ =
true;
533 if (self->longPressCallback_) {
534 self->longPressCallback_(self->pressedKey_);
546 LOG_D(
TAG,
"Preparing keypad for sleep...");
559 LOG_D(
TAG,
"Recovering keypad after sleep...");
567 vTaskDelay(pdMS_TO_TICKS(10));
570 vTaskDelay(pdMS_TO_TICKS(30));
576 lastRawState_ = readInputs();
579 inSleepMode_ =
false;
582 gpio_set_intr_type(
EXP_IRQ_PIN, GPIO_INTR_NEGEDGE);
587 LOG_D(
TAG,
"Keypad recovered, state: 0x%04X", lastRawState_);
594 portENTER_CRITICAL(&bufferMux_);
597 portEXIT_CRITICAL(&bufferMux_);
CDC Log: logging over TinyUSB CDC and UART.
#define LOG_D(tag, fmt,...)
#define LOG_I(tag, fmt,...)
#define LOG_E(tag, fmt,...)
static SystemLock & instance()
Returns the process-wide lockdown latch singleton.
virtual esp_err_t readReg(I2cDeviceHandle dev, uint8_t reg, uint8_t *data, size_t len)=0
void(*)() PanicChordCallback
void(*)(Key key) LongPressCallback
void setPanicChordCallback(PanicChordCallback callback) override
bool hasKey() const override
Returns whether key buffer currently contains entries.
core::ServiceState getState() const override
void setDeferShortPress(uint32_t source, bool enabled) override
bool anyKeyDown() const override
Returns whether any keypad key is physically held down.
bool init() override
Initializes keypad hardware, ISR, and worker task.
Key getNextKey() override
Retrieves next buffered key press.
void stop() override
Stops keypad service state.
bool isKeyPressed(Key key) const override
Checks whether specified key is currently pressed.
const char * getName() const override
void clearBuffer() override
Clears pending key buffer.
bool start() override
Starts keypad service state.
void setKeyRepeat(uint16_t initial_ms, uint16_t period_ms) override
void recoverFromSleep() override
Restores keypad operation after wakeup.
void setLongPressEnabled(bool enabled, uint32_t thresholdMs) override
Enables/disables long-press detection and sets threshold.
void prepareForSleep() override
Prepares keypad interrupt handling for system sleep entry.
void setLongPressCallback(LongPressCallback callback) override
void setCallback(KeyCallback callback) override
static TCA9535Keypad g_keypad
Singleton keypad instance.
static constexpr uint16_t KEY_STATE_INVALID
Sentinel value returned when an I2C read fails or no key is mapped.
static constexpr uint8_t REG_POLARITY_0
static constexpr uint8_t REG_INPUT_1
static constexpr uint32_t DEBOUNCE_MS
static constexpr uint8_t REG_OUTPUT_0
static constexpr uint8_t KEY_BIT_3
static constexpr uint32_t TASK_STACK_SIZE
Keypad task scheduling configuration.
void(*)(Key key, bool pressed) KeyCallback
II2cBus * getI2cBus0()
Returns singleton instance of I2C bus 0.
static Key rawToKey(uint16_t raw)
Converts raw 16-bit keypad state to a Key enum value.
static constexpr uint8_t KEY_BIT_8
static constexpr uint16_t KEY_STATE_IDLE
Idle state: every key released, all 12 bits high.
IKeypad * getKeypadInstance()
Returns the singleton keypad service instance.
static constexpr uint8_t REG_CONFIG_1
static constexpr uint8_t KEY_BIT_5
static constexpr uint8_t KEY_BIT_1
static constexpr uint8_t KEY_BIT_4
static constexpr uint8_t KEY_BIT_0
Bit positions of each physical key on the TCA9535 P0/P1 ports. Active-low: a pressed key drives its c...
static constexpr uint8_t REG_POLARITY_1
static constexpr uint8_t REG_INPUT_0
TCA9535 register-address constants.
static constexpr uint16_t PANIC_CHORD_BITS
Reserved rescue chord: N and Y held together (anti-block instant lock).
static constexpr uint8_t REG_CONFIG_0
static uint16_t keyToMask(Key key)
Converts a Key enum value to its expected raw 12-bit mask.
static constexpr UBaseType_t TASK_PRIORITY
static constexpr uint32_t LONG_PRESS_THRESHOLD_MS
Default long-press detection threshold in milliseconds. Long enough to avoid accidental triggers,...
static constexpr uint8_t KEY_BIT_7
static constexpr uint8_t KEY_BIT_6
static constexpr uint8_t KEY_BIT_9
static constexpr uint16_t maskForBit(uint8_t bit)
Builds the active-low raw state for a single pressed key.
static constexpr uint32_t POLL_TIMEOUT_MS
static constexpr uint8_t KEY_BIT_NO
static constexpr size_t KEY_BUFFER_SIZE
Ring-buffer configuration for queued key events.
static constexpr uint8_t REG_OUTPUT_1
static constexpr uint16_t KEY_MASK_ALL
Mask of all 12 keypad bits (P0.0..P1.3).
static constexpr uint8_t KEY_BIT_2
static constexpr uint8_t KEY_BIT_YES