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.

499 lines
14 KiB

providers: relays, lights and buttons refactoring (#2414) - gpio module now tracks the known providers (right now, hardware and mcp expander) - refactored relay struct to use 'Provider' implementing setup,notify,change,boot instead of just BasePin actions - refactored button module to use gpio provider instead of referencing types itself - removed dual & stm code from buttons, migrate both to relay module - added status notify and change callbacks for relayStatus (i.e. 'notify' when relay status was called, but not changed. and 'changed' when it did) - relays runtime configuration keys - relay command now shows configured relays and current & target statuses - refactor the code using relayStatus(0, blah) under LIGHT_PROVIDER check to use lightState instead - remove rfbridge code form relay module. implement through a basic state listener in the rfbridge module, depend on RELAY_SUPPORT - allow to bind rf codes to real relays - drop tuya-specific lights provider, remove tuya code from relays and lights modules - integrate tuya via relay listeners and providers, use lights custom provider - implement channel transitions for tuya. disabled by default, and transition time and step are overridden to 2000 + 100. needs to be set to some value below the total time (i.e. `total transition time / step time == number of steps`, we need to figure out a correct time that serial comms could handle) - lights custom provider (global, not per-pin) and state listeners - remove lights code from relay module. implement through providers & listeners in the lights module, depend on RELAY_SUPPORT - lights per-channel relay provider (unused atm), depend on RELAY_SUPPORT - refactored channel transition - calculate step only once, make sure time + step values are sane, generate quick transitions with very small delay (10ms hardcoded) for transitions during OFF state i.e. we no longer waste 500ms (or whatever transition time is set to) on boot doing nothing - transition time + step parameter for the lightUpdate - report mask parameter for the lightUpdate - minor fixes across the board resolve #2222
3 years ago
providers: relays, lights and buttons refactoring (#2414) - gpio module now tracks the known providers (right now, hardware and mcp expander) - refactored relay struct to use 'Provider' implementing setup,notify,change,boot instead of just BasePin actions - refactored button module to use gpio provider instead of referencing types itself - removed dual & stm code from buttons, migrate both to relay module - added status notify and change callbacks for relayStatus (i.e. 'notify' when relay status was called, but not changed. and 'changed' when it did) - relays runtime configuration keys - relay command now shows configured relays and current & target statuses - refactor the code using relayStatus(0, blah) under LIGHT_PROVIDER check to use lightState instead - remove rfbridge code form relay module. implement through a basic state listener in the rfbridge module, depend on RELAY_SUPPORT - allow to bind rf codes to real relays - drop tuya-specific lights provider, remove tuya code from relays and lights modules - integrate tuya via relay listeners and providers, use lights custom provider - implement channel transitions for tuya. disabled by default, and transition time and step are overridden to 2000 + 100. needs to be set to some value below the total time (i.e. `total transition time / step time == number of steps`, we need to figure out a correct time that serial comms could handle) - lights custom provider (global, not per-pin) and state listeners - remove lights code from relay module. implement through providers & listeners in the lights module, depend on RELAY_SUPPORT - lights per-channel relay provider (unused atm), depend on RELAY_SUPPORT - refactored channel transition - calculate step only once, make sure time + step values are sane, generate quick transitions with very small delay (10ms hardcoded) for transitions during OFF state i.e. we no longer waste 500ms (or whatever transition time is set to) on boot doing nothing - transition time + step parameter for the lightUpdate - report mask parameter for the lightUpdate - minor fixes across the board resolve #2222
3 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
providers: relays, lights and buttons refactoring (#2414) - gpio module now tracks the known providers (right now, hardware and mcp expander) - refactored relay struct to use 'Provider' implementing setup,notify,change,boot instead of just BasePin actions - refactored button module to use gpio provider instead of referencing types itself - removed dual & stm code from buttons, migrate both to relay module - added status notify and change callbacks for relayStatus (i.e. 'notify' when relay status was called, but not changed. and 'changed' when it did) - relays runtime configuration keys - relay command now shows configured relays and current & target statuses - refactor the code using relayStatus(0, blah) under LIGHT_PROVIDER check to use lightState instead - remove rfbridge code form relay module. implement through a basic state listener in the rfbridge module, depend on RELAY_SUPPORT - allow to bind rf codes to real relays - drop tuya-specific lights provider, remove tuya code from relays and lights modules - integrate tuya via relay listeners and providers, use lights custom provider - implement channel transitions for tuya. disabled by default, and transition time and step are overridden to 2000 + 100. needs to be set to some value below the total time (i.e. `total transition time / step time == number of steps`, we need to figure out a correct time that serial comms could handle) - lights custom provider (global, not per-pin) and state listeners - remove lights code from relay module. implement through providers & listeners in the lights module, depend on RELAY_SUPPORT - lights per-channel relay provider (unused atm), depend on RELAY_SUPPORT - refactored channel transition - calculate step only once, make sure time + step values are sane, generate quick transitions with very small delay (10ms hardcoded) for transitions during OFF state i.e. we no longer waste 500ms (or whatever transition time is set to) on boot doing nothing - transition time + step parameter for the lightUpdate - report mask parameter for the lightUpdate - minor fixes across the board resolve #2222
3 years ago
providers: relays, lights and buttons refactoring (#2414) - gpio module now tracks the known providers (right now, hardware and mcp expander) - refactored relay struct to use 'Provider' implementing setup,notify,change,boot instead of just BasePin actions - refactored button module to use gpio provider instead of referencing types itself - removed dual & stm code from buttons, migrate both to relay module - added status notify and change callbacks for relayStatus (i.e. 'notify' when relay status was called, but not changed. and 'changed' when it did) - relays runtime configuration keys - relay command now shows configured relays and current & target statuses - refactor the code using relayStatus(0, blah) under LIGHT_PROVIDER check to use lightState instead - remove rfbridge code form relay module. implement through a basic state listener in the rfbridge module, depend on RELAY_SUPPORT - allow to bind rf codes to real relays - drop tuya-specific lights provider, remove tuya code from relays and lights modules - integrate tuya via relay listeners and providers, use lights custom provider - implement channel transitions for tuya. disabled by default, and transition time and step are overridden to 2000 + 100. needs to be set to some value below the total time (i.e. `total transition time / step time == number of steps`, we need to figure out a correct time that serial comms could handle) - lights custom provider (global, not per-pin) and state listeners - remove lights code from relay module. implement through providers & listeners in the lights module, depend on RELAY_SUPPORT - lights per-channel relay provider (unused atm), depend on RELAY_SUPPORT - refactored channel transition - calculate step only once, make sure time + step values are sane, generate quick transitions with very small delay (10ms hardcoded) for transitions during OFF state i.e. we no longer waste 500ms (or whatever transition time is set to) on boot doing nothing - transition time + step parameter for the lightUpdate - report mask parameter for the lightUpdate - minor fixes across the board resolve #2222
3 years ago
  1. /*
  2. LED MODULE
  3. Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
  4. */
  5. #include "led.h"
  6. #if LED_SUPPORT
  7. #include <algorithm>
  8. #include "broker.h"
  9. #include "mqtt.h"
  10. #include "relay.h"
  11. #include "rpc.h"
  12. #include "ws.h"
  13. #include "led_pattern.h"
  14. #include "led_config.h"
  15. // LED helper class
  16. led_t::led_t(unsigned char pin_, bool inverse_, unsigned char mode_, unsigned char relayID_) :
  17. pin(pin_),
  18. inverse(inverse_),
  19. mode(mode_),
  20. relayID(relayID_)
  21. {
  22. pinMode(pin, OUTPUT);
  23. status(false);
  24. }
  25. bool led_t::status() {
  26. bool result = digitalRead(pin);
  27. return inverse ? !result : result;
  28. }
  29. bool led_t::status(bool new_status) {
  30. digitalWrite(pin, inverse ? !new_status : new_status);
  31. return new_status;
  32. }
  33. bool led_t::toggle() {
  34. return status(!status());
  35. }
  36. led_delay_t::led_delay_t(unsigned long on_ms, unsigned long off_ms, unsigned char repeats) :
  37. type(repeats ? led_delay_mode_t::Finite : led_delay_mode_t::Infinite),
  38. on(microsecondsToClockCycles(on_ms * 1000)),
  39. off(microsecondsToClockCycles(off_ms * 1000)),
  40. repeats(repeats ? repeats : 0)
  41. {}
  42. led_delay_t::led_delay_t(unsigned long on_ms, unsigned long off_ms) :
  43. led_delay_t(on_ms, off_ms, 0)
  44. {}
  45. led_pattern_t::led_pattern_t(const std::vector<led_delay_t>& delays) :
  46. delays(delays),
  47. queue(),
  48. clock_last(ESP.getCycleCount()),
  49. clock_delay(delays.size() ? delays.back().on : 0)
  50. {}
  51. bool led_pattern_t::started() {
  52. return queue.size() > 0;
  53. }
  54. bool led_pattern_t::ready() {
  55. return delays.size() > 0;
  56. }
  57. void led_pattern_t::start() {
  58. clock_last = ESP.getCycleCount();
  59. clock_delay = 0;
  60. queue = {
  61. delays.rbegin(), delays.rend()
  62. };
  63. }
  64. void led_pattern_t::stop() {
  65. queue.clear();
  66. }
  67. // For relay-based modes
  68. bool _led_update = false;
  69. // For network-based modes, cycle ON & OFF (time in milliseconds)
  70. // XXX: internals convert these to clock cycles, delay cannot be longer than 25000 / 50000 ms
  71. const led_delay_t _ledDelays[] {
  72. {100, 100}, // Autoconfig
  73. {100, 4900}, // Connected
  74. {4900, 100}, // Connected (inverse)
  75. {100, 900}, // Config / AP
  76. {900, 100}, // Config / AP (inverse)
  77. {500, 500} // Idle
  78. };
  79. std::vector<led_t> _leds;
  80. // -----------------------------------------------------------------------------
  81. unsigned char ledCount() {
  82. return _leds.size();
  83. }
  84. bool _ledStatus(led_t& led) {
  85. return led.pattern.started() || led.status();
  86. }
  87. bool _ledStatus(led_t& led, bool status) {
  88. bool result = false;
  89. // when led has pattern, status depends on whether it's running
  90. if (led.pattern.ready()) {
  91. if (status) {
  92. if (!led.pattern.started()) {
  93. led.pattern.start();
  94. }
  95. result = true;
  96. } else {
  97. led.pattern.stop();
  98. led.status(false);
  99. result = false;
  100. }
  101. // if not, simply proxy status directly to the led pin
  102. } else {
  103. result = led.status(status);
  104. }
  105. return result;
  106. }
  107. bool _ledToggle(led_t& led) {
  108. return _ledStatus(led, !_ledStatus(led));
  109. }
  110. bool ledStatus(unsigned char id, bool status) {
  111. if (id >= ledCount()) return false;
  112. return _ledStatus(_leds[id], status);
  113. }
  114. bool ledStatus(unsigned char id) {
  115. if (id >= ledCount()) return false;
  116. return _ledStatus(_leds[id]);
  117. }
  118. const led_delay_t& _ledModeToDelay(LedMode mode) {
  119. static_assert(
  120. (sizeof(_ledDelays) / sizeof(_ledDelays[0])) <= static_cast<int>(LedMode::None),
  121. "LedMode mapping out-of-bounds"
  122. );
  123. return _ledDelays[static_cast<int>(mode)];
  124. }
  125. void _ledPattern(led_t& led) {
  126. const auto clock_current = ESP.getCycleCount();
  127. if (clock_current - led.pattern.clock_last >= led.pattern.clock_delay) {
  128. const bool status = led.toggle();
  129. auto& current = led.pattern.queue.back();
  130. switch (current.type) {
  131. case led_delay_mode_t::Finite:
  132. if (status && !--current.repeats) {
  133. led.pattern.queue.pop_back();
  134. if (!led.pattern.queue.size()) {
  135. led.status(false);
  136. return;
  137. }
  138. }
  139. break;
  140. case led_delay_mode_t::Infinite:
  141. case led_delay_mode_t::None:
  142. default:
  143. break;
  144. }
  145. led.pattern.clock_delay = status ? current.on : current.off;
  146. led.pattern.clock_last = ESP.getCycleCount();
  147. }
  148. }
  149. void _ledBlink(led_t& led, const led_delay_t& delays) {
  150. static auto clock_last = ESP.getCycleCount();
  151. static auto delay_for = delays.on;
  152. const auto clock_current = ESP.getCycleCount();
  153. if (clock_current - clock_last >= delay_for) {
  154. delay_for = led.toggle() ? delays.on : delays.off;
  155. clock_last = clock_current;
  156. }
  157. }
  158. inline void _ledBlink(led_t& led, const LedMode mode) {
  159. _ledBlink(led, _ledModeToDelay(mode));
  160. }
  161. #if WEB_SUPPORT
  162. bool _ledWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
  163. return (strncmp(key, "led", 3) == 0);
  164. }
  165. void _ledWebSocketOnVisible(JsonObject& root) {
  166. if (ledCount() > 0) {
  167. root["ledVisible"] = 1;
  168. }
  169. }
  170. void _ledWebSocketOnConnected(JsonObject& root) {
  171. if (!ledCount()) return;
  172. JsonObject& module = root.createNestedObject("led");
  173. JsonArray& schema = module.createNestedArray("schema");
  174. schema.add("GPIO");
  175. schema.add("Inv");
  176. schema.add("Mode");
  177. schema.add("Relay");
  178. JsonArray& leds = module.createNestedArray("list");
  179. for (unsigned char index = 0; index < ledCount(); ++index) {
  180. JsonArray& led = leds.createNestedArray();
  181. led.add(getSetting({"ledGPIO", index}, _ledPin(index)));
  182. led.add(static_cast<int>(getSetting({"ledInv", index}, _ledInverse(index))));
  183. led.add(getSetting({"ledMode", index}, _ledMode(index)));
  184. led.add(getSetting({"ledRelay", index}, _ledRelay(index)));
  185. }
  186. }
  187. #endif
  188. void _ledBrokerCallback(const String& topic, unsigned char, unsigned int) {
  189. // Only process status messages for switches
  190. if (topic.equals(MQTT_TOPIC_RELAY)) {
  191. ledUpdate(true);
  192. }
  193. }
  194. #if MQTT_SUPPORT
  195. void _ledMQTTCallback(unsigned int type, const char * topic, const char * payload) {
  196. if (type == MQTT_CONNECT_EVENT) {
  197. char buffer[strlen(MQTT_TOPIC_LED) + 3];
  198. snprintf_P(buffer, sizeof(buffer), PSTR("%s/+"), MQTT_TOPIC_LED);
  199. mqttSubscribe(buffer);
  200. }
  201. if (type == MQTT_MESSAGE_EVENT) {
  202. // Only want `led/+/<MQTT_SETTER>`
  203. const String magnitude = mqttMagnitude((char *) topic);
  204. if (!magnitude.startsWith(MQTT_TOPIC_LED)) return;
  205. // Get led ID from after the slash when t is `led/<LED_ID>`
  206. unsigned int ledID = magnitude.substring(strlen(MQTT_TOPIC_LED) + 1).toInt();
  207. if (ledID >= ledCount()) {
  208. DEBUG_MSG_P(PSTR("[LED] Wrong ledID (%d)\n"), ledID);
  209. return;
  210. }
  211. // Check if LED is managed
  212. if (_leds[ledID].mode != LED_MODE_MANUAL) return;
  213. // Get value based on rpc payload logic (see rpc.ino)
  214. const auto value = rpcParsePayload(payload);
  215. switch (value) {
  216. case PayloadStatus::On:
  217. case PayloadStatus::Off:
  218. _ledStatus(_leds[ledID], (value == PayloadStatus::On));
  219. break;
  220. case PayloadStatus::Toggle:
  221. _ledToggle(_leds[ledID]);
  222. break;
  223. case PayloadStatus::Unknown:
  224. default:
  225. _ledLoadPattern(_leds[ledID], payload);
  226. _ledStatus(_leds[ledID], true);
  227. break;
  228. }
  229. }
  230. }
  231. #endif
  232. void _ledConfigure() {
  233. for (unsigned char id = 0; id < _leds.size(); ++id) {
  234. _leds[id].mode = getSetting({"ledMode", id}, _ledMode(id));
  235. _leds[id].relayID = getSetting({"ledRelay", id}, _ledRelay(id));
  236. _leds[id].pattern.stop();
  237. _ledLoadPattern(_leds[id], getSetting({"ledPattern", id}).c_str());
  238. }
  239. _led_update = true;
  240. }
  241. // -----------------------------------------------------------------------------
  242. void ledUpdate(bool do_update) {
  243. _led_update = do_update;
  244. }
  245. void ledLoop() {
  246. const auto wifi_state = wifiState();
  247. for (auto& led : _leds) {
  248. switch (led.mode) {
  249. case LED_MODE_WIFI:
  250. if ((wifi_state & WIFI_STATE_WPS) || (wifi_state & WIFI_STATE_SMARTCONFIG)) {
  251. _ledBlink(led, LedMode::NetworkAutoconfig);
  252. } else if (wifi_state & WIFI_STATE_STA) {
  253. _ledBlink(led, LedMode::NetworkConnected);
  254. } else if (wifi_state & WIFI_STATE_AP) {
  255. _ledBlink(led, LedMode::NetworkConfig);
  256. } else {
  257. _ledBlink(led, LedMode::NetworkIdle);
  258. }
  259. break;
  260. #if RELAY_SUPPORT
  261. case LED_MODE_FINDME_WIFI:
  262. if ((wifi_state & WIFI_STATE_WPS) || (wifi_state & WIFI_STATE_SMARTCONFIG)) {
  263. _ledBlink(led, LedMode::NetworkAutoconfig);
  264. } else if (wifi_state & WIFI_STATE_STA) {
  265. if (relayStatus(led.relayID)) {
  266. _ledBlink(led, LedMode::NetworkConnected);
  267. } else {
  268. _ledBlink(led, LedMode::NetworkConnectedInverse);
  269. }
  270. } else if (wifi_state & WIFI_STATE_AP) {
  271. if (relayStatus(led.relayID)) {
  272. _ledBlink(led, LedMode::NetworkConfig);
  273. } else {
  274. _ledBlink(led, LedMode::NetworkConfigInverse);
  275. }
  276. } else {
  277. _ledBlink(led, LedMode::NetworkIdle);
  278. }
  279. break;
  280. case LED_MODE_RELAY_WIFI:
  281. if ((wifi_state & WIFI_STATE_WPS) || (wifi_state & WIFI_STATE_SMARTCONFIG)) {
  282. _ledBlink(led, LedMode::NetworkAutoconfig);
  283. } else if (wifi_state & WIFI_STATE_STA) {
  284. if (relayStatus(led.relayID)) {
  285. _ledBlink(led, LedMode::NetworkConnected);
  286. } else {
  287. _ledBlink(led, LedMode::NetworkConnectedInverse);
  288. }
  289. } else if (wifi_state & WIFI_STATE_AP) {
  290. if (relayStatus(led.relayID)) {
  291. _ledBlink(led, LedMode::NetworkConfig);
  292. } else {
  293. _ledBlink(led, LedMode::NetworkConfigInverse);
  294. }
  295. } else {
  296. _ledBlink(led, LedMode::NetworkIdle);
  297. }
  298. break;
  299. case LED_MODE_FOLLOW:
  300. if (!_led_update) break;
  301. _ledStatus(led, relayStatus(led.relayID));
  302. break;
  303. case LED_MODE_FOLLOW_INVERSE:
  304. if (!_led_update) break;
  305. led.status(!relayStatus(led.relayID));
  306. _ledStatus(led, !relayStatus(led.relayID));
  307. break;
  308. case LED_MODE_FINDME: {
  309. if (!_led_update) break;
  310. bool status = true;
  311. for (unsigned char relayID = 0; relayID < relayCount(); ++relayID) {
  312. if (relayStatus(relayID)) {
  313. status = false;
  314. break;
  315. }
  316. }
  317. _ledStatus(led, status);
  318. break;
  319. }
  320. case LED_MODE_RELAY: {
  321. if (!_led_update) break;
  322. bool status = false;
  323. for (unsigned char relayID = 0; relayID < relayCount(); ++relayID) {
  324. if (relayStatus(relayID)) {
  325. status = true;
  326. break;
  327. }
  328. }
  329. _ledStatus(led, status);
  330. break;
  331. }
  332. #endif // RELAY_SUPPORT == 1
  333. case LED_MODE_ON:
  334. if (!_led_update) break;
  335. _ledStatus(led, true);
  336. break;
  337. case LED_MODE_OFF:
  338. if (!_led_update) break;
  339. _ledStatus(led, false);
  340. break;
  341. }
  342. if (led.pattern.started()) {
  343. _ledPattern(led);
  344. continue;
  345. }
  346. }
  347. _led_update = false;
  348. }
  349. void ledSetup() {
  350. size_t leds = 0;
  351. #if LED1_PIN != GPIO_NONE
  352. ++leds;
  353. #endif
  354. #if LED2_PIN != GPIO_NONE
  355. ++leds;
  356. #endif
  357. #if LED3_PIN != GPIO_NONE
  358. ++leds;
  359. #endif
  360. #if LED4_PIN != GPIO_NONE
  361. ++leds;
  362. #endif
  363. #if LED5_PIN != GPIO_NONE
  364. ++leds;
  365. #endif
  366. #if LED6_PIN != GPIO_NONE
  367. ++leds;
  368. #endif
  369. #if LED7_PIN != GPIO_NONE
  370. ++leds;
  371. #endif
  372. #if LED8_PIN != GPIO_NONE
  373. ++leds;
  374. #endif
  375. _leds.reserve(leds);
  376. for (unsigned char index=0; index < LedsMax; ++index) {
  377. const auto pin = getSetting({"ledGPIO", index}, _ledPin(index));
  378. if (!gpioLock(pin)) {
  379. break;
  380. }
  381. _leds.emplace_back(
  382. pin,
  383. getSetting({"ledInv", index}, _ledInverse(index)),
  384. getSetting({"ledMode", index}, _ledMode(index)),
  385. getSetting({"ledRelay", index}, _ledRelay(index))
  386. );
  387. }
  388. _led_update = true;
  389. #if MQTT_SUPPORT
  390. mqttRegister(_ledMQTTCallback);
  391. #endif
  392. #if WEB_SUPPORT
  393. wsRegister()
  394. .onVisible(_ledWebSocketOnVisible)
  395. .onConnected(_ledWebSocketOnConnected)
  396. .onKeyCheck(_ledWebSocketOnKeyCheck);
  397. #endif
  398. StatusBroker::Register(_ledBrokerCallback);
  399. DEBUG_MSG_P(PSTR("[LED] Number of leds: %d\n"), _leds.size());
  400. // Main callbacks
  401. espurnaRegisterLoop(ledLoop);
  402. espurnaRegisterReload(_ledConfigure);
  403. }
  404. #endif // LED_SUPPORT