Mirror of espurna firmware for wireless switches and more
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.

1130 lines
25 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
6 years ago
6 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. Copyright (C) 2019-2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
  5. To (re)create the string -> pattern decoder .ipp files, add `re2c` to the $PATH and 'run' the environment:
  6. ```
  7. $ pio run -e ... -t espurna/led_pattern.re.ipp
  8. ```
  9. (see scripts/pio_pre.py and scripts/espurna_utils/build.py for more info)
  10. */
  11. #include "espurna.h"
  12. #if LED_SUPPORT
  13. #include <algorithm>
  14. #include <cstring>
  15. #include <ctime>
  16. #include <chrono>
  17. #include <forward_list>
  18. #include <vector>
  19. #include "led.h"
  20. #include "mqtt.h"
  21. #include "relay.h"
  22. #include "rpc.h"
  23. #if WEB_SUPPORT
  24. #include "ws.h"
  25. #endif
  26. namespace espurna {
  27. namespace led {
  28. namespace {
  29. // Some local-only time & counters implementation:
  30. // - Core conversion is done through macros, implement stronger types
  31. // - force unsigned instead of chrono's 'int64_t', since we want safe overflow
  32. // - bound to 32bits, to seamlessly handle ccount conversion from the 'time source'
  33. // - explicitly check for the maximum number of milliseconds that may be represented with ccount
  34. // TODO: full-width int for repeats instead of 8bit? right now, string parser will *force* [min:max] range,
  35. // but anything else is experiencing overflow mechanics
  36. struct alignas(8) Delay {
  37. using Source = espurna::time::CpuClock;
  38. using Duration = Source::duration;
  39. using TimePoint = Source::time_point;
  40. static constexpr auto ClockCyclesMax = Duration(Duration::max());
  41. static constexpr auto MillisecondsMax = std::chrono::duration_cast<espurna::duration::Milliseconds>(ClockCyclesMax);
  42. using Repeats = size_t;
  43. static constexpr Repeats RepeatsMin { std::numeric_limits<Repeats>::min() };
  44. static constexpr Repeats RepeatsMax { std::numeric_limits<Repeats>::max() };
  45. enum class Mode {
  46. Finite,
  47. Infinite,
  48. None
  49. };
  50. Delay() = delete;
  51. constexpr Delay(Duration on, Duration off, Repeats repeats) :
  52. _mode(repeats ? Mode::Finite : Mode::Infinite),
  53. _on(on),
  54. _off(off),
  55. _repeats(repeats)
  56. {}
  57. constexpr Mode mode() const {
  58. return _mode;
  59. }
  60. constexpr Duration on() const {
  61. return _on;
  62. }
  63. constexpr Duration off() const {
  64. return _off;
  65. }
  66. constexpr Repeats repeats() const {
  67. return _repeats;
  68. }
  69. private:
  70. Mode _mode;
  71. Duration _on;
  72. Duration _off;
  73. Repeats _repeats;
  74. };
  75. constexpr espurna::duration::ClockCycles Delay::ClockCyclesMax;
  76. constexpr espurna::duration::Milliseconds Delay::MillisecondsMax;
  77. struct Pattern {
  78. using Delays = std::vector<Delay>;
  79. Pattern() = default;
  80. Pattern(Pattern&&) = default;
  81. Pattern& operator=(Pattern&&) = default;
  82. explicit Pattern(espurna::StringView);
  83. explicit Pattern(Delays&& delays) :
  84. _delays(std::move(delays)),
  85. _sequence(_delays),
  86. _cycle(_delays)
  87. {}
  88. explicit operator bool() const {
  89. return _delays.size() > 0;
  90. }
  91. void start() {
  92. if (!_sequence) {
  93. _cycle = Delay::Duration::min();
  94. _sequence = _delays;
  95. }
  96. }
  97. void stop() {
  98. _cycle = Delay::Duration::min();
  99. std::move(_sequence) = _delays;
  100. }
  101. bool started() const {
  102. return static_cast<bool>(_sequence);
  103. }
  104. const Delays& delays() const {
  105. return _delays;
  106. }
  107. template <typename Status, typename Last>
  108. void run(Status&& status, Last&& last) {
  109. if (!_sequence) {
  110. return;
  111. }
  112. if (!_cycle) {
  113. return;
  114. }
  115. const auto currentStatus = status();
  116. _cycle = currentStatus
  117. ? _sequence.on() : _sequence.off();
  118. switch (_sequence.mode()) {
  119. case Delay::Mode::Finite:
  120. if (currentStatus && !_sequence.repeat()) {
  121. if (!_sequence.next()) {
  122. last();
  123. }
  124. }
  125. break;
  126. case Delay::Mode::Infinite:
  127. case Delay::Mode::None:
  128. break;
  129. }
  130. }
  131. private:
  132. // Sequence of pending 'delays', by default it's in the order they are specified in the underlying vector.
  133. // Notice that there are no checks that '_current' is dereferencable, it's up to the consumer to check via 'operator bool()' first
  134. // TODO: is it actually valid to have 'Sequence() = default', and not actually reference any particular object?
  135. struct Sequence {
  136. Sequence() = delete;
  137. Sequence(const Sequence&) = default;
  138. Sequence(Sequence&&) = default;
  139. explicit Sequence(const Delays& delays) :
  140. _current(delays.cbegin()),
  141. _end(delays.cend()),
  142. _repeats((_current != _end) ? (*_current).repeats() : 0)
  143. {}
  144. Sequence& operator=(const Sequence&) = default;
  145. Sequence& operator=(Sequence&&) = default;
  146. Sequence& operator=(const Delays& delays) & {
  147. return (*this = Sequence(delays));
  148. }
  149. Sequence& operator=(const Delays& delays) && {
  150. _current = delays.cend();
  151. _end = delays.cend();
  152. _repeats = 0;
  153. return *this;
  154. }
  155. explicit operator bool() const {
  156. return _current != _end;
  157. }
  158. Delay::Repeats repeats() const {
  159. return _repeats;
  160. }
  161. Delay::Mode mode() const {
  162. return (*_current).mode();
  163. }
  164. Delay::Duration on() const {
  165. return (*_current).on();
  166. }
  167. Delay::Duration off() const {
  168. return (*_current).off();
  169. }
  170. bool repeat() {
  171. if (_repeats) {
  172. --_repeats;
  173. return true;
  174. }
  175. return false;
  176. }
  177. bool next() {
  178. if (_current != _end) {
  179. ++_current;
  180. if (_current != _end) {
  181. _repeats = (*_current).repeats();
  182. return true;
  183. }
  184. }
  185. return false;
  186. }
  187. private:
  188. Delays::const_iterator _current;
  189. Delays::const_iterator _end;
  190. Delay::Repeats _repeats;
  191. };
  192. // Currently used delay value cycles between 'on' and 'off',
  193. // allow to set the current one and to wait until it expires
  194. struct Cycle {
  195. explicit Cycle(const Delays& delays) :
  196. _last(Delay::Source::now()),
  197. _delay(delays.size() ? delays.front().on() : Delay::Duration::min())
  198. {}
  199. Cycle& operator=(const Delays& delays) {
  200. return (*this = Cycle(delays));
  201. }
  202. Cycle& operator=(Delay::Duration duration) {
  203. _last = Delay::Source::now();
  204. _delay = duration;
  205. return *this;
  206. }
  207. explicit operator bool() const {
  208. return (Delay::Source::now() - _last > _delay);
  209. }
  210. private:
  211. Delay::TimePoint _last;
  212. Delay::Duration _delay;
  213. };
  214. Delays _delays;
  215. Sequence _sequence { _delays };
  216. Cycle _cycle { _delays };
  217. };
  218. struct Led {
  219. Led() = delete;
  220. Led(unsigned char pin, bool inverse, LedMode mode) :
  221. _pin(pin),
  222. _inverse(inverse),
  223. _mode(mode)
  224. {
  225. init();
  226. }
  227. unsigned char pin() const {
  228. return _pin;
  229. }
  230. LedMode mode() const {
  231. return _mode;
  232. }
  233. void mode(LedMode mode) {
  234. _mode = mode;
  235. }
  236. bool inverse() const {
  237. return _inverse;
  238. }
  239. Pattern& pattern() {
  240. return _pattern;
  241. }
  242. void pattern(Pattern&& pattern) {
  243. _pattern = std::move(pattern);
  244. }
  245. bool started() {
  246. return _pattern.started();
  247. }
  248. void stop() {
  249. _pattern.stop();
  250. }
  251. void init();
  252. bool status();
  253. bool status(bool new_status);
  254. bool toggle();
  255. void run() {
  256. _pattern.run(
  257. // notify the pattern about the 'current' status
  258. [&]() {
  259. return toggle();
  260. },
  261. // what happens when the pattern ends
  262. [&]() {
  263. status(false);
  264. });
  265. }
  266. private:
  267. unsigned char _pin;
  268. bool _inverse;
  269. LedMode _mode;
  270. Pattern _pattern;
  271. };
  272. void Led::init() {
  273. pinMode(_pin, OUTPUT);
  274. status(false);
  275. }
  276. bool Led::status() {
  277. bool result = digitalRead(_pin);
  278. return _inverse ? !result : result;
  279. }
  280. bool Led::status(bool new_status) {
  281. digitalWrite(_pin, _inverse ? !new_status : new_status);
  282. return new_status;
  283. }
  284. bool Led::toggle() {
  285. return status(!status());
  286. }
  287. #include "led_pattern.re.ipp"
  288. namespace settings {
  289. namespace keys {
  290. PROGMEM_STRING(Gpio, "ledGpio");
  291. PROGMEM_STRING(Inverse, "ledInv");
  292. PROGMEM_STRING(Mode, "ledMode");
  293. PROGMEM_STRING(Relay, "ledRelay");
  294. PROGMEM_STRING(Pattern, "ledPattern");
  295. } // namespace keys
  296. namespace options {
  297. using espurna::settings::options::Enumeration;
  298. PROGMEM_STRING(Manual, "manual");
  299. PROGMEM_STRING(WiFi, "wifi");
  300. PROGMEM_STRING(On, "on");
  301. PROGMEM_STRING(Off, "off");
  302. #if RELAY_SUPPORT
  303. PROGMEM_STRING(Relay, "relay");
  304. PROGMEM_STRING(RelayInverse, "relay-inverse");
  305. PROGMEM_STRING(FindMe, "findme");
  306. PROGMEM_STRING(FindMeWiFi, "findme-wifi");
  307. PROGMEM_STRING(Relays, "relays");
  308. PROGMEM_STRING(RelaysWiFi, "relays-wifi");
  309. #endif
  310. static constexpr Enumeration<LedMode> LedModeOptions[] PROGMEM {
  311. {LedMode::Manual, Manual},
  312. {LedMode::WiFi, WiFi},
  313. #if RELAY_SUPPORT
  314. {LedMode::Relay, Relay},
  315. {LedMode::RelayInverse, RelayInverse},
  316. {LedMode::FindMe, FindMe},
  317. {LedMode::FindMeWiFi, FindMeWiFi},
  318. #endif
  319. {LedMode::On, On},
  320. {LedMode::Off, Off},
  321. #if RELAY_SUPPORT
  322. {LedMode::Relays, Relays},
  323. {LedMode::RelaysWiFi, RelaysWiFi},
  324. #endif
  325. };
  326. } // namespace options
  327. } // namespace settings
  328. } // namespace
  329. } // namespace led
  330. // -----------------------------------------------------------------------------
  331. namespace settings {
  332. namespace internal {
  333. namespace {
  334. using led::settings::options::LedModeOptions;
  335. } // namespace
  336. template <>
  337. LedMode convert(const String& value) {
  338. return convert(LedModeOptions, value, LedMode::Manual);
  339. }
  340. String serialize(LedMode mode) {
  341. return serialize(LedModeOptions, mode);
  342. }
  343. } // namespace internal
  344. } // namespace settings
  345. // -----------------------------------------------------------------------------
  346. namespace led {
  347. namespace {
  348. namespace build {
  349. constexpr size_t LedsMax { 8ul };
  350. constexpr size_t preconfiguredLeds() {
  351. return 0ul
  352. #if LED1_PIN != GPIO_NONE
  353. + 1ul
  354. #endif
  355. #if LED2_PIN != GPIO_NONE
  356. + 1ul
  357. #endif
  358. #if LED3_PIN != GPIO_NONE
  359. + 1ul
  360. #endif
  361. #if LED4_PIN != GPIO_NONE
  362. + 1ul
  363. #endif
  364. #if LED5_PIN != GPIO_NONE
  365. + 1ul
  366. #endif
  367. #if LED6_PIN != GPIO_NONE
  368. + 1ul
  369. #endif
  370. #if LED7_PIN != GPIO_NONE
  371. + 1ul
  372. #endif
  373. #if LED8_PIN != GPIO_NONE
  374. + 1ul
  375. #endif
  376. ;
  377. }
  378. constexpr unsigned char pin(size_t index) {
  379. return (
  380. (index == 0) ? LED1_PIN :
  381. (index == 1) ? LED2_PIN :
  382. (index == 2) ? LED3_PIN :
  383. (index == 3) ? LED4_PIN :
  384. (index == 4) ? LED5_PIN :
  385. (index == 5) ? LED6_PIN :
  386. (index == 6) ? LED7_PIN :
  387. (index == 7) ? LED8_PIN : GPIO_NONE
  388. );
  389. }
  390. constexpr LedMode mode(size_t index) {
  391. return (
  392. (index == 0) ? LED1_MODE :
  393. (index == 1) ? LED2_MODE :
  394. (index == 2) ? LED3_MODE :
  395. (index == 3) ? LED4_MODE :
  396. (index == 4) ? LED5_MODE :
  397. (index == 5) ? LED6_MODE :
  398. (index == 6) ? LED7_MODE :
  399. (index == 7) ? LED8_MODE : LedMode::Manual
  400. );
  401. }
  402. constexpr unsigned char relay(size_t index) {
  403. return (
  404. (index == 0) ? (LED1_RELAY - 1) :
  405. (index == 1) ? (LED2_RELAY - 1) :
  406. (index == 2) ? (LED3_RELAY - 1) :
  407. (index == 3) ? (LED4_RELAY - 1) :
  408. (index == 4) ? (LED5_RELAY - 1) :
  409. (index == 5) ? (LED6_RELAY - 1) :
  410. (index == 6) ? (LED7_RELAY - 1) :
  411. (index == 7) ? (LED8_RELAY - 1) : RELAY_NONE
  412. );
  413. }
  414. constexpr bool inverse(size_t index) {
  415. return (
  416. (index == 0) ? (1 == LED1_PIN_INVERSE) :
  417. (index == 1) ? (1 == LED2_PIN_INVERSE) :
  418. (index == 2) ? (1 == LED3_PIN_INVERSE) :
  419. (index == 3) ? (1 == LED4_PIN_INVERSE) :
  420. (index == 4) ? (1 == LED5_PIN_INVERSE) :
  421. (index == 5) ? (1 == LED6_PIN_INVERSE) :
  422. (index == 6) ? (1 == LED7_PIN_INVERSE) :
  423. (index == 7) ? (1 == LED8_PIN_INVERSE) : false
  424. );
  425. }
  426. } // namespace build
  427. namespace settings {
  428. unsigned char pin(size_t id) {
  429. return getSetting({keys::Gpio, id}, build::pin(id));
  430. }
  431. bool inverse(size_t id) {
  432. return getSetting({keys::Inverse, id}, build::inverse(id));
  433. }
  434. LedMode mode(size_t id) {
  435. return getSetting({keys::Mode, id}, build::mode(id));
  436. }
  437. #if RELAY_SUPPORT
  438. size_t relay(size_t id) {
  439. return getSetting({keys::Relay, id}, build::relay(id));
  440. }
  441. #endif
  442. Pattern pattern(size_t id) {
  443. return Pattern(getSetting({keys::Pattern, id}));
  444. }
  445. void migrate(int version) {
  446. if (version < 5) {
  447. delSettingPrefix({
  448. keys::Gpio,
  449. PSTR("ledGPIO"),
  450. PSTR("ledLogic")
  451. });
  452. }
  453. }
  454. } // namespace settings
  455. // For network-based modes, indefinitely cycle ON <-> OFF
  456. // (TODO: template params containing structs like duration need -std=c++2a)
  457. #define LED_STATIC_DELAY(NAME, ON, OFF)\
  458. static constexpr auto NAME ## MillisecondsOn PROGMEM = espurna::duration::Milliseconds(ON);\
  459. static constexpr auto NAME ## MillisecondsOff PROGMEM = espurna::duration::Milliseconds(OFF);\
  460. static_assert(NAME ## MillisecondsOn < Delay::MillisecondsMax, "");\
  461. static_assert(NAME ## MillisecondsOff < Delay::MillisecondsMax, "");\
  462. static constexpr Delay NAME PROGMEM = Delay {\
  463. std::chrono::duration_cast<Delay::Duration>(NAME ## MillisecondsOn),\
  464. std::chrono::duration_cast<Delay::Duration>(NAME ## MillisecondsOff),\
  465. Delay::RepeatsMin }
  466. LED_STATIC_DELAY(NetworkConnected, 100, 4900);
  467. LED_STATIC_DELAY(NetworkConnectedInverse, 4900, 100);
  468. LED_STATIC_DELAY(NetworkConfig, 100, 900);
  469. LED_STATIC_DELAY(NetworkConfigInverse, 900, 100);
  470. LED_STATIC_DELAY(NetworkIdle, 500, 500);
  471. namespace internal {
  472. std::vector<Led> leds;
  473. bool update { false };
  474. } // namespace internal
  475. namespace settings {
  476. namespace query {
  477. namespace internal {
  478. #define ID_VALUE(NAME)\
  479. String NAME (size_t id) {\
  480. return espurna::settings::internal::serialize(\
  481. ::espurna::led::settings::NAME(id));\
  482. }
  483. ID_VALUE(pin)
  484. ID_VALUE(inverse)
  485. ID_VALUE(mode)
  486. #if RELAY_SUPPORT
  487. ID_VALUE(relay)
  488. #endif
  489. #undef ID_VALUE
  490. } // namespace internal
  491. static constexpr espurna::settings::query::IndexedSetting IndexedSettings[] PROGMEM {
  492. {keys::Gpio, internal::pin},
  493. {keys::Inverse, internal::inverse},
  494. {keys::Mode, internal::mode},
  495. #if RELAY_SUPPORT
  496. {keys::Relay, internal::relay},
  497. #endif
  498. };
  499. bool checkSamePrefix(StringView key) {
  500. return espurna::settings::query::samePrefix(key, STRING_VIEW("led"));
  501. }
  502. String findValueFrom(StringView key) {
  503. return espurna::settings::query::IndexedSetting::findValueFrom(
  504. ::espurna::led::internal::leds.size(), IndexedSettings, key);
  505. }
  506. void setup() {
  507. ::settingsRegisterQueryHandler({
  508. .check = checkSamePrefix,
  509. .get = findValueFrom
  510. });
  511. }
  512. } // namespace query
  513. } // namespace settings
  514. #if RELAY_SUPPORT
  515. namespace relay {
  516. namespace internal {
  517. struct Link {
  518. Led& led;
  519. size_t relayId;
  520. };
  521. std::forward_list<Link> relays;
  522. bool linked(const Link& link, const Led& led) {
  523. return &link.led == &led;
  524. }
  525. void unlink(Led& led) {
  526. relays.remove_if([&](const Link& link) {
  527. return linked(link, led);
  528. });
  529. }
  530. void link(Led& led, size_t id) {
  531. auto it = std::find_if(relays.begin(), relays.end(), [&](const Link& link) {
  532. return linked(link, led);
  533. });
  534. if (it != relays.end()) {
  535. (*it).relayId = id;
  536. return;
  537. }
  538. relays.emplace_front(Link{led, id});
  539. }
  540. size_t find(Led& led) {
  541. auto it = std::find_if(relays.begin(), relays.end(), [&](const Link& link) {
  542. return linked(link, led);
  543. });
  544. if (it != relays.end()) {
  545. return (*it).relayId;
  546. }
  547. return RelaysMax;
  548. }
  549. } // namespace internal
  550. void unlink(Led& led) {
  551. internal::unlink(led);
  552. }
  553. void link(Led& led, size_t id) {
  554. internal::link(led, id);
  555. }
  556. size_t find(Led& led) {
  557. return internal::find(led);
  558. }
  559. bool status(Led& led) {
  560. return relayStatus(find(led));
  561. }
  562. bool areAnyOn() {
  563. bool result { false };
  564. for (size_t id = 0; id < relayCount(); ++id) {
  565. if (relayStatus(id)) {
  566. result = true;
  567. break;
  568. }
  569. }
  570. return result;
  571. }
  572. } // namespace relay
  573. #endif
  574. size_t count() {
  575. return internal::leds.size();
  576. }
  577. bool scheduled() {
  578. return internal::update;
  579. }
  580. void schedule() {
  581. internal::update = true;
  582. }
  583. void cancel() {
  584. internal::update = false;
  585. }
  586. bool status(Led& led) {
  587. return led.started() || led.status();
  588. }
  589. bool status(size_t id) {
  590. return status(internal::leds[id]);
  591. }
  592. bool status(Led& led, bool status) {
  593. bool result = false;
  594. // when led has pattern, status depends on whether it's running
  595. // (notice that sending 'true' status multiple times does not restart the pattern)
  596. auto& pattern = led.pattern();
  597. if (pattern) {
  598. if (status) {
  599. pattern.start();
  600. result = true;
  601. } else {
  602. pattern.stop();
  603. led.status(false);
  604. result = false;
  605. }
  606. // if not, simply proxy status directly to the led pin
  607. } else {
  608. result = led.status(status);
  609. }
  610. return result;
  611. }
  612. bool status(size_t id, bool value) {
  613. return status(internal::leds[id], value);
  614. }
  615. void turn_off() {
  616. for (auto& led : internal::leds) {
  617. status(led, false);
  618. }
  619. }
  620. [[gnu::unused]]
  621. void pattern(Led& led, Pattern&& other) {
  622. led.pattern(std::move(other));
  623. status(led, true);
  624. }
  625. void payload_status(Led& led, StringView payload) {
  626. led.stop();
  627. led.status(false);
  628. led.mode(LedMode::Manual);
  629. const auto value = rpcParsePayload(payload);
  630. switch (value) {
  631. case PayloadStatus::On:
  632. case PayloadStatus::Off:
  633. led::status(led, (value == PayloadStatus::On));
  634. break;
  635. case PayloadStatus::Toggle:
  636. led::status(led, !led::status(led));
  637. break;
  638. case PayloadStatus::Unknown:
  639. pattern(led, Pattern(payload));
  640. break;
  641. }
  642. }
  643. void run(Led& led, const Delay& delay) {
  644. using TimeSource = espurna::time::CpuClock;
  645. static auto clock_last = TimeSource::now();
  646. static auto delay_for = delay.on();
  647. const auto clock_current = TimeSource::now();
  648. if (clock_current - clock_last >= delay_for) {
  649. delay_for = led.toggle() ? delay.on() : delay.off();
  650. clock_last = clock_current;
  651. }
  652. }
  653. void configure() {
  654. for (size_t id = 0; id < internal::leds.size(); ++id) {
  655. auto& led = internal::leds[id];
  656. led.mode(settings::mode(id));
  657. led.pattern(settings::pattern(id));
  658. #if RELAY_SUPPORT
  659. switch (internal::leds[id].mode()) {
  660. case LedMode::Relay:
  661. case LedMode::RelayInverse:
  662. relay::link(led, settings::relay(id));
  663. break;
  664. default:
  665. relay::unlink(led);
  666. break;
  667. }
  668. #endif
  669. }
  670. schedule();
  671. }
  672. void loop(Led& led) {
  673. switch (led.mode()) {
  674. case LedMode::Manual:
  675. break;
  676. case LedMode::WiFi:
  677. if (wifiConnected()) {
  678. run(led, NetworkConnected);
  679. } else if (wifiConnectable()) {
  680. run(led, NetworkConfig);
  681. } else {
  682. run(led, NetworkIdle);
  683. }
  684. break;
  685. case LedMode::FindMeWiFi:
  686. #if RELAY_SUPPORT
  687. if (wifiConnected()) {
  688. if (relay::areAnyOn()) {
  689. run(led, NetworkConnected);
  690. } else {
  691. run(led, NetworkConnectedInverse);
  692. }
  693. } else if (wifiConnectable()) {
  694. if (relay::areAnyOn()) {
  695. run(led, NetworkConfig);
  696. } else {
  697. run(led, NetworkConfigInverse);
  698. }
  699. } else {
  700. run(led, NetworkIdle);
  701. }
  702. #endif
  703. break;
  704. case LedMode::RelaysWiFi:
  705. #if RELAY_SUPPORT
  706. if (wifiConnected()) {
  707. if (!relay::areAnyOn()) {
  708. run(led, NetworkConnected);
  709. } else {
  710. run(led, NetworkConnectedInverse);
  711. }
  712. } else if (wifiConnectable()) {
  713. if (!relay::areAnyOn()) {
  714. run(led, NetworkConfig);
  715. } else {
  716. run(led, NetworkConfigInverse);
  717. }
  718. } else {
  719. run(led, NetworkIdle);
  720. }
  721. #endif
  722. break;
  723. case LedMode::Relay:
  724. #if RELAY_SUPPORT
  725. if (scheduled()) {
  726. status(led, relay::status(led));
  727. }
  728. #endif
  729. break;
  730. case LedMode::RelayInverse:
  731. #if RELAY_SUPPORT
  732. if (scheduled()) {
  733. status(led, !relay::status(led));
  734. }
  735. #endif
  736. break;
  737. case LedMode::FindMe:
  738. #if RELAY_SUPPORT
  739. if (scheduled()) {
  740. led::status(led, !relay::areAnyOn());
  741. }
  742. #endif
  743. break;
  744. case LedMode::Relays:
  745. #if RELAY_SUPPORT
  746. if (scheduled()) {
  747. led::status(led, relay::areAnyOn());
  748. }
  749. #endif
  750. break;
  751. case LedMode::On:
  752. if (scheduled()) {
  753. status(led, true);
  754. }
  755. break;
  756. case LedMode::Off:
  757. if (scheduled()) {
  758. status(led, false);
  759. }
  760. break;
  761. }
  762. led.run();
  763. }
  764. void loop() {
  765. for (auto& led : internal::leds) {
  766. loop(led);
  767. }
  768. cancel();
  769. }
  770. #if MQTT_SUPPORT
  771. namespace mqtt {
  772. void callback(unsigned int type, StringView topic, StringView payload) {
  773. if (type == MQTT_CONNECT_EVENT) {
  774. mqttSubscribe(MQTT_TOPIC_LED "/+");
  775. return;
  776. }
  777. // Only want `led/+/<MQTT_SETTER>`
  778. // We get the led ID from the `+`
  779. if (type == MQTT_MESSAGE_EVENT) {
  780. const auto magnitude = mqttMagnitude(topic);
  781. if (!magnitude.startsWith(MQTT_TOPIC_LED)) {
  782. return;
  783. }
  784. size_t ledID;
  785. if (tryParseIdPath(magnitude, ledCount(), ledID)) {
  786. payload_status(internal::leds[ledID], payload);
  787. }
  788. return;
  789. }
  790. }
  791. } // namespace mqtt
  792. #endif // MQTT_SUPPORT
  793. #if WEB_SUPPORT
  794. namespace web {
  795. bool onKeyCheck(StringView key, const JsonVariant&) {
  796. return settings::query::checkSamePrefix(key);
  797. }
  798. void onVisible(JsonObject& root) {
  799. wsPayloadModule(root, PSTR("led"));
  800. }
  801. void onConnected(JsonObject& root) {
  802. if (count()) {
  803. espurna::web::ws::EnumerableConfig config{root, STRING_VIEW("ledConfig")};
  804. config(STRING_VIEW("leds"), ::espurna::led::count(), settings::query::IndexedSettings);
  805. }
  806. }
  807. } // namespace web
  808. #endif // WEB_SUPPORT
  809. #if TERMINAL_SUPPORT
  810. namespace terminal {
  811. PROGMEM_STRING(Led, "LED");
  812. void led(::terminal::CommandContext&& ctx) {
  813. if (ctx.argv.size() > 1) {
  814. size_t id;
  815. if (!tryParseId(ctx.argv[1], ledCount(), id)) {
  816. terminalError(ctx, F("Invalid ledID"));
  817. return;
  818. }
  819. auto& led = internal::leds[id];
  820. if (ctx.argv.size() == 2) {
  821. settingsDump(ctx, settings::query::IndexedSettings, id);
  822. } else if (ctx.argv.size() > 2) {
  823. payload_status(led, ctx.argv[2]);
  824. }
  825. schedule();
  826. terminalOK(ctx);
  827. return;
  828. }
  829. size_t id { 0 };
  830. for (const auto& led : internal::leds) {
  831. ctx.output.printf_P(
  832. PSTR("led%u {Gpio=%hhu Mode=%s}\n"), id++, led.pin(),
  833. espurna::settings::internal::serialize(led.mode()).c_str());
  834. }
  835. }
  836. static constexpr ::terminal::Command Commands[] PROGMEM {
  837. {Led, led},
  838. };
  839. void setup() {
  840. espurna::terminal::add(Commands);
  841. }
  842. } // namespace terminal
  843. #endif
  844. void setup() {
  845. migrateVersion(settings::migrate);
  846. internal::leds.reserve(build::preconfiguredLeds());
  847. for (size_t index = 0; index < build::LedsMax; ++index) {
  848. const auto pin = settings::pin(index);
  849. if (!gpioLock(pin)) {
  850. break;
  851. }
  852. internal::leds.emplace_back(pin,
  853. settings::inverse(index), settings::mode(index));
  854. }
  855. const auto leds = internal::leds.size();
  856. DEBUG_MSG_P(PSTR("[LED] Number of leds: %u\n"), leds);
  857. if (leds) {
  858. espurna::led::settings::query::setup();
  859. #if MQTT_SUPPORT
  860. ::mqttRegister(mqtt::callback);
  861. #endif
  862. #if WEB_SUPPORT
  863. ::wsRegister()
  864. .onVisible(web::onVisible)
  865. .onConnected(web::onConnected)
  866. .onKeyCheck(web::onKeyCheck);
  867. #endif
  868. #if RELAY_SUPPORT
  869. ::relayOnStatusChange([](size_t, bool) {
  870. schedule();
  871. });
  872. #endif
  873. #if TERMINAL_SUPPORT
  874. terminal::setup();
  875. #endif
  876. systemBeforeSleep(turn_off);
  877. systemAfterSleep(schedule);
  878. ::espurnaRegisterLoop(loop);
  879. ::espurnaRegisterReload(configure);
  880. configure();
  881. }
  882. }
  883. } // namespace
  884. } // namespace led
  885. } // namespace espurna
  886. bool ledStatus(size_t id, bool status) {
  887. if (id < espurna::led::count()) {
  888. return espurna::led::status(id, status);
  889. }
  890. return status;
  891. }
  892. bool ledStatus(size_t id) {
  893. if (id < espurna::led::count()) {
  894. return espurna::led::status(id);
  895. }
  896. return false;
  897. }
  898. size_t ledCount() {
  899. return espurna::led::count();
  900. }
  901. void ledLoop() {
  902. espurna::led::loop();
  903. }
  904. void ledSetup() {
  905. espurna::led::setup();
  906. }
  907. #endif // LED_SUPPORT