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
This commit is contained in:
Max Prokhorov
2021-01-14 10:39:18 +03:00
committed by GitHub
parent c1243fda31
commit 8ceeebdb24
74 changed files with 27667 additions and 26262 deletions

View File

@@ -123,6 +123,19 @@ String serialize(const debounce_event::types::PinMode& mode) {
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;
}
} // namespace settings::internal
} // namespace settings
@@ -209,18 +222,15 @@ button_event_delays_t::button_event_delays_t(unsigned long debounce, unsigned lo
lnglngclick(lnglngclick)
{}
button_t::button_t(unsigned char relayID, const button_actions_t& actions, const button_event_delays_t& delays) :
event_emitter(nullptr),
event_delays(delays),
actions(actions),
relayID(relayID)
button_t::button_t(button_actions_t&& actions_, button_event_delays_t&& delays_) :
actions(std::move(actions_)),
event_delays(std::move(delays_))
{}
button_t::button_t(std::shared_ptr<BasePin> pin, const debounce_event::types::Config& config, unsigned char relayID, const button_actions_t& actions, const button_event_delays_t& delays) :
event_emitter(std::make_unique<debounce_event::EventEmitter>(pin, config, delays.debounce, delays.repeat)),
event_delays(delays),
actions(actions),
relayID(relayID)
button_t::button_t(BasePinPtr&& pin, const debounce_event::types::Config& config, button_actions_t&& actions_, button_event_delays_t&& 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() {
@@ -327,7 +337,7 @@ void _buttonWebSocketOnConnected(JsonObject& root) {
// TODO: configure PIN object instead of button specifically, link PIN<->BUTTON
button.add(getSetting({"btnProv", index}, _buttonProvider(index)));
if (_buttons[i].getPin()) {
if (_buttons[i].pin()) {
button.add(getSetting({"btnGPIO", index}, _buttonPin(index)));
const auto config = _buttonRuntimeConfig(index);
button.add(static_cast<int>(config.mode));
@@ -417,6 +427,41 @@ String _buttonEventString(button_event_t event) {
return String(ptr);
}
#if RELAY_SUPPORT
unsigned char _buttonRelaySetting(unsigned char id) {
static std::vector<uint8_t> relays;
if (!relays.size()) {
relays.reserve(_buttons.size());
for (unsigned char button = 0; button < _buttons.size(); ++button) {
relays.push_back(getSetting({"btnRelay", button}, _buttonRelay(button)));
}
}
return relays[id];
}
void _buttonRelayAction(unsigned char id, button_action_t action) {
auto relayId = _buttonRelaySetting(id);
switch (action) {
case BUTTON_ACTION_TOGGLE:
relayToggle(relayId);
break;
case BUTTON_ACTION_ON:
relayStatus(relayId, true);
break;
case BUTTON_ACTION_OFF:
relayStatus(relayId, false);
break;
}
}
#endif // RELAY_SUPPORT
void buttonEvent(unsigned char id, button_event_t event) {
DEBUG_MSG_P(PSTR("[BUTTON] Button #%u event %d (%s)\n"),
@@ -441,17 +486,11 @@ void buttonEvent(unsigned char id, button_event_t event) {
#if RELAY_SUPPORT
case BUTTON_ACTION_TOGGLE:
relayToggle(button.relayID);
break;
case BUTTON_ACTION_ON:
relayStatus(button.relayID, true);
break;
case BUTTON_ACTION_OFF:
relayStatus(button.relayID, false);
_buttonRelayAction(id, action);
break;
#endif // RELAY_SUPPORT == 1
#endif
case BUTTON_ACTION_AP:
if (wifiState() & WIFI_STATE_AP) {
@@ -486,12 +525,12 @@ void buttonEvent(unsigned char id, button_event_t event) {
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
case BUTTON_ACTION_DIM_UP:
lightBrightnessStep(1);
lightUpdate(true, true);
lightUpdate();
break;
case BUTTON_ACTION_DIM_DOWN:
lightBrightnessStep(-1);
lightUpdate(true, true);
lightUpdate();
break;
#endif // LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
@@ -520,69 +559,7 @@ unsigned long _buttonGetSetting(const char* key, unsigned char index, T default_
return getSetting({key, index}, getSetting(key, default_value));
}
// Sonoff Dual does not do real GPIO readings and we
// depend on the external MCU to send us relay / button events
// Lightfox uses the same protocol as Dual, but has slightly different actions
// TODO: move this to a separate 'hardware' setup file?
void _buttonLoopSonoffDual() {
if (Serial.available() < 4) {
return;
}
unsigned char bytes[4] = {0};
Serial.readBytes(bytes, 4);
if ((bytes[0] != 0xA0) && (bytes[1] != 0x04) && (bytes[3] != 0xA1)) {
return;
}
const unsigned char value [[gnu::unused]] = bytes[2];
#if BUTTON_PROVIDER_ITEAD_SONOFF_DUAL_SUPPORT
// RELAYs and BUTTONs are synchonized in the SIL F330
// The on-board BUTTON2 should toggle RELAY0 value
// Since we are not passing back RELAY2 value
// (in the relayStatus method) it will only be present
// here if it has actually been pressed
if ((value & 4) == 4) {
buttonEvent(2, button_event_t::Click);
return;
}
// Otherwise check if any of the other two BUTTONs
// (in the header) has been pressed, but we should
// ensure that we only toggle one of them to avoid
// the synchronization going mad
// This loop is generic for any PSB-04 module
for (unsigned int i=0; i<relayCount(); i++) {
const bool status = (value & (1 << i)) > 0;
// Check if the status for that relay has changed
if (relayStatus(i) != status) {
buttonEvent(i, button_event_t::Click);
break;
}
}
#elif BUTTON_PROVIDER_FOXEL_LIGHTFOX_DUAL_SUPPORT
DEBUG_MSG_P(PSTR("[BUTTON] [LIGHTFOX] Received buttons mask: %u\n"), value);
for (unsigned int i=0; i<_buttons.size(); i++) {
if ((value & (1 << i)) > 0) {
buttonEvent(i, button_event_t::Click);
}
}
#endif // BUTTON_PROVIDER_ITEAD_SONOFF_DUAL
}
void _buttonLoopGeneric() {
void buttonLoop() {
for (size_t id = 0; id < _buttons.size(); ++id) {
auto event = _buttons[id].loop();
if (event != button_event_t::None) {
@@ -591,17 +568,6 @@ void _buttonLoopGeneric() {
}
}
void buttonLoop() {
_buttonLoopGeneric();
// Unconditionally call these. By default, generic loop will discard everything without the configured events emmiter
#if BUTTON_PROVIDER_ITEAD_SONOFF_DUAL_SUPPORT || BUTTON_PROVIDER_FOXEL_LIGHTFOX_DUAL
_buttonLoopSonoffDual();
#endif
}
// 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!)
@@ -611,18 +577,14 @@ void buttonLoop() {
#if BUTTON_PROVIDER_ANALOG_SUPPORT
class AnalogPin final : public BasePin {
public:
public:
static constexpr int RangeFrom { 0 };
static constexpr int RangeTo { 1023 };
AnalogPin() = delete;
AnalogPin(unsigned char) = delete;
AnalogPin(unsigned char pin_, int expected_) :
BasePin(pin_),
_expected(expected_)
explicit AnalogPin(unsigned char pin, int expected) :
_pin(pin),
_expected(expected)
{
pins.reserve(ButtonsPresetMax);
pins.push_back(this);
@@ -634,17 +596,26 @@ class AnalogPin final : public BasePin {
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) };
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);
last = ::analogRead(_pin);
}
return last;
@@ -665,14 +636,12 @@ class AnalogPin final : public BasePin {
return true;
}
String description() const override {
char buffer[64] {0};
snprintf_P(buffer, sizeof(buffer),
PSTR("AnalogPin @ GPIO%u, expected %d (%d, %d)"),
pin, _expected, _from, _to
);
unsigned char pin() const override {
return _pin;
}
return String(buffer);
const char* id() const override {
return "AnalogPin";
}
// Simulate LOW level when the range matches and HIGH when it does not
@@ -687,8 +656,7 @@ class AnalogPin final : public BasePin {
void digitalWrite(int8_t val) override {
}
private:
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:
@@ -701,6 +669,8 @@ class AnalogPin final : public BasePin {
unsigned long _read_interval { microsecondsToClockCycles(200u) };
unsigned char _pin { A0 };
int _expected { 0u };
int _from { RangeFrom };
int _to { RangeTo };
@@ -731,174 +701,154 @@ std::vector<AnalogPin*> AnalogPin::pins;
#endif // BUTTON_PROVIDER_ANALOG_SUPPORT
std::shared_ptr<BasePin> _buttonFromProvider([[gnu::unused]] unsigned char index, int provider, unsigned char pin) {
BasePinPtr _buttonGpioPin(unsigned char index, ButtonProvider provider) {
BasePinPtr result;
auto pin = getSetting({"btnGPIO", index}, _buttonPin(index));
switch (provider) {
case BUTTON_PROVIDER_GENERIC:
if (!gpioValid(pin)) {
case ButtonProvider::Gpio: {
#if BUTTON_PROVIDER_GPIO_SUPPORT
auto* base = gpioBase(getSetting({"btnGPIOType", index}, _buttonPinType(index)));
if (!base) {
break;
}
return std::shared_ptr<BasePin>(new GpioPin(pin));
#if BUTTON_PROVIDER_MCP23S08_SUPPORT
case BUTTON_PROVIDER_MCP23S08:
if (!mcpGpioValid(pin)) {
if (!gpioLock(*base, pin)) {
break;
}
return std::shared_ptr<BasePin>(new McpGpioPin(pin));
result = std::move(base->pin(pin));
#endif
break;
}
case ButtonProvider::Analog: {
#if BUTTON_PROVIDER_ANALOG_SUPPORT
case BUTTON_PROVIDER_ANALOG: {
if (A0 != pin) {
break;
}
const auto level = getSetting({"btnLevel", index}, _buttonAnalogLevel(index));
auto level = getSetting({"btnLevel", index}, _buttonAnalogLevel(index));
if (!AnalogPin::checkExpectedLevel(level)) {
break;
}
return std::shared_ptr<BasePin>(new AnalogPin(pin, level));
}
result.reset(new AnalogPin(pin, level));
#endif
break;
}
default:
break;
}
return {};
return result;
}
inline button_actions_t _buttonActions(unsigned char index) {
button_actions_t actions {
getSetting({"btnPress", index}, _buttonPress(index)),
getSetting({"btnRlse", index}, _buttonRelease(index)),
getSetting({"btnClick", index}, _buttonClick(index)),
getSetting({"btnDclk", index}, _buttonDoubleClick(index)),
getSetting({"btnLclk", index}, _buttonLongClick(index)),
getSetting({"btnLLclk", index}, _buttonLongLongClick(index)),
getSetting({"btnTclk", index}, _buttonTripleClick(index))
};
return actions;
}
// Note that we use settings without indexes as default values
button_event_delays_t _buttonDelays(unsigned char index) {
button_event_delays_t delays {
_buttonGetSetting("btnDebDel", index, _buttonDebounceDelay(index)),
_buttonGetSetting("btnRepDel", index, _buttonRepeatDelay(index)),
_buttonGetSetting("btnLclkDel", index, _buttonLongClickDelay(index)),
_buttonGetSetting("btnLLclkDel", index, _buttonLongLongClickDelay(index)),
};
return delays;
}
bool _buttonSetupProvider(unsigned char 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 buttonSetup() {
// Backwards compatibility
moveSetting("btnDelay", "btnRepDel");
// Special hardware cases
#if BUTTON_PROVIDER_ITEAD_SONOFF_DUAL_SUPPORT || BUTTON_PROVIDER_FOXEL_LIGHTFOX_DUAL
{
size_t buttons = 0;
#if BUTTON1_RELAY != RELAY_NONE
++buttons;
#endif
#if BUTTON2_RELAY != RELAY_NONE
++buttons;
#endif
#if BUTTON3_RELAY != RELAY_NONE
++buttons;
#endif
#if BUTTON4_RELAY != RELAY_NONE
++buttons;
#endif
_buttons.reserve(buttons);
// Ignore real button delays since we don't use them here
const auto delays = button_event_delays_t();
for (unsigned char index = 0; index < buttons; ++index) {
const button_actions_t actions {
BUTTON_ACTION_NONE,
BUTTON_ACTION_NONE,
// The only generated event is ::Click
getSetting({"btnClick", index}, _buttonClick(index)),
BUTTON_ACTION_NONE,
BUTTON_ACTION_NONE,
BUTTON_ACTION_NONE,
BUTTON_ACTION_NONE
};
_buttons.emplace_back(
getSetting({"btnRelay", index}, _buttonRelay(index)),
actions,
delays
);
for (unsigned char index = 0; index < ButtonsMax; ++index) {
auto provider = getSetting({"btnProv", index}, _buttonProvider(index));
if (!_buttonSetupProvider(index, provider)) {
break;
}
}
#endif // BUTTON_PROVIDER_ITEAD_SONOFF_DUAL_SUPPORT || BUTTON_PROVIDER_FOXEL_LIGHTFOX_DUAL
#if BUTTON_PROVIDER_GENERIC_SUPPORT
// Generic GPIO input handlers
{
_buttons.reserve(_buttonPreconfiguredPins());
for (unsigned char index = _buttons.size(); index < ButtonsMax; ++index) {
const auto provider = getSetting({"btnProv", index}, _buttonProvider(index));
const auto pin = getSetting({"btnGPIO", index}, _buttonPin(index));
auto managed_pin = _buttonFromProvider(index, provider, pin);
if (!managed_pin) {
break;
}
const auto relayID = getSetting({"btnRelay", index}, _buttonRelay(index));
// TODO: compatibility proxy, fetch global key before indexed
const button_event_delays_t delays {
_buttonGetSetting("btnDebDel", index, _buttonDebounceDelay(index)),
_buttonGetSetting("btnRepDel", index, _buttonRepeatDelay(index)),
_buttonGetSetting("btnLclkDel", index, _buttonLongClickDelay(index)),
_buttonGetSetting("btnLLclkDel", index, _buttonLongLongClickDelay(index)),
};
const button_actions_t actions {
getSetting({"btnPress", index}, _buttonPress(index)),
getSetting({"btnRlse", index}, _buttonRelease(index)),
getSetting({"btnClick", index}, _buttonClick(index)),
getSetting({"btnDclk", index}, _buttonDoubleClick(index)),
getSetting({"btnLclk", index}, _buttonLongClick(index)),
getSetting({"btnLLclk", index}, _buttonLongLongClick(index)),
getSetting({"btnTclk", index}, _buttonTripleClick(index))
};
const auto config = _buttonRuntimeConfig(index);
_buttons.emplace_back(
managed_pin, config,
relayID, actions, delays
);
}
auto count = _buttons.size();
DEBUG_MSG_P(PSTR("[BUTTON] Number of buttons: %u\n"), count);
if (!count) {
return;
}
#endif
#if TERMINAL_SUPPORT
if (_buttons.size()) {
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->getPin();
ctx.output.println(pin->description());
} else {
ctx.output.println(F("Virtual"));
}
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);
});
}
terminalOK(ctx);
});
#endif
_buttonConfigure();
DEBUG_MSG_P(PSTR("[BUTTON] Number of buttons: %u\n"), _buttons.size());
// Websocket Callbacks
#if WEB_SUPPORT
wsRegister()
.onConnected(_buttonWebSocketOnVisible)
.onVisible(_buttonWebSocketOnVisible)
.onConnected(_buttonWebSocketOnConnected)
.onKeyCheck(_buttonWebSocketOnKeyCheck);
#endif
// Register system callbacks
espurnaRegisterLoop(buttonLoop);
espurnaRegisterReload(_buttonConfigure);
}
#endif // BUTTON_SUPPORT