/* BUTTON MODULE Copyright (C) 2016-2019 by Xose PĂ©rez Copyright (C) 2019-2021 by Maxim Prokhorov */ #include "button.h" #if BUTTON_SUPPORT #include #include #include #include "compat.h" #include "fan.h" #include "gpio.h" #include "light.h" #include "mqtt.h" #include "relay.h" #include "system.h" #include "ws.h" #include "libs/BasePin.h" #include "libs/DebounceEvent.h" #include "gpio_pin.h" #include "mcp23s08_pin.h" #include "button_config.h" // ----------------------------------------------------------------------------- namespace settings { namespace internal { template<> debounce_event::types::Mode convert(const String& value) { switch (value.toInt()) { case 1: return debounce_event::types::Mode::Switch; case 0: default: return debounce_event::types::Mode::Pushbutton; } } String serialize(debounce_event::types::Mode value) { String result; switch (value) { case debounce_event::types::Mode::Switch: result = "1"; break; case debounce_event::types::Mode::Pushbutton: default: result = "0"; break; } return result; } template<> debounce_event::types::PinValue convert(const String& value) { switch (value.toInt()) { case 1: return debounce_event::types::PinValue::High; case 2: return debounce_event::types::PinValue::Initial; default: case 0: return debounce_event::types::PinValue::Low; } } String serialize(debounce_event::types::PinValue value) { String result; switch (value) { case debounce_event::types::PinValue::Low: result = "0"; break; case debounce_event::types::PinValue::High: result = "1"; break; case debounce_event::types::PinValue::Initial: result = "2"; break; } return result; } template<> debounce_event::types::PinMode convert(const String& value) { switch (value.toInt()) { case 1: return debounce_event::types::PinMode::InputPullup; case 2: return debounce_event::types::PinMode::InputPulldown; case 0: default: return debounce_event::types::PinMode::Input; } } String serialize(debounce_event::types::PinMode mode) { String result; switch (mode) { case debounce_event::types::PinMode::InputPullup: result = "1"; break; case debounce_event::types::PinMode::InputPulldown: result = "2"; break; case debounce_event::types::PinMode::Input: default: result = "0"; break; } return result; } template <> ButtonProvider convert(const String& value) { auto type = static_cast(value.toInt()); switch (type) { case ButtonProvider::None: case ButtonProvider::Gpio: case ButtonProvider::Analog: return type; } return ButtonProvider::None; } template<> ButtonAction convert(const String& value) { auto num = strtoul(value.c_str(), nullptr, 10); if (num < ButtonsActionMax) { auto action = static_cast(num); switch (action) { case ButtonAction::None: case ButtonAction::Toggle: case ButtonAction::On: case ButtonAction::Off: case ButtonAction::AccessPoint: case ButtonAction::Reset: case ButtonAction::Pulse: case ButtonAction::FactoryReset: case ButtonAction::Wps: case ButtonAction::SmartConfig: case ButtonAction::BrightnessIncrease: case ButtonAction::BrightnessDecrease: case ButtonAction::DisplayOn: case ButtonAction::Custom: case ButtonAction::FanLow: case ButtonAction::FanMedium: case ButtonAction::FanHigh: return action; } } return ButtonAction::None; } } // namespace internal } // namespace settings // ----------------------------------------------------------------------------- constexpr ButtonAction _buttonDecodeEventAction(const ButtonActions& actions, ButtonEvent event) { return ( (event == ButtonEvent::Pressed) ? actions.pressed : (event == ButtonEvent::Released) ? actions.released : (event == ButtonEvent::Click) ? actions.click : (event == ButtonEvent::DoubleClick) ? actions.dblclick : (event == ButtonEvent::LongClick) ? actions.lngclick : (event == ButtonEvent::LongLongClick) ? actions.lnglngclick : (event == ButtonEvent::TripleClick) ? actions.trplclick : ButtonAction::None ); } constexpr ButtonEvent _buttonMapReleased(uint8_t count, unsigned long length, unsigned long lngclick_delay, unsigned long lnglngclick_delay) { return ( (0 == count) ? ButtonEvent::Released : (1 == count) ? ( (length > lnglngclick_delay) ? ButtonEvent::LongLongClick : (length > lngclick_delay) ? ButtonEvent::LongClick : ButtonEvent::Click ) : (2 == count) ? ButtonEvent::DoubleClick : (3 == count) ? ButtonEvent::TripleClick : ButtonEvent::None ); } ButtonActions _buttonConstructActions(size_t index) { return { button::build::press(index), button::build::release(index), button::build::click(index), button::build::doubleClick(index), button::build::longClick(index), button::build::longLongClick(index), button::build::tripleClick(index) }; } debounce_event::types::Config _buttonRuntimeConfig(size_t index) { return { getSetting({"btnMode", index}, button::build::mode(index)), getSetting({"btnDefVal", index}, button::build::defaultValue(index)), getSetting({"btnPinMode", index}, button::build::pinMode(index)) }; } int _buttonEventNumber(ButtonEvent event) { return static_cast(event); } // ----------------------------------------------------------------------------- ButtonEventDelays::ButtonEventDelays() : debounce(button::build::debounceDelay()), repeat(button::build::repeatDelay()), lngclick(button::build::longClickDelay()), lnglngclick(button::build::longLongClickDelay()) {} ButtonEventDelays::ButtonEventDelays(unsigned long debounce, unsigned long repeat, unsigned long lngclick, unsigned long lnglngclick) : debounce(debounce), repeat(repeat), lngclick(lngclick), lnglngclick(lnglngclick) {} button_t::button_t(ButtonActions&& actions_, ButtonEventDelays&& delays_) : actions(std::move(actions_)), event_delays(std::move(delays_)) {} button_t::button_t(BasePinPtr&& pin, const debounce_event::types::Config& config, ButtonActions&& actions_, ButtonEventDelays&& delays_) : event_emitter(std::make_unique(std::move(pin), config, delays_.debounce, delays_.repeat)), actions(std::move(actions_)), event_delays(std::move(delays_)) {} bool button_t::state() { return event_emitter->isPressed(); } ButtonEvent button_t::loop() { if (event_emitter) { switch (event_emitter->loop()) { case debounce_event::types::EventPressed: return ButtonEvent::Pressed; case debounce_event::types::EventReleased: { return _buttonMapReleased( event_emitter->getEventCount(), event_emitter->getEventLength(), event_delays.lngclick, event_delays.lnglngclick ); } case debounce_event::types::EventNone: break; } } return ButtonEvent::None; } std::vector _buttons; // ----------------------------------------------------------------------------- size_t buttonCount() { return _buttons.size(); } #if MQTT_SUPPORT std::bitset _buttons_mqtt_send_all( button::build::mqttSendAllEvents() ? std::numeric_limits::max() : std::numeric_limits::min() ); std::bitset _buttons_mqtt_retain( button::build::mqttRetain() ? std::numeric_limits::max() : std::numeric_limits::min() ); #endif // ----------------------------------------------------------------------------- #if RELAY_SUPPORT std::vector _button_relays; size_t _buttonRelay(size_t id) { return _button_relays[id]; } void _buttonRelayAction(size_t id, ButtonAction action) { auto relayId = _buttonRelay(id); switch (action) { case ButtonAction::Toggle: relayToggle(relayId); break; case ButtonAction::On: relayStatus(relayId, true); break; case ButtonAction::Off: relayStatus(relayId, false); break; case ButtonAction::Pulse: // TODO break; default: break; } } #endif // RELAY_SUPPORT // ----------------------------------------------------------------------------- #if WEB_SUPPORT namespace { void _buttonWebSocketOnVisible(JsonObject& root) { if (buttonCount()) { root["btnVisible"] = 1; } } void _buttonWebSocketOnConnected(JsonObject& root) { if (buttonCount()) { root["btnRepDel"] = getSetting("btnRepDel", button::build::repeatDelay()); } } bool _buttonWebSocketOnKeyCheck(const char * key, JsonVariant&) { return (strncmp(key, "btn", 3) == 0); } } // namespace #endif // WEB_SUPPORT //------------------------------------------------------------------------------ ButtonEventHandler _button_custom_action { nullptr }; void buttonSetCustomAction(ButtonEventHandler handler) { _button_custom_action = handler; } std::forward_list _button_notify_event; void buttonSetEventNotify(ButtonEventHandler handler) { _button_notify_event.push_front(handler); } //------------------------------------------------------------------------------ bool buttonState(size_t id) { return (id < _buttons.size()) ? _buttons[id].state() : false; } ButtonAction buttonAction(size_t id, ButtonEvent event) { return (id < _buttons.size()) ? _buttonDecodeEventAction(_buttons[id].actions, event) : ButtonAction::None; } // Note that we don't directly return F(...), but use a temporary to assign it conditionally // (ref. https://github.com/esp8266/Arduino/pull/6950 "PROGMEM footprint cleanup for responseCodeToString") // In this particular case, saves 76 bytes (120 vs 44) String _buttonEventString(ButtonEvent event) { const __FlashStringHelper* ptr = nullptr; switch (event) { case ButtonEvent::Pressed: ptr = F("pressed"); break; case ButtonEvent::Released: ptr = F("released"); break; case ButtonEvent::Click: ptr = F("click"); break; case ButtonEvent::DoubleClick: ptr = F("double-click"); break; case ButtonEvent::LongClick: ptr = F("long-click"); break; case ButtonEvent::LongLongClick: ptr = F("looong-click"); break; case ButtonEvent::TripleClick: ptr = F("triple-click"); break; case ButtonEvent::None: ptr = F("none"); break; } return String(ptr); } void buttonEvent(size_t id, ButtonEvent event) { DEBUG_MSG_P(PSTR("[BUTTON] Button #%u event %d (%s)\n"), id, _buttonEventNumber(event), _buttonEventString(event).c_str() ); if (event == ButtonEvent::None) { return; } auto& button = _buttons[id]; auto action = _buttonDecodeEventAction(button.actions, event); for (auto& notify : _button_notify_event) { notify(id, event); } #if MQTT_SUPPORT if ((action != ButtonAction::None) || _buttons_mqtt_send_all[id]) { mqttSend(MQTT_TOPIC_BUTTON, id, _buttonEventString(event).c_str(), false, _buttons_mqtt_retain[id]); } #endif switch (action) { #if RELAY_SUPPORT case ButtonAction::Toggle: case ButtonAction::On: case ButtonAction::Off: case ButtonAction::Pulse: _buttonRelayAction(id, action); break; #endif case ButtonAction::AccessPoint: wifiToggleAp(); break; case ButtonAction::Reset: deferredReset(100, CustomResetReason::Button); break; case ButtonAction::FactoryReset: factoryReset(); break; case ButtonAction::Wps: break; case ButtonAction::SmartConfig: break; case ButtonAction::BrightnessIncrease: #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE lightBrightnessStep(1); lightUpdate(); #endif break; case ButtonAction::BrightnessDecrease: #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE lightBrightnessStep(-1); lightUpdate(); #endif break; case ButtonAction::DisplayOn: #if THERMOSTAT_DISPLAY_SUPPORT displayOn(); #endif break; case ButtonAction::Custom: if (_button_custom_action) { _button_custom_action(id, event); } break; case ButtonAction::FanLow: #if FAN_SUPPORT fanSpeed(FanSpeed::Low); #endif break; case ButtonAction::FanMedium: #if FAN_SUPPORT fanSpeed(FanSpeed::Medium); #endif break; case ButtonAction::FanHigh: #if FAN_SUPPORT fanSpeed(FanSpeed::High); #endif break; case ButtonAction::None: break; } } void _buttonConfigure() { auto buttons = _buttons.size(); #if RELAY_SUPPORT _button_relays.clear(); #endif for (decltype(buttons) id = 0; id < buttons; ++id) { #if RELAY_SUPPORT _button_relays.push_back(getSetting({"btnRelay", id}, button::build::relay(id))); #endif #if MQTT_SUPPORT _buttons_mqtt_send_all[id] = getSetting({"btnMqttSendAll", id}, button::build::mqttSendAllEvents(id)); _buttons_mqtt_retain[id] = getSetting({"btnMqttRetain", id}, button::build::mqttRetain(id)); #endif } } // TODO: compatibility proxy, fetch global key before indexed unsigned long _buttonGetDelay(const char* key, size_t index, unsigned long default_value) { unsigned long result { default_value }; bool found { false }; auto indexed = SettingsKey(key, index); auto global = String(key); settings::kv_store.foreach([&](settings::kvs_type::KeyValueResult&& kv) { if (found) { return; } if ((kv.key.length != indexed.length()) && (kv.key.length != global.length())) { return; } auto other = kv.key.read(); found = indexed == other; if (found || (global == other)) { result = settings::internal::convert(kv.value.read()); } }); return result; } void buttonLoop() { for (size_t id = 0; id < _buttons.size(); ++id) { auto event = _buttons[id].loop(); if (event != ButtonEvent::None) { buttonEvent(id, event); } } } // Resistor ladder buttons. Inspired by: // - https://gitter.im/tinkerman-cat/espurna?at=5f5d44c8df4af236f902e25d // - https://github.com/bxparks/AceButton/tree/develop/docs/resistor_ladder (especially thx @bxparks for the great documentation!) // - https://github.com/bxparks/AceButton/blob/develop/src/ace_button/LadderButtonConfig.cpp // - https://github.com/dxinteractive/AnalogMultiButton #if BUTTON_PROVIDER_ANALOG_SUPPORT class AnalogPin final : public BasePin { public: static constexpr int RangeFrom { 0 }; static constexpr int RangeTo { 1023 }; AnalogPin() = delete; explicit AnalogPin(unsigned char pin, int expected) : _pin(pin), _expected(expected) { pins.reserve(ButtonsPresetMax); pins.push_back(this); adjustPinRanges(); } ~AnalogPin() { pins.erase(std::remove(pins.begin(), pins.end(), this), pins.end()); adjustPinRanges(); } String description() const override { char buffer[64]; snprintf_P(buffer, sizeof(buffer), PSTR("%s @ level %d (%d...%d)\n"), id(), _expected, _from, _to); return buffer; } // Notice that 'static' method vars are shared between instances // This way we will throttle every invocation (which should be safe to do, since we only read things through the button loop) int analogRead() { static unsigned long ts { ESP.getCycleCount() }; static int last { ::analogRead(_pin) }; // Cannot hammer analogRead() all the time: // https://github.com/esp8266/Arduino/issues/1634 if (ESP.getCycleCount() - ts >= _read_interval) { ts = ESP.getCycleCount(); last = ::analogRead(_pin); } return last; } // XXX: make static ctor and call this implicitly? static bool checkExpectedLevel(int expected) { if (expected > RangeTo) { return false; } for (auto pin : pins) { if (expected == pin->_expected) { return false; } } return true; } unsigned char pin() const override { return _pin; } const char* id() const override { return "AnalogPin"; } // Simulate LOW level when the range matches and HIGH when it does not int digitalRead() override { const auto reading = analogRead(); return !((_from < reading) && (reading < _to)); } void pinMode(int8_t) override { } void digitalWrite(int8_t val) override { } private: // ref. https://github.com/bxparks/AceButton/tree/develop/docs/resistor_ladder#level-matching-tolerance-range // fuzzy matching instead of directly comparing with the `_expected` level and / or specifying tolerance manually // for example, for pins with expected values 0, 327, 512 and 844 we match analogRead() when: // - 0..163 for 0 // - 163..419 for 327 // - 419..678 for 512 // - 678..933 for 844 // - 933..1024 is ignored static std::vector pins; unsigned long _read_interval { microsecondsToClockCycles(200u) }; unsigned char _pin { A0 }; int _expected { 0u }; int _from { RangeFrom }; int _to { RangeTo }; static void adjustPinRanges() { std::sort(pins.begin(), pins.end(), [](const AnalogPin* lhs, const AnalogPin* rhs) -> bool { return lhs->_expected < rhs->_expected; }); AnalogPin* last { nullptr }; for (unsigned index = 0; index < pins.size(); ++index) { int edge = (index + 1 != pins.size()) ? pins[index + 1]->_expected : RangeTo; pins[index]->_from = last ? last->_to : RangeFrom; pins[index]->_to = (pins[index]->_expected + edge) / 2; last = pins[index]; } } }; std::vector AnalogPin::pins; #endif // BUTTON_PROVIDER_ANALOG_SUPPORT BasePinPtr _buttonGpioPin(size_t index, ButtonProvider provider) { BasePinPtr result; auto pin = getSetting({"btnGpio", index}, button::build::pin(index)); switch (provider) { case ButtonProvider::Gpio: { #if BUTTON_PROVIDER_GPIO_SUPPORT auto* base = gpioBase(getSetting({"btnGpioType", index}, button::build::pinType(index))); if (!base) { break; } if (!gpioLock(*base, pin)) { break; } result = std::move(base->pin(pin)); #endif break; } case ButtonProvider::Analog: { #if BUTTON_PROVIDER_ANALOG_SUPPORT if (A0 != pin) { break; } auto level = getSetting({"btnLevel", index}, button::build::analogLevel(index)); if (!AnalogPin::checkExpectedLevel(level)) { break; } result.reset(new AnalogPin(pin, level)); #endif break; } default: break; } return result; } ButtonActions _buttonActions(size_t index) { return { getSetting({"btnPress", index}, button::build::press(index)), getSetting({"btnRlse", index}, button::build::release(index)), getSetting({"btnClick", index}, button::build::click(index)), getSetting({"btnDclk", index}, button::build::doubleClick(index)), getSetting({"btnLclk", index}, button::build::longClick(index)), getSetting({"btnLLclk", index}, button::build::longLongClick(index)), getSetting({"btnTclk", index}, button::build::tripleClick(index)) }; } // Note that we use settings without indexes as default values to preserve backwards compatibility ButtonEventDelays _buttonDelays(size_t index) { return { _buttonGetDelay("btnDebDel", index, button::build::debounceDelay(index)), _buttonGetDelay("btnRepDel", index, button::build::repeatDelay(index)), _buttonGetDelay("btnLclkDel", index, button::build::longClickDelay(index)), _buttonGetDelay("btnLLclkDel", index, button::build::longLongClickDelay(index)), }; } bool _buttonSetupProvider(size_t index, ButtonProvider provider) { bool result { false }; switch (provider) { case ButtonProvider::Analog: case ButtonProvider::Gpio: { #if BUTTON_PROVIDER_GPIO_SUPPORT || BUTTON_PROVIDER_ANALOG_SUPPORT auto pin = _buttonGpioPin(index, provider); if (!pin) { break; } _buttons.emplace_back( std::move(pin), _buttonRuntimeConfig(index), _buttonActions(index), _buttonDelays(index)); result = true; #endif break; } case ButtonProvider::None: break; } return result; } void _buttonSettingsMigrate(int version) { if (!version || (version >= 5)) { return; } delSettingPrefix("btnGPIO"); moveSetting("btnDelay", "btnRepDel"); } void buttonSetup() { _buttonSettingsMigrate(migrateVersion()); for (size_t index = 0; index < ButtonsMax; ++index) { auto provider = getSetting({"btnProv", index}, button::build::provider(index)); if (!_buttonSetupProvider(index, provider)) { break; } } auto count = _buttons.size(); DEBUG_MSG_P(PSTR("[BUTTON] Number of buttons: %u\n"), count); #if TERMINAL_SUPPORT terminalRegisterCommand(F("BUTTON"), [](const terminal::CommandContext& ctx) { unsigned index { 0u }; for (auto& button : _buttons) { ctx.output.printf("%u - ", index++); if (button.event_emitter) { auto& pin = button.event_emitter->pin(); ctx.output.println(pin->description()); } else { ctx.output.println(F("Virtual")); } } terminalOK(ctx); }); #endif if (count) { #if WEB_SUPPORT wsRegister() .onVisible(_buttonWebSocketOnVisible) .onConnected(_buttonWebSocketOnConnected) .onKeyCheck(_buttonWebSocketOnKeyCheck); #endif _buttonConfigure(); espurnaRegisterReload(_buttonConfigure); espurnaRegisterLoop(buttonLoop); } } #endif // BUTTON_SUPPORT