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.

708 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::DefaultState convert(const String& value) {
  32. switch (value.toInt()) {
  33. case 0:
  34. return debounce_event::types::DefaultState::Low;
  35. case 1:
  36. default:
  37. return debounce_event::types::DefaultState::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::DefaultState::High
  62. : debounce_event::types::DefaultState::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 uint16_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({"btnDefState", index}, config.default_state),
  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("DefState");
  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_state));
  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. uint16_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. case BUTTON_ACTION_TOGGLE:
  342. relayToggle(button.relayID);
  343. break;
  344. case BUTTON_ACTION_ON:
  345. relayStatus(button.relayID, true);
  346. break;
  347. case BUTTON_ACTION_OFF:
  348. relayStatus(button.relayID, false);
  349. break;
  350. case BUTTON_ACTION_AP:
  351. if (wifiState() & WIFI_STATE_AP) {
  352. wifiStartSTA();
  353. } else {
  354. wifiStartAP();
  355. }
  356. break;
  357. case BUTTON_ACTION_RESET:
  358. deferredReset(100, CUSTOM_RESET_HARDWARE);
  359. break;
  360. case BUTTON_ACTION_FACTORY:
  361. DEBUG_MSG_P(PSTR("\n\nFACTORY RESET\n\n"));
  362. resetSettings();
  363. deferredReset(100, CUSTOM_RESET_FACTORY);
  364. break;
  365. #if defined(JUSTWIFI_ENABLE_WPS)
  366. case BUTTON_ACTION_WPS:
  367. wifiStartWPS();
  368. break;
  369. #endif // defined(JUSTWIFI_ENABLE_WPS)
  370. #if defined(JUSTWIFI_ENABLE_SMARTCONFIG)
  371. case BUTTON_ACTION_SMART_CONFIG:
  372. wifiStartSmartConfig();
  373. break;
  374. #endif // defined(JUSTWIFI_ENABLE_SMARTCONFIG)
  375. #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
  376. case BUTTON_ACTION_DIM_UP:
  377. lightBrightnessStep(1);
  378. lightUpdate(true, true);
  379. break;
  380. case BUTTON_ACTION_DIM_DOWN:
  381. lightBrightnessStep(-1);
  382. lightUpdate(true, true);
  383. break;
  384. #endif // LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
  385. #if THERMOSTAT_DISPLAY_SUPPORT
  386. case BUTTON_ACTION_DISPLAY_ON:
  387. displayOn();
  388. break;
  389. #endif
  390. }
  391. }
  392. void _buttonConfigure() {
  393. #if MQTT_SUPPORT
  394. for (unsigned char index = 0; index < _buttons.size(); ++index) {
  395. _buttons_mqtt_send_all[index] = getSetting({"btnMqttSendAll", index}, _buttonMqttSendAllEvents(index));
  396. _buttons_mqtt_retain[index] = getSetting({"btnMqttRetain", index}, _buttonMqttRetain(index));
  397. }
  398. #endif
  399. }
  400. // TODO: compatibility proxy, fetch global key before indexed
  401. template<typename T>
  402. unsigned long _buttonGetSetting(const char* key, unsigned char index, T default_value) {
  403. return getSetting(key, getSetting({key, index}, default_value));
  404. }
  405. void buttonSetup() {
  406. // Backwards compatibility
  407. moveSetting("btnDelay", "btnRepDel");
  408. // Special hardware cases
  409. #if (BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_ITEAD_SONOFF_DUAL) || \
  410. (BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_FOXEL_LIGHTFOX_DUAL)
  411. size_t buttons = 0;
  412. #if BUTTON1_RELAY != RELAY_NONE
  413. ++buttons;
  414. #endif
  415. #if BUTTON2_RELAY != RELAY_NONE
  416. ++buttons;
  417. #endif
  418. #if BUTTON3_RELAY != RELAY_NONE
  419. ++buttons;
  420. #endif
  421. #if BUTTON4_RELAY != RELAY_NONE
  422. ++buttons;
  423. #endif
  424. _buttons.reserve(buttons);
  425. // Ignore real button delays since we don't use them here
  426. const auto delays = button_event_delays_t();
  427. for (unsigned char index = 0; index < buttons; ++index) {
  428. const button_actions_t actions {
  429. BUTTON_ACTION_NONE,
  430. // The only generated event is ::Click
  431. getSetting({"btnClick", index}, _buttonClick(index)),
  432. BUTTON_ACTION_NONE,
  433. BUTTON_ACTION_NONE,
  434. BUTTON_ACTION_NONE,
  435. BUTTON_ACTION_NONE
  436. };
  437. _buttons.emplace_back(
  438. getSetting({"btnRelay", index}, _buttonRelay(index)),
  439. actions,
  440. delays
  441. );
  442. }
  443. // Generic GPIO input handlers
  444. #elif BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_GENERIC
  445. size_t buttons = 0;
  446. #if BUTTON1_PIN != GPIO_NONE
  447. ++buttons;
  448. #endif
  449. #if BUTTON2_PIN != GPIO_NONE
  450. ++buttons;
  451. #endif
  452. #if BUTTON3_PIN != GPIO_NONE
  453. ++buttons;
  454. #endif
  455. #if BUTTON4_PIN != GPIO_NONE
  456. ++buttons;
  457. #endif
  458. #if BUTTON5_PIN != GPIO_NONE
  459. ++buttons;
  460. #endif
  461. #if BUTTON6_PIN != GPIO_NONE
  462. ++buttons;
  463. #endif
  464. #if BUTTON7_PIN != GPIO_NONE
  465. ++buttons;
  466. #endif
  467. #if BUTTON8_PIN != GPIO_NONE
  468. ++buttons;
  469. #endif
  470. _buttons.reserve(buttons);
  471. for (unsigned char index = 0; index < ButtonsMax; ++index) {
  472. const auto pin = getSetting({"btnGPIO", index}, _buttonPin(index));
  473. if (!gpioValid(pin)) {
  474. break;
  475. }
  476. const auto relayID = getSetting({"btnRelay", index}, _buttonRelay(index));
  477. // TODO: compatibility proxy, fetch global key before indexed
  478. const button_event_delays_t delays {
  479. _buttonGetSetting("btnDebDel", index, _buttonDebounceDelay(index)),
  480. _buttonGetSetting("btnRepDel", index, _buttonRepeatDelay(index)),
  481. _buttonGetSetting("btnLclkDel", index, _buttonLongClickDelay(index)),
  482. _buttonGetSetting("btnLLclkDel", index, _buttonLongLongClickDelay(index)),
  483. };
  484. const button_actions_t actions {
  485. getSetting({"btnPress", index}, _buttonPress(index)),
  486. getSetting({"btnClick", index}, _buttonClick(index)),
  487. getSetting({"btnDclk", index}, _buttonDoubleClick(index)),
  488. getSetting({"btnLclk", index}, _buttonLongClick(index)),
  489. getSetting({"btnLLclk", index}, _buttonLongLongClick(index)),
  490. getSetting({"btnTclk", index}, _buttonTripleClick(index))
  491. };
  492. const auto config = _buttonConfig(index);
  493. // TODO: allow to change GpioPin to something else based on config?
  494. _buttons.emplace_back(
  495. std::make_shared<GpioPin>(pin), config,
  496. relayID, actions, delays
  497. );
  498. }
  499. #endif
  500. _buttonConfigure();
  501. DEBUG_MSG_P(PSTR("[BUTTON] Number of buttons: %u\n"), _buttons.size());
  502. // Websocket Callbacks
  503. #if WEB_SUPPORT
  504. wsRegister()
  505. .onConnected(_buttonWebSocketOnVisible)
  506. .onVisible(_buttonWebSocketOnVisible)
  507. .onKeyCheck(_buttonWebSocketOnKeyCheck);
  508. #endif
  509. // Register system callbacks
  510. espurnaRegisterLoop(buttonLoop);
  511. espurnaRegisterReload(_buttonConfigure);
  512. }
  513. // Sonoff Dual does not do real GPIO readings and we
  514. // depend on the external MCU to send us relay / button events
  515. // Lightfox uses the same protocol as Dual, but has slightly different actions
  516. // TODO: move this to a separate 'hardware' setup file?
  517. void _buttonLoopSonoffDual() {
  518. if (Serial.available() < 4) {
  519. return;
  520. }
  521. unsigned char bytes[4] = {0};
  522. Serial.readBytes(bytes, 4);
  523. if ((bytes[0] != 0xA0) && (bytes[1] != 0x04) && (bytes[3] != 0xA1)) {
  524. return;
  525. }
  526. const unsigned char value [[gnu::unused]] = bytes[2];
  527. #if BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_ITEAD_SONOFF_DUAL
  528. // RELAYs and BUTTONs are synchonized in the SIL F330
  529. // The on-board BUTTON2 should toggle RELAY0 value
  530. // Since we are not passing back RELAY2 value
  531. // (in the relayStatus method) it will only be present
  532. // here if it has actually been pressed
  533. if ((value & 4) == 4) {
  534. buttonEvent(2, button_event_t::Click);
  535. return;
  536. }
  537. // Otherwise check if any of the other two BUTTONs
  538. // (in the header) has been pressed, but we should
  539. // ensure that we only toggle one of them to avoid
  540. // the synchronization going mad
  541. // This loop is generic for any PSB-04 module
  542. for (unsigned int i=0; i<relayCount(); i++) {
  543. const bool status = (value & (1 << i)) > 0;
  544. // Check if the status for that relay has changed
  545. if (relayStatus(i) != status) {
  546. buttonEvent(i, button_event_t::Click);
  547. break;
  548. }
  549. }
  550. #elif BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_FOXEL_LIGHTFOX_DUAL
  551. DEBUG_MSG_P(PSTR("[BUTTON] [LIGHTFOX] Received buttons mask: %u\n"), value);
  552. for (unsigned int i=0; i<_buttons.size(); i++) {
  553. if ((value & (1 << i)) > 0);
  554. buttonEvent(i, button_event_t::Click);
  555. }
  556. }
  557. #endif // BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_ITEAD_SONOFF_DUAL
  558. }
  559. void _buttonLoopGeneric() {
  560. for (size_t id = 0; id < _buttons.size(); ++id) {
  561. auto event = _buttons[id].loop();
  562. if (event != button_event_t::None) {
  563. buttonEvent(id, event);
  564. }
  565. }
  566. }
  567. void buttonLoop() {
  568. #if BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_GENERIC
  569. _buttonLoopGeneric();
  570. #elif (BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_ITEAD_SONOFF_DUAL) || \
  571. (BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_FOXEL_LIGHTFOX_DUAL)
  572. _buttonLoopSonoffDual();
  573. #else
  574. #warning "Unknown value for BUTTON_EVENTS_SOURCE"
  575. #endif
  576. }
  577. #endif // BUTTON_SUPPORT