/*
|
|
|
|
BUTTON MODULE
|
|
|
|
Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
|
|
Copyright (C) 2019-2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
|
|
|
|
*/
|
|
|
|
#include "button.h"
|
|
|
|
#if BUTTON_SUPPORT
|
|
|
|
#include <bitset>
|
|
#include <memory>
|
|
#include <vector>
|
|
|
|
#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<ButtonProvider>(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<ButtonAction>(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<int>(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<debounce_event::EventEmitter>(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<button_t> _buttons;
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
size_t buttonCount() {
|
|
return _buttons.size();
|
|
}
|
|
|
|
#if MQTT_SUPPORT
|
|
|
|
std::bitset<ButtonsMax> _buttons_mqtt_send_all(
|
|
button::build::mqttSendAllEvents()
|
|
? std::numeric_limits<unsigned long>::max()
|
|
: std::numeric_limits<unsigned long>::min()
|
|
);
|
|
std::bitset<ButtonsMax> _buttons_mqtt_retain(
|
|
button::build::mqttRetain()
|
|
? std::numeric_limits<unsigned long>::max()
|
|
: std::numeric_limits<unsigned long>::min()
|
|
);
|
|
|
|
#endif
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
#if RELAY_SUPPORT
|
|
|
|
std::vector<unsigned char> _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<ButtonEventHandler> _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<unsigned long>(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<AnalogPin*> 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*> 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
|