Fork of the espurna firmware for `mhsw` switches
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

711 lines
21 KiB

6 years ago
6 years ago
  1. /*
  2. BUTTON MODULE
  3. Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
  4. */
  5. #if BUTTON_SUPPORT
  6. #include <bitset>
  7. #include <memory>
  8. #include <vector>
  9. #include "compat.h"
  10. #include "gpio.h"
  11. #include "system.h"
  12. #include "relay.h"
  13. #include "light.h"
  14. #include "button.h"
  15. #include "button_config.h"
  16. #include "libs/DebounceEvent.h"
  17. // TODO: if we are using such conversion helpers across the codebase, should convert() be in internal ns?
  18. namespace settings {
  19. namespace internal {
  20. template<>
  21. debounce_event::types::Mode convert(const String& value) {
  22. switch (value.toInt()) {
  23. case 1:
  24. return debounce_event::types::Mode::Switch;
  25. case 0:
  26. default:
  27. return debounce_event::types::Mode::Pushbutton;
  28. }
  29. }
  30. template<>
  31. debounce_event::types::PinValue convert(const String& value) {
  32. switch (value.toInt()) {
  33. case 0:
  34. return debounce_event::types::PinValue::Low;
  35. case 1:
  36. default:
  37. return debounce_event::types::PinValue::High;
  38. }
  39. }
  40. template<>
  41. debounce_event::types::PinMode convert(const String& value) {
  42. switch (value.toInt()) {
  43. case 1:
  44. return debounce_event::types::PinMode::InputPullup;
  45. case 2:
  46. return debounce_event::types::PinMode::InputPulldown;
  47. case 0:
  48. default:
  49. return debounce_event::types::PinMode::Input;
  50. }
  51. }
  52. } // namespace settings::internal
  53. } // namespace settings
  54. // -----------------------------------------------------------------------------
  55. constexpr const debounce_event::types::Config _buttonDecodeConfigBitmask(const unsigned char bitmask) {
  56. return {
  57. ((bitmask & ButtonMask::Pushbutton)
  58. ? debounce_event::types::Mode::Pushbutton
  59. : debounce_event::types::Mode::Switch),
  60. ((bitmask & ButtonMask::DefaultHigh)
  61. ? debounce_event::types::PinValue::High
  62. : debounce_event::types::PinValue::Low),
  63. ((bitmask & ButtonMask::SetPullup)
  64. ? debounce_event::types::PinMode::InputPullup : (bitmask & ButtonMask::SetPulldown)
  65. ? debounce_event::types::PinMode::InputPullup : debounce_event::types::PinMode::Input)
  66. };
  67. }
  68. constexpr const button_action_t _buttonDecodeEventAction(const button_actions_t& actions, button_event_t event) {
  69. return (
  70. (event == button_event_t::Pressed) ? actions.pressed :
  71. (event == button_event_t::Click) ? actions.click :
  72. (event == button_event_t::DoubleClick) ? actions.dblclick :
  73. (event == button_event_t::LongClick) ? actions.lngclick :
  74. (event == button_event_t::LongLongClick) ? actions.lnglngclick :
  75. (event == button_event_t::TripleClick) ? actions.trplclick : 0U
  76. );
  77. }
  78. constexpr const button_event_t _buttonMapReleased(uint8_t count, unsigned long length, unsigned long lngclick_delay, unsigned long lnglngclick_delay) {
  79. return (
  80. (1 == count) ? (
  81. (length > lnglngclick_delay) ? button_event_t::LongLongClick :
  82. (length > lngclick_delay) ? button_event_t::LongClick : button_event_t::Click
  83. ) :
  84. (2 == count) ? button_event_t::DoubleClick :
  85. (3 == count) ? button_event_t::TripleClick :
  86. button_event_t::None
  87. );
  88. }
  89. button_actions_t _buttonConstructActions(unsigned char index) {
  90. return {
  91. _buttonPress(index),
  92. _buttonClick(index),
  93. _buttonDoubleClick(index),
  94. _buttonLongClick(index),
  95. _buttonLongLongClick(index),
  96. _buttonTripleClick(index)
  97. };
  98. }
  99. debounce_event::types::Config _buttonConfig(unsigned char index) {
  100. const auto config = _buttonDecodeConfigBitmask(_buttonConfigBitmask(index));
  101. return {
  102. getSetting({"btnMode", index}, config.mode),
  103. getSetting({"btnDefVal", index}, config.default_value),
  104. getSetting({"btnPinMode", index}, config.pin_mode)
  105. };
  106. }
  107. // -----------------------------------------------------------------------------
  108. button_event_delays_t::button_event_delays_t() :
  109. debounce(_buttonDebounceDelay()),
  110. repeat(_buttonRepeatDelay()),
  111. lngclick(_buttonLongClickDelay()),
  112. lnglngclick(_buttonLongLongClickDelay())
  113. {}
  114. button_event_delays_t::button_event_delays_t(unsigned long debounce, unsigned long repeat, unsigned long lngclick, unsigned long lnglngclick) :
  115. debounce(debounce),
  116. repeat(repeat),
  117. lngclick(lngclick),
  118. lnglngclick(lnglngclick)
  119. {}
  120. button_t::button_t(unsigned char relayID, const button_actions_t& actions, const button_event_delays_t& delays) :
  121. event_emitter(nullptr),
  122. event_delays(delays),
  123. actions(actions),
  124. relayID(relayID)
  125. {}
  126. button_t::button_t(std::shared_ptr<BasePin> pin, const debounce_event::types::Config& config, unsigned char relayID, const button_actions_t& actions, const button_event_delays_t& delays) :
  127. event_emitter(std::make_unique<debounce_event::EventEmitter>(pin, config, delays.debounce, delays.repeat)),
  128. event_delays(delays),
  129. actions(actions),
  130. relayID(relayID)
  131. {}
  132. bool button_t::state() {
  133. return event_emitter->isPressed();
  134. }
  135. button_event_t button_t::loop() {
  136. if (!event_emitter) {
  137. return button_event_t::None;
  138. }
  139. auto event = event_emitter->loop();
  140. if (event == debounce_event::types::EventNone) {
  141. return button_event_t::None;
  142. }
  143. switch (event) {
  144. case debounce_event::types::EventPressed:
  145. return button_event_t::Pressed;
  146. case debounce_event::types::EventChanged:
  147. return button_event_t::Click;
  148. case debounce_event::types::EventReleased: {
  149. return _buttonMapReleased(
  150. event_emitter->getEventCount(),
  151. event_emitter->getEventLength(),
  152. event_delays.lngclick,
  153. event_delays.lnglngclick
  154. );
  155. }
  156. case debounce_event::types::EventNone:
  157. default:
  158. break;
  159. }
  160. return button_event_t::None;
  161. }
  162. std::vector<button_t> _buttons;
  163. // -----------------------------------------------------------------------------
  164. unsigned char buttonCount() {
  165. return _buttons.size();
  166. }
  167. #if MQTT_SUPPORT
  168. std::bitset<ButtonsMax> _buttons_mqtt_send_all(
  169. (1 == BUTTON_MQTT_SEND_ALL_EVENTS) ? 0xFFFFFFFFUL : 0UL
  170. );
  171. std::bitset<ButtonsMax> _buttons_mqtt_retain(
  172. (1 == BUTTON_MQTT_RETAIN) ? 0xFFFFFFFFUL : 0UL
  173. );
  174. void buttonMQTT(unsigned char id, button_event_t event) {
  175. char payload[4] = {0};
  176. itoa(_buttonEventNumber(event), payload, 10);
  177. // mqttSend(topic, id, payload, force, retain)
  178. mqttSend(MQTT_TOPIC_BUTTON, id, payload, false, _buttons_mqtt_retain[id]);
  179. }
  180. #endif
  181. #if WEB_SUPPORT
  182. void _buttonWebSocketOnVisible(JsonObject& root) {
  183. if (buttonCount() > 0) {
  184. root["btnVisible"] = 1;
  185. }
  186. }
  187. #if (BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_ITEAD_SONOFF_DUAL) || \
  188. (BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_FOXEL_LIGHTFOX_DUAL)
  189. void _buttonWebSocketOnConnected(JsonObject& root) {
  190. root["btnRepDel"] = getSetting("btnRepDel", _buttonRepeatDelay());
  191. }
  192. #else
  193. void _buttonWebSocketOnConnected(JsonObject& root) {
  194. root["btnRepDel"] = getSetting("btnRepDel", _buttonRepeatDelay());
  195. // XXX: unused! pending webui changes
  196. #if 0
  197. if (buttonCount() < 1) return;
  198. JsonObject& module = root.createNestedObject("btn");
  199. // TODO: hardware can sometimes use a different event source
  200. // e.g. Sonoff Dual does not need `Pin`, `Mode` or any of `Del`
  201. // TODO: schema names are uppercase to easily match settings?
  202. // TODO: schema name->type map to generate WebUI elements?
  203. JsonArray& schema = module.createNestedArray("_schema");
  204. schema.add("GPIO");
  205. schema.add("Mode");
  206. schema.add("DefVal");
  207. schema.add("PinMode");
  208. schema.add("Relay");
  209. schema.add("Press");
  210. schema.add("Click");
  211. schema.add("Dclk");
  212. schema.add("Lclk");
  213. schema.add("LLclk");
  214. schema.add("Tclk");
  215. schema.add("DebDel");
  216. schema.add("RepDel");
  217. schema.add("LclkDel");
  218. schema.add("LLclkDel");
  219. #if MQTT_SUPPORT
  220. schema.add("MqttSendAll");
  221. schema.add("MqttRetain");
  222. #endif
  223. JsonArray& buttons = module.createNestedArray("list");
  224. for (unsigned char i=0; i<buttonCount(); i++) {
  225. JsonArray& button = buttons.createNestedArray();
  226. // TODO: configure PIN object instead of button specifically, link PIN<->BUTTON
  227. if (_buttons[i].getPin()) {
  228. button.add(getSetting({"btnGPIO", index}, _buttonPin(index)));
  229. const auto config = _buttonConfig(index);
  230. button.add(static_cast<int>(config.mode));
  231. button.add(static_cast<int>(config.default_value));
  232. button.add(static_cast<int>(config.pin_mode));
  233. } else {
  234. button.add(GPIO_NONE);
  235. button.add(static_cast<int>(BUTTON_PUSHBUTTON));
  236. button.add(0);
  237. button.add(0);
  238. button.add(0);
  239. }
  240. button.add(_buttons[i].relayID);
  241. button.add(_buttons[i].actions.pressed);
  242. button.add(_buttons[i].actions.click);
  243. button.add(_buttons[i].actions.dblclick);
  244. button.add(_buttons[i].actions.lngclick);
  245. button.add(_buttons[i].actions.lnglngclick);
  246. button.add(_buttons[i].actions.trplclick);
  247. button.add(_buttons[i].event_delays.debounce);
  248. button.add(_buttons[i].event_delays.repeat);
  249. button.add(_buttons[i].event_delays.lngclick);
  250. button.add(_buttons[i].event_delays.lnglngclick);
  251. // TODO: send bitmask as number?
  252. #if MQTT_SUPPORT
  253. button.add(_buttons_mqtt_send_all[i] ? 1 : 0);
  254. button.add(_buttons_mqtt_retain[i] ? 1 : 0);
  255. #endif
  256. }
  257. #endif
  258. }
  259. #endif // BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_GENERIC
  260. bool _buttonWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
  261. return (strncmp(key, "btn", 3) == 0);
  262. }
  263. #endif
  264. bool buttonState(unsigned char id) {
  265. if (id >= _buttons.size()) return false;
  266. return _buttons[id].state();
  267. }
  268. button_action_t buttonAction(unsigned char id, const button_event_t event) {
  269. if (id >= _buttons.size()) return 0;
  270. return _buttonDecodeEventAction(_buttons[id].actions, event);
  271. }
  272. int _buttonEventNumber(button_event_t event) {
  273. return static_cast<int>(event);
  274. }
  275. // Approach based on https://github.com/esp8266/Arduino/pull/6950
  276. // "PROGMEM footprint cleanup for responseCodeToString (#6950)"
  277. // In this particular case, saves 76 bytes (120 vs 44)
  278. #if 1
  279. String _buttonEventString(button_event_t event) {
  280. const __FlashStringHelper* ptr = nullptr;
  281. switch (event) {
  282. case button_event_t::Pressed:
  283. ptr = F("Pressed");
  284. break;
  285. case button_event_t::Click:
  286. ptr = F("Click");
  287. break;
  288. case button_event_t::DoubleClick:
  289. ptr = F("Double-click");
  290. break;
  291. case button_event_t::LongClick:
  292. ptr = F("Long-click");
  293. break;
  294. case button_event_t::LongLongClick:
  295. ptr = F("Looong-click");
  296. break;
  297. case button_event_t::TripleClick:
  298. ptr = F("Triple-click");
  299. break;
  300. case button_event_t::None:
  301. default:
  302. ptr = F("None");
  303. break;
  304. }
  305. return String(ptr);
  306. }
  307. #else
  308. String _buttonEventString(button_event_t event) {
  309. switch (event) {
  310. case button_event_t::Pressed:
  311. return F("Pressed");
  312. case button_event_t::Click:
  313. return F("Click");
  314. case button_event_t::DoubleClick:
  315. return F("Double-click");
  316. case button_event_t::LongClick:
  317. return F("Long-click");
  318. case button_event_t::LongLongClick:
  319. return F("Looong-click");
  320. case button_event_t::TripleClick:
  321. return F("Triple-click");
  322. case button_event_t::None:
  323. default:
  324. return F("None");
  325. }
  326. }
  327. #endif
  328. void buttonEvent(unsigned char id, button_event_t event) {
  329. DEBUG_MSG_P(PSTR("[BUTTON] Button #%u event %d (%s)\n"),
  330. id, _buttonEventNumber(event), _buttonEventString(event).c_str()
  331. );
  332. if (event == button_event_t::None) return;
  333. auto& button = _buttons[id];
  334. auto action = _buttonDecodeEventAction(button.actions, event);
  335. #if MQTT_SUPPORT
  336. if (action || _buttons_mqtt_send_all[id]) {
  337. buttonMQTT(id, event);
  338. }
  339. #endif
  340. switch (action) {
  341. #if RELAY_SUPPORT
  342. case BUTTON_ACTION_TOGGLE:
  343. relayToggle(button.relayID);
  344. break;
  345. case BUTTON_ACTION_ON:
  346. relayStatus(button.relayID, true);
  347. break;
  348. case BUTTON_ACTION_OFF:
  349. relayStatus(button.relayID, false);
  350. break;
  351. #endif // RELAY_SUPPORT == 1
  352. case BUTTON_ACTION_AP:
  353. if (wifiState() & WIFI_STATE_AP) {
  354. wifiStartSTA();
  355. } else {
  356. wifiStartAP();
  357. }
  358. break;
  359. case BUTTON_ACTION_RESET:
  360. deferredReset(100, CUSTOM_RESET_HARDWARE);
  361. break;
  362. case BUTTON_ACTION_FACTORY:
  363. DEBUG_MSG_P(PSTR("\n\nFACTORY RESET\n\n"));
  364. resetSettings();
  365. deferredReset(100, CUSTOM_RESET_FACTORY);
  366. break;
  367. #if defined(JUSTWIFI_ENABLE_WPS)
  368. case BUTTON_ACTION_WPS:
  369. wifiStartWPS();
  370. break;
  371. #endif // defined(JUSTWIFI_ENABLE_WPS)
  372. #if defined(JUSTWIFI_ENABLE_SMARTCONFIG)
  373. case BUTTON_ACTION_SMART_CONFIG:
  374. wifiStartSmartConfig();
  375. break;
  376. #endif // defined(JUSTWIFI_ENABLE_SMARTCONFIG)
  377. #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
  378. case BUTTON_ACTION_DIM_UP:
  379. lightBrightnessStep(1);
  380. lightUpdate(true, true);
  381. break;
  382. case BUTTON_ACTION_DIM_DOWN:
  383. lightBrightnessStep(-1);
  384. lightUpdate(true, true);
  385. break;
  386. #endif // LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
  387. #if THERMOSTAT_DISPLAY_SUPPORT
  388. case BUTTON_ACTION_DISPLAY_ON:
  389. displayOn();
  390. break;
  391. #endif
  392. }
  393. }
  394. void _buttonConfigure() {
  395. #if MQTT_SUPPORT
  396. for (unsigned char index = 0; index < _buttons.size(); ++index) {
  397. _buttons_mqtt_send_all[index] = getSetting({"btnMqttSendAll", index}, _buttonMqttSendAllEvents(index));
  398. _buttons_mqtt_retain[index] = getSetting({"btnMqttRetain", index}, _buttonMqttRetain(index));
  399. }
  400. #endif
  401. }
  402. // TODO: compatibility proxy, fetch global key before indexed
  403. template<typename T>
  404. unsigned long _buttonGetSetting(const char* key, unsigned char index, T default_value) {
  405. return getSetting(key, getSetting({key, index}, default_value));
  406. }
  407. void buttonSetup() {
  408. // Backwards compatibility
  409. moveSetting("btnDelay", "btnRepDel");
  410. // Special hardware cases
  411. #if (BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_ITEAD_SONOFF_DUAL) || \
  412. (BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_FOXEL_LIGHTFOX_DUAL)
  413. size_t buttons = 0;
  414. #if BUTTON1_RELAY != RELAY_NONE
  415. ++buttons;
  416. #endif
  417. #if BUTTON2_RELAY != RELAY_NONE
  418. ++buttons;
  419. #endif
  420. #if BUTTON3_RELAY != RELAY_NONE
  421. ++buttons;
  422. #endif
  423. #if BUTTON4_RELAY != RELAY_NONE
  424. ++buttons;
  425. #endif
  426. _buttons.reserve(buttons);
  427. // Ignore real button delays since we don't use them here
  428. const auto delays = button_event_delays_t();
  429. for (unsigned char index = 0; index < buttons; ++index) {
  430. const button_actions_t actions {
  431. BUTTON_ACTION_NONE,
  432. // The only generated event is ::Click
  433. getSetting({"btnClick", index}, _buttonClick(index)),
  434. BUTTON_ACTION_NONE,
  435. BUTTON_ACTION_NONE,
  436. BUTTON_ACTION_NONE,
  437. BUTTON_ACTION_NONE
  438. };
  439. _buttons.emplace_back(
  440. getSetting({"btnRelay", index}, _buttonRelay(index)),
  441. actions,
  442. delays
  443. );
  444. }
  445. // Generic GPIO input handlers
  446. #elif BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_GENERIC
  447. size_t buttons = 0;
  448. #if BUTTON1_PIN != GPIO_NONE
  449. ++buttons;
  450. #endif
  451. #if BUTTON2_PIN != GPIO_NONE
  452. ++buttons;
  453. #endif
  454. #if BUTTON3_PIN != GPIO_NONE
  455. ++buttons;
  456. #endif
  457. #if BUTTON4_PIN != GPIO_NONE
  458. ++buttons;
  459. #endif
  460. #if BUTTON5_PIN != GPIO_NONE
  461. ++buttons;
  462. #endif
  463. #if BUTTON6_PIN != GPIO_NONE
  464. ++buttons;
  465. #endif
  466. #if BUTTON7_PIN != GPIO_NONE
  467. ++buttons;
  468. #endif
  469. #if BUTTON8_PIN != GPIO_NONE
  470. ++buttons;
  471. #endif
  472. _buttons.reserve(buttons);
  473. for (unsigned char index = 0; index < ButtonsMax; ++index) {
  474. const auto pin = getSetting({"btnGPIO", index}, _buttonPin(index));
  475. if (!gpioValid(pin)) {
  476. break;
  477. }
  478. const auto relayID = getSetting({"btnRelay", index}, _buttonRelay(index));
  479. // TODO: compatibility proxy, fetch global key before indexed
  480. const button_event_delays_t delays {
  481. _buttonGetSetting("btnDebDel", index, _buttonDebounceDelay(index)),
  482. _buttonGetSetting("btnRepDel", index, _buttonRepeatDelay(index)),
  483. _buttonGetSetting("btnLclkDel", index, _buttonLongClickDelay(index)),
  484. _buttonGetSetting("btnLLclkDel", index, _buttonLongLongClickDelay(index)),
  485. };
  486. const button_actions_t actions {
  487. getSetting({"btnPress", index}, _buttonPress(index)),
  488. getSetting({"btnClick", index}, _buttonClick(index)),
  489. getSetting({"btnDclk", index}, _buttonDoubleClick(index)),
  490. getSetting({"btnLclk", index}, _buttonLongClick(index)),
  491. getSetting({"btnLLclk", index}, _buttonLongLongClick(index)),
  492. getSetting({"btnTclk", index}, _buttonTripleClick(index))
  493. };
  494. const auto config = _buttonConfig(index);
  495. // TODO: allow to change GpioPin to something else based on config?
  496. _buttons.emplace_back(
  497. std::make_shared<GpioPin>(pin), config,
  498. relayID, actions, delays
  499. );
  500. }
  501. #endif
  502. _buttonConfigure();
  503. DEBUG_MSG_P(PSTR("[BUTTON] Number of buttons: %u\n"), _buttons.size());
  504. // Websocket Callbacks
  505. #if WEB_SUPPORT
  506. wsRegister()
  507. .onConnected(_buttonWebSocketOnVisible)
  508. .onVisible(_buttonWebSocketOnVisible)
  509. .onKeyCheck(_buttonWebSocketOnKeyCheck);
  510. #endif
  511. // Register system callbacks
  512. espurnaRegisterLoop(buttonLoop);
  513. espurnaRegisterReload(_buttonConfigure);
  514. }
  515. // Sonoff Dual does not do real GPIO readings and we
  516. // depend on the external MCU to send us relay / button events
  517. // Lightfox uses the same protocol as Dual, but has slightly different actions
  518. // TODO: move this to a separate 'hardware' setup file?
  519. void _buttonLoopSonoffDual() {
  520. if (Serial.available() < 4) {
  521. return;
  522. }
  523. unsigned char bytes[4] = {0};
  524. Serial.readBytes(bytes, 4);
  525. if ((bytes[0] != 0xA0) && (bytes[1] != 0x04) && (bytes[3] != 0xA1)) {
  526. return;
  527. }
  528. const unsigned char value [[gnu::unused]] = bytes[2];
  529. #if BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_ITEAD_SONOFF_DUAL
  530. // RELAYs and BUTTONs are synchonized in the SIL F330
  531. // The on-board BUTTON2 should toggle RELAY0 value
  532. // Since we are not passing back RELAY2 value
  533. // (in the relayStatus method) it will only be present
  534. // here if it has actually been pressed
  535. if ((value & 4) == 4) {
  536. buttonEvent(2, button_event_t::Click);
  537. return;
  538. }
  539. // Otherwise check if any of the other two BUTTONs
  540. // (in the header) has been pressed, but we should
  541. // ensure that we only toggle one of them to avoid
  542. // the synchronization going mad
  543. // This loop is generic for any PSB-04 module
  544. for (unsigned int i=0; i<relayCount(); i++) {
  545. const bool status = (value & (1 << i)) > 0;
  546. // Check if the status for that relay has changed
  547. if (relayStatus(i) != status) {
  548. buttonEvent(i, button_event_t::Click);
  549. break;
  550. }
  551. }
  552. #elif BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_FOXEL_LIGHTFOX_DUAL
  553. DEBUG_MSG_P(PSTR("[BUTTON] [LIGHTFOX] Received buttons mask: %u\n"), value);
  554. for (unsigned int i=0; i<_buttons.size(); i++) {
  555. if ((value & (1 << i)) > 0);
  556. buttonEvent(i, button_event_t::Click);
  557. }
  558. }
  559. #endif // BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_ITEAD_SONOFF_DUAL
  560. }
  561. void _buttonLoopGeneric() {
  562. for (size_t id = 0; id < _buttons.size(); ++id) {
  563. auto event = _buttons[id].loop();
  564. if (event != button_event_t::None) {
  565. buttonEvent(id, event);
  566. }
  567. }
  568. }
  569. void buttonLoop() {
  570. #if BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_GENERIC
  571. _buttonLoopGeneric();
  572. #elif (BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_ITEAD_SONOFF_DUAL) || \
  573. (BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_FOXEL_LIGHTFOX_DUAL)
  574. _buttonLoopSonoffDual();
  575. #else
  576. #warning "Unknown value for BUTTON_EVENTS_SOURCE"
  577. #endif
  578. }
  579. #endif // BUTTON_SUPPORT