buttons.c 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. /**
  2. * Hardware button driver — interrupt-driven with timer-based debounce.
  3. *
  4. * Each button GPIO triggers an ISR on any edge. The ISR resets a
  5. * FreeRTOS software timer (the debounce window). When the timer
  6. * expires — meaning the signal has been stable for DEBOUNCE_MS —
  7. * the callback reads the GPIO and acts on the new state.
  8. *
  9. * Volume buttons support auto-repeat: after REPEAT_DELAY_MS held,
  10. * the action repeats every REPEAT_INTERVAL_MS.
  11. *
  12. * Button actions are dispatched to a dedicated task via a queue so that
  13. * playback_control functions (which may do mDNS + HTTP) never block
  14. * the FreeRTOS timer daemon.
  15. */
  16. #include "buttons.h"
  17. #include "audio_output.h"
  18. #include "playback_control.h"
  19. #include "spiram_task.h"
  20. #include "board_common.h"
  21. #include "driver/gpio.h"
  22. #include "esp_log.h"
  23. #include "esp_sleep.h"
  24. #include "freertos/FreeRTOS.h"
  25. #include "freertos/queue.h"
  26. #include "freertos/timers.h"
  27. static const char *TAG = "buttons";
  28. #define DEBOUNCE_MS 50 // Stable period before accepting state change
  29. #define REPEAT_DELAY_MS 500 // Hold duration before auto-repeat starts
  30. #define REPEAT_INTERVAL 200 // Auto-repeat interval for volume buttons
  31. #define LONG_PRESS_MS 3000 // Play/pause long press for deep sleep
  32. #define DOUBLE_CLICK_MS 350 // Window to detect a second play/pause press
  33. #define ACTION_QUEUE_LEN 8
  34. typedef enum {
  35. BTN_PLAY_PAUSE,
  36. BTN_VOLUME_UP,
  37. BTN_VOLUME_DOWN,
  38. BTN_NEXT,
  39. BTN_PREV,
  40. BTN_LONG_PRESS,
  41. // Actions below are dispatched directly (not 1:1 with a GPIO button)
  42. BTN_CHANNEL_CYCLE,
  43. BTN_COUNT
  44. } button_id_t;
  45. typedef struct {
  46. int gpio;
  47. bool pressed; // Debounced state
  48. bool repeatable; // Supports auto-repeat (volume buttons)
  49. TimerHandle_t debounce_timer;
  50. TimerHandle_t repeat_timer; // Only created for repeatable buttons
  51. TimerHandle_t long_press_timer; // Only created for play/pause button
  52. TimerHandle_t click_timer; // Double-click window (play/pause only)
  53. int click_count; // Presses seen within the double-click window
  54. } button_state_t;
  55. static button_state_t buttons[BTN_COUNT];
  56. static QueueHandle_t s_action_queue;
  57. // Post a button action to the dedicated task (safe from timer callbacks)
  58. static void post_button_action(button_id_t id) {
  59. int action = (int)id;
  60. // Non-blocking: drop if queue is full (better than blocking the timer task)
  61. xQueueSend(s_action_queue, &action, 0);
  62. }
  63. // Dedicated task that processes button actions — has enough stack for
  64. // mDNS discovery + HTTP requests that DACP requires.
  65. static void button_action_task(void *pvParameters) {
  66. (void)pvParameters;
  67. int action;
  68. while (1) {
  69. if (xQueueReceive(s_action_queue, &action, portMAX_DELAY) == pdTRUE) {
  70. switch ((button_id_t)action) {
  71. case BTN_PLAY_PAUSE:
  72. playback_control_play_pause();
  73. break;
  74. case BTN_VOLUME_UP:
  75. playback_control_volume_up();
  76. break;
  77. case BTN_VOLUME_DOWN:
  78. playback_control_volume_down();
  79. break;
  80. case BTN_NEXT:
  81. playback_control_next();
  82. break;
  83. case BTN_PREV:
  84. playback_control_prev();
  85. break;
  86. case BTN_LONG_PRESS:
  87. // Deep sleep — this is handled in the timer callback; just log
  88. // in case the action fires (shouldn't normally happen since
  89. // long_press_timer_cb calls esp_deep_sleep_start immediately)
  90. ESP_LOGW(TAG,
  91. "Long press action received (deep sleep already attempted)");
  92. break;
  93. case BTN_CHANNEL_CYCLE:
  94. audio_output_cycle_channel_mode();
  95. break;
  96. default:
  97. break;
  98. }
  99. }
  100. }
  101. }
  102. // Called when repeat timer fires (runs in timer daemon task)
  103. static void repeat_timer_cb(TimerHandle_t timer) {
  104. int id = (int)(intptr_t)pvTimerGetTimerID(timer);
  105. button_state_t *btn = &buttons[id];
  106. if (!btn->pressed) {
  107. return;
  108. }
  109. post_button_action((button_id_t)id);
  110. // After the initial REPEAT_DELAY_MS, switch to the faster interval
  111. xTimerChangePeriod(btn->repeat_timer, pdMS_TO_TICKS(REPEAT_INTERVAL), 0);
  112. }
  113. // Called when debounce timer expires (runs in timer daemon task)
  114. static void debounce_timer_cb(TimerHandle_t timer) {
  115. int id = (int)(intptr_t)pvTimerGetTimerID(timer);
  116. button_state_t *btn = &buttons[id];
  117. // Read settled GPIO state (active low)
  118. bool now_pressed = (gpio_get_level(btn->gpio) == 0);
  119. if (now_pressed == btn->pressed) {
  120. return; // No actual state change after debounce
  121. }
  122. btn->pressed = now_pressed;
  123. if (now_pressed) {
  124. if (btn->click_timer) {
  125. // Play/pause button: defer the action so we can tell a single click
  126. // (play/pause) from a double click (cycle channel mode). Count the
  127. // press and (re)start the double-click window; the click_timer_cb
  128. // dispatches the right action when the window expires.
  129. btn->click_count++;
  130. xTimerChangePeriod(btn->click_timer, pdMS_TO_TICKS(DOUBLE_CLICK_MS), 0);
  131. } else {
  132. // Other buttons — fire immediately via the dedicated task
  133. post_button_action((button_id_t)id);
  134. }
  135. // Start repeat timer for volume buttons (initial delay)
  136. if (btn->repeatable && btn->repeat_timer) {
  137. xTimerChangePeriod(btn->repeat_timer, pdMS_TO_TICKS(REPEAT_DELAY_MS), 0);
  138. }
  139. // Start long-press timer for play/pause button
  140. if (btn->long_press_timer) {
  141. xTimerStart(btn->long_press_timer, 0);
  142. }
  143. } else {
  144. // Button just released — stop repeat and long-press timers
  145. if (btn->repeat_timer) {
  146. xTimerStop(btn->repeat_timer, 0);
  147. }
  148. if (btn->long_press_timer) {
  149. xTimerStop(btn->long_press_timer, 0);
  150. }
  151. #if CONFIG_BTN_PLAY_PAUSE_DOUBLE_CLICK
  152. // If the click window expired while the button was still held, dispatch
  153. // play/pause on release (not on the timer — that would fire mid-hold).
  154. if (btn->click_timer && btn->click_count == 1 &&
  155. xTimerIsTimerActive(btn->click_timer) == pdFALSE) {
  156. btn->click_count = 0;
  157. post_button_action(BTN_PLAY_PAUSE);
  158. }
  159. #endif
  160. }
  161. }
  162. // Long-press timer callback — powers the board off on play/pause.
  163. // On boards with a battery power latch (Waveshare ESP32-S3-Touch-LCD-1.54)
  164. // board_power_off() releases the latch and enters deep sleep so the rail
  165. // collapses cleanly. On boards without a latch the weak default just enters
  166. // deep sleep; arm the play/pause button as the wakeup source for those.
  167. static void long_press_timer_cb(TimerHandle_t timer) {
  168. int id = (int)(intptr_t)pvTimerGetTimerID(timer);
  169. button_state_t *btn = &buttons[id];
  170. // Cancel any pending click so we don't also play/pause or cycle channels.
  171. if (btn->click_timer) {
  172. xTimerStop(btn->click_timer, 0);
  173. btn->click_count = 0;
  174. }
  175. ESP_LOGI(TAG, "Long press detected — powering off");
  176. gpio_wakeup_enable(CONFIG_BTN_PLAY_PAUSE_GPIO, GPIO_INTR_LOW_LEVEL);
  177. esp_sleep_enable_gpio_wakeup();
  178. board_power_off();
  179. }
  180. // Double-click window expired — dispatch based on how many presses we saw.
  181. // 1 press -> play/pause; 2 presses -> cycle channel mode (L/R/stereo).
  182. static void click_timer_cb(TimerHandle_t timer) {
  183. int id = (int)(intptr_t)pvTimerGetTimerID(timer);
  184. button_state_t *btn = &buttons[id];
  185. int count = btn->click_count;
  186. if (count >= 2) {
  187. btn->click_count = 0;
  188. post_button_action(BTN_CHANNEL_CYCLE);
  189. } else if (count == 1 && !btn->pressed) {
  190. btn->click_count = 0;
  191. post_button_action(BTN_PLAY_PAUSE);
  192. }
  193. // count == 1 && still pressed: long-press hold — defer play/pause to release
  194. }
  195. // GPIO ISR — just resets the debounce timer. Each new edge restarts the
  196. // debounce window so the callback only fires once bouncing stops.
  197. static void IRAM_ATTR gpio_isr_handler(void *arg) {
  198. int id = (int)(intptr_t)arg;
  199. BaseType_t woken = pdFALSE;
  200. xTimerResetFromISR(buttons[id].debounce_timer, &woken);
  201. if (woken) {
  202. portYIELD_FROM_ISR();
  203. }
  204. }
  205. static void configure_button(button_id_t id, int gpio, bool repeatable) {
  206. buttons[id].gpio = gpio;
  207. buttons[id].repeatable = repeatable;
  208. buttons[id].pressed = false;
  209. buttons[id].debounce_timer = NULL;
  210. buttons[id].repeat_timer = NULL;
  211. buttons[id].click_timer = NULL;
  212. buttons[id].click_count = 0;
  213. if (gpio < 0) {
  214. return;
  215. }
  216. // Create one-shot debounce timer
  217. buttons[id].debounce_timer =
  218. xTimerCreate("btn_db", pdMS_TO_TICKS(DEBOUNCE_MS), pdFALSE, // one-shot
  219. (void *)(intptr_t)id, debounce_timer_cb);
  220. // Create one-shot repeat timer for volume buttons (manually restarted)
  221. if (repeatable) {
  222. buttons[id].repeat_timer = xTimerCreate(
  223. "btn_rpt", pdMS_TO_TICKS(REPEAT_DELAY_MS), pdFALSE, // one-shot
  224. (void *)(intptr_t)id, repeat_timer_cb);
  225. }
  226. // Create one-shot long-press and double-click timers for play/pause button
  227. if (id == BTN_PLAY_PAUSE) {
  228. buttons[id].long_press_timer = xTimerCreate(
  229. "btn_lp", pdMS_TO_TICKS(LONG_PRESS_MS), pdFALSE, // one-shot
  230. (void *)(intptr_t)id, long_press_timer_cb);
  231. #if CONFIG_BTN_PLAY_PAUSE_DOUBLE_CLICK
  232. buttons[id].click_timer = xTimerCreate(
  233. "btn_clk", pdMS_TO_TICKS(DOUBLE_CLICK_MS), pdFALSE, // one-shot
  234. (void *)(intptr_t)id, click_timer_cb);
  235. #endif
  236. }
  237. // GPIOs 34-39 on ESP32 are input-only and lack internal pull-ups.
  238. // An external pull-up resistor is required for those pins.
  239. bool has_internal_pullup = (gpio < 34);
  240. gpio_config_t io_conf = {
  241. .pin_bit_mask = (1ULL << gpio),
  242. .mode = GPIO_MODE_INPUT,
  243. .pull_up_en =
  244. has_internal_pullup ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE,
  245. .pull_down_en = GPIO_PULLDOWN_DISABLE,
  246. .intr_type = GPIO_INTR_ANYEDGE,
  247. };
  248. gpio_config(&io_conf);
  249. gpio_isr_handler_add(gpio, gpio_isr_handler, (void *)(intptr_t)id);
  250. if (!has_internal_pullup) {
  251. ESP_LOGW(TAG, "Button %d on GPIO %d: no internal pull-up, needs external",
  252. id, gpio);
  253. }
  254. ESP_LOGI(TAG, "Button %d on GPIO %d (interrupt)", id, gpio);
  255. }
  256. esp_err_t buttons_init(void) {
  257. // Action-only IDs — not backed by GPIO
  258. buttons[BTN_LONG_PRESS].gpio = -1;
  259. buttons[BTN_CHANNEL_CYCLE].gpio = -1;
  260. // Ensure the shared GPIO ISR service is installed (idempotent)
  261. esp_err_t err = board_gpio_isr_init();
  262. if (err != ESP_OK) {
  263. return err;
  264. }
  265. // Configure each button from Kconfig (adds ISR handlers)
  266. configure_button(BTN_PLAY_PAUSE, CONFIG_BTN_PLAY_PAUSE_GPIO, false);
  267. configure_button(BTN_VOLUME_UP, CONFIG_BTN_VOLUME_UP_GPIO, true);
  268. configure_button(BTN_VOLUME_DOWN, CONFIG_BTN_VOLUME_DOWN_GPIO, true);
  269. configure_button(BTN_NEXT, CONFIG_BTN_NEXT_GPIO, false);
  270. configure_button(BTN_PREV, CONFIG_BTN_PREV_GPIO, false);
  271. bool any_configured = false;
  272. for (int i = BTN_PLAY_PAUSE; i <= BTN_PREV; i++) {
  273. if (buttons[i].gpio >= 0) {
  274. any_configured = true;
  275. break;
  276. }
  277. }
  278. if (!any_configured) {
  279. ESP_LOGI(TAG, "No buttons configured");
  280. return ESP_OK;
  281. }
  282. // Queue + task for dispatching actions off the timer daemon task.
  283. // Stack 4096 is enough for mDNS + HTTP operations in DACP.
  284. s_action_queue = xQueueCreate(ACTION_QUEUE_LEN, sizeof(int));
  285. task_create_spiram(button_action_task, "btn_act", 4096, NULL, 5, NULL, NULL);
  286. ESP_LOGI(TAG, "Buttons initialized (interrupt-driven)");
  287. return ESP_OK;
  288. }