/* LED MODULE Copyright (C) 2016-2019 by Xose PĂ©rez */ #if LED_SUPPORT #include #include "broker.h" #include "relay.h" #include "rpc.h" #include "led.h" #include "led_pattern.h" #include "led_config.h" // LED helper class led_t::led_t(unsigned char pin, bool inverse, unsigned char mode, unsigned char relayID) : pin(pin), inverse(inverse), mode(mode), relayID(relayID) { if (pin != GPIO_NONE) { pinMode(pin, OUTPUT); status(false); } } bool led_t::status() { bool result = digitalRead(pin); return inverse ? !result : result; } bool led_t::status(bool new_status) { digitalWrite(pin, inverse ? !new_status : new_status); return new_status; } bool led_t::toggle() { return status(!status()); } led_delay_t::led_delay_t(unsigned long on_ms, unsigned long off_ms, unsigned char repeats) : type(repeats ? led_delay_mode_t::Finite : led_delay_mode_t::Infinite), on(microsecondsToClockCycles(on_ms * 1000)), off(microsecondsToClockCycles(off_ms * 1000)), repeats(repeats ? repeats : 0) {} led_delay_t::led_delay_t(unsigned long on_ms, unsigned long off_ms) : led_delay_t(on_ms, off_ms, 0) {} led_pattern_t::led_pattern_t(const std::vector& delays) : delays(delays), queue(), clock_last(ESP.getCycleCount()), clock_delay(delays.size() ? delays.back().on : 0) {} bool led_pattern_t::started() { return queue.size() > 0; } bool led_pattern_t::ready() { return delays.size() > 0; } void led_pattern_t::start() { clock_last = ESP.getCycleCount(); clock_delay = 0; queue = { delays.rbegin(), delays.rend() }; } void led_pattern_t::stop() { queue.clear(); } // For relay-based modes bool _led_update = false; // For network-based modes, cycle ON & OFF (time in milliseconds) // XXX: internals convert these to clock cycles, delay cannot be longer than 25000 / 50000 ms const led_delay_t _ledDelays[] { {100, 100}, // Autoconfig {100, 4900}, // Connected {4900, 100}, // Connected (inverse) {100, 900}, // Config / AP {900, 100}, // Config / AP (inverse) {500, 500} // Idle }; std::vector _leds; // ----------------------------------------------------------------------------- unsigned char ledCount() { return _leds.size(); } bool _ledStatus(led_t& led) { return led.pattern.started() || led.status(); } bool _ledStatus(led_t& led, bool status) { bool result = false; // when led has pattern, status depends on whether it's running if (led.pattern.ready()) { if (status) { if (!led.pattern.started()) { led.pattern.start(); } result = true; } else { led.pattern.stop(); led.status(false); result = false; } // if not, simply proxy status directly to the led pin } else { result = led.status(status); } return result; } bool _ledToggle(led_t& led) { return _ledStatus(led, !_ledStatus(led)); } bool ledStatus(unsigned char id, bool status) { if (id >= ledCount()) return false; return _ledStatus(_leds[id], status); } bool ledStatus(unsigned char id) { if (id >= ledCount()) return false; return _ledStatus(_leds[id]); } const led_delay_t& _ledModeToDelay(LedMode mode) { static_assert( (sizeof(_ledDelays) / sizeof(_ledDelays[0])) <= static_cast(LedMode::None), "LedMode mapping out-of-bounds" ); return _ledDelays[static_cast(mode)]; } void _ledPattern(led_t& led) { const auto clock_current = ESP.getCycleCount(); if (clock_current - led.pattern.clock_last >= led.pattern.clock_delay) { const bool status = led.toggle(); auto& current = led.pattern.queue.back(); switch (current.type) { case led_delay_mode_t::Finite: if (status && !--current.repeats) { led.pattern.queue.pop_back(); if (!led.pattern.queue.size()) { led.status(false); return; } } break; case led_delay_mode_t::Infinite: case led_delay_mode_t::None: default: break; } led.pattern.clock_delay = status ? current.on : current.off; led.pattern.clock_last = ESP.getCycleCount(); } } void _ledBlink(led_t& led, const led_delay_t& delays) { static auto clock_last = ESP.getCycleCount(); static auto delay_for = delays.on; const auto clock_current = ESP.getCycleCount(); if (clock_current - clock_last >= delay_for) { delay_for = led.toggle() ? delays.on : delays.off; clock_last = clock_current; } } inline void _ledBlink(led_t& led, const LedMode mode) { _ledBlink(led, _ledModeToDelay(mode)); } #if WEB_SUPPORT bool _ledWebSocketOnKeyCheck(const char * key, JsonVariant& value) { return (strncmp(key, "led", 3) == 0); } void _ledWebSocketOnVisible(JsonObject& root) { if (ledCount() > 0) { root["ledVisible"] = 1; } } void _ledWebSocketOnConnected(JsonObject& root) { if (!ledCount()) return; JsonObject& module = root.createNestedObject("led"); JsonArray& schema = module.createNestedArray("schema"); schema.add("GPIO"); schema.add("Inv"); schema.add("Mode"); schema.add("Relay"); JsonArray& leds = module.createNestedArray("list"); for (unsigned char index = 0; index < ledCount(); ++index) { JsonArray& led = leds.createNestedArray(); led.add(getSetting({"ledGPIO", index}, _ledPin(index))); led.add(static_cast(getSetting({"ledInv", index}, _ledInverse(index)))); led.add(getSetting({"ledMode", index}, _ledMode(index))); led.add(getSetting({"ledRelay", index}, _ledRelay(index))); } } #endif #if BROKER_SUPPORT void _ledBrokerCallback(const String& topic, unsigned char, unsigned int) { // Only process status messages for switches if (topic.equals(MQTT_TOPIC_RELAY)) { ledUpdate(true); } } #endif // BROKER_SUPPORT #if MQTT_SUPPORT void _ledMQTTCallback(unsigned int type, const char * topic, const char * payload) { if (type == MQTT_CONNECT_EVENT) { char buffer[strlen(MQTT_TOPIC_LED) + 3]; snprintf_P(buffer, sizeof(buffer), PSTR("%s/+"), MQTT_TOPIC_LED); mqttSubscribe(buffer); } if (type == MQTT_MESSAGE_EVENT) { // Only want `led/+/` const String magnitude = mqttMagnitude((char *) topic); if (!magnitude.startsWith(MQTT_TOPIC_LED)) return; // Get led ID from after the slash when t is `led/` unsigned int ledID = magnitude.substring(strlen(MQTT_TOPIC_LED) + 1).toInt(); if (ledID >= ledCount()) { DEBUG_MSG_P(PSTR("[LED] Wrong ledID (%d)\n"), ledID); return; } // Check if LED is managed if (_leds[ledID].mode != LED_MODE_MANUAL) return; // Get value based on rpc payload logic (see rpc.ino) const auto value = rpcParsePayload(payload); switch (value) { case PayloadStatus::On: case PayloadStatus::Off: _ledStatus(_leds[ledID], (value == PayloadStatus::On)); break; case PayloadStatus::Toggle: _ledToggle(_leds[ledID]); break; case PayloadStatus::Unknown: default: _ledLoadPattern(_leds[ledID], payload); _ledStatus(_leds[ledID], true); break; } } } #endif void _ledConfigure() { for (unsigned char id = 0; id < _leds.size(); ++id) { _leds[id].mode = getSetting({"ledMode", id}, _ledMode(id)); _leds[id].relayID = getSetting({"ledRelay", id}, _ledRelay(id)); _leds[id].pattern.stop(); _ledLoadPattern(_leds[id], getSetting({"ledPattern", id}).c_str()); } _led_update = true; } // ----------------------------------------------------------------------------- void ledUpdate(bool do_update) { _led_update = do_update; } void ledSetup() { size_t leds = 0; #if LED1_PIN != GPIO_NONE ++leds; #endif #if LED2_PIN != GPIO_NONE ++leds; #endif #if LED3_PIN != GPIO_NONE ++leds; #endif #if LED4_PIN != GPIO_NONE ++leds; #endif #if LED5_PIN != GPIO_NONE ++leds; #endif #if LED6_PIN != GPIO_NONE ++leds; #endif #if LED7_PIN != GPIO_NONE ++leds; #endif #if LED8_PIN != GPIO_NONE ++leds; #endif _leds.reserve(leds); for (unsigned char index=0; index < leds; ++index) { const auto pin = getSetting({"ledGPIO", index}, _ledPin(index)); if (!gpioValid(pin)) { break; } _leds.emplace_back( pin, getSetting({"ledInv", index}, _ledInverse(index)), getSetting({"ledMode", index}, _ledMode(index)), getSetting({"ledRelay", index}, _ledRelay(index)) ); } _led_update = true; #if MQTT_SUPPORT mqttRegister(_ledMQTTCallback); #endif #if WEB_SUPPORT wsRegister() .onVisible(_ledWebSocketOnVisible) .onConnected(_ledWebSocketOnConnected) .onKeyCheck(_ledWebSocketOnKeyCheck); #endif #if BROKER_SUPPORT StatusBroker::Register(_ledBrokerCallback); #endif DEBUG_MSG_P(PSTR("[LED] Number of leds: %d\n"), _leds.size()); // Main callbacks espurnaRegisterLoop(ledLoop); espurnaRegisterReload(_ledConfigure); } void ledLoop() { const auto wifi_state = wifiState(); for (auto& led : _leds) { switch (led.mode) { case LED_MODE_WIFI: if ((wifi_state & WIFI_STATE_WPS) || (wifi_state & WIFI_STATE_SMARTCONFIG)) { _ledBlink(led, LedMode::NetworkAutoconfig); } else if (wifi_state & WIFI_STATE_STA) { _ledBlink(led, LedMode::NetworkConnected); } else if (wifi_state & WIFI_STATE_AP) { _ledBlink(led, LedMode::NetworkConfig); } else { _ledBlink(led, LedMode::NetworkIdle); } break; case LED_MODE_FINDME_WIFI: if ((wifi_state & WIFI_STATE_WPS) || (wifi_state & WIFI_STATE_SMARTCONFIG)) { _ledBlink(led, LedMode::NetworkAutoconfig); } else if (wifi_state & WIFI_STATE_STA) { if (relayStatus(led.relayID)) { _ledBlink(led, LedMode::NetworkConnected); } else { _ledBlink(led, LedMode::NetworkConnectedInverse); } } else if (wifi_state & WIFI_STATE_AP) { if (relayStatus(led.relayID)) { _ledBlink(led, LedMode::NetworkConfig); } else { _ledBlink(led, LedMode::NetworkConfigInverse); } } else { _ledBlink(led, LedMode::NetworkIdle); } break; case LED_MODE_RELAY_WIFI: if ((wifi_state & WIFI_STATE_WPS) || (wifi_state & WIFI_STATE_SMARTCONFIG)) { _ledBlink(led, LedMode::NetworkAutoconfig); } else if (wifi_state & WIFI_STATE_STA) { if (relayStatus(led.relayID)) { _ledBlink(led, LedMode::NetworkConnected); } else { _ledBlink(led, LedMode::NetworkConnectedInverse); } } else if (wifi_state & WIFI_STATE_AP) { if (relayStatus(led.relayID)) { _ledBlink(led, LedMode::NetworkConfig); } else { _ledBlink(led, LedMode::NetworkConfigInverse); } } else { _ledBlink(led, LedMode::NetworkIdle); } break; case LED_MODE_FOLLOW: if (!_led_update) break; _ledStatus(led, relayStatus(led.relayID)); break; case LED_MODE_FOLLOW_INVERSE: if (!_led_update) break; led.status(!relayStatus(led.relayID)); _ledStatus(led, !relayStatus(led.relayID)); break; case LED_MODE_FINDME: { if (!_led_update) break; bool status = true; for (unsigned char relayID = 0; relayID < relayCount(); ++relayID) { if (relayStatus(relayID)) { status = false; break; } } _ledStatus(led, status); break; } case LED_MODE_RELAY: { if (!_led_update) break; bool status = false; for (unsigned char relayID = 0; relayID < relayCount(); ++relayID) { if (relayStatus(relayID)) { status = true; break; } } _ledStatus(led, status); break; } case LED_MODE_ON: if (!_led_update) break; _ledStatus(led, true); break; case LED_MODE_OFF: if (!_led_update) break; _ledStatus(led, false); break; } if (led.pattern.started()) { _ledPattern(led); continue; } } _led_update = false; } #endif // LED_SUPPORT