Browse Source

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
mcspr-patch-1
Max Prokhorov 3 years ago
committed by GitHub
parent
commit
8ceeebdb24
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 27625 additions and 26220 deletions
  1. +8
    -19
      code/espurna/DebounceEvent.cpp
  2. +5
    -7
      code/espurna/alexa.cpp
  3. +177
    -227
      code/espurna/button.cpp
  4. +15
    -13
      code/espurna/button.h
  5. +15
    -15
      code/espurna/button_config.h
  6. +9
    -0
      code/espurna/compat.h
  7. +106
    -20
      code/espurna/config/defaults.h
  8. +6
    -17
      code/espurna/config/dependencies.h
  9. +44
    -27
      code/espurna/config/general.h
  10. +51
    -123
      code/espurna/config/hardware.h
  11. +25
    -21
      code/espurna/config/types.h
  12. BIN
      code/espurna/data/index.all.html.gz
  13. BIN
      code/espurna/data/index.curtain.html.gz
  14. BIN
      code/espurna/data/index.garland.html.gz
  15. BIN
      code/espurna/data/index.light.html.gz
  16. BIN
      code/espurna/data/index.lightfox.html.gz
  17. BIN
      code/espurna/data/index.rfbridge.html.gz
  18. BIN
      code/espurna/data/index.rfm69.html.gz
  19. BIN
      code/espurna/data/index.sensor.html.gz
  20. BIN
      code/espurna/data/index.small.html.gz
  21. BIN
      code/espurna/data/index.thermostat.html.gz
  22. +23
    -10
      code/espurna/debug.cpp
  23. +3
    -3
      code/espurna/domoticz.cpp
  24. +1
    -1
      code/espurna/encoder.cpp
  25. +126
    -42
      code/espurna/gpio.cpp
  26. +73
    -4
      code/espurna/gpio.h
  27. +25
    -11
      code/espurna/gpio_pin.h
  28. +2
    -2
      code/espurna/ir.cpp
  29. +9
    -10
      code/espurna/led.cpp
  30. +11
    -11
      code/espurna/libs/BasePin.h
  31. +5
    -7
      code/espurna/libs/DebounceEvent.h
  32. +534
    -193
      code/espurna/light.cpp
  33. +76
    -18
      code/espurna/light.h
  34. +1
    -1
      code/espurna/main.cpp
  35. +46
    -1
      code/espurna/mcp23s08.cpp
  36. +1
    -0
      code/espurna/mcp23s08.h
  37. +16
    -10
      code/espurna/mcp23s08_pin.h
  38. +693
    -439
      code/espurna/relay.cpp
  39. +46
    -3
      code/espurna/relay.h
  40. +43
    -5
      code/espurna/relay_config.h
  41. +10
    -6
      code/espurna/rfbridge.cpp
  42. +3
    -3
      code/espurna/rpnrules.cpp
  43. +2
    -2
      code/espurna/scheduler.cpp
  44. +4
    -2
      code/espurna/sensor.cpp
  45. +8
    -4
      code/espurna/sensors/DHTSensor.h
  46. +7
    -4
      code/espurna/sensors/DallasSensor.h
  47. +6
    -3
      code/espurna/sensors/GUVAS12SDSensor.h
  48. +13
    -6
      code/espurna/settings.h
  49. +3122
    -3106
      code/espurna/static/index.all.html.gz.h
  50. +1931
    -1924
      code/espurna/static/index.curtain.html.gz.h
  51. +2423
    -2416
      code/espurna/static/index.garland.html.gz.h
  52. +2870
    -2853
      code/espurna/static/index.light.html.gz.h
  53. +1908
    -1901
      code/espurna/static/index.lightfox.html.gz.h
  54. +1933
    -1926
      code/espurna/static/index.rfbridge.html.gz.h
  55. +3838
    -3830
      code/espurna/static/index.rfm69.html.gz.h
  56. +1978
    -1971
      code/espurna/static/index.sensor.html.gz.h
  57. +2384
    -2377
      code/espurna/static/index.small.html.gz.h
  58. +1919
    -1912
      code/espurna/static/index.thermostat.html.gz.h
  59. +0
    -8
      code/espurna/system.cpp
  60. +4
    -3
      code/espurna/terminal.cpp
  61. +360
    -279
      code/espurna/tuya.cpp
  62. +15
    -8
      code/espurna/tuya.h
  63. +167
    -114
      code/espurna/tuya_dataframe.h
  64. +43
    -32
      code/espurna/tuya_protocol.h
  65. +21
    -33
      code/espurna/tuya_transport.h
  66. +6
    -5
      code/espurna/tuya_types.h
  67. +170
    -89
      code/espurna/tuya_util.h
  68. +89
    -60
      code/html/custom.js
  69. +44
    -7
      code/html/index.html
  70. +0
    -2
      code/test/build/light_dimmer.h
  71. +0
    -2
      code/test/build/light_my92xx.h
  72. +2
    -4
      code/test/build/light_tuya.h
  73. +2
    -2
      code/test/build/nondefault.h
  74. +148
    -66
      code/test/unit/tuya/tuya.cpp

+ 8
- 19
code/espurna/DebounceEvent.cpp View File

@ -30,38 +30,27 @@
#include <functional>
#include <memory>
#include "compat.h"
#include "libs/DebounceEvent.h"
namespace debounce_event {
EventEmitter::EventEmitter(types::Pin pin, types::EventHandler callback, const types::Config& config, unsigned long debounce_delay, unsigned long repeat) :
_pin(pin),
EventEmitter::EventEmitter(BasePinPtr&& pin, types::EventHandler callback, const types::Config& config, unsigned long debounce_delay, unsigned long repeat) :
_pin(std::move(pin)),
_callback(callback),
_config(config),
_is_switch(config.mode == types::Mode::Switch),
_delay(debounce_delay),
_repeat(repeat)
{
if (!pin) return;
if (!_pin) return;
switch (_config.pin_mode) {
case types::PinMode::InputPullup:
_pin->pinMode(INPUT_PULLUP);
break;
case types::PinMode::InputPulldown:
// ESP8266 does not have INPUT_PULLDOWN definition, and instead
// has a GPIO16-specific INPUT_PULLDOWN_16:
// - https://github.com/esp8266/Arduino/issues/478
// - https://github.com/esp8266/Arduino/commit/1b3581d55ebf0f8c91e081f9af4cf7433d492ec9
#ifdef ESP8266
if (_pin->pin == 16) {
_pin->pinMode(INPUT_PULLDOWN_16);
} else {
_pin->pinMode(INPUT);
}
#else
_pin->pinMode(INPUT_PULLDOWN);
#endif
break;
case types::PinMode::Input:
_pin->pinMode(INPUT);
@ -83,19 +72,19 @@ EventEmitter::EventEmitter(types::Pin pin, types::EventHandler callback, const t
_value = _default_value;
}
EventEmitter::EventEmitter(types::Pin pin, const types::Config& config, unsigned long delay, unsigned long repeat) :
EventEmitter(pin, nullptr, config, delay, repeat)
EventEmitter::EventEmitter(BasePinPtr&& pin, const types::Config& config, unsigned long delay, unsigned long repeat) :
EventEmitter(std::move(pin), nullptr, config, delay, repeat)
{}
bool EventEmitter::isPressed() {
return (_value != _default_value);
}
const types::Pin EventEmitter::getPin() const {
const BasePinPtr& EventEmitter::pin() const {
return _pin;
}
const types::Config EventEmitter::getConfig() const {
const types::Config& EventEmitter::config() const {
return _config;
}


+ 5
- 7
code/espurna/alexa.cpp View File

@ -74,9 +74,7 @@ void _alexaBrokerCallback(const String& topic, unsigned char id, unsigned int va
}
if (topic.equals(MQTT_TOPIC_RELAY)) {
#if RELAY_PROVIDER == RELAY_PROVIDER_LIGHT
if (id > 0) return;
#endif
if (id > 0) return;
_alexa.setState(id, value, value > 0 ? 255 : 0);
}
@ -97,13 +95,13 @@ void alexaLoop() {
alexa_queue_element_t element = _alexa_queue.front();
DEBUG_MSG_P(PSTR("[ALEXA] Device #%u state: %s value: %d\n"), element.device_id, element.state ? "ON" : "OFF", element.value);
#if RELAY_PROVIDER == RELAY_PROVIDER_LIGHT
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
if (0 == element.device_id) {
relayStatus(0, element.state);
lightState(element.state);
} else {
lightState(element.device_id - 1, element.state);
lightChannel(element.device_id - 1, element.value);
lightUpdate(true, true);
lightUpdate();
}
#else
relayStatus(element.device_id, element.state);
@ -130,7 +128,7 @@ void alexaSetup() {
}
// Lights
#if RELAY_PROVIDER == RELAY_PROVIDER_LIGHT
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
// Global switch
_alexa.addDevice(hostname.c_str());


+ 177
- 227
code/espurna/button.cpp 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) {
switch (provider) {
BasePinPtr _buttonGpioPin(unsigned char index, ButtonProvider provider) {
BasePinPtr result;
case BUTTON_PROVIDER_GENERIC:
if (!gpioValid(pin)) {
auto pin = getSetting({"btnGPIO", index}, _buttonPin(index));
switch (provider) {
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;
}
void buttonSetup() {
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))
};
// Backwards compatibility
moveSetting("btnDelay", "btnRepDel");
return actions;
}
// 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
);
}
}
#endif // BUTTON_PROVIDER_ITEAD_SONOFF_DUAL_SUPPORT || BUTTON_PROVIDER_FOXEL_LIGHTFOX_DUAL
// 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)),
};
#if BUTTON_PROVIDER_GENERIC_SUPPORT
return delays;
}
// Generic GPIO input handlers
{
_buttons.reserve(_buttonPreconfiguredPins());
bool _buttonSetupProvider(unsigned char index, ButtonProvider provider) {
bool result { false };
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));
switch (provider) {
auto managed_pin = _buttonFromProvider(index, provider, pin);
if (!managed_pin) {
break;
}
case ButtonProvider::Analog:
case ButtonProvider::Gpio: {
#if BUTTON_PROVIDER_GPIO_SUPPORT || BUTTON_PROVIDER_ANALOG_SUPPORT
auto pin = _buttonGpioPin(index, provider);
if (!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))
};
_buttons.emplace_back(
std::move(pin),
_buttonRuntimeConfig(index),
_buttonActions(index),
_buttonDelays(index));
result = true;
#endif
break;
}
const auto config = _buttonRuntimeConfig(index);
case ButtonProvider::None:
break;
}
_buttons.emplace_back(
managed_pin, config,
relayID, actions, delays
);
}
return result;
}
void buttonSetup() {
// Backwards compatibility
moveSetting("btnDelay", "btnRepDel");
for (unsigned char index = 0; index < ButtonsMax; ++index) {
auto provider = getSetting({"btnProv", index}, _buttonProvider(index));
if (!_buttonSetupProvider(index, provider)) {
break;
}
}
#endif
auto count = _buttons.size();
DEBUG_MSG_P(PSTR("[BUTTON] Number of buttons: %u\n"), count);
if (!count) {
return;
}
#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

+ 15
- 13
code/espurna/button.h View File

@ -22,6 +22,12 @@ constexpr size_t ButtonsMax = 32;
using button_action_t = uint8_t;
enum class ButtonProvider : int {
None,
Gpio,
Analog
};
enum class button_event_t {
None,
Pressed,
@ -47,28 +53,24 @@ struct button_event_delays_t {
button_event_delays_t();
button_event_delays_t(unsigned long debounce, unsigned long repeat, unsigned long lngclick, unsigned long lnglngclick);
const unsigned long debounce;
const unsigned long repeat;
const unsigned long lngclick;
const unsigned long lnglngclick;
unsigned long debounce;
unsigned long repeat;
unsigned long lngclick;
unsigned long lnglngclick;
};
struct button_t {
button_t(unsigned char relayID, const button_actions_t& actions, const button_event_delays_t& delays);
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);
button_t(button_actions_t&& actions, button_event_delays_t&& delays);
button_t(BasePinPtr&& pin, const debounce_event::types::Config& config,
button_actions_t&& actions, button_event_delays_t&& delays);
bool state();
button_event_t loop();
std::unique_ptr<debounce_event::EventEmitter> event_emitter;
const button_event_delays_t event_delays;
const button_actions_t actions;
const unsigned char relayID;
button_actions_t actions;
button_event_delays_t event_delays;
};
BrokerDeclare(ButtonBroker, void(unsigned char id, button_event_t event));


+ 15
- 15
code/espurna/button_config.h View File

@ -33,6 +33,19 @@ constexpr unsigned char _buttonPin(unsigned char index) {
);
}
constexpr GpioType _buttonPinType(unsigned char index) {
return (
(index == 0) ? BUTTON1_PIN_TYPE :
(index == 1) ? BUTTON2_PIN_TYPE :
(index == 2) ? BUTTON3_PIN_TYPE :
(index == 3) ? BUTTON4_PIN_TYPE :
(index == 4) ? BUTTON5_PIN_TYPE :
(index == 5) ? BUTTON6_PIN_TYPE :
(index == 6) ? BUTTON7_PIN_TYPE :
(index == 7) ? BUTTON8_PIN_TYPE : GPIO_TYPE_NONE
);
}
constexpr int _buttonConfigBitmask(unsigned char index) {
return (
(index == 0) ? (BUTTON1_CONFIG) :
@ -244,7 +257,7 @@ constexpr bool _buttonMqttRetain(unsigned char index) {
);
}
constexpr int _buttonProvider(unsigned char index) {
constexpr ButtonProvider _buttonProvider(unsigned char index) {
return (
(index == 0) ? (BUTTON1_PROVIDER) :
(index == 1) ? (BUTTON2_PROVIDER) :
@ -253,7 +266,7 @@ constexpr int _buttonProvider(unsigned char index) {
(index == 4) ? (BUTTON5_PROVIDER) :
(index == 5) ? (BUTTON6_PROVIDER) :
(index == 6) ? (BUTTON7_PROVIDER) :
(index == 7) ? (BUTTON8_PROVIDER) : BUTTON_PROVIDER_GENERIC
(index == 7) ? (BUTTON8_PROVIDER) : BUTTON_PROVIDER_NONE
);
}
@ -269,16 +282,3 @@ constexpr int _buttonAnalogLevel(unsigned char index) {
(index == 7) ? (BUTTON8_ANALOG_LEVEL) : 0
);
}
constexpr unsigned char _buttonPreconfiguredPins() {
return (
(GPIO_NONE != _buttonPin(0))
+ (GPIO_NONE != _buttonPin(1))
+ (GPIO_NONE != _buttonPin(2))
+ (GPIO_NONE != _buttonPin(3))
+ (GPIO_NONE != _buttonPin(4))
+ (GPIO_NONE != _buttonPin(5))
+ (GPIO_NONE != _buttonPin(6))
+ (GPIO_NONE != _buttonPin(7))
);
}

+ 9
- 0
code/espurna/compat.h View File

@ -111,3 +111,12 @@ namespace std {
#endif
// -----------------------------------------------------------------------------
// Make sure all INPUT modes are available to the source
// (even if those do nothing)
// -----------------------------------------------------------------------------
// TODO: esp8266/Arduino issue
#if defined(ESP8266) and not defined(INPUT_PULLDOWN)
#define INPUT_PULLDOWN 0x3
#endif

+ 106
- 20
code/espurna/config/defaults.h View File

@ -33,6 +33,31 @@
#define BUTTON8_PIN GPIO_NONE
#endif
#ifndef BUTTON1_PIN_TYPE
#define BUTTON1_PIN_TYPE GPIO_TYPE_HARDWARE
#endif
#ifndef BUTTON2_PIN_TYPE
#define BUTTON2_PIN_TYPE GPIO_TYPE_HARDWARE
#endif
#ifndef BUTTON3_PIN_TYPE
#define BUTTON3_PIN_TYPE GPIO_TYPE_HARDWARE
#endif
#ifndef BUTTON4_PIN_TYPE
#define BUTTON4_PIN_TYPE GPIO_TYPE_HARDWARE
#endif
#ifndef BUTTON5_PIN_TYPE
#define BUTTON5_PIN_TYPE GPIO_TYPE_HARDWARE
#endif
#ifndef BUTTON6_PIN_TYPE
#define BUTTON6_PIN_TYPE GPIO_TYPE_HARDWARE
#endif
#ifndef BUTTON7_PIN_TYPE
#define BUTTON7_PIN_TYPE GPIO_TYPE_HARDWARE
#endif
#ifndef BUTTON8_PIN_TYPE
#define BUTTON8_PIN_TYPE GPIO_TYPE_HARDWARE
#endif
#ifndef BUTTON1_CONFIG
#define BUTTON1_CONFIG BUTTON_PUSHBUTTON | BUTTON_SET_PULLUP | BUTTON_DEFAULT_HIGH
#endif
@ -423,28 +448,28 @@
#endif
#ifndef BUTTON1_PROVIDER
#define BUTTON1_PROVIDER BUTTON_PROVIDER_GENERIC
#define BUTTON1_PROVIDER BUTTON_PROVIDER_GPIO
#endif
#ifndef BUTTON2_PROVIDER
#define BUTTON2_PROVIDER BUTTON_PROVIDER_GENERIC
#define BUTTON2_PROVIDER BUTTON_PROVIDER_GPIO
#endif
#ifndef BUTTON3_PROVIDER
#define BUTTON3_PROVIDER BUTTON_PROVIDER_GENERIC
#define BUTTON3_PROVIDER BUTTON_PROVIDER_GPIO
#endif
#ifndef BUTTON4_PROVIDER
#define BUTTON4_PROVIDER BUTTON_PROVIDER_GENERIC
#define BUTTON4_PROVIDER BUTTON_PROVIDER_GPIO
#endif
#ifndef BUTTON5_PROVIDER
#define BUTTON5_PROVIDER BUTTON_PROVIDER_GENERIC
#define BUTTON5_PROVIDER BUTTON_PROVIDER_GPIO
#endif
#ifndef BUTTON6_PROVIDER
#define BUTTON6_PROVIDER BUTTON_PROVIDER_GENERIC
#define BUTTON6_PROVIDER BUTTON_PROVIDER_GPIO
#endif
#ifndef BUTTON7_PROVIDER
#define BUTTON7_PROVIDER BUTTON_PROVIDER_GENERIC
#define BUTTON7_PROVIDER BUTTON_PROVIDER_GPIO
#endif
#ifndef BUTTON8_PROVIDER
#define BUTTON8_PROVIDER BUTTON_PROVIDER_GENERIC
#define BUTTON8_PROVIDER BUTTON_PROVIDER_GPIO
#endif
#ifndef BUTTON1_ANALOG_LEVEL
@ -737,8 +762,79 @@
#define RELAY8_DELAY_OFF 0
#endif
#ifndef RELAY_DELAY_INTERLOCK
#define RELAY_DELAY_INTERLOCK 0
#ifndef RELAY1_PROVIDER
#define RELAY1_PROVIDER RELAY_PROVIDER_GPIO
#endif
#ifndef RELAY2_PROVIDER
#define RELAY2_PROVIDER RELAY_PROVIDER_GPIO
#endif
#ifndef RELAY3_PROVIDER
#define RELAY3_PROVIDER RELAY_PROVIDER_GPIO
#endif
#ifndef RELAY4_PROVIDER
#define RELAY4_PROVIDER RELAY_PROVIDER_GPIO
#endif
#ifndef RELAY5_PROVIDER
#define RELAY5_PROVIDER RELAY_PROVIDER_GPIO
#endif
#ifndef RELAY6_PROVIDER
#define RELAY6_PROVIDER RELAY_PROVIDER_GPIO
#endif
#ifndef RELAY7_PROVIDER
#define RELAY7_PROVIDER RELAY_PROVIDER_GPIO
#endif
#ifndef RELAY8_PROVIDER
#define RELAY8_PROVIDER RELAY_PROVIDER_GPIO
#endif
#ifndef RELAY1_PIN_TYPE
#define RELAY1_PIN_TYPE GPIO_TYPE_HARDWARE
#endif
#ifndef RELAY2_PIN_TYPE
#define RELAY2_PIN_TYPE GPIO_TYPE_HARDWARE
#endif
#ifndef RELAY3_PIN_TYPE
#define RELAY3_PIN_TYPE GPIO_TYPE_HARDWARE
#endif
#ifndef RELAY4_PIN_TYPE
#define RELAY4_PIN_TYPE GPIO_TYPE_HARDWARE
#endif
#ifndef RELAY5_PIN_TYPE
#define RELAY5_PIN_TYPE GPIO_TYPE_HARDWARE
#endif
#ifndef RELAY6_PIN_TYPE
#define RELAY6_PIN_TYPE GPIO_TYPE_HARDWARE
#endif
#ifndef RELAY7_PIN_TYPE
#define RELAY7_PIN_TYPE GPIO_TYPE_HARDWARE
#endif
#ifndef RELAY8_PIN_TYPE
#define RELAY8_PIN_TYPE GPIO_TYPE_HARDWARE
#endif
#ifndef RELAY1_BOOT_MODE
#define RELAY1_BOOT_MODE RELAY_BOOT_MODE
#endif
#ifndef RELAY2_BOOT_MODE
#define RELAY2_BOOT_MODE RELAY_BOOT_MODE
#endif
#ifndef RELAY3_BOOT_MODE
#define RELAY3_BOOT_MODE RELAY_BOOT_MODE
#endif
#ifndef RELAY4_BOOT_MODE
#define RELAY4_BOOT_MODE RELAY_BOOT_MODE
#endif
#ifndef RELAY5_BOOT_MODE
#define RELAY5_BOOT_MODE RELAY_BOOT_MODE
#endif
#ifndef RELAY6_BOOT_MODE
#define RELAY6_BOOT_MODE RELAY_BOOT_MODE
#endif
#ifndef RELAY7_BOOT_MODE
#define RELAY7_BOOT_MODE RELAY_BOOT_MODE
#endif
#ifndef RELAY8_BOOT_MODE
#define RELAY8_BOOT_MODE RELAY_BOOT_MODE
#endif
// -----------------------------------------------------------------------------
@ -909,13 +1005,3 @@
#ifndef HOSTNAME
#define HOSTNAME ""
#endif
// Relay providers
#ifndef RELAY_PROVIDER
#define RELAY_PROVIDER RELAY_PROVIDER_RELAY
#endif
// Light provider
#ifndef LIGHT_PROVIDER
#define LIGHT_PROVIDER LIGHT_PROVIDER_NONE
#endif

+ 6
- 17
code/espurna/config/dependencies.h View File

@ -102,11 +102,13 @@
#define TELNET_SERVER_ASYNC_BUFFERED 1 // enable buffered telnet by default on latest Cores
#endif
#if LIGHT_PROVIDER == LIGHT_PROVIDER_TUYA
#undef TUYA_SUPPORT
#define TUYA_SUPPORT 1 // Need base Tuya module for this to work
#if TUYA_SUPPORT
#undef LIGHT_TRANSITION_TIME
#define LIGHT_TRANSITION_TIME 1600 // longer transition than the default
#undef LIGHT_TRANSITION_STEP
#define LIGHT_TRANSITION_STEP 200 // step can't be 10ms since most tuya serial connections are not fast
#undef LIGHT_USE_TRANSITIONS
#define LIGHT_USE_TRANSITIONS 0 // TODO: temporary, maybe slower step instead?
#define LIGHT_USE_TRANSITIONS 0 // also, disable transitions unless set at runtime
#endif
#if TUYA_SUPPORT
@ -184,19 +186,6 @@
#define NTP_LEGACY_SUPPORT 0
#endif
//------------------------------------------------------------------------------
// Remove serial debug support completely in case hardware does not support it
// TODO: provide runtime check as well?
#if (BUTTON_PROVIDER_ITEAD_SONOFF_DUAL_SUPPORT) || \
(BUTTON_PROVIDER_FOXEL_LIGHTFOX_DUAL_SUPPORT)
#if DEBUG_SERIAL_SUPPORT
#warning "DEBUG_SERIAL_SUPPORT will be disabled because it conflicts with the BUTTON_PROVIDER_{ITEAD_SONOFF_DUAL,FOXEL_LIGHTFOX_DUAL}"
#undef DEBUG_SERIAL_SUPPORT
#define DEBUG_SERIAL_SUPPORT 0
#endif
#endif
//------------------------------------------------------------------------------
// It looks more natural that one click will enable display
// and long click will switch relay


+ 44
- 27
code/espurna/config/general.h View File

@ -418,25 +418,8 @@
// Generic digital pin support
#ifndef BUTTON_PROVIDER_GENERIC_SUPPORT
#define BUTTON_PROVIDER_GENERIC_SUPPORT 1
#endif
// Hardware specific, drive buttons through serial connection
// (mutually exclusive)
#ifndef BUTTON_PROVIDER_ITEAD_SONOFF_DUAL_SUPPORT
#define BUTTON_PROVIDER_ITEAD_SONOFF_DUAL_SUPPORT 0
#endif
#ifndef BUTTON_PROVIDER_FOXEL_LIGHTFOX_DUAL
#define BUTTON_PROVIDER_FOXEL_LIGHTFOX_DUAL 0
#endif
// Support MCP23S08 8-Bit I/O Expander via the SPI interface
#ifndef BUTTON_PROVIDER_MCP23S08_SUPPORT
#define BUTTON_PROVIDER_MCP23S08_SUPPORT MCP23S08_SUPPORT
#ifndef BUTTON_PROVIDER_GPIO_SUPPORT
#define BUTTON_PROVIDER_GPIO_SUPPORT 1
#endif
// Resistor ladder support. Poll analog pin and return digital LOW when analog reading is in a certain range
@ -444,7 +427,7 @@
// Uses BUTTON#_ANALOG_LEVEL for the individual button level configuration
#ifndef BUTTON_PROVIDER_ANALOG_SUPPORT
#define BUTTON_PROVIDER_ANALOG_SUPPORT 0
#define BUTTON_PROVIDER_ANALOG_SUPPORT 0
#endif
//------------------------------------------------------------------------------
@ -471,8 +454,19 @@
// RELAY
//------------------------------------------------------------------------------
// Enable general support for relays (aka switches)
#ifndef RELAY_SUPPORT
#define RELAY_SUPPORT 1
#define RELAY_SUPPORT 1
#endif
// ESP01-relays with STM co-MCU driving the relays
#ifndef RELAY_PROVIDER_STM_SUPPORT
#define RELAY_PROVIDER_STM_SUPPORT 0
#endif
// Sonoff Dual, using serial protocol
#ifndef RELAY_PROVIDER_DUAL_SUPPORT
#define RELAY_PROVIDER_DUAL_SUPPORT 0
#endif
// Default boot mode: 0 means OFF, 1 ON and 2 whatever was before
@ -480,11 +474,18 @@
#define RELAY_BOOT_MODE RELAY_BOOT_OFF
#endif
// 0 means ANY, 1 zero or one and 2 one and only one
// One of RELAY_SYNC_ANY, RELAY_SYNC_NONE_OR_ONE, RELAY_SYNC_SAME or RELAY_SYNC_FIRST
// Default to ANY i.e. don't do anything
#ifndef RELAY_SYNC
#define RELAY_SYNC RELAY_SYNC_ANY
#endif
// 0 (ms) means EVERY relay switches as soon as possible
// otherwise, wait up until this much time before changing the status
#ifndef RELAY_DELAY_INTERLOCK
#define RELAY_DELAY_INTERLOCK 0
#endif
// Default pulse mode: 0 means no pulses, 1 means normally off, 2 normally on
#ifndef RELAY_PULSE_MODE
#define RELAY_PULSE_MODE RELAY_PULSE_NONE
@ -1231,6 +1232,7 @@
#define MQTT_TOPIC_CMD "cmd"
// Light module
#define MQTT_TOPIC_LIGHT "light"
#define MQTT_TOPIC_CHANNEL "channel"
#define MQTT_TOPIC_COLOR_RGB "rgb"
#define MQTT_TOPIC_COLOR_HSV "hsv"
@ -1304,16 +1306,20 @@
// 4 channels => RGBW
// 5 channels => RGBWW
#ifndef LIGHT_SAVE_ENABLED
#define LIGHT_SAVE_ENABLED 1 // Light channel values saved by default after each change
#ifndef LIGHT_PROVIDER
#define LIGHT_PROVIDER LIGHT_PROVIDER_NONE
#endif
#ifndef LIGHT_COMMS_DELAY
#define LIGHT_COMMS_DELAY 100 // Delay communication after light update (in ms)
#ifndef LIGHT_REPORT_DELAY
#define LIGHT_REPORT_DELAY 100 // Delay reporting current state for the specified number of ms after light update
#endif
#ifndef LIGHT_SAVE_ENABLED
#define LIGHT_SAVE_ENABLED 1 // Light channel values saved by default after each change
#endif
#ifndef LIGHT_SAVE_DELAY
#define LIGHT_SAVE_DELAY 5 // Persist color after 5 seconds to avoid wearing out
#define LIGHT_SAVE_DELAY 5000 // Persist channel & brightness values after the specified number of ms
#endif
#ifndef LIGHT_MIN_PWM
@ -1414,6 +1420,9 @@
#define LIGHT_TRANSITION_TIME 500 // Time in millis from color to color
#endif
#ifndef LIGHT_RELAY_ENABLED
#define LIGHT_RELAY_ENABLED 1 // Add a virtual switch that controls the global light state. Depends on RELAY_SUPPORT
#endif
// -----------------------------------------------------------------------------
// DOMOTICZ
@ -1804,6 +1813,14 @@
#define TUYA_SERIAL Serial
#endif
#ifndef TUYA_FILTER_ENABLED
#define TUYA_FILTER_ENABLED 1
#endif
#ifndef TUYA_DEBUG_ENABLED
#define TUYA_DEBUG_ENABLED 1
#endif
//--------------------------------------------------------------------------------
// Support expander MCP23S08
//--------------------------------------------------------------------------------


+ 51
- 123
code/espurna/config/hardware.h View File

@ -565,21 +565,24 @@
#define MANUFACTURER "ITEAD"
#define DEVICE "SONOFF_DUAL"
#define SERIAL_BAUDRATE 19230
#define RELAY_PROVIDER RELAY_PROVIDER_DUAL
#define DUMMY_RELAY_COUNT 2
#define DEBUG_SERIAL_SUPPORT 0
// Buttons
#define BUTTON1_RELAY 1
#define BUTTON2_RELAY 2
#define BUTTON3_RELAY 1
#define BUTTON_PROVIDER_ITEAD_SONOFF_DUAL_SUPPORT 1
// LEDs
#define LED1_PIN 13
#define LED1_PIN_INVERSE 1
// Relays
#define RELAY_PROVIDER_DUAL_SUPPORT 1
#define RELAY1_PROVIDER RELAY_PROVIDER_DUAL
#define RELAY2_PROVIDER RELAY_PROVIDER_DUAL
// No need to include generic GPIO support
// "Buttons" are attached to a secondary MCU and RELAY_PROVIDER_DUAL handles that
#define BUTTON_PROVIDER_GPIO_SUPPORT 0
// Conflicts with relay operation
#define DEBUG_SERIAL_SUPPORT 0
#elif defined(ITEAD_SONOFF_DUAL_R2)
#define MANUFACTURER "ITEAD"
@ -765,9 +768,7 @@
// Info
#define MANUFACTURER "ITEAD"
#define DEVICE "BNSZ01"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// LEDs
#define LED1_PIN 13
@ -782,7 +783,6 @@
// Info
#define MANUFACTURER "ITEAD"
#define DEVICE "SONOFF_RFBRIDGE"
#define RELAY_PROVIDER RELAY_PROVIDER_RFBRIDGE
// Number of virtual switches
#ifndef DUMMY_RELAY_COUNT
@ -820,9 +820,7 @@
// Info
#define MANUFACTURER "ITEAD"
#define DEVICE "SONOFF_B1"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_MY92XX
#define DUMMY_RELAY_COUNT 1
// Light
#define LIGHT_CHANNELS 5
@ -839,9 +837,7 @@
// Info
#define MANUFACTURER "ITEAD"
#define DEVICE "SONOFF_LED"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// LEDs
#define LED1_PIN 13
@ -1246,9 +1242,7 @@
// Info
#define MANUFACTURER "AITHINKER"
#define DEVICE "AI_LIGHT"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_MY92XX
#define DUMMY_RELAY_COUNT 1
// Light
#define LIGHT_CHANNELS 4
@ -1268,9 +1262,7 @@
// Info
#define MANUFACTURER "LYASI"
#define DEVICE "RGB_LED"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_MY92XX
#define DUMMY_RELAY_COUNT 1
// Light
#define LIGHT_CHANNELS 4
@ -1290,9 +1282,7 @@
// Info
#define MANUFACTURER "MAGICHOME"
#define DEVICE "LED_CONTROLLER"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// LEDs
#define LED1_PIN 2
@ -1315,9 +1305,7 @@
// Info
#define MANUFACTURER "MAGICHOME"
#define DEVICE "LED_CONTROLLER_20"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// LEDs
#define LED1_PIN 2
@ -1340,9 +1328,7 @@
// Info
#define MANUFACTURER "MAGICHOME"
#define DEVICE "ZJ_WFMN_A_11"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// LEDs
#define LED1_PIN 2
@ -1367,9 +1353,7 @@
// Info
#define MANUFACTURER "MAGICHOME"
#define DEVICE "ZJ_WFMN_B_11"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// LEDs
#define LED1_PIN 2
@ -1394,9 +1378,7 @@
// Info
#define MANUFACTURER "MAGICHOME"
#define DEVICE "ZJ_WFMN_C_11"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// Buttons
#define BUTTON1_PIN 0
@ -1416,9 +1398,7 @@
// Info
#define MANUFACTURER "MAGICHOME"
#define DEVICE "ZJ_ESPM_5CH_B_13"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// Buttons
#define BUTTON1_PIN 0
@ -1442,9 +1422,7 @@
// Info
#define MANUFACTURER "MAGICHOME"
#define DEVICE "ZJ_LB_RGBWW_L"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// Light
#define LIGHT_CHANNELS 5
@ -1463,9 +1441,7 @@
// Info
#define MANUFACTURER "HUACANXING"
#define DEVICE "H801"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
#define DEBUG_PORT Serial1
#define SERIAL_RX_ENABLED 1
@ -1486,9 +1462,7 @@
// Info
#define MANUFACTURER "HUACANXING"
#define DEVICE "H802"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
#define DEBUG_PORT Serial1
#define SERIAL_RX_ENABLED 1
@ -1832,9 +1806,7 @@
// Info
#define MANUFACTURER "INTERMITTECH"
#define DEVICE "QUINLED"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// LEDs
#define LED1_PIN 5
@ -1854,9 +1826,7 @@
// Info
#define MANUFACTURER "ARILUX"
#define DEVICE "AL_LC01"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// Light
#define LIGHT_CHANNELS 3
@ -1869,9 +1839,7 @@
// Info
#define MANUFACTURER "ARILUX"
#define DEVICE "AL_LC02"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// Light
#define LIGHT_CHANNELS 4
@ -1885,9 +1853,7 @@
// Info
#define MANUFACTURER "ARILUX"
#define DEVICE "AL_LC02_V14"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// Light
#define LIGHT_CHANNELS 4
@ -1901,9 +1867,7 @@
// Info
#define MANUFACTURER "ARILUX"
#define DEVICE "AL_LC06"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// Buttons
#define BUTTON1_PIN 0
@ -1923,9 +1887,7 @@
// Info
#define MANUFACTURER "ARILUX"
#define DEVICE "AL_LC11"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// Light
#define LIGHT_CHANNELS 5
@ -1940,9 +1902,7 @@
// Info
#define MANUFACTURER "ARILUX"
#define DEVICE "E27"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_MY92XX
#define DUMMY_RELAY_COUNT 1
// Light
#define LIGHT_CHANNELS 4
@ -2011,9 +1971,7 @@
// Info
#define MANUFACTURER "AUTHOMETION"
#define DEVICE "LYT8266"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// Light
#define LIGHT_CHANNELS 4
@ -2029,9 +1987,7 @@
// Info
#define MANUFACTURER "GIZWITS"
#define DEVICE "WITTY_CLOUD"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// Buttons
#define BUTTON1_PIN 4
@ -2188,10 +2144,19 @@
#define DEVICE "2CH"
// Relays
#define DUMMY_RELAY_COUNT 2
#define RELAY_PROVIDER RELAY_PROVIDER_STM
#define RELAY_PROVIDER_STM_SUPPORT 1
#define RELAY1_PROVIDER RELAY_PROVIDER_STM
#define RELAY2_PROVIDER RELAY_PROVIDER_STM
// Make sure we space out serial writes when relays are in sync. ref:
// - https://github.com/xoseperez/espurna/issues/1130
// - https://github.com/xoseperez/espurna/issues/1519
// - https://github.com/xoseperez/espurna/pull/1520
#define RELAY_DELAY_INTERLOCK 100
// Remove UART noise on serial line
// (or use `#define DEBUG_PORT Serial1` instead)
#define DEBUG_SERIAL_SUPPORT 0
// -----------------------------------------------------------------------------
@ -3488,13 +3453,16 @@
//Enable this to view buttons analog level.
//Or, use adc terminal command
//#define ANALOG_SUPPORT 1
// Disable UART noise
#define DEBUG_SERIAL_SUPPORT 0
// Buttons
#define BUTTON_PROVIDER_GPIO_SUPPORT 1
#define BUTTON_PROVIDER_ANALOG_SUPPORT 1
#define BUTTON1_PIN 16
#define BUTTON1_CONFIG BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON1_RELAY 4
@ -3635,9 +3603,7 @@
// Info
#define MANUFACTURER "GENERIC"
#define DEVICE "AG_L4"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// button 1: "power" button
#define BUTTON1_PIN 4
@ -3679,9 +3645,7 @@
// Info
#define MANUFACTURER "GENERIC"
#define DEVICE "AG_L4_V3"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// button 1: "power" button
#define BUTTON1_PIN 13
@ -3907,9 +3871,7 @@
// Info
#define MANUFACTURER "LOHAS"
#define DEVICE "E27_9W"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_MY92XX
#define DUMMY_RELAY_COUNT 1
// Light
#define LIGHT_CHANNELS 5
@ -3928,9 +3890,7 @@
// Info
#define MANUFACTURER "LOHAS"
#define DEVICE "E26_A19"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// Light
#define LIGHT_CHANNELS 5
@ -3947,9 +3907,7 @@
// Info
#define MANUFACTURER "TECKIN"
#define DEVICE "SB53"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// Light
#define LIGHT_CHANNELS 5
@ -3988,9 +3946,7 @@
#define BUTTON2_LNGLNGCLICK BUTTON_ACTION_RESET
// Light
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
#define LIGHT_STEP 8
#define LIGHT_CHANNELS 2
#define LIGHT_CH1_PIN 5 // warm white
@ -4019,9 +3975,7 @@
// Info
#define MANUFACTURER "PHYX"
#define DEVICE "ESP12_RGB"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// Light
#define LIGHT_CHANNELS 3
@ -4039,9 +3993,7 @@
// Info
#define MANUFACTURER "IWOOLE"
#define DEVICE "LED_TABLE_LAMP"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// Light
#define LIGHT_CHANNELS 4
@ -4060,9 +4012,7 @@
// Info
#define MANUFACTURER "GENERIC"
#define DEVICE "GU10"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// Light
#define LIGHT_CHANNELS 4
@ -4081,9 +4031,7 @@
// Info
#define MANUFACTURER "GENERIC"
#define DEVICE "E14"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// Light
#define LIGHT_CHANNELS 4
@ -4101,9 +4049,7 @@
// Info
#define MANUFACTURER "DELTACO"
#define DEVICE "SH_LEXXW"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// Light
#define LIGHT_CHANNELS 2
@ -4119,9 +4065,7 @@
// Info
#define MANUFACTURER "DELTACO"
#define DEVICE "SH_LEXXRGB"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// Light
#define LIGHT_CHANNELS 5
@ -4141,9 +4085,7 @@
// Info
#define MANUFACTURER "NEXETE"
#define DEVICE "A19"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// Light
#define LIGHT_CHANNELS 4
@ -4161,9 +4103,7 @@
// Info
#define MANUFACTURER "LOMBEX"
#define DEVICE "LUX_NOVA2_TUNABLE_WHITE"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_MY92XX
#define DUMMY_RELAY_COUNT 1
// Light
#define LIGHT_CHANNELS 5
@ -4184,9 +4124,7 @@
// Info
#define MANUFACTURER "LOMBEX"
#define DEVICE "LUX_NOVA2_WHITE_COLOR"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_MY92XX
#define DUMMY_RELAY_COUNT 1
// Light
#define LIGHT_CHANNELS 4
@ -4243,9 +4181,7 @@
#define RELAY1_TYPE RELAY_TYPE_NORMAL
// Light RGBW
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
#define LIGHT_CHANNELS 4
#define LIGHT_CH1_PIN 5 // RED
@ -4281,9 +4217,7 @@
//Blue LED: 2
// Light
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
#define LIGHT_CHANNELS 3
#define LIGHT_CH1_PIN 0 // RED
#define LIGHT_CH2_PIN 4 // GREEN
@ -4383,17 +4317,19 @@
#define MANUFACTURER "FOXEL"
#define DEVICE "LIGHTFOX_DUAL"
#define SERIAL_BAUDRATE 19200
#define RELAY_PROVIDER RELAY_PROVIDER_DUAL
#define DUMMY_RELAY_COUNT 2
#define DEBUG_SERIAL_SUPPORT 0
// Buttons
#define BUTTON1_RELAY 1
#define BUTTON2_RELAY 2
#define BUTTON3_RELAY 2
#define BUTTON4_RELAY 1
// Relays
#define RELAY_PROVIDER_DUAL_SUPPORT 1
#define BUTTON_PROVIDER_FOXEL_LIGHTFOX_DUAL_SUPPORT 1
#define RELAY1_PROVIDER RELAY_PROVIDER_DUAL
#define RELAY2_PROVIDER RELAY_PROVIDER_DUAL
// No need to include generic GPIO support
// "Buttons" are attached to a secondary MCU and RELAY_PROVIDER_DUAL handles that
#define BUTTON_PROVIDER_GPIO_SUPPORT 0
// Conflicts with relay operation
#define DEBUG_SERIAL_SUPPORT 0
// -----------------------------------------------------------------------------
// Teckin SP20
@ -4487,9 +4423,7 @@
// Info
#define MANUFACTURER "PSH"
#define DEVICE "RGBW_CONTROLLER"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// LEDs
#define LED1_PIN 13
@ -4554,10 +4488,8 @@
#define MANUFACTURER "TUYA"
#define DEVICE "GENERIC_DIMMER"
#define LIGHT_PROVIDER LIGHT_PROVIDER_TUYA
#define LIGHT_CHANNELS 0
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define DUMMY_RELAY_COUNT 0
#define TUYA_SUPPORT 1
#define LIGHT_PROVIDER LIGHT_PROVIDER_CUSTOM
// -----------------------------------------------------------------------------
// Etekcity ESW01-USA
@ -4684,9 +4616,7 @@
// Info
#define MANUFACTURER "MUVIT_IO"
#define DEVICE "MIOBULB001"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// Light
#define LIGHT_CHANNELS 4
@ -4815,9 +4745,7 @@
// Info
#define MANUFACTURER "LSC"
#define DEVICE "SMART_LED_LIGHT_STRIP"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// Light RGBW
#define LIGHT_CHANNELS 4
@ -4983,28 +4911,34 @@
#define MCP23S08_SUPPORT 1
// Relays
#define RELAY_PROVIDER RELAY_PROVIDER_MCP23S08
#define RELAY1_PIN 4
#define RELAY1_PIN_TYPE GPIO_TYPE_MCP23S08
#define RELAY2_PIN 5
#define RELAY2_PIN_TYPE GPIO_TYPE_MCP23S08
#define RELAY3_PIN 6
#define RELAY3_PIN_TYPE GPIO_TYPE_MCP23S08
#define RELAY4_PIN 7
#define RELAY4_PIN_TYPE GPIO_TYPE_MCP23S08
// Buttons
#define BUTTON1_PROVIDER BUTTON_PROVIDER_MCP23S08
#define BUTTON1_CONFIG BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON1_PIN 0
#define BUTTON1_PIN_TYPE GPIO_TYPE_MCP23S08
#define BUTTON2_PROVIDER BUTTON_PROVIDER_MCP23S08
#define BUTTON2_CONFIG BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON2_PIN 1
#define BUTTON2_PIN_TYPE GPIO_TYPE_MCP23S08
#define BUTTON3_PROVIDER BUTTON_PROVIDER_MCP23S08
#define BUTTON3_CONFIG BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON3_PIN 2
#define BUTTON3_PIN_TYPE GPIO_TYPE_MCP23S08
#define BUTTON4_PROVIDER BUTTON_PROVIDER_MCP23S08
#define BUTTON4_CONFIG BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON4_PIN 3
#define BUTTON4_PIN_TYPE GPIO_TYPE_MCP23S08
#define BUTTON1_RELAY 1
#define BUTTON2_RELAY 2
@ -5025,9 +4959,7 @@
// Info
#define MANUFACTURER "FCMILA"
#define DEVICE "E27_7W_RGBW"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// Light
#define LIGHT_CHANNELS 4
@ -5046,9 +4978,7 @@
// Info
#define MANUFACTURER "BENEXMART"
#define DEVICE "GU53_RGBWW"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// Light
#define LIGHT_CHANNELS 5
@ -5067,9 +4997,7 @@
// Info
#define MANUFACTURER "LSC"
#define DEVICE "E27_10W_WHITE"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// Light
#define LIGHT_CHANNELS 2


+ 25
- 21
code/espurna/config/types.h View File

@ -15,6 +15,16 @@
#define WIFI_STATE_WPS 4
#define WIFI_STATE_SMARTCONFIG 8
// -----------------------------------------------------------------------------
// GPIO
// -----------------------------------------------------------------------------
#define GPIO_NONE 0x99
#define GPIO_TYPE_NONE GpioType::None
#define GPIO_TYPE_HARDWARE GpioType::Hardware
#define GPIO_TYPE_MCP23S08 GpioType::Mcp23s08
//------------------------------------------------------------------------------
// BUTTONS
//------------------------------------------------------------------------------
@ -60,9 +70,9 @@
#define BUTTON_SET_PULLDOWN ButtonMask::SetPulldown
// configure where do we get the button events
#define BUTTON_PROVIDER_GENERIC 0
#define BUTTON_PROVIDER_MCP23S08 1
#define BUTTON_PROVIDER_ANALOG 2
#define BUTTON_PROVIDER_NONE ButtonProvider::None
#define BUTTON_PROVIDER_GPIO ButtonProvider::Gpio
#define BUTTON_PROVIDER_ANALOG ButtonProvider::Analog
//------------------------------------------------------------------------------
// ENCODER
@ -75,6 +85,8 @@
// RELAY
//------------------------------------------------------------------------------
#define RELAY_NONE 0x99
#define RELAY_BOOT_OFF 0
#define RELAY_BOOT_ON 1
#define RELAY_BOOT_SAME 2
@ -82,10 +94,10 @@
#define RELAY_BOOT_LOCKED_OFF 4
#define RELAY_BOOT_LOCKED_ON 5
#define RELAY_TYPE_NORMAL 0
#define RELAY_TYPE_INVERSE 1
#define RELAY_TYPE_LATCHED 2
#define RELAY_TYPE_LATCHED_INVERSE 3
#define RELAY_TYPE_NORMAL RelayType::Normal
#define RELAY_TYPE_INVERSE RelayType::Inverse
#define RELAY_TYPE_LATCHED RelayType::Latched
#define RELAY_TYPE_LATCHED_INVERSE RelayType::LatchedInverse
#define RELAY_SYNC_ANY 0
#define RELAY_SYNC_NONE_OR_ONE 1
@ -97,12 +109,11 @@
#define RELAY_PULSE_OFF 1
#define RELAY_PULSE_ON 2
#define RELAY_PROVIDER_RELAY 0
#define RELAY_PROVIDER_DUAL 1
#define RELAY_PROVIDER_LIGHT 2
#define RELAY_PROVIDER_RFBRIDGE 3
#define RELAY_PROVIDER_STM 4
#define RELAY_PROVIDER_MCP23S08 5
#define RELAY_PROVIDER_NONE RelayProvider::None
#define RELAY_PROVIDER_DUMMY RelayProvider::Dummy
#define RELAY_PROVIDER_GPIO RelayProvider::Gpio
#define RELAY_PROVIDER_DUAL RelayProvider::Dual
#define RELAY_PROVIDER_STM RelayProvider::Stm
#define RFB_PROVIDER_RCSWITCH 0
#define RFB_PROVIDER_EFM8BB1 1
@ -200,7 +211,7 @@
#define LIGHT_PROVIDER_NONE 0
#define LIGHT_PROVIDER_MY92XX 1 // works with MY9291 and MY9231
#define LIGHT_PROVIDER_DIMMER 2
#define LIGHT_PROVIDER_TUYA 3
#define LIGHT_PROVIDER_CUSTOM 3
// -----------------------------------------------------------------------------
// SCHEDULER
@ -409,10 +420,3 @@
#define SECURE_CLIENT_CHECK_NONE 0 // !!! INSECURE CONNECTION !!!
#define SECURE_CLIENT_CHECK_FINGERPRINT 1 // legacy fingerprint validation
#define SECURE_CLIENT_CHECK_CA 2 // set trust anchor from PROGMEM CA certificate
// -----------------------------------------------------------------------------
// Hardware default values
// -----------------------------------------------------------------------------
#define GPIO_NONE 0x99
#define RELAY_NONE 0x99

BIN
code/espurna/data/index.all.html.gz View File


BIN
code/espurna/data/index.curtain.html.gz View File


BIN
code/espurna/data/index.garland.html.gz View File


BIN
code/espurna/data/index.light.html.gz View File


BIN
code/espurna/data/index.lightfox.html.gz View File


BIN
code/espurna/data/index.rfbridge.html.gz View File


BIN
code/espurna/data/index.rfm69.html.gz View File


BIN
code/espurna/data/index.sensor.html.gz View File


BIN
code/espurna/data/index.small.html.gz View File


BIN
code/espurna/data/index.thermostat.html.gz View File


+ 23
- 10
code/espurna/debug.cpp View File

@ -373,21 +373,34 @@ void debugConfigure() {
// `#if defined(DEBUG_ESP_PORT) && !defined(NDEBUG)`
// Core debugging also depends on various DEBUG_ESP_... being defined
{
#if defined(DEBUG_ESP_PORT)
#if not defined(NDEBUG)
constexpr bool debug_sdk = true;
#endif // !defined(NDEBUG)
#else
constexpr bool debug_sdk = false;
#endif // defined(DEBUG_ESP_PORT)
#if defined(DEBUG_ESP_PORT)
#if not defined(NDEBUG)
constexpr bool debug_sdk = true;
#endif // !defined(NDEBUG)
#else
constexpr bool debug_sdk = false;
#endif // defined(DEBUG_ESP_PORT)
DEBUG_PORT.setDebugOutput(getSetting("dbgSDK", debug_sdk));
}
// Make sure other modules are aware of used GPIOs
#if DEBUG_SERIAL_SUPPORT
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wpragmas"
#pragma GCC diagnostic ignored "-Wtautological-compare"
if (&(DEBUG_PORT) == &Serial) {
gpioLock(1);
gpioLock(3);
} else if (&(DEBUG_PORT) == &Serial1) {
gpioLock(2);
}
#pragma GCC diagnostic pop
#endif
#if DEBUG_LOG_BUFFER_SUPPORT
{
const auto enabled = getSetting("dbgBufEnabled", 1 == DEBUG_LOG_BUFFER_ENABLED);
const auto size = getSetting("dbgBufSize", DEBUG_LOG_BUFFER_SIZE);
const auto enabled = getSetting("dbgLogBuf", 1 == DEBUG_LOG_BUFFER_ENABLED);
const auto size = getSetting("dbgLogBufSize", DEBUG_LOG_BUFFER_SIZE);
if (enabled) {
_debug_log_buffer_enabled = true;
_debug_log_buffer.reserve(size);


+ 3
- 3
code/espurna/domoticz.cpp View File

@ -112,9 +112,9 @@ void _domoticzLight(unsigned int idx, const JsonObject& root) {
}
}
// domoticz uses 100 as maximum value while we're using Light::BRIGHTNESS_MAX (unsigned char)
lightBrightness((root["Level"].as<unsigned char>() / 100.0) * Light::BRIGHTNESS_MAX);
lightUpdate(true, mqttForward());
// domoticz uses 100 as maximum value while we're using Light::BRIGHTNESS_MAX (default 255)
lightBrightness((root["Level"].as<long>() / 100l) * Light::BRIGHTNESS_MAX);
lightUpdate();
}


+ 1
- 1
code/espurna/encoder.cpp View File

@ -137,7 +137,7 @@ void _encoderLoop() {
}
lightUpdate(true, true);
lightUpdate();
}


+ 126
- 42
code/espurna/gpio.cpp View File

@ -11,61 +11,145 @@ Copyright (C) 2017-2019 by Xose Pérez <xose dot perez at gmail dot com>
// --------------------------------------------------------------------------
#include <bitset>
#include <utility>
#include "gpio_pin.h"
#include "mcp23s08_pin.h"
namespace settings {
namespace internal {
template <>
GpioType convert(const String& value) {
auto type = static_cast<GpioType>(value.toInt());
switch (type) {
case GpioType::Hardware:
case GpioType::Mcp23s08:
return type;
case GpioType::None:
break;
}
std::bitset<GpioPins> _gpio_locked;
std::bitset<GpioPins> _gpio_available;
return GpioType::None;
}
bool gpioValid(unsigned char gpio) {
if (gpio >= GpioPins) return false;
} // namespace internal
} // namespace settings
namespace {
class GpioHardware : public GpioBase {
public:
constexpr static size_t Pins { 17ul };
using Mask = std::bitset<Pins>;
using Pin = GpioPin;
GpioHardware() {
// https://github.com/espressif/esptool/blob/f04d34bcab29ace798d2d3800ba87020cccbbfdd/esptool.py#L1060-L1070
// "One or the other efuse bit is set for ESP8285"
// https://github.com/espressif/ESP8266_RTOS_SDK/blob/3c055779e9793e5f082afff63a011d6615e73639/components/esp8266/include/esp8266/efuse_register.h#L20-L21
// "define EFUSE_IS_ESP8285 (1 << 4)"
const uint32_t efuse_blocks[4] {
READ_PERI_REG(0x3ff00050),
READ_PERI_REG(0x3ff00054),
READ_PERI_REG(0x3ff00058),
READ_PERI_REG(0x3ff0005c)
};
_esp8285 = (
(efuse_blocks[0] & (1 << 4))
|| (efuse_blocks[2] & (1 << 16))
);
}
return _gpio_available.test(gpio);
}
const char* id() const override {
return "hardware";
}
size_t pins() const override {
return Pins;
}
bool gpioGetLock(unsigned char gpio) {
if (gpioValid(gpio)) {
if (!_gpio_locked.test(gpio)) {
_gpio_locked.set(gpio);
DEBUG_MSG_P(PSTR("[GPIO] GPIO%u locked\n"), gpio);
bool lock(unsigned char index) const override {
return _lock[index];
}
void lock(unsigned char index, bool value) override {
_lock.set(index, value);
}
bool valid(unsigned char index) const override {
switch (index) {
case 0 ... 5:
return true;
case 9:
case 10:
return _esp8285;
case 12 ... 16:
return true;
}
return false;
}
BasePinPtr pin(unsigned char index) {
return std::make_unique<GpioPin>(index);
}
DEBUG_MSG_P(PSTR("[GPIO] Failed getting lock for GPIO%u\n"), gpio);
return false;
private:
bool _esp8285 { false };
Mask _lock;
};
} // namespace
String BasePin::description() const {
char buffer[64];
snprintf_P(buffer, sizeof(buffer), PSTR("%s @ GPIO%02u"), id(), pin());
return buffer;
}
BasePin::~BasePin() {
}
GpioBase& hardwareGpio() {
static GpioHardware gpio;
return gpio;
}
bool gpioReleaseLock(unsigned char gpio) {
if (gpioValid(gpio)) {
_gpio_locked.reset(gpio);
DEBUG_MSG_P(PSTR("[GPIO] GPIO%u lock released\n"), gpio);
return true;
GpioBase* gpioBase(GpioType type) {
GpioBase* ptr { nullptr };
switch (type) {
case GpioType::Hardware:
ptr = &hardwareGpio();
break;
case GpioType::Mcp23s08:
#if MCP23S08_SUPPORT
ptr = &mcp23s08Gpio();
#endif
break;
case GpioType::None:
break;
}
DEBUG_MSG_P(PSTR("[GPIO] Failed releasing lock for GPIO%u\n"), gpio);
return false;
return ptr;
}
void gpioSetup() {
BasePinPtr gpioRegister(GpioBase& base, unsigned char gpio) {
BasePinPtr result;
// https://github.com/espressif/esptool/blob/f04d34bcab29ace798d2d3800ba87020cccbbfdd/esptool.py#L1060-L1070
// "One or the other efuse bit is set for ESP8285"
// https://github.com/espressif/ESP8266_RTOS_SDK/blob/3c055779e9793e5f082afff63a011d6615e73639/components/esp8266/include/esp8266/efuse_register.h#L20-L21
// "define EFUSE_IS_ESP8285 (1 << 4)"
const uint32_t efuse_blocks[4] {
READ_PERI_REG(0x3ff00050),
READ_PERI_REG(0x3ff00054),
READ_PERI_REG(0x3ff00058),
READ_PERI_REG(0x3ff0005c)
};
const bool esp8285 = (
(efuse_blocks[0] & (1 << 4))
|| (efuse_blocks[2] & (1 << 16))
);
// TODO: GPIO16 is only for basic I/O, gpioGetLock before attachInterrupt should check for that
for (unsigned char pin=0; pin < GpioPins; ++pin) {
if (pin <= 5) _gpio_available.set(pin);
if (((pin == 9) || (pin == 10)) && (esp8285)) _gpio_available.set(pin);
if (12 <= pin && pin <= 16) _gpio_available.set(pin);
if (gpioLock(base, gpio)) {
result = std::move(base.pin(gpio));
}
return result;
}
BasePinPtr gpioRegister(unsigned char gpio) {
return gpioRegister(hardwareGpio(), gpio);
}
void gpioSetup() {
}

+ 73
- 4
code/espurna/gpio.h View File

@ -11,10 +11,79 @@ Copyright (C) 2017-2019 by Xose Pérez <xose dot perez at gmail dot com>
#include "espurna.h"
#include "libs/BasePin.h"
constexpr const size_t GpioPins = 17;
enum class GpioType : int {
None,
Hardware,
Mcp23s08
};
bool gpioValid(unsigned char gpio);
bool gpioGetLock(unsigned char gpio);
bool gpioReleaseLock(unsigned char gpio);
class GpioBase {
public:
virtual const char* id() const = 0;
virtual size_t pins() const = 0;
virtual bool lock(unsigned char index) const = 0;
virtual void lock(unsigned char index, bool value) = 0;
virtual bool valid(unsigned char index) const = 0;
virtual BasePinPtr pin(unsigned char index) = 0;
};
GpioBase& hardwareGpio();
GpioBase* gpioBase(GpioType);
BasePinPtr gpioRegister(GpioBase& base, unsigned char gpio);
BasePinPtr gpioRegister(unsigned char gpio);
void gpioSetup();
inline size_t gpioPins(const GpioBase& base) {
return base.pins();
}
inline size_t gpioPins() {
return gpioPins(hardwareGpio());
}
inline bool gpioValid(const GpioBase& base, unsigned char gpio) {
return base.valid(gpio);
}
inline bool gpioValid(unsigned char gpio) {
return gpioValid(hardwareGpio(), gpio);
}
inline bool gpioLock(GpioBase& base, unsigned char gpio, bool value) {
if (base.valid(gpio)) {
bool old = base.lock(gpio);
base.lock(gpio, value);
return (value != old);
}
return false;
}
inline bool gpioLock(GpioBase& base, unsigned char gpio) {
return gpioLock(base, gpio, true);
}
inline bool gpioLock(unsigned char gpio) {
return gpioLock(hardwareGpio(), gpio);
}
inline bool gpioUnlock(GpioBase& base, unsigned char gpio) {
return gpioLock(base, gpio, false);
}
inline bool gpioUnlock(unsigned char gpio) {
return gpioUnlock(hardwareGpio(), gpio);
}
inline bool gpioLocked(const GpioBase& base, unsigned char gpio) {
if (base.valid(gpio)) {
return base.lock(gpio);
}
return false;
}
inline bool gpioLocked(unsigned char gpio) {
return gpioLocked(hardwareGpio(), gpio);
}

+ 25
- 11
code/espurna/gpio_pin.h View File

@ -13,27 +13,41 @@ Copyright (C) 2017-2019 by Xose Pérez <xose dot perez at gmail dot com>
#include <cstdint>
class GpioPin final : public BasePin {
public:
explicit GpioPin(unsigned char pin_) :
BasePin(pin_)
public:
explicit GpioPin(unsigned char pin) :
_pin(pin)
{}
// ESP8266 does not have INPUT_PULLDOWN definition, and instead
// has a GPIO16-specific INPUT_PULLDOWN_16:
// - https://github.com/esp8266/Arduino/issues/478
// - https://github.com/esp8266/Arduino/commit/1b3581d55ebf0f8c91e081f9af4cf7433d492ec9
void pinMode(int8_t mode) override {
::pinMode(this->pin, mode);
#ifdef ESP8266
if ((INPUT_PULLDOWN == mode) && (_pin == 16)) {
mode = INPUT_PULLDOWN_16;
}
#endif
::pinMode(_pin, mode);
}
void digitalWrite(int8_t val) override {
::digitalWrite(this->pin, val);
::digitalWrite(_pin, val);
}
int digitalRead() override {
return ::digitalRead(_pin);
}
String description() const override {
static String desc(String(F("GpioPin @ GPIO")) + static_cast<int>(pin));
return desc;
unsigned char pin() const override {
return _pin;
}
int digitalRead() {
return ::digitalRead(this->pin);
const char* id() const override {
return "GpioPin";
}
private:
unsigned char _pin { GPIO_NONE };
};

+ 2
- 2
code/espurna/ir.cpp View File

@ -301,13 +301,13 @@ void _irProcess(unsigned char type, unsigned long code) {
case IR_BUTTON_ACTION_BRIGHTER:
lightBrightnessStep(button_value ? 1 : -1);
lightUpdate(true, true);
lightUpdate();
nice_delay(150); //debounce
break;
case IR_BUTTON_ACTION_RGB:
lightColor(button_value);
lightUpdate(true, true);
lightUpdate();
break;
/*


+ 9
- 10
code/espurna/led.cpp View File

@ -23,16 +23,14 @@ Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
// 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)
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);
}
pinMode(pin, OUTPUT);
status(false);
}
bool led_t::status() {
@ -462,9 +460,10 @@ void ledSetup() {
for (unsigned char index=0; index < LedsMax; ++index) {
const auto pin = getSetting({"ledGPIO", index}, _ledPin(index));
if (!gpioValid(pin)) {
if (!gpioLock(pin)) {
break;
}
_leds.emplace_back(
pin,
getSetting({"ledInv", index}, _ledInverse(index)),


+ 11
- 11
code/espurna/libs/BasePin.h View File

@ -11,6 +11,8 @@ Copyright (C) 2020 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
#include <Arduino.h>
#include <cstdint>
#include <memory>
#include "../config/types.h"
class BasePin {
@ -32,21 +34,19 @@ class BasePin {
// > This technique is unfortunate as it relies on detailed knowledge of how common toolchains work, and it may also require creating
// > a dummy virtual function.
explicit BasePin(unsigned char pin) :
pin(pin)
{}
virtual ~BasePin() {
}
virtual ~BasePin();
virtual String description() const;
virtual operator bool() {
return GPIO_NONE != pin;
}
virtual const char* id() const = 0;
virtual unsigned char pin() const = 0;
virtual void pinMode(int8_t mode) = 0;
virtual void digitalWrite(int8_t val) = 0;
virtual int digitalRead() = 0;
virtual String description() const = 0;
const unsigned char pin { GPIO_NONE };
operator bool() const {
return GPIO_NONE != pin();
}
};
using BasePinPtr = std::unique_ptr<BasePin>;

+ 5
- 7
code/espurna/libs/DebounceEvent.h View File

@ -74,7 +74,6 @@ namespace types {
EventReleased
};
using Pin = std::shared_ptr<BasePin>;
using EventHandler = std::function<void(const EventEmitter& self, types::Event event, uint8_t count, unsigned long length)>;
}
@ -83,21 +82,20 @@ class EventEmitter {
public:
EventEmitter(types::Pin pin, const types::Config& config = {types::Mode::Pushbutton, types::PinValue::High, types::PinMode::Input}, unsigned long delay = DebounceDelay, unsigned long repeat = RepeatDelay);
EventEmitter(types::Pin pin, types::EventHandler callback, const types::Config& = {types::Mode::Pushbutton, types::PinValue::High, types::PinMode::Input}, unsigned long delay = DebounceDelay, unsigned long repeat = RepeatDelay);
EventEmitter(BasePinPtr&& pin, const types::Config& config = {types::Mode::Pushbutton, types::PinValue::High, types::PinMode::Input}, unsigned long delay = DebounceDelay, unsigned long repeat = RepeatDelay);
EventEmitter(BasePinPtr&& pin, types::EventHandler callback, const types::Config& = {types::Mode::Pushbutton, types::PinValue::High, types::PinMode::Input}, unsigned long delay = DebounceDelay, unsigned long repeat = RepeatDelay);
types::Event loop();
bool isPressed();
const types::Pin getPin() const;
const types::Config getConfig() const;
const BasePinPtr& pin() const;
const types::Config& config() const;
unsigned long getEventLength();
unsigned long getEventCount();
private:
types::Pin _pin;
BasePinPtr _pin;
types::EventHandler _callback;
const types::Config _config;


+ 534
- 193
code/espurna/light.cpp
File diff suppressed because it is too large
View File


+ 76
- 18
code/espurna/light.h View File

@ -8,28 +8,77 @@
// TODO: lowercase
namespace Light {
constexpr size_t ChannelsMax = 5;
constexpr long VALUE_MIN = LIGHT_MIN_VALUE;
constexpr long VALUE_MAX = LIGHT_MAX_VALUE;
constexpr size_t Channels = LIGHT_CHANNELS;
constexpr size_t ChannelsMax = 5;
constexpr long BRIGHTNESS_MIN = LIGHT_MIN_BRIGHTNESS;
constexpr long BRIGHTNESS_MAX = LIGHT_MAX_BRIGHTNESS;
constexpr long VALUE_MIN = LIGHT_MIN_VALUE;
constexpr long VALUE_MAX = LIGHT_MAX_VALUE;
constexpr long PWM_MIN = LIGHT_MIN_PWM;
constexpr long PWM_MAX = LIGHT_MAX_PWM;
constexpr long PWM_LIMIT = LIGHT_LIMIT_PWM;
constexpr long BRIGHTNESS_MIN = LIGHT_MIN_BRIGHTNESS;
constexpr long BRIGHTNESS_MAX = LIGHT_MAX_BRIGHTNESS;
enum Communications : unsigned char {
COMMS_NONE = 0,
COMMS_NORMAL = 1 << 0,
COMMS_GROUP = 1 << 1
};
constexpr long PWM_MIN = LIGHT_MIN_PWM;
constexpr long PWM_MAX = LIGHT_MAX_PWM;
constexpr long PWM_LIMIT = LIGHT_LIMIT_PWM;
enum class Report {
None = 0,
Web = 1 << 0,
Mqtt = 1 << 1,
MqttGroup = 1 << 2,
Broker = 1 << 3
};
constexpr int operator|(Report lhs, int rhs) {
return static_cast<int>(lhs) | rhs;
}
constexpr int operator|(int lhs, Report rhs) {
return lhs | static_cast<int>(rhs);
}
constexpr int operator|(Report lhs, Report rhs) {
return static_cast<int>(lhs) | static_cast<int>(rhs);
}
constexpr int operator&(int lhs, Report rhs) {
return lhs & static_cast<int>(rhs);
}
constexpr int operator&(Report lhs, int rhs) {
return static_cast<int>(lhs) & rhs;
}
constexpr int DefaultReport {
Report::Web | Report::Mqtt | Report::MqttGroup | Report::Broker
};
} // namespace Light
using LightStateListener = std::function<void(bool)>;
class LightProvider {
public:
virtual void update() = 0;
virtual void state(bool) = 0;
virtual void channel(unsigned char ch, double value) = 0;
};
struct LightTransition {
unsigned long time;
unsigned long step;
};
size_t lightChannels();
unsigned int lightTransitionTime();
void lightTransitionTime(unsigned long ms);
LightTransition lightTransition();
unsigned long lightTransitionTime();
unsigned long lightTransitionStep();
void lightTransition(unsigned long time, unsigned long step);
void lightTransition(LightTransition transition);
void lightColor(const char* color, bool rgb);
void lightColor(const String& color, bool rgb);
@ -41,6 +90,9 @@ void lightColor(unsigned long color);
String lightColor(bool rgb);
String lightColor();
bool lightSave();
void lightSave(bool save);
void lightState(unsigned char i, bool state);
bool lightState(unsigned char i);
@ -56,13 +108,19 @@ void lightChannel(unsigned char id, long value);
void lightBrightnessStep(long steps, long multiplier = LIGHT_STEP);
void lightChannelStep(unsigned char id, long steps, long multiplier = LIGHT_STEP);
void lightUpdate(bool save, bool forward, bool group_forward);
void lightUpdate(bool save, bool forward);
void lightUpdate(bool save, LightTransition transition, Light::Report report);
void lightUpdate(bool save, LightTransition transition, int report);
void lightUpdate(LightTransition transition);
void lightUpdate(bool save);
void lightUpdate();
bool lightHasColor();
bool lightUseCCT();
void lightMQTT();
void lightSetupChannels(unsigned char size);
void lightSetStateListener(LightStateListener);
void lightSetProvider(std::unique_ptr<LightProvider>&&);
bool lightAdd();
void lightSetup();

+ 1
- 1
code/espurna/main.cpp View File

@ -299,7 +299,7 @@ void setup() {
displaySetup();
#endif
#if TUYA_SUPPORT
Tuya::tuyaSetup();
tuya::setup();
#endif
#if KINGART_CURTAIN_SUPPORT
kingartCurtainSetup();


+ 46
- 1
code/espurna/mcp23s08.cpp View File

@ -15,8 +15,9 @@ Copyright (C) 2016 Plamen Kovandjiev <p.kovandiev@kmpelectronics.eu> & Dimitar A
#if MCP23S08_SUPPORT
#include <SPI.h>
#include "mcp23s08_pin.h"
#include <SPI.h>
#include <bitset>
// TODO: check if this needed for SPI operation
@ -42,6 +43,45 @@ Copyright (C) 2016 Plamen Kovandjiev <p.kovandiev@kmpelectronics.eu> & Dimitar A
static uint8_t _mcp23s08TxData[16] __attribute__((aligned(4)));
static uint8_t _mcp23s08RxData[16] __attribute__((aligned(4)));
namespace {
class GpioMcp23s08 : public GpioBase {
public:
constexpr static size_t Pins { 8ul };
using Pin = McpGpioPin;
using Mask = std::bitset<Pins>;
const char* id() const {
return "mcp23s08";
}
size_t pins() const {
return Pins;
}
bool lock(unsigned char index) const override {
return _lock[index];
}
void lock(unsigned char index, bool value) override {
_lock.set(index, value);
}
bool valid(unsigned char index) const override {
return (index < Pins);
}
BasePinPtr pin(unsigned char index) override {
return std::make_unique<McpGpioPin>(index);
}
private:
Mask _lock;
};
} // namespace
void MCP23S08Setup()
{
DEBUG_MSG_P(PSTR("[MCP23S08] Initialize SPI bus\n"));
@ -168,4 +208,9 @@ bool mcpGpioValid(unsigned char gpio)
return gpio < McpGpioPins;
}
GpioBase& mcp23s08Gpio() {
static GpioMcp23s08 gpio;
return gpio;
}
#endif // MCP23S08_SUPPORT

+ 1
- 0
code/espurna/mcp23s08.h View File

@ -27,3 +27,4 @@ void MCP23S08SetPin(uint8_t pinNumber, bool state);
bool MCP23S08GetPin(uint8_t pinNumber);
bool mcpGpioValid(unsigned char gpio);
GpioBase& mcp23s08Gpio();

+ 16
- 10
code/espurna/mcp23s08_pin.h View File

@ -16,28 +16,34 @@ Copyright (C) 2016 Plamen Kovandjiev <p.kovandiev@kmpelectronics.eu> & Dimitar A
#include "libs/BasePin.h"
#include "mcp23s08.h"
class McpGpioPin final : public BasePin {
public:
#include <bitset>
class McpGpioPin final : public BasePin {
public:
explicit McpGpioPin(unsigned char pin) :
BasePin(pin)
_pin(pin)
{}
void pinMode(int8_t mode) override {
::MCP23S08SetDirection(this->pin, mode);
::MCP23S08SetDirection(_pin, mode);
}
void digitalWrite(int8_t val) override {
::MCP23S08SetPin(this->pin, val);
::MCP23S08SetPin(_pin, val);
}
int digitalRead() override {
return ::MCP23S08GetPin(this->pin);
return ::MCP23S08GetPin(_pin);
}
String description() const override {
static String desc(String(F("McpGpioPin @ GPIO")) + static_cast<int>(pin));
return desc;
unsigned char pin() const override {
return _pin;
}
};
const char* id() const override {
return "McpGpioPin";
}
private:
unsigned char _pin { GPIO_NONE };
};

+ 693
- 439
code/espurna/relay.cpp
File diff suppressed because it is too large
View File


+ 46
- 3
code/espurna/relay.h View File

@ -11,10 +11,47 @@ Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
#include "espurna.h"
#include "rpc.h"
#include <bitset>
constexpr size_t RelaysMax = 32;
enum class RelayType : int {
None,
Normal,
Inverse,
Latched,
LatchedInverse
};
enum class RelayProvider: int {
None,
Dummy,
Gpio,
Dual,
Stm
};
class RelayProviderBase {
public:
RelayProviderBase() = default;
virtual ~RelayProviderBase();
virtual void dump();
// whether the provider is ready
virtual bool setup();
// status requested at boot
virtual void boot(bool status);
// when 'status' was requested, but target status remains the same or is canceled
virtual void notify(bool status);
// when relay 'status' is changed from target to current
virtual void change(bool status) = 0;
// unique id of the provider
virtual const char* id() const = 0;
};
PayloadStatus relayParsePayload(const char * payload);
bool relayStatus(unsigned char id, bool status, bool report, bool group_report);
@ -42,7 +79,13 @@ void relayMQTT();
void relayPulse(unsigned char id);
void relaySync(unsigned char id);
void relaySave(bool eeprom);
void relaySave(bool persist);
using RelayStatusCallback = void(*)(unsigned char id, bool status);
bool relayAdd(std::unique_ptr<RelayProviderBase>&& provider);
void relaySetStatusNotify(RelayStatusCallback);
void relaySetStatusChange(RelayStatusCallback);
void relaySetupDummy(size_t size, bool reconfigure = false);
void relaySetup();

+ 43
- 5
code/espurna/relay_config.h View File

@ -8,7 +8,7 @@ RELAY MODULE
#include "espurna.h"
constexpr const unsigned long _relayDelayOn(unsigned char index) {
constexpr unsigned long _relayDelayOn(unsigned char index) {
return (
(index == 0) ? RELAY1_DELAY_ON :
(index == 1) ? RELAY2_DELAY_ON :
@ -21,7 +21,7 @@ constexpr const unsigned long _relayDelayOn(unsigned char index) {
);
}
constexpr const unsigned long _relayDelayOff(unsigned char index) {
constexpr unsigned long _relayDelayOff(unsigned char index) {
return (
(index == 0) ? RELAY1_DELAY_OFF :
(index == 1) ? RELAY2_DELAY_OFF :
@ -34,7 +34,7 @@ constexpr const unsigned long _relayDelayOff(unsigned char index) {
);
}
constexpr const unsigned char _relayPin(unsigned char index) {
constexpr unsigned char _relayPin(unsigned char index) {
return (
(index == 0) ? RELAY1_PIN :
(index == 1) ? RELAY2_PIN :
@ -47,7 +47,7 @@ constexpr const unsigned char _relayPin(unsigned char index) {
);
}
constexpr const unsigned char _relayType(unsigned char index) {
constexpr RelayType _relayType(unsigned char index) {
return (
(index == 0) ? RELAY1_TYPE :
(index == 1) ? RELAY2_TYPE :
@ -60,7 +60,20 @@ constexpr const unsigned char _relayType(unsigned char index) {
);
}
constexpr const unsigned char _relayResetPin(unsigned char index) {
constexpr GpioType _relayPinType(unsigned char index) {
return (
(index == 0) ? RELAY1_PIN_TYPE :
(index == 1) ? RELAY2_PIN_TYPE :
(index == 2) ? RELAY3_PIN_TYPE :
(index == 3) ? RELAY4_PIN_TYPE :
(index == 4) ? RELAY5_PIN_TYPE :
(index == 5) ? RELAY6_PIN_TYPE :
(index == 6) ? RELAY7_PIN_TYPE :
(index == 7) ? RELAY8_PIN_TYPE : GPIO_TYPE_NONE
);
}
constexpr unsigned char _relayResetPin(unsigned char index) {
return (
(index == 0) ? RELAY1_RESET_PIN :
(index == 1) ? RELAY2_RESET_PIN :
@ -73,3 +86,28 @@ constexpr const unsigned char _relayResetPin(unsigned char index) {
);
}
constexpr int _relayBootMode(unsigned char index) {
return (
(index == 0) ? RELAY1_BOOT_MODE :
(index == 1) ? RELAY2_BOOT_MODE :
(index == 2) ? RELAY3_BOOT_MODE :
(index == 3) ? RELAY4_BOOT_MODE :
(index == 4) ? RELAY5_BOOT_MODE :
(index == 5) ? RELAY6_BOOT_MODE :
(index == 6) ? RELAY7_BOOT_MODE :
(index == 7) ? RELAY8_BOOT_MODE : GPIO_NONE
);
}
constexpr RelayProvider _relayProvider(unsigned char index) {
return (
(index == 0) ? (RELAY1_PROVIDER) :
(index == 1) ? (RELAY2_PROVIDER) :
(index == 2) ? (RELAY3_PROVIDER) :
(index == 3) ? (RELAY4_PROVIDER) :
(index == 4) ? (RELAY5_PROVIDER) :
(index == 5) ? (RELAY6_PROVIDER) :
(index == 6) ? (RELAY7_PROVIDER) :
(index == 7) ? (RELAY8_PROVIDER) : RelayProvider::None
);
}

+ 10
- 6
code/espurna/rfbridge.cpp View File

@ -1244,21 +1244,20 @@ void rfbSetup() {
auto rx = getSetting("rfbRX", RFB_RX_PIN);
auto tx = getSetting("rfbTX", RFB_TX_PIN);
// TODO: tag gpioGetLock with a NAME string, skip log here
_rfb_receive = gpioValid(rx);
_rfb_transmit = gpioValid(tx);
if (!_rfb_transmit && !_rfb_receive) {
if ((GPIO_NONE == rx) && (GPIO_NONE == tx)) {
DEBUG_MSG_P(PSTR("[RF] Neither RX or TX are set\n"));
return;
}
_rfb_modem = new RCSwitch();
if (_rfb_receive) {
if (gpioLock(rx)) {
_rfb_receive = true;
_rfb_modem->enableReceive(rx);
DEBUG_MSG_P(PSTR("[RF] RF receiver on GPIO %u\n"), rx);
}
if (_rfb_transmit) {
if (gpioLock(tx)) {
auto transmit = getSetting("rfbTransmit", RFB_TRANSMIT_REPEATS);
_rfb_transmit = true;
_rfb_modem->enableTransmit(tx);
_rfb_modem->setRepeatTransmit(transmit);
DEBUG_MSG_P(PSTR("[RF] RF transmitter on GPIO %u\n"), tx);
@ -1267,6 +1266,11 @@ void rfbSetup() {
#endif
#if RELAY_SUPPORT
relaySetStatusNotify(rfbStatus);
relaySetStatusChange(rfbStatus);
#endif
#if MQTT_SUPPORT
mqttRegister(_rfbMqttCallback);
#endif


+ 3
- 3
code/espurna/rpnrules.cpp View File

@ -560,15 +560,15 @@ void _rpnInit() {
#endif // RELAY_SUPPORT == 1
// Channel operators
#if RELAY_PROVIDER == RELAY_PROVIDER_LIGHT
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
rpn_operator_set(_rpn_ctxt, "update", 0, [](rpn_context & ctxt) -> rpn_error {
lightUpdate(true, true);
lightUpdate();
return 0;
});
rpn_operator_set(_rpn_ctxt, "black", 0, [](rpn_context & ctxt) -> rpn_error {
lightColor((unsigned long) 0);
lightColor(0ul);
return 0;
});


+ 2
- 2
code/espurna/scheduler.cpp View File

@ -126,7 +126,7 @@ void _schConfigure() {
PSTR("[SCH] Schedule #%d: %s #%d to %d at %02d:%02d %s on %s%s\n"),
i, sch_type, sch_switch,
sch_action, sch_hour, sch_minute, sch_utc ? "UTC" : "local time",
(char *) sch_weekdays.c_str(),
sch_weekdays.c_str(),
sch_enabled ? "" : " (disabled)"
);
@ -174,7 +174,7 @@ void _schAction(unsigned char sch_id, int sch_action, int sch_switch) {
if (SCHEDULER_TYPE_DIM == sch_type) {
DEBUG_MSG_P(PSTR("[SCH] Set channel %d value to %d\n"), sch_switch, sch_action);
lightChannel(sch_switch, sch_action);
lightUpdate(true, true);
lightUpdate();
}
#endif


+ 4
- 2
code/espurna/sensor.cpp View File

@ -1794,7 +1794,8 @@ void _sensorLoad() {
}
};
for (unsigned char index = 0; index < GpioPins; ++index) {
auto pins = gpioPins();
for (unsigned char index = 0; index < pins; ++index) {
const auto pin = getPin(index);
if (pin == GPIO_NONE) break;
@ -1913,7 +1914,8 @@ void _sensorLoad() {
}
};
for (unsigned char index = 0; index < GpioPins; ++index) {
auto pins = gpioPins();
for (unsigned char index = 0; index < pins; ++index) {
const auto pin = getPin(index);
if (pin == GPIO_NONE) break;


+ 8
- 4
code/espurna/sensors/DHTSensor.h View File

@ -9,6 +9,7 @@
#include <Arduino.h>
#include "../gpio.h"
#include "../utils.h"
#include "BaseSensor.h"
@ -65,7 +66,7 @@ class DHTSensor : public BaseSensor {
}
~DHTSensor() {
if (_previous != GPIO_NONE) gpioReleaseLock(_previous);
gpioUnlock(_gpio);
}
// ---------------------------------------------------------------------
@ -101,10 +102,13 @@ class DHTSensor : public BaseSensor {
_count = 0;
// Manage GPIO lock
if (_previous != GPIO_NONE) gpioReleaseLock(_previous);
// Manage GPIO lock (note that this only handles the basic *hw* I/O)
if (_previous != GPIO_NONE) {
gpioUnlock(_previous);
}
_previous = GPIO_NONE;
if (!gpioGetLock(_gpio)) {
if (!gpioLock(_gpio)) {
_error = SENSOR_ERROR_GPIO_USED;
return;
}


+ 7
- 4
code/espurna/sensors/DallasSensor.h View File

@ -76,7 +76,7 @@ class DallasSensor : public BaseSensor {
~DallasSensor() {
if (_wire) delete _wire;
if (_previous != GPIO_NONE) gpioReleaseLock(_previous);
gpioUnlock(_gpio);
}
// ---------------------------------------------------------------------
@ -103,9 +103,12 @@ class DallasSensor : public BaseSensor {
if (!_dirty) return;
// Manage GPIO lock
if (_previous != GPIO_NONE) gpioReleaseLock(_previous);
if (_previous != GPIO_NONE) {
gpioUnlock(_previous);
}
_previous = GPIO_NONE;
if (!gpioGetLock(_gpio)) {
if (!gpioLock(_gpio)) {
_error = SENSOR_ERROR_GPIO_USED;
return;
}
@ -125,7 +128,7 @@ class DallasSensor : public BaseSensor {
// Check connection
if (_count == 0) {
gpioReleaseLock(_gpio);
gpioUnlock(_gpio);
} else {
_previous = _gpio;
}


+ 6
- 3
code/espurna/sensors/GUVAS12SDSensor.h View File

@ -42,7 +42,7 @@ class GUVAS12SDSensor : public BaseSensor {
}
~GUVAS12SDSensor() {
if (_previous != GPIO_NONE) gpioReleaseLock(_previous);
gpioUnlock(_gpio);
}
// ---------------------------------------------------------------------
@ -65,9 +65,12 @@ class GUVAS12SDSensor : public BaseSensor {
void begin() {
// Manage GPIO lock
if (_previous != GPIO_NONE) gpioReleaseLock(_previous);
if (_previous != GPIO_NONE) {
gpioUnlock(_previous);
}
_previous = GPIO_NONE;
if (!gpioGetLock(_gpio)) {
if (!gpioLock(_gpio)) {
_error = SENSOR_ERROR_GPIO_USED;
return;
}


+ 13
- 6
code/espurna/settings.h View File

@ -13,6 +13,7 @@ Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
#include <functional>
#include <utility>
#include <vector>
#include <type_traits>
#include <ArduinoJson.h>
@ -114,6 +115,14 @@ using settings_cfg_list_t = std::initializer_list<settings_cfg_t>;
namespace settings {
namespace internal {
template <typename T>
using is_arduino_string = std::is_same<String, typename std::decay<T>::type>;
template <typename T>
using enable_if_arduino_string = std::enable_if<is_arduino_string<T>::value>;
// --------------------------------------------------------------------------
uint32_t u32fromString(const String& string, int base);
template <typename T>
@ -124,6 +133,9 @@ T convert(const String& value);
// --------------------------------------------------------------------------
template <>
GpioType convert(const String& value);
template <>
float convert(const String& value);
@ -154,12 +166,7 @@ unsigned char convert(const String& value);
template<typename T>
String serialize(const T& value);
template<typename T>
String serialize(const T& value) {
return String(value);
}
} // namespace settings::internal
} // namespace internal
} // namespace settings
// --------------------------------------------------------------------------


+ 3122
- 3106
code/espurna/static/index.all.html.gz.h
File diff suppressed because it is too large
View File


+ 1931
- 1924
code/espurna/static/index.curtain.html.gz.h
File diff suppressed because it is too large
View File


+ 2423
- 2416
code/espurna/static/index.garland.html.gz.h
File diff suppressed because it is too large
View File


+ 2870
- 2853
code/espurna/static/index.light.html.gz.h
File diff suppressed because it is too large
View File


+ 1908
- 1901
code/espurna/static/index.lightfox.html.gz.h
File diff suppressed because it is too large
View File


+ 1933
- 1926
code/espurna/static/index.rfbridge.html.gz.h
File diff suppressed because it is too large
View File


+ 3838
- 3830
code/espurna/static/index.rfm69.html.gz.h
File diff suppressed because it is too large
View File


+ 1978
- 1971
code/espurna/static/index.sensor.html.gz.h
File diff suppressed because it is too large
View File


+ 2384
- 2377
code/espurna/static/index.small.html.gz.h
File diff suppressed because it is too large
View File


+ 1919
- 1912
code/espurna/static/index.thermostat.html.gz.h
File diff suppressed because it is too large
View File


+ 0
- 8
code/espurna/system.cpp View File

@ -243,19 +243,11 @@ void systemLoop() {
}
void _systemSetupSpecificHardware() {
//The ESPLive has an ADC MUX which needs to be configured.
#if defined(MANCAVEMADE_ESPLIVE)
pinMode(16, OUTPUT);
digitalWrite(16, HIGH); //Defualt CT input (pin B, solder jumper B)
#endif
// These devices use the hardware UART
// to communicate to secondary microcontrollers
#if (RFB_SUPPORT && (RFB_PROVIDER == RFB_PROVIDER_EFM8BB1)) || (RELAY_PROVIDER == RELAY_PROVIDER_DUAL) || (RELAY_PROVIDER == RELAY_PROVIDER_STM)
Serial.begin(SERIAL_BAUDRATE);
#endif
}
void systemSetup() {


+ 4
- 3
code/espurna/terminal.cpp View File

@ -313,7 +313,7 @@ void _terminalInitCommands() {
}
int start = 0;
int end = GpioPins;
int end = gpioPins();
switch (ctx.argc) {
case 3:
@ -327,8 +327,9 @@ void _terminalInitCommands() {
case 1:
for (auto current = start; current < end; ++current) {
if (gpioValid(current)) {
ctx.output.printf_P(PSTR("%s @ GPIO%02d (%s)\n"),
GPEP(current) ? "OUTPUT" : " INPUT",
ctx.output.printf_P(PSTR("%c %s @ GPIO%02d (%s)\n"),
gpioLocked(current) ? '*' : ' ',
GPEP(current) ? "OUTPUT" : "INPUT ",
current,
(HIGH == digitalRead(current)) ? "HIGH" : "LOW"
);


+ 360
- 279
code/espurna/tuya.cpp View File

@ -19,6 +19,7 @@ Copyright (C) 2019 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
#include <functional>
#include <queue>
#include <forward_list>
#include <StreamString.h>
#include "tuya_types.h"
@ -27,168 +28,319 @@ Copyright (C) 2019 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
#include "tuya_protocol.h"
#include "tuya_util.h"
namespace Tuya {
namespace tuya {
constexpr size_t SERIAL_SPEED { 9600u };
bool operator<(const DataFrame& lhs, const DataFrame& rhs) {
if ((lhs.command() != Command::Heartbeat) && (rhs.command() == Command::Heartbeat)) {
return true;
}
constexpr unsigned char SWITCH_MAX { 8u };
constexpr unsigned char DIMMER_MAX { 5u };
return false;
}
constexpr uint32_t DISCOVERY_TIMEOUT { 1500u };
constexpr unsigned long SerialSpeed { 9600u };
constexpr uint32_t HEARTBEAT_SLOW { 9000u };
constexpr uint32_t HEARTBEAT_FAST { 3000u };
constexpr unsigned long DiscoveryTimeout { 1500u };
// --------------------------------------------
constexpr unsigned long HeartbeatSlow { 9000u };
constexpr unsigned long HeartbeatFast { 3000u };
constexpr unsigned long HeartbeatVeryFast { 200u };
struct dp_states_filter_t {
using type = unsigned char;
static const type NONE = 0;
static const type BOOL = 1 << 0;
static const type INT = 1 << 1;
static const type ALL = (INT | BOOL);
constexpr unsigned long HeartbeatIncrement { 200u };
static type clamp(type value) {
return constrain(value, NONE, ALL);
}
struct Config {
Config(const Config&) = delete;
Config(Config&& other) noexcept :
key(std::move(other.key)),
value(std::move(other.value))
{}
Config(String&& key_, String&& value_) noexcept :
key(std::move(key_)),
value(std::move(value_))
{}
String key;
String value;
};
size_t getHeartbeatInterval(Heartbeat hb) {
switch (hb) {
case Heartbeat::FAST:
return HEARTBEAT_FAST;
case Heartbeat::SLOW:
return HEARTBEAT_SLOW;
case Heartbeat::NONE:
default:
return 0;
}
Transport tuyaSerial(TUYA_SERIAL);
std::priority_queue<DataFrame> outputFrames;
template <typename T>
void send(unsigned char dp, T value) {
outputFrames.emplace(
Command::SetDP, DataProtocol<T>(dp, value).serialize()
);
}
uint8_t getWiFiState() {
// --------------------------------------------
uint8_t state = wifiState();
if (state & WIFI_STATE_SMARTCONFIG) return 0x00;
if (state & WIFI_STATE_AP) return 0x01;
if (state & WIFI_STATE_STA) return 0x04;
Discovery discovery(DiscoveryTimeout);
OnceFlag configDone;
return 0x02;
}
bool transportDebug { false };
bool reportWiFi { false };
bool filter { false };
// TODO: is v2 required to modify pin assigments?
void updatePins(uint8_t led, uint8_t rst) {
setSetting("ledGPIO0", led);
setSetting("btnGPIO0", rst);
//espurnaReload();
}
String product;
std::forward_list<Config> config;
DpMap switchIds;
#if LIGHT_PROVIDER == LIGHT_PROVIDER_CUSTOM
DpMap channelIds;
StateId channelStateId;
#endif
// --------------------------------------------
States<bool> switchStates(SWITCH_MAX);
#if LIGHT_PROVIDER == LIGHT_PROVIDER_TUYA
States<uint32_t> channelStates(DIMMER_MAX);
#endif
class TuyaRelayProvider : public RelayProviderBase {
public:
explicit TuyaRelayProvider(unsigned char dp) :
_dp(dp)
{}
// Handle DP data from the MCU, mapping incoming DP ID to the specific relay / channel ID
const char* id() const {
return "tuya";
}
void applySwitch() {
for (unsigned char id=0; id < switchStates.size(); ++id) {
relayStatus(id, switchStates[id].value);
void change(bool status) {
send(_dp, status);
}
}
private:
unsigned char _dp;
};
#if LIGHT_PROVIDER == LIGHT_PROVIDER_CUSTOM
#if LIGHT_PROVIDER == LIGHT_PROVIDER_TUYA
void applyChannel() {
for (unsigned char id=0; id < channelStates.size(); ++id) {
lightChannel(id, channelStates[id].value);
class TuyaLightProvider : public LightProvider {
public:
TuyaLightProvider() = default;
explicit TuyaLightProvider(const DpMap& channels) :
_channels(channels)
{}
explicit TuyaLightProvider(const DpMap& channels, StateId* state) :
_channels(channels),
_state(state)
{}
void update() override {
}
void state(bool status) override {
if (_state && *_state) {
_state->filter(false);
if (!status) {
send(_state->id(), status);
}
}
lightUpdate(true, true);
}
#endif
void channel(unsigned char channel, double value) override {
auto* entry = _channels.find_local(channel);
if (!entry) {
return;
}
// tuya dimmer is precious, and can't handle 0...some-kind-of-threshold
// just ignore it, the associated switch will handle turning it off
auto rounded = static_cast<unsigned int>(value);
if (rounded <= 0x10) {
return;
}
// Filtering for incoming data
// ref. https://github.com/xoseperez/espurna/issues/2222
// TODO: should be fixed when relay & channel transition states are implemented as transactions?
if (_state && *_state) {
_state->filter(true);
}
send(entry->dp_id, rounded);
}
private:
const DpMap& _channels;
StateId* _state { nullptr };
};
#endif
// --------------------------------------------
Transport tuyaSerial(TUYA_SERIAL);
std::queue<DataFrame> outputFrames;
uint8_t getWiFiState() {
uint8_t state = wifiState();
if (state & WIFI_STATE_SMARTCONFIG) return 0x00;
if (state & WIFI_STATE_AP) return 0x01;
if (state & WIFI_STATE_STA) return 0x04;
DiscoveryTimeout discoveryTimeout(DISCOVERY_TIMEOUT);
bool transportDebug = false;
bool configDone = false;
bool reportWiFi = false;
dp_states_filter_t::type filterDP = dp_states_filter_t::NONE;
return 0x02;
}
String product;
// --------------------------------------------
void addConfig(String&& key, String&& value) {
Config kv{std::move(key), std::move(value)};
config.push_front(std::move(kv));
}
void updatePins(uint8_t led, uint8_t rst) {
static bool done { false };
if (!done) {
addConfig("ledGPIO0", String(led));
addConfig("btnGPIO0", String(rst));
done = true;
}
}
void showProduct() {
if (product.length()) DEBUG_MSG_P(PSTR("[TUYA] Product: %s\n"), product.c_str());
DEBUG_MSG_P(PSTR("[TUYA] Product: %s\n"), product.length() ? product.c_str() : "(unknown)");
}
inline void dataframeDebugSend(const char* tag, const DataFrame& frame) {
template <typename T>
void dataframeDebugSend(const char* tag, const T& frame) {
if (!transportDebug) return;
StreamString out;
Output writer(out, frame.length);
Output writer(out, frame.length());
writer.writeHex(frame.serialize());
DEBUG_MSG("[TUYA] %s: %s\n", tag, out.c_str());
}
void sendHeartbeat(Heartbeat hb, State state) {
unsigned long heartbeatInterval(Heartbeat heartbeat) {
static unsigned long interval { 0ul };
static uint32_t last = 0;
if (millis() - last > getHeartbeatInterval(hb)) {
outputFrames.emplace(Command::Heartbeat);
last = millis();
switch (heartbeat) {
case Heartbeat::Boot:
if (interval < HeartbeatFast) {
interval += HeartbeatIncrement;
} else {
interval = HeartbeatFast;
}
break;
case Heartbeat::Fast:
interval = HeartbeatFast;
break;
case Heartbeat::Slow:
interval = HeartbeatSlow;
break;
case Heartbeat::None:
interval = 0;
break;
}
return interval;
}
void sendHeartbeat(Heartbeat heartbeat) {
static unsigned long interval = 0ul;
static unsigned long last = millis() + 1ul;
if (millis() - last > interval) {
interval = heartbeatInterval(heartbeat);
last = millis();
outputFrames.emplace(Command::Heartbeat);
}
}
void sendWiFiStatus() {
if (!reportWiFi) return;
outputFrames.emplace(
Command::WiFiStatus, std::initializer_list<uint8_t> { getWiFiState() }
);
if (reportWiFi) {
outputFrames.emplace(
Command::WiFiStatus, std::initializer_list<uint8_t> { getWiFiState() }
);
}
}
void pushOrUpdateState(const Type type, const DataFrame& frame) {
if (Type::BOOL == type) {
const DataProtocol<bool> proto(frame);
switchStates.pushOrUpdate(proto.id(), proto.value());
//DEBUG_MSG_P(PSTR("[TUYA] apply BOOL id=%02u value=%s\n"), proto.id(), proto.value() ? "true" : "false");
} else if (Type::INT == type) {
#if LIGHT_PROVIDER == LIGHT_PROVIDER_TUYA
const DataProtocol<uint32_t> proto(frame);
channelStates.pushOrUpdate(proto.id(), proto.value());
//DEBUG_MSG_P(PSTR("[TUYA] apply INT id=%02u value=%u\n"), proto.id(), proto.value());
#endif
void updateState(const DataProtocol<bool>& proto) {
#if LIGHT_PROVIDER == LIGHT_PROVIDER_CUSTOM
if (channelStateId && (channelStateId.id() == proto.id())) {
// See above. Ignore the selected state ID while we are sending the data,
// to avoid resetting the state to ON while we are turning OFF
// (and vice versa)
if (!channelStateId.filter()) {
lightState(proto.value());
}
return;
}
#endif
auto* entry = switchIds.find_dp(proto.id());
if (!entry) {
return;
}
relayStatus(entry->local_id, proto.value());
}
void updateState(const DataProtocol<uint32_t>& proto) {
}
// XXX: sometimes we need to ignore incoming state, when not in discovery mode
// XXX: sometimes we need to ignore incoming state
// ref: https://github.com/xoseperez/espurna/issues/1729#issuecomment-509234195
void updateState(const Type type, const DataFrame& frame) {
template <typename T>
void updateState(Type type, const T& frame) {
if (Type::BOOL == type) {
if (filterDP & dp_states_filter_t::BOOL) return;
const DataProtocol<bool> proto(frame);
switchStates.update(proto.id(), proto.value());
DataProtocol<bool> proto(frame.data());
updateState(proto);
} else if (Type::INT == type) {
if (filterDP & dp_states_filter_t::INT) return;
#if LIGHT_PROVIDER == LIGHT_PROVIDER_TUYA
const DataProtocol<uint32_t> proto(frame);
channelStates.update(proto.id(), proto.value());
#endif
DataProtocol<uint32_t> proto(frame.data());
updateState(proto);
}
}
void processDP(State state, const DataFrame& frame) {
void updateDiscovered(Discovery&& discovery) {
auto& dps = discovery.get();
// TODO: do not log protocol errors without transport debug enabled
if (!frame.length) {
DEBUG_MSG_P(PSTR("[TUYA] DP frame must have data\n"));
return;
if (configDone) {
goto error;
}
const Type type {dataType(frame)};
for (auto& dp : dps) {
switch (dp.type) {
case Type::BOOL:
if (!switchIds.add(relayCount(), dp.id)) {
DEBUG_MSG_P(PSTR("[TUYA] Switch for DP id=%u already exists\n"), dp.id);
goto error;
}
if (!relayAdd(std::make_unique<TuyaRelayProvider>(dp.id))) {
DEBUG_MSG_P(PSTR("[TUYA] Cannot add relay for DP id=%u\n"), dp.id);
goto error;
}
break;
case Type::INT:
#if LIGHT_PROVIDER == LIGHT_PROVIDER_CUSTOM
if (!channelIds.add(lightChannels(), dp.id)) {
DEBUG_MSG_P(PSTR("[TUYA] Channel for DP id=%u already exists\n"), dp.id);
goto error;
}
if (!lightAdd()) {
DEBUG_MSG_P(PSTR("[TUYA] Cannot add channel for DP id=%u\n"), dp.id);
goto error;
}
#endif
break;
default:
break;
}
}
#if LIGHT_PROVIDER == LIGHT_PROVIDER_CUSTOM
if (channelIds.size()) {
lightSetProvider(std::make_unique<TuyaLightProvider>(channelIds));
}
#endif
error:
dps.clear();
}
template <typename T>
void processDP(State state, const T& frame) {
auto type = dataType(frame);
if (Type::UNKNOWN == type) {
if (frame.length >= 2) {
if (frame.length() >= 2) {
DEBUG_MSG_P(PSTR("[TUYA] Unknown DP id=%u type=%u\n"), frame[0], frame[1]);
} else {
DEBUG_MSG_P(PSTR("[TUYA] Invalid DP frame\n"));
@ -197,43 +349,49 @@ namespace Tuya {
}
if (State::DISCOVERY == state) {
discoveryTimeout.feed();
pushOrUpdateState(type, frame);
} else {
discovery.add(type, frame[0]);
} else if (!filter) {
updateState(type, frame);
}
}
void processFrame(State& state, const Transport& buffer) {
const DataFrame frame(buffer);
const DataFrameView frame(buffer);
dataframeDebugSend("<=", frame);
// initial packet has 0, do the initial setup
// all after that have 1. might be a good idea to re-do the setup when that happens on boot
if (frame.commandEquals(Command::Heartbeat) && (frame.length == 1)) {
if (State::HEARTBEAT == state) {
if ((frame[0] == 0) || !configDone) {
DEBUG_MSG_P(PSTR("[TUYA] Starting configuration ...\n"));
if ((frame.command() == Command::Heartbeat) && (frame.length() == 1)) {
if (State::BOOT == state) {
if (!configDone) {
DEBUG_MSG_P(PSTR("[TUYA] Attempting to configure the board ...\n"));
#if LIGHT_PROVIDER == LIGHT_PROVIDER_CUSTOM
setupChannels();
#endif
setupSwitches();
}
if (!configDone) {
DEBUG_MSG_P(PSTR("[TUYA] Starting discovery\n"));
state = State::QUERY_PRODUCT;
return;
} else {
DEBUG_MSG_P(PSTR("[TUYA] Already configured\n"));
state = State::IDLE;
}
state = State::IDLE;
}
sendWiFiStatus();
return;
}
if (frame.commandEquals(Command::QueryProduct) && frame.length) {
if ((frame.command() == Command::QueryProduct) && frame.length()) {
if (product.length()) {
product = "";
}
product.reserve(frame.length);
for (unsigned int n = 0; n < frame.length; ++n) {
product.reserve(frame.length());
for (unsigned int n = 0; n < frame.length(); ++n) {
product += static_cast<char>(frame[n]);
}
showProduct();
@ -241,13 +399,13 @@ namespace Tuya {
return;
}
if (frame.commandEquals(Command::QueryMode)) {
if (frame.command() == Command::QueryMode) {
// first and second byte are GPIO pin for WiFi status and RST respectively
if (frame.length == 2) {
if (frame.length() == 2) {
DEBUG_MSG_P(PSTR("[TUYA] Mode: ESP only, led=GPIO%02u rst=GPIO%02u\n"), frame[0], frame[1]);
updatePins(frame[0], frame[1]);
// ... or nothing. we need to report wifi status to the mcu via Command::WiFiStatus
} else if (!frame.length) {
} else if (!frame.length()) {
DEBUG_MSG_P(PSTR("[TUYA] Mode: ESP & MCU\n"));
reportWiFi = true;
sendWiFiStatus();
@ -256,23 +414,23 @@ namespace Tuya {
return;
}
if (frame.commandEquals(Command::WiFiResetCfg) && !frame.length) {
if ((frame.command() == Command::WiFiResetCfg) && !frame.length()) {
DEBUG_MSG_P(PSTR("[TUYA] WiFi reset request\n"));
outputFrames.emplace(Command::WiFiResetCfg);
return;
}
if (frame.commandEquals(Command::WiFiResetSelect) && (frame.length == 1)) {
if ((frame.command() == Command::WiFiResetSelect) && (frame.length() == 1)) {
DEBUG_MSG_P(PSTR("[TUYA] WiFi configuration mode request: %s\n"),
(frame[0] == 0) ? "Smart Config" : "AP");
outputFrames.emplace(Command::WiFiResetSelect);
return;
}
if (frame.commandEquals(Command::ReportDP) && frame.length) {
if ((frame.command() == Command::ReportDP) && frame.length()) {
processDP(state, frame);
if (state == State::DISCOVERY) return;
if (state == State::HEARTBEAT) return;
if (state == State::BOOT) return;
state = State::IDLE;
return;
}
@ -298,84 +456,9 @@ namespace Tuya {
}
// Push local state data, mapping it to the appropriate DP
void tuyaSendSwitch(unsigned char id) {
if (id >= switchStates.size()) return;
outputFrames.emplace(
Command::SetDP, DataProtocol<bool>(switchStates[id].dp, switchStates[id].value).serialize()
);
}
void tuyaSendSwitch(unsigned char id, bool value) {
if (id >= switchStates.size()) return;
if (value == switchStates[id].value) return;
switchStates[id].value = value;
tuyaSendSwitch(id);
}
#if LIGHT_PROVIDER == LIGHT_PROVIDER_TUYA
void tuyaSendChannel(unsigned char id) {
if (id >= channelStates.size()) return;
outputFrames.emplace(
Command::SetDP, DataProtocol<uint32_t>(channelStates[id].dp, channelStates[id].value).serialize()
);
}
void tuyaSendChannel(unsigned char id, unsigned int value) {
if (id >= channelStates.size()) return;
if (value == channelStates[id].value) return;
channelStates[id].value = value;
tuyaSendChannel(id);
}
#endif
void brokerCallback(const String& topic, unsigned char id, unsigned int value) {
// Only process status messages for switches and channels
if (!topic.equals(MQTT_TOPIC_CHANNEL)
&& !topic.equals(MQTT_TOPIC_RELAY)) {
return;
}
#if (RELAY_PROVIDER == RELAY_PROVIDER_LIGHT) && (LIGHT_PROVIDER == LIGHT_PROVIDER_TUYA)
if (topic.equals(MQTT_TOPIC_CHANNEL)) {
if (lightState(id) != switchStates[id].value) {
tuyaSendSwitch(id, value > 0);
}
if (lightState(id)) tuyaSendChannel(id, value);
return;
}
if (topic.equals(MQTT_TOPIC_RELAY)) {
if (lightState(id) != switchStates[id].value) {
tuyaSendSwitch(id, bool(value));
}
if (lightState(id)) tuyaSendChannel(id, value);
return;
}
#elif (LIGHT_PROVIDER == LIGHT_PROVIDER_TUYA)
if (topic.equals(MQTT_TOPIC_CHANNEL)) {
tuyaSendChannel(id, value);
return;
}
if (topic.equals(MQTT_TOPIC_RELAY)) {
tuyaSendSwitch(id, bool(value));
return;
}
#else
if (topic.equals(MQTT_TOPIC_RELAY)) {
tuyaSendSwitch(id, bool(value));
return;
}
#endif
}
// Main loop state machine. Process input data and manage output queue
void tuyaLoop() {
void loop() {
static State state = State::INIT;
@ -389,9 +472,9 @@ namespace Tuya {
// send fast heartbeat until mcu responds with something
case State::INIT:
tuyaSerial.rewind();
state = State::HEARTBEAT;
case State::HEARTBEAT:
sendHeartbeat(Heartbeat::FAST, state);
state = State::BOOT;
case State::BOOT:
sendHeartbeat(Heartbeat::Boot);
break;
// general info about the device (which we don't care about)
case State::QUERY_PRODUCT:
@ -411,22 +494,18 @@ namespace Tuya {
// full read-out of the data protocol values
case State::QUERY_DP:
{
DEBUG_MSG_P(PSTR("[TUYA] Starting discovery\n"));
DEBUG_MSG_P(PSTR("[TUYA] Querying DP(s)\n"));
outputFrames.emplace(Command::QueryDP);
discoveryTimeout.feed();
discovery.feed();
state = State::DISCOVERY;
break;
}
// parse known data protocols until discovery timeout expires
case State::DISCOVERY:
{
if (discoveryTimeout) {
if (discovery) {
DEBUG_MSG_P(PSTR("[TUYA] Discovery finished\n"));
relaySetupDummy(switchStates.size(), true);
#if LIGHT_PROVIDER == LIGHT_PROVIDER_TUYA
lightSetupChannels(channelStates.size());
#endif
configDone = true;
updateDiscovered(std::move(discovery));
state = State::IDLE;
}
break;
@ -434,17 +513,13 @@ namespace Tuya {
// initial config is done, only doing heartbeat periodically
case State::IDLE:
{
if (switchStates.changed()) applySwitch();
#if LIGHT_PROVIDER == LIGHT_PROVIDER_TUYA
if (channelStates.changed()) applyChannel();
#endif
sendHeartbeat(Heartbeat::SLOW, state);
sendHeartbeat(Heartbeat::Slow);
break;
}
}
if (TUYA_SERIAL && !outputFrames.empty()) {
const DataFrame frame = std::move(outputFrames.front());
auto& frame = outputFrames.top();
dataframeDebugSend("=>", frame);
tuyaSerial.write(frame.serialize());
outputFrames.pop();
@ -456,102 +531,108 @@ namespace Tuya {
// Respective provider setup should be called before state restore,
// so we can use dummy values
void initBrokerCallback() {
static bool done = false;
if (done) {
return;
}
::StatusBroker::Register(brokerCallback);
done = true;
}
void setupSwitches() {
bool done { false };
for (unsigned char id = 0; id < RelaysMax; ++id) {
auto dp = getSetting({"tuyaSwitch", id}, 0);
if (!dp) {
break;
}
void tuyaSetupSwitch() {
if (!switchIds.add(relayCount(), dp)) {
break;
}
initBrokerCallback();
if (!relayAdd(std::make_unique<TuyaRelayProvider>(dp))) {
break;
}
for (unsigned char n = 0; n < switchStates.capacity(); ++n) {
if (!hasSetting({"tuyaSwitch", n})) break;
const auto dp = getSetting({"tuyaSwitch", n}, 0);
switchStates.pushOrUpdate(dp, false);
done = true;
}
relaySetupDummy(switchStates.size());
if (switchStates.size()) configDone = true;
}
void tuyaSyncSwitchStatus() {
for (unsigned char n = 0; n < switchStates.size(); ++n) {
switchStates[n].value = relayStatus(n);
if (done) {
configDone.set();
}
}
#if LIGHT_PROVIDER == LIGHT_PROVIDER_TUYA
void tuyaSetupLight() {
#if LIGHT_PROVIDER == LIGHT_PROVIDER_CUSTOM
initBrokerCallback();
void setupChannels() {
bool done { false };
for (unsigned char id = 0; id < Light::ChannelsMax; ++id) {
auto dp = getSetting({"tuyaChannel", id}, 0);
if (!dp) {
break;
}
for (unsigned char n = 0; n < channelStates.capacity(); ++n) {
if (!hasSetting({"tuyaChannel", n})) break;
const auto dp = getSetting({"tuyaChannel", n}, 0);
channelStates.pushOrUpdate(dp, 0);
if (!channelIds.add(lightChannels(), dp)) {
break;
}
lightSetupChannels(channelStates.size());
if (channelStates.size()) configDone = true;
if (!lightAdd()) {
break;
}
done = true;
}
if (done) {
channelStateId = getSetting("tuyaChanState", 0u);
lightSetProvider(std::make_unique<TuyaLightProvider>(channelIds, &channelStateId));
}
if (done) {
configDone.set();
}
#endif
}
#endif
void tuyaSetup() {
void setup() {
// Print all known DP associations
#if TERMINAL_SUPPORT
terminalRegisterCommand(F("TUYA.SHOW"), [](const terminal::CommandContext&) {
static const char fmt[] PROGMEM = "%12s%u => dp=%u value=%u\n";
showProduct();
for (unsigned char id=0; id < switchStates.size(); ++id) {
DEBUG_MSG_P(fmt, "tuyaSwitch", id, switchStates[id].dp, switchStates[id].value);
terminalRegisterCommand(F("TUYA.SHOW"), [](const terminal::CommandContext& ctx) {
ctx.output.printf_P(PSTR("Product: %s\n"), product.length() ? product.c_str() : "(unknown)");
ctx.output.println(F("\nConfig:"));
for (auto& kv : config) {
ctx.output.printf_P(PSTR("\"%s\" => \"%s\"\n"), kv.key.c_str(), kv.value.c_str());
}
#if LIGHT_PROVIDER == LIGHT_PROVIDER_TUYA
for (unsigned char id=0; id < channelStates.size(); ++id) {
DEBUG_MSG_P(fmt, "tuyaChannel", id, channelStates[id].dp, channelStates[id].value);
}
#endif
ctx.output.println(F("\nKnown DP(s):"));
#if LIGHT_PROVIDER == LIGHT_PROVIDER_CUSTOM
if (channelStateId) {
ctx.output.printf_P(PSTR("%u (bool) => lights state\n"), channelStateId.id());
}
for (auto& entry : channelIds.map()) {
ctx.output.printf_P(PSTR("%u (int) => %d (channel)\n"), entry.dp_id, entry.local_id);
}
#endif
for (auto& entry : switchIds.map()) {
ctx.output.printf_P(PSTR("%u (bool) => %d (relay)\n"), entry.dp_id, entry.local_id);
}
});
terminalRegisterCommand(F("TUYA.SAVE"), [](const terminal::CommandContext&) {
DEBUG_MSG_P(PSTR("[TUYA] Saving current configuration ...\n"));
for (unsigned char n=0; n < switchStates.size(); ++n) {
setSetting({"tuyaSwitch", n}, switchStates[n].dp);
for (auto& kv : config) {
setSetting(kv.key, kv.value);
}
#if LIGHT_PROVIDER == LIGHT_PROVIDER_TUYA
for (unsigned char n=0; n < channelStates.size(); ++n) {
setSetting({"tuyaChannel", n}, channelStates[n].dp);
}
#endif
});
#endif
// Filtering for incoming data
auto filter_raw = getSetting("tuyaFilter", dp_states_filter_t::NONE);
filterDP = dp_states_filter_t::clamp(filter_raw);
// Print all IN and OUT messages
transportDebug = getSetting("tuyaDebug", 1 == TUYA_DEBUG_ENABLED);
transportDebug = getSetting("tuyaDebug", true);
// Whether to ignore the incoming state messages
filter = getSetting("tuyaFilter", 1 == TUYA_FILTER_ENABLED);
// Install main loop method and WiFiStatus ping (only works with specific mode)
TUYA_SERIAL.begin(SerialSpeed);
TUYA_SERIAL.begin(SERIAL_SPEED);
::espurnaRegisterLoop(tuyaLoop);
::espurnaRegisterLoop(loop);
::wifiRegister([](justwifi_messages_t code, char * parameter) {
if ((MESSAGE_CONNECTED == code) || (MESSAGE_DISCONNECTED == code)) {
sendWiFiStatus();


+ 15
- 8
code/espurna/tuya.h View File

@ -6,11 +6,18 @@
#include "espurna.h"
namespace Tuya {
void tuyaSendChannel(unsigned char, unsigned int);
void tuyaSendSwitch(unsigned char, bool);
void tuyaSetup();
void tuyaSetupLight();
void tuyaSyncSwitchStatus();
void tuyaSetupSwitch();
}
namespace tuya {
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
void setupChannels();
void sendChannel(unsigned char, unsigned int);
#endif
#if RELAY_SUPPORT
void setupSwitches();
void sendSwitch(unsigned char, bool);
#endif
void setup();
} // namespace tuya

+ 167
- 114
code/espurna/tuya_dataframe.h View File

@ -14,121 +14,174 @@ Copyright (C) 2019 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
#include "tuya_types.h"
#include "tuya_transport.h"
namespace Tuya {
class DataFrame {
public:
using container = std::vector<uint8_t>;
using const_iterator = container::const_iterator;
DataFrame(DataFrame& rhs) = delete;
~DataFrame() = default;
DataFrame(DataFrame&& rhs) = default;
DataFrame(uint8_t command) :
command(command),
length(0)
{}
DataFrame(Command command) :
DataFrame(static_cast<uint8_t>(command))
{}
DataFrame(Command command, uint16_t length,
const const_iterator begin,
const const_iterator end) :
command(static_cast<uint8_t>(command)),
length(length),
_begin(begin),
_end(end)
{}
DataFrame(uint8_t version, uint8_t command, uint16_t length,
const const_iterator begin,
const const_iterator end) :
version(version),
command(command),
length(length),
_begin(begin),
_end(end)
{}
DataFrame(Command command, std::initializer_list<uint8_t> data) :
command(static_cast<uint8_t>(command)),
length(data.size()),
_data(data),
_begin(_data.cbegin()),
_end(_data.cend())
{}
DataFrame(Command command, std::vector<uint8_t>&& data) :
command(static_cast<uint8_t>(command)),
length(data.size()),
_data(std::move(data)),
_begin(_data.cbegin()),
_end(_data.cend())
{}
DataFrame(const_iterator iter) :
version(iter[2]),
command(iter[3]),
length((iter[4] << 8) + iter[5]),
_begin(iter + 6),
_end(iter + 6 + length)
{}
DataFrame(const Transport& input) :
DataFrame(input.cbegin())
{}
bool commandEquals(Command command) const {
return (static_cast<uint8_t>(command) == this->command);
}
const_iterator cbegin() const {
return _begin;
};
const_iterator cend() const {
return _end;
};
uint8_t operator[](size_t i) const {
if (!length) return 0;
return _begin[i];
}
container serialize() const {
container result;
result.reserve(6 + length);
result.assign({
version, command,
uint8_t(length >> 8),
uint8_t(length & 0xff)
});
if (length && (_begin != _end)) {
result.insert(result.end(), _begin, _end);
}
return result;
}
uint8_t version = 0;
uint8_t command = 0;
uint16_t length = 0;
protected:
container _data;
const_iterator _begin;
const_iterator _end;
namespace tuya {
};
using container = std::vector<uint8_t>;
using const_iterator = container::const_iterator;
namespace util {
template <typename T>
container serialize_frame(const T& frame) {
container result;
result.reserve(6 + frame.length());
result.push_back(frame.version());
result.push_back(frame.command());
result.push_back(static_cast<uint8_t>(frame.length() >> 8) & 0xff);
result.push_back(static_cast<uint8_t>(frame.length() & 0xff));
if (frame.length()) {
result.insert(result.end(), frame.cbegin(), frame.cend());
}
return result;
}
} // namespace util
class DataFrameView {
public:
explicit DataFrameView(const Transport& input) :
_version(input[2]),
_command(input[3]),
_length((input[4] << 8) + input[5]),
_begin(input.cbegin() + 6),
_end(_begin + _length)
{}
explicit DataFrameView(const_iterator it) :
_version(it[0]),
_command(it[1]),
_length((it[2] << 8) + it[3]),
_begin(it + 4),
_end(_begin + _length)
{}
explicit DataFrameView(const container& data) :
DataFrameView(data.cbegin())
{}
container data() const {
return container(_begin, _end);
}
container serialize() const {
return util::serialize_frame(*this);
}
const_iterator cbegin() const {
return _begin;
};
const_iterator cend() const {
return _end;
};
uint8_t operator[](size_t i) const {
return *(_begin + i);
}
uint8_t version() const {
return _version;
}
uint8_t command() const {
return _command;
}
uint16_t length() const {
return _length;
}
private:
uint8_t _version { 0u };
uint8_t _command { 0u };
uint16_t _length { 0u };
const_iterator _begin;
const_iterator _end;
};
class DataFrame {
public:
template <typename T>
DataFrame(Command command, uint8_t version, T&& data) :
_data(std::forward<T>(data)),
_command(static_cast<uint8_t>(command)),
_version(version)
{}
template <typename T>
DataFrame(Command command, T&& data) :
_data(std::forward<T>(data)),
_command(static_cast<uint8_t>(command))
{}
explicit DataFrame(uint8_t command) :
_command(command)
{}
explicit DataFrame(Command command) :
DataFrame(static_cast<uint8_t>(command))
{}
explicit DataFrame(const Transport& input) :
_version(input[2]),
_command(input[3])
{
auto length = (input[4] << 8) + input[5];
_data.reserve(length);
auto data = input.cbegin() + 6;
_data.insert(_data.begin(), data, data + length);
}
explicit DataFrame(const DataFrameView& view) :
_data(view.cbegin(), view.cend()),
_version(view.version()),
_command(view.command())
{}
const container& data() const {
return _data;
}
const_iterator cbegin() const {
return _data.cbegin();
};
const_iterator cend() const {
return _data.cend();
};
uint8_t operator[](size_t i) const {
return _data[i];
}
container serialize() const {
return util::serialize_frame(*this);
}
uint8_t version() const {
return _version;
}
uint8_t command() const {
return _command;
}
uint16_t length() const {
return _data.size();
}
private:
container _data;
uint8_t _version { 0u };
uint8_t _command { 0u };
};
} // namespace

+ 43
- 32
code/espurna/tuya_protocol.h View File

@ -14,7 +14,7 @@ Copyright (C) 2019 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
#include "tuya_types.h"
#include "tuya_transport.h"
namespace Tuya {
namespace tuya {
// 2 known Data Protocols:
//
@ -26,19 +26,29 @@ namespace Tuya {
// Note: 'int' type is mostly used for dimmer and while it is 4 byte value,
// only the first byte is used (i.e. value is between 0 and 255)
Type dataType(const DataFrame& frame) {
template <typename T>
uint8_t dataProtocol(const T& frame) {
if (!frame.length()) {
return 0;
}
if (!frame.length) return Type::UNKNOWN;
return frame[0];
}
const Type type = static_cast<Type>(frame[1]);
template <typename T>
Type dataType(const T& frame) {
if (!frame.length()) {
return Type::UNKNOWN;
}
const Type type = static_cast<Type>(frame[1]);
switch (type) {
case Type::BOOL:
if (frame.length != 5) break;
if (frame.length() != 5) break;
if (frame[3] != 0x01) break;
return type;
case Type::INT:
if (frame.length != 8) break;
if (frame.length() != 8) break;
if (frame[3] != 0x04) break;
return type;
default:
@ -51,44 +61,45 @@ namespace Tuya {
// Since we know of the type only at runtime, specialize the protocol container
template <typename T>
template <typename Value>
class DataProtocol {
public:
explicit DataProtocol(const container& data);
DataProtocol(uint8_t id, Value value) :
_id(id),
_value(value)
{}
uint8_t id() const {
return _id;
}
public:
DataProtocol(const uint8_t id, const T value) :
_id(id), _value(value)
{}
DataProtocol(const DataFrame& frame);
uint8_t id() const { return _id; }
T value() const { return _value; }
std::vector<uint8_t> serialize();
private:
Value value() const {
return _value;
}
uint8_t _id;
T _value;
container serialize();
private:
uint8_t _id;
Value _value;
};
template <typename T>
DataProtocol<T>::DataProtocol(const DataFrame& frame) {
#if 0
template <typename T, typename Frame>
DataProtocol<T>::DataProtocol(const Frame& frame) {
static_assert(sizeof(T) != sizeof(T), "No constructor yet for this type!");
}
#endif
template <>
DataProtocol<bool>::DataProtocol(const DataFrame& frame) {
auto data = frame.cbegin();
_id = data[0],
DataProtocol<bool>::DataProtocol(const container& data) {
_id = data[0];
_value = data[4];
}
template <>
DataProtocol<uint32_t>::DataProtocol(const DataFrame& frame) {
auto data = frame.cbegin();
DataProtocol<uint32_t>::DataProtocol(const container& data) {
_id = data[0];
_value = static_cast<uint32_t>(data[4] << 24)
| static_cast<uint32_t>(data[5] << 16)
@ -97,7 +108,7 @@ namespace Tuya {
}
template <>
std::vector<uint8_t> DataProtocol<bool>::serialize() {
container DataProtocol<bool>::serialize() {
return std::vector<uint8_t> {
_id, static_cast<uint8_t>(Type::BOOL), 0x00, 0x01,
static_cast<uint8_t>(_value)
@ -105,7 +116,7 @@ namespace Tuya {
}
template <>
std::vector<uint8_t> DataProtocol<uint32_t>::serialize() {
container DataProtocol<uint32_t>::serialize() {
return std::vector<uint8_t> {
_id, static_cast<uint8_t>(Type::INT), 0x00, 0x04,
static_cast<uint8_t>((_value >> 24) & 0xff),


+ 21
- 33
code/espurna/tuya_transport.h View File

@ -14,44 +14,40 @@ Copyright (C) 2019 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
#include <iterator>
#include <vector>
namespace Tuya {
namespace tuya {
class PrintRaw {
public:
static void write(Print& printer, uint8_t data) {
printer.write(data);
}
public:
static void write(Print& printer, uint8_t data) {
printer.write(data);
}
static void write(Print& printer, const uint8_t* data, size_t size) {
printer.write(data, size);
}
static void write(Print& printer, const uint8_t* data, size_t size) {
printer.write(data, size);
}
};
class PrintHex {
public:
static void write(Print& printer, uint8_t data) {
char buffer[3] = {0};
snprintf(buffer, sizeof(buffer), "%02x", data);
printer.write(buffer, 2);
}
public:
static void write(Print& printer, uint8_t data) {
char buffer[3] = {0};
snprintf(buffer, sizeof(buffer), "%02x", data);
printer.write(buffer, 2);
}
static void write(Print& printer, const uint8_t* data, size_t size) {
for (size_t n=0; n<size; ++n) {
char buffer[3] = {0};
snprintf(buffer, sizeof(buffer), "%02x", data[n]);
printer.write(buffer, 2);
}
static void write(Print& printer, const uint8_t* data, size_t size) {
for (size_t n = 0; n < size; ++n) {
write(printer, data[n]);
}
}
};
class StreamWrapper {
class StreamWrapper {
protected:
Stream& _stream;
public:
StreamWrapper(Stream& stream) :
_stream(stream)
{}
@ -63,14 +59,10 @@ namespace Tuya {
void rewind() {
while(_stream.read() != -1);
}
};
class Output : public virtual StreamWrapper {
public:
Output(Stream& stream) :
StreamWrapper(stream)
{}
@ -112,7 +104,6 @@ namespace Tuya {
};
class Input : public virtual StreamWrapper {
// Buffer depth based on the SDK recommendations
constexpr static size_t LIMIT = 256;
@ -120,9 +111,8 @@ namespace Tuya {
// 256 * 1.04 = 266.24
constexpr static size_t TIME_LIMIT = 267;
using const_iterator = std::vector<uint8_t>::const_iterator;
public:
using const_iterator = std::vector<uint8_t>::const_iterator;
Input(Stream& stream) :
StreamWrapper(stream)
@ -134,7 +124,7 @@ namespace Tuya {
bool done() { return _done; }
size_t size() { return _index; }
uint8_t operator[](size_t i) {
uint8_t operator[](size_t i) const {
if (i > LIMIT) return 0;
return _buffer[i];
}
@ -208,14 +198,12 @@ namespace Tuya {
}
private:
bool _done = false;
size_t _index = 0;
size_t _read_until = LIMIT;
uint8_t _checksum = 0;
std::vector<uint8_t> _buffer;
unsigned long _last = 0;
};
class Transport : public Input, public Output, public virtual StreamWrapper {


+ 6
- 5
code/espurna/tuya_types.h View File

@ -8,7 +8,7 @@ Copyright (C) 2019 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
#pragma once
namespace Tuya {
namespace tuya {
enum class Command : uint8_t {
Heartbeat = 0x00,
@ -28,7 +28,7 @@ namespace Tuya {
enum class State {
INIT,
HEARTBEAT,
BOOT,
QUERY_PRODUCT,
QUERY_MODE,
QUERY_DP,
@ -37,9 +37,10 @@ namespace Tuya {
};
enum class Heartbeat {
NONE,
SLOW,
FAST
None,
Boot,
Fast,
Slow
};
enum class Type : uint8_t {


+ 170
- 89
code/espurna/tuya_util.h View File

@ -12,114 +12,195 @@ Copyright (C) 2019 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
#include <algorithm>
#include <vector>
namespace Tuya {
#include "tuya_types.h"
template <typename T>
class States {
namespace tuya {
public:
inline bool operator==(uint8_t lhs, Command rhs) {
return lhs == static_cast<uint8_t>(rhs);
}
struct Container {
uint8_t dp;
T value;
};
inline bool operator==(Command lhs, uint8_t rhs) {
return static_cast<uint8_t>(lhs) == rhs;
}
using iterator = typename std::vector<Container>::iterator;
using const_iterator = typename std::vector<Container>::const_iterator;
inline bool operator!=(uint8_t lhs, Command rhs) {
return !(lhs == rhs);
}
States(size_t capacity) :
_capacity(capacity)
{
_states.reserve(capacity);
}
inline bool operator!=(Command lhs, uint8_t rhs) {
return !(lhs == rhs);
}
bool update(const uint8_t dp, const T value, bool create=false) {
auto found = std::find_if(_states.begin(), _states.end(), [dp](const Container& internal) {
return dp == internal.dp;
});
if (found != _states.end()) {
if (found->value != value) {
found->value = value;
_changed = true;
return true;
}
} else if (create) {
_changed = true;
_states.emplace_back(States::Container{dp, value});
return true;
}
return false;
}
struct StateId {
StateId() = default;
void filter(bool value) {
_filter = value;
}
bool filter() {
return _filter;
}
uint8_t id() {
return _id;
}
StateId& operator=(uint8_t value) {
_id = value;
return *this;
}
explicit operator bool() {
return _id != 0u;
}
private:
uint8_t _id { 0 };
bool _filter { false };
};
struct OnceFlag {
OnceFlag() = default;
OnceFlag(const OnceFlag&) = delete;
OnceFlag(OnceFlag&&) = delete;
explicit operator bool() const {
return _value;
}
OnceFlag& operator=(bool value) {
if (!_value) {
_value = value;
}
return *this;
}
void set() {
_value = true;
}
bool get() const {
return _value;
}
private:
bool _value { false };
};
struct Dp {
Type type;
uint8_t id;
};
struct DpRelation {
uint8_t local_id;
uint8_t dp_id;
};
bool operator==(const DpRelation& lhs, const DpRelation& rhs) {
return (lhs.local_id == rhs.local_id) || (lhs.dp_id == rhs.dp_id);
}
bool pushOrUpdate(const uint8_t dp, const T value) {
if (_states.size() >= _capacity) return false;
return update(dp, value, true);
}
// Specifically for relay (or channel) <=> DP id association
// Caller is expected to check for uniqueness manually, when `add(...)`ing
bool changed() {
bool res = _changed;
if (_changed) _changed = false;
return res;
}
struct DpMap {
using map_type = std::vector<DpRelation>;
DpMap() = default;
Container& operator[] (const size_t n) {
return _states[n];
bool exists(const DpRelation& other) {
for (const auto& entry : _map) {
if (entry == other) {
return true;
}
}
size_t size() const {
return _states.size();
}
return false;
}
size_t capacity() const {
return _capacity;
}
bool add(const DpRelation& entry) {
if (!exists(entry)) {
_map.push_back(entry);
return true;
}
iterator begin() {
return _states.begin();
}
return false;
}
iterator end() {
return _states.end();
}
bool add(uint8_t local_id, uint8_t dp_id) {
return add(DpRelation{local_id, dp_id});
}
const_iterator begin() const {
return _states.begin();
}
const map_type& map() {
return _map;
}
const_iterator end() const {
return _states.end();
const DpRelation* find_local(unsigned char local_id) const {
for (const auto& entry : _map) {
if (entry.local_id == local_id) {
return &entry;
}
}
private:
bool _changed = false;
size_t _capacity = 0;
std::vector<Container> _states;
};
class DiscoveryTimeout {
public:
DiscoveryTimeout(uint32_t start, uint32_t timeout) :
_start(start),
_timeout(timeout)
{}
DiscoveryTimeout(uint32_t timeout) :
DiscoveryTimeout(millis(), timeout)
{}
operator bool() {
return (millis() - _start > _timeout);
}
return nullptr;
}
void feed() {
_start = millis();
const DpRelation* find_dp(unsigned char dp_id) const {
for (const auto& entry : _map) {
if (entry.dp_id == dp_id) {
return &entry;
}
}
private:
uint32_t _start;
const uint32_t _timeout;
};
return nullptr;
}
}
size_t size() const {
return _map.size();
}
private:
map_type _map;
};
using Dps = std::vector<Dp>;
class Discovery {
public:
Discovery() = delete;
Discovery(uint32_t start, uint32_t timeout) :
_start(start),
_timeout(timeout)
{}
explicit Discovery(uint32_t timeout) :
Discovery(millis(), timeout)
{}
explicit operator bool() {
return (millis() - _start > _timeout);
}
void feed() {
_start = millis();
}
void add(Type type, uint8_t dp) {
feed();
_dps.push_back(Dp{type, dp});
}
Dps& get() {
return _dps;
}
private:
Dps _dps;
uint32_t _start;
const uint32_t _timeout;
};
} // namespace tuya

+ 89
- 60
code/html/custom.js View File

@ -64,6 +64,31 @@ $.fn.enterKey = function (fnc) {
});
};
function followScroll(id, threshold) {
if (threshold === undefined) {
threshold = 90;
}
var elem = document.getElementById(id);
var offset = (elem.scrollTop + elem.offsetHeight) / elem.scrollHeight * 100;
if (offset > threshold) {
elem.scrollTop = elem.scrollHeight;
}
}
function fromSchema(source, schema) {
if (schema.length !== source.length) {
throw "Schema mismatch!";
}
var target = {};
schema.forEach(function(key, index) {
target[key] = source[index];
});
return target;
}
function keepTime() {
$("span[name='ago']").html(ago);
@ -772,6 +797,7 @@ function doDebugCommand() {
var command = el.val();
el.val("");
sendAction("dbgcmd", {command: command});
followScroll("weblog", 0);
return false;
}
@ -1085,28 +1111,49 @@ function addSchedule(values) {
// Relays
// -----------------------------------------------------------------------------
function initRelayFromSchema(id, relay, schema) {
var result = fromSchema(relay, schema)
if (!result.name.length) {
result.name = "Switch #" + id;
}
return result;
}
function initRelays(data) {
var current = $("#relays > div").length;
if (current > 0) { return; }
var schema = data.schema;
var template = $("#relayTemplate .pure-g")[0];
for (var i=0; i<data.length; i++) {
// Add relay fields
data["relays"].forEach(function(relay, id) {
var _relay = initRelayFromSchema(id, relay, schema);
var line = $(template).clone();
$(".id", line).html(i);
$(":checkbox", line).prop('checked', data[i]).attr("data", i)
.prop("id", "relay" + i)
$("span.relay-name", line)
.text(_relay.name)
.attr("data", id);
$(":checkbox", line)
.prop('checked', false)
.prop('disabled', true)
.attr("data", id)
.prop("id", "relay" + id)
.on("change", function (event) {
var id = parseInt($(event.target).attr("data"), 10);
var target= parseInt($(event.target).attr("data"), 10);
var status = $(event.target).prop("checked");
doToggle(id, status);
doToggle(target, status);
});
$("label.toggle", line).prop("for", "relay" + i)
$("label.toggle", line)
.prop("for", "relay" + id)
line.appendTo("#relays");
}
});
}
@ -1141,49 +1188,49 @@ function initRelayConfig(data) {
var current = $("#relayConfig > legend").length; // there is a legend per relay
if (current > 0) { return; }
var size = data.size;
var start = data.start;
var template = $("#relayConfigTemplate").children();
var schema = data.schema;
for (var i=start; i<size; ++i) {
data["relays"].forEach(function(relay, id) {
var _relay = initRelayFromSchema(id, relay, schema);
var line = $(template).clone();
$("span.id", line).html(i);
$("span.gpio", line).html(data.gpio[i]);
$("select[name='relayBoot']", line).val(data.boot[i]);
$("select[name='relayPulse']", line).val(data.pulse[i]);
$("input[name='relayTime']", line).val(data.pulse_time[i]);
$("span.name", line).html(_relay.name);
$("span.prov", line).html(_relay.prov);
$("select[name='relayBoot']", line).val(_relay.boot);
$("select[name='relayPulse']", line).val(_relay.pulse);
$("input[name='relayTime']", line).val(_relay.pulse_time);
if ("sch_last" in data) {
if (schema.includes("sch_last")) {
$("input[name='relayLastSch']", line)
.prop('checked', data.sch_last[i])
.attr("id", "relayLastSch" + i)
.attr("name", "relayLastSch" + i)
.next().attr("for","relayLastSch" + (i));
.prop("checked", _relay.sch_last)
.attr("id", "relayLastSch" + id)
.attr("name", "relayLastSch" + id)
.next().attr("for","relayLastSch" + (id));
}
if ("group" in data) {
$("input[name='mqttGroup']", line).val(data.group[i]);
if (schema.includes("group")) {
$("input[name='mqttGroup']", line).val(_relay.group);
}
if ("group_sync" in data) {
$("select[name='mqttGroupSync']", line).val(data.group_sync[i]);
if (schema.includes("group_sync")) {
$("select[name='mqttGroupSync']", line).val(_relay.group_sync);
}
if ("on_disc" in data) {
$("select[name='relayOnDisc']", line).val(data.on_disc[i]);
if (schema.includes("on_disc")) {
$("select[name='relayOnDisc']", line).val(_relay.on_disc);
}
setOriginalsFromValues($("input,select", line));
line.appendTo("#relayConfig");
// Populate the relay SELECTs
// Populate the relay SELECTs on the configuration panel
$("select.isrelay").append(
$("<option></option>")
.attr("value", i)
.text("Switch #" + i)
.attr("value", id)
.text(name)
);
}
++id;
});
}
@ -1886,17 +1933,7 @@ function processData(data) {
if ("wifi" === key) {
maxNetworks = parseInt(value["max"], 10);
value["networks"].forEach(function(network) {
var schema = value["schema"];
if (schema.length !== network.length) {
throw "WiFi schema mismatch!";
}
var _network = {};
schema.forEach(function(key, index) {
_network[key] = network[index];
});
addNetwork(_network);
addNetwork(fromSchema(network, value.schema));
});
return;
}
@ -1941,15 +1978,14 @@ function processData(data) {
// Relays
// ---------------------------------------------------------------------
if ("relayState" === key) {
initRelays(value.status);
updateRelays(value);
if ("relayConfig" === key) {
initRelays(value);
initRelayConfig(value);
return;
}
// Relay configuration
if ("relayConfig" === key) {
initRelayConfig(value);
if ("relayState" === key) {
updateRelays(value);
return;
}
@ -1978,21 +2014,14 @@ function processData(data) {
var schema = value["schema"];
value["list"].forEach(function(led_data, index) {
if (schema.length !== led_data.length) {
throw "LED schema mismatch!";
}
var led = {};
schema.forEach(function(key, index) {
led[key] = led_data[index];
});
var line = $($("#ledConfigTemplate").children()).clone();
$("span.id", line).html(index);
$("select", line).attr("data", index);
$("input", line).attr("data", index);
var led = fromSchema(led_data, schema);
$("select[name='ledGPIO']", line).val(led.GPIO);
// XXX: checkbox implementation depends on unique id
// $("input[name='ledInv']", line).val(led.Inv);
@ -2081,7 +2110,7 @@ function processData(data) {
$("#weblog").append(new Text(msg[i]));
}
$("#weblog").scrollTop($("#weblog")[0].scrollHeight - $("#weblog").height());
followScroll("weblog");
return;
}


+ 44
- 7
code/html/index.html View File

@ -489,6 +489,11 @@
<div class="pure-u-1 pure-u-lg-3-4 hint">Define how the different switches should be synchronized.</div>
</div>
<div class="pure-g module module-multirelay">
<label class="pure-u-1 pure-u-lg-1-4">Interlock delay</label>
<div class="pure-u-1 pure-u-lg-1-4"><input name="relayDelayInterlock" class="pure-u-1" type="number" min="0" /></div>
</div>
<div id="relayConfig"></div>
</fieldset>
@ -558,6 +563,30 @@
<fieldset>
<div class="pure-g module-relay">
<label class="pure-u-1 pure-u-lg-1-4">Light state switch</label>
<div class="pure-u-1 pure-u-lg-1-4"><input type="checkbox" name="ltRelay" tabindex="5" /></div>
<div class="pure-u-0 pure-u-lg-1-2"></div>
<div class="pure-u-0 pure-u-lg-1-4"></div>
<div class="pure-u-1 pure-u-lg-3-4 hint">If enabled, add virtual relay switch that controls the ON / OFF state.</div>
</div>
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Save values</label>
<div class="pure-u-1 pure-u-lg-1-4"><input type="checkbox" name="ltSave" tabindex="6" /></div>
<div class="pure-u-0 pure-u-lg-1-2"></div>
<div class="pure-u-0 pure-u-lg-1-4"></div>
<div class="pure-u-1 pure-u-lg-3-4 hint">Save channel &amp; brightness values in settings</div>
</div>
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Save delay</label>
<div class="pure-u-1 pure-u-lg-1-4"><input class="pure-u-1" type="number" name="ltSaveDelay" min="5000" max="60000" tabindex="7" /></div>
<div class="pure-u-0 pure-u-lg-1-2"></div>
<div class="pure-u-0 pure-u-lg-1-4"></div>
<div class="pure-u-1 pure-u-lg-3-4 hint">When "Save values" is enabled, wait for the delay (ms) before the values are saved</div>
</div>
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Use color</label>
<div class="pure-u-1 pure-u-lg-1-4"><input type="checkbox" name="useColor" action="reload" tabindex="8" /></div>
@ -612,24 +641,32 @@
</div>
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Color transitions</label>
<label class="pure-u-1 pure-u-lg-1-4">Channel transitions</label>
<div class="pure-u-1 pure-u-lg-1-4"><input type="checkbox" name="useTransitions" tabindex="13" /></div>
<div class="pure-u-0 pure-u-lg-1-2"></div>
<div class="pure-u-0 pure-u-lg-1-4"></div>
<div class="pure-u-1 pure-u-lg-3-4 hint">If enabled color changes will be smoothed.</div>
<div class="pure-u-1 pure-u-lg-3-4 hint">If enabled, channel changes will be smoothed.</div>
</div>
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Transition time</label>
<div class="pure-u-1 pure-u-lg-1-4"><input class="pure-u-1" type="number" name="lightTime" min="10" max="5000" tabindex="14" /></div>
<div class="pure-u-1 pure-u-lg-1-4"><input class="pure-u-1" type="number" name="ltTime" min="10" max="5000" tabindex="14" action="reload" /></div>
<div class="pure-u-0 pure-u-lg-1-2"></div>
<div class="pure-u-0 pure-u-lg-1-4"></div>
<div class="pure-u-1 pure-u-lg-3-4 hint">Time to transition from one color to another (ms)</div>
</div>
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Transition step</label>
<div class="pure-u-1 pure-u-lg-1-4"><input class="pure-u-1" type="number" name="ltStep" min="10" max="5000" tabindex="15" action="reload" /></div>
<div class="pure-u-0 pure-u-lg-1-2"></div>
<div class="pure-u-0 pure-u-lg-1-4"></div>
<div class="pure-u-1 pure-u-lg-3-4 hint">Time in millisecons to transition from one color to another.</div>
<div class="pure-u-1 pure-u-lg-3-4 hint">(ms, depends on the transition time)</div>
</div>
<div class="pure-g">
<div class="pure-u-1 pure-u-lg-1-4"><label>MQTT group</label></div>
<div class="pure-u-1 pure-u-lg-3-4"><input name="mqttGroupColor" class="pure-u-1" tabindex="15" action="reconnect" /></div>
<div class="pure-u-1 pure-u-lg-3-4"><input name="mqttGroupColor" class="pure-u-1" tabindex="16" action="reconnect" /></div>
<div class="pure-u-0 pure-u-lg-1-4"></div>
<div class="pure-u-1 pure-u-lg-3-4 hint">Sync color between different lights.</div>
</div>
@ -2048,13 +2085,13 @@
<div id="relayTemplate" class="template">
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Switch #<span class="id"></span></label>
<label class="pure-u-1 pure-u-lg-1-4"><span class="relay-name"></span></label>
<div><input name="relay" type="checkbox" on="ON" off="OFF" /></div>
</div>
</div>
<div id="relayConfigTemplate" class="template">
<legend>Switch #<span class="id"></span> (<span class="gpio"></span>)</legend>
<legend><span class="name"></span> (<span class="prov"></span>)</legend>
<div class="pure-g">
<div class="pure-u-1 pure-u-lg-1-4"><label>Boot mode</label></div>
<select class="pure-u-1 pure-u-lg-3-4" name="relayBoot">


+ 0
- 2
code/test/build/light_dimmer.h View File

@ -1,6 +1,4 @@
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
#define LIGHT_CHANNELS 5
#define LIGHT_CH1_PIN 5
#define LIGHT_CH2_PIN 4


+ 0
- 2
code/test/build/light_my92xx.h View File

@ -1,6 +1,4 @@
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_MY92XX
#define DUMMY_RELAY_COUNT 1
#define LIGHT_CHANNELS 5
#define MY92XX_MODEL MY92XX_MODEL_MY9231
#define MY92XX_CHIPS 2


+ 2
- 4
code/test/build/light_tuya.h View File

@ -1,4 +1,2 @@
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_TUYA
#define DUMMY_RELAY_COUNT 1
#define LIGHT_CHANNELS 5
#define TUYA_SUPPORT 1
#define LIGHT_PROVIDER LIGHT_PROVIDER_CUSTOM

+ 2
- 2
code/test/build/nondefault.h View File

@ -16,5 +16,5 @@
#define RFB_SUPPORT 1
#define RFB_PROVIDER RFB_PROVIDER_RCSWITCH
#define MCP23S08_SUPPORT 1
#define BUTTON_PROVIDER_ANALOG_SUPPORT 1
#define BUTTON_PROVIDER_ITEAD_SONOFF_DUAL_SUPPORT 1
#define RELAY_PROVIDER_DUAL_SUPPORT 1
#define RELAY_PROVIDER_STM_SUPPORT 1

+ 148
- 66
code/test/unit/tuya/tuya.cpp View File

@ -16,62 +16,69 @@
#include "tuya_protocol.h"
#include "tuya_dataframe.h"
using namespace Tuya;
using namespace tuya;
static bool datatype_same(const DataFrame& frame, const Type expect_type) {
template <typename T>
static bool datatype_same(const T& frame, const Type expect_type) {
const auto type = dataType(frame);
return expect_type == type;
}
void test_states() {
States<bool> states(8);
// Will not update anything without explicit push
states.update(1, false);
states.update(1, true);
states.update(2, true);
states.update(2, false);
TEST_ASSERT_EQUAL_MESSAGE(8, states.capacity(),
"Capacity has changed");
TEST_ASSERT_EQUAL_MESSAGE(0, states.size(),
"Size should not change when updating non-existant id");
// Push something at specific ID
states.pushOrUpdate(2, true);
TEST_ASSERT_MESSAGE(states.changed(),
"Should change after explicit push");
states.pushOrUpdate(2, false);
TEST_ASSERT_MESSAGE(states.changed(),
"Should change after explicit update");
TEST_ASSERT_EQUAL_MESSAGE(1, states.size(),
"Size should not change when updating existing id");
states.pushOrUpdate(3, true);
TEST_ASSERT_MESSAGE(states.changed(),
"Should change after explicit push");
// Do not trigger "changed" state when value remains the same
states.pushOrUpdate(2, false);
TEST_ASSERT_MESSAGE(!states.changed(),
"Should not change after not changing any values");
// Still shouldn't trigger "changed" without explicit push
states.update(4, false);
TEST_ASSERT_MESSAGE(!states.changed(),
"Should not change after updating non-existant id");
TEST_ASSERT_EQUAL_MESSAGE(2, states.size(),
"Size should remain the same after updating non-existant id");
void test_dpmap() {
DpMap map;
// id <-> dp
map.add(1, 2);
map.add(3, 4);
map.add(5, 6);
map.add(7, 8);
TEST_ASSERT_EQUAL(4, map.size());
map.add(7,10);
map.add(5,5);
// dpmap is a 'set' of values
TEST_ASSERT_EQUAL(4, map.size());
#define TEST_FIND_DP_ID(EXPECTED_DP_ID, EXPECTED_LOCAL_ID) \
{\
auto* entry = map.find_dp(EXPECTED_DP_ID);\
TEST_ASSERT(entry != nullptr);\
TEST_ASSERT_EQUAL(EXPECTED_DP_ID, entry->dp_id);\
TEST_ASSERT_EQUAL(EXPECTED_LOCAL_ID, entry->local_id);\
}
TEST_FIND_DP_ID(2, 1);
TEST_FIND_DP_ID(4, 3);
TEST_FIND_DP_ID(6, 5);
TEST_FIND_DP_ID(8, 7);
#define TEST_FIND_LOCAL_ID(EXPECTED_LOCAL_ID, EXPECTED_DP_ID) \
{\
auto* entry = map.find_local(EXPECTED_LOCAL_ID);\
TEST_ASSERT(entry != nullptr);\
TEST_ASSERT_EQUAL(EXPECTED_DP_ID, entry->dp_id);\
TEST_ASSERT_EQUAL(EXPECTED_LOCAL_ID, entry->local_id);\
}
TEST_FIND_LOCAL_ID(1, 2);
TEST_FIND_LOCAL_ID(3, 4);
TEST_FIND_LOCAL_ID(5, 6);
TEST_FIND_LOCAL_ID(7, 8);
#undef TEST_FIND_LOCAL_ID
#undef TEST_FIND_DP_ID
}
void test_static_dataframe_bool() {
DataFrame frame(Command::SetDP, DataProtocol<bool>(0x02, false).serialize());
TEST_ASSERT_EQUAL_MESSAGE(0, frame.version,
TEST_ASSERT_EQUAL_MESSAGE(0, frame.version(),
"Version should stay 0 unless explicitly set");
TEST_ASSERT_MESSAGE(frame.commandEquals(Command::SetDP),
TEST_ASSERT_MESSAGE((frame.command() == Command::SetDP),
"commandEquals should return true with the same arg as in the constructor");
TEST_ASSERT_MESSAGE(datatype_same(frame, Type::BOOL),
"DataProtocol<bool> should translate to Type::BOOL");
@ -81,11 +88,11 @@ void test_static_dataframe_bool() {
void test_static_dataframe_int() {
DataFrame frame(Command::ReportDP, DataProtocol<uint32_t>(0x03, 255).serialize());
TEST_ASSERT_EQUAL_MESSAGE(0, frame.version,
TEST_ASSERT_EQUAL_MESSAGE(0, frame.version(),
"Version should stay 0 unless explicitly set");
TEST_ASSERT_MESSAGE(frame.commandEquals(Command::ReportDP),
TEST_ASSERT_MESSAGE((frame.command() == Command::ReportDP),
"commandEquals should return true with the same arg as in the constructor");
TEST_ASSERT_EQUAL_UINT_MESSAGE(std::distance(frame.cbegin(), frame.cend()), frame.length,
TEST_ASSERT_EQUAL_UINT_MESSAGE(std::distance(frame.cbegin(), frame.cend()), frame.length(),
"Data is expected to be stored in a contigious memory and be equal in length to the ::length attribute");
TEST_ASSERT_EQUAL_MESSAGE(0, frame[5],
"Only last byte should be set");
@ -97,7 +104,7 @@ void test_static_dataframe_int() {
void test_static_dataframe_heartbeat() {
DataFrame frame(Command::Heartbeat);
TEST_ASSERT_EQUAL_MESSAGE(0, frame.length,
TEST_ASSERT_EQUAL_MESSAGE(0, frame.length(),
"Frame with Command::Heartbeat should not have any data attached to it");
TEST_ASSERT_EQUAL_MESSAGE(0, std::distance(frame.cbegin(), frame.cend()),
"Frame with Command::SetDP should not have any data attached to it");
@ -108,7 +115,7 @@ void test_static_dataframe_heartbeat() {
void test_dataframe_const() {
const DataFrame frame(Command::SetDP);
TEST_ASSERT_EQUAL_MESSAGE(0, frame.length,
TEST_ASSERT_EQUAL_MESSAGE(0, frame.length(),
"Frame with Command::SetDP should not have any data attached to it");
TEST_ASSERT_EQUAL_MESSAGE(0, std::distance(frame.cbegin(), frame.cend()),
"Frame with Command::SetDP should not have any data attached to it");
@ -118,14 +125,17 @@ void test_dataframe_const() {
void test_dataframe_copy() {
DataFrame frame(Command::Heartbeat);
frame.version = 0x7f;
DataFrame frame(Command::Heartbeat, 0x7f, container{1,2,3});
DataFrame moved_frame(std::move(frame));
TEST_ASSERT_EQUAL_MESSAGE(0x7f, moved_frame.version,
TEST_ASSERT_EQUAL(3, moved_frame.length());
TEST_ASSERT_EQUAL(3, moved_frame.length());
TEST_ASSERT_EQUAL_MESSAGE(0x7f, moved_frame.version(),
"DataFrame should be movable object");
TEST_ASSERT_MESSAGE(!std::is_copy_constructible<DataFrame>::value,
DataFrame copied_frame(moved_frame);
TEST_ASSERT_EQUAL(3, copied_frame.length());
TEST_ASSERT_EQUAL_MESSAGE(0x7f, copied_frame.version(),
"DataFrame should not be copyable");
}
@ -133,31 +143,41 @@ void test_dataframe_copy() {
void test_dataframe_raw_data() {
{
const std::vector<uint8_t> data = {0x55, 0xaa, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01};
DataFrame frame(data.cbegin());
TEST_ASSERT_MESSAGE(frame.commandEquals(Command::Heartbeat),
container data = {0x00, 0x00, 0x00, 0x01, 0x01};
DataFrameView frame(data);
TEST_ASSERT_MESSAGE((frame.command() == Command::Heartbeat),
"This message should be parsed as heartbeat");
TEST_ASSERT_EQUAL_MESSAGE(0, frame.version,
TEST_ASSERT_EQUAL_MESSAGE(0, frame.version(),
"This message should have version == 0");
TEST_ASSERT_EQUAL_MESSAGE(1, frame.length,
TEST_ASSERT_EQUAL_MESSAGE(1, frame.length(),
"Heartbeat message contains a single byte");
TEST_ASSERT_EQUAL_MESSAGE(1, frame[0],
"Heartbeat message contains a single 0x01");
auto serialized = frame.serialize();
TEST_ASSERT_MESSAGE(std::equal(data.begin(), data.end(), serialized.begin()),
"Serialized frame should match the original data");
}
{
const std::vector<uint8_t> data = {0x55, 0xaa, 0x00, 0x07, 0x00, 0x05, 0x01, 0x01, 0x00, 0x01, 0x01, 0x0f};
DataFrame frame(data.cbegin());
TEST_ASSERT_MESSAGE(frame.commandEquals(Command::ReportDP),
container data = {0x00, 0x07, 0x00, 0x05, 0x01, 0x01, 0x00, 0x01, 0x01};
DataFrameView frame(data);
TEST_ASSERT_MESSAGE((frame.command() == Command::ReportDP),
"This message should be parsed as data protocol");
TEST_ASSERT_MESSAGE(datatype_same(frame, Type::BOOL),
"This message should have boolean datatype attached to it");
TEST_ASSERT_EQUAL_MESSAGE(5, frame.length,
TEST_ASSERT_EQUAL_MESSAGE(5, frame.length(),
"Boolean DP contains 5 bytes");
const DataProtocol<bool> dp(frame);
const DataProtocol<bool> dp(frame.data());
TEST_ASSERT_EQUAL_MESSAGE(1, dp.id(), "This boolean DP id should be 1");
TEST_ASSERT_MESSAGE(dp.value(), "This boolean DP value should be true");
auto serialized = frame.serialize();
TEST_ASSERT_MESSAGE(std::equal(data.begin(), data.end(), serialized.begin()),
"Serialized frame should match the original data");
}
//show_datatype(frame);
@ -210,27 +230,89 @@ class BufferedStream : public Stream {
};
void test_transport() {
const std::vector<uint8_t> data = {0x55, 0xaa, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01};
container data = {0x55, 0xaa, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01};
BufferedStream stream;
stream.write(data.data(), data.size());
Transport transport(stream);
TEST_ASSERT_MESSAGE(transport.available(), "Available data");
TEST_ASSERT(transport.available());
for (size_t n = 0; n < data.size(); ++n) {
transport.read();
}
TEST_ASSERT(transport.done());
}
void test_dataframe_report() {
container input = {0x55, 0xaa, 0x00, 0x07, 0x00, 0x08, 0x02, 0x02, 0x00, 0x04, 0x00, 0x00, 0x00, 0x10, 0x26};
BufferedStream stream;
stream.write(input.data(), input.size());
Transport transport(stream);
while (transport.available()) {
transport.read();
}
TEST_ASSERT(transport.done());
DataFrameView frame(transport);
TEST_ASSERT(frame.command() == Command::ReportDP);
TEST_ASSERT_EQUAL(Type::INT, dataType(frame));
TEST_ASSERT_EQUAL(8, frame.length());
TEST_ASSERT_EQUAL(0, frame.version());
DataProtocol<uint32_t> proto(frame.data());
TEST_ASSERT_EQUAL(0x02, proto.id());
TEST_ASSERT_EQUAL(0x10, proto.value());
}
void test_dataframe_echo() {
BufferedStream stream;
Transport transport(stream);
{
DataProtocol<uint32_t> proto(0x02, 0x66);
TEST_ASSERT_EQUAL(0x02, proto.id());
TEST_ASSERT_EQUAL(0x66,proto.value());
DataFrame frame(Command::SetDP, proto.serialize());
transport.write(frame.serialize());
}
while (transport.available()) {
transport.read();
}
TEST_ASSERT(transport.done());
{
DataFrameView frame(transport);
TEST_ASSERT(frame.command() == Command::SetDP);
TEST_ASSERT_EQUAL(Type::INT, dataType(frame));
TEST_ASSERT_EQUAL(8, frame.length());
TEST_ASSERT_EQUAL(0, frame.version());
DataProtocol<uint32_t> proto(frame.data());
TEST_ASSERT_EQUAL(0x02, proto.id());
TEST_ASSERT_EQUAL(0x66, proto.value());
}
}
int main(int argc, char** argv) {
UNITY_BEGIN();
RUN_TEST(test_states);
RUN_TEST(test_dpmap);
RUN_TEST(test_static_dataframe_bool);
RUN_TEST(test_static_dataframe_int);
RUN_TEST(test_static_dataframe_heartbeat);
RUN_TEST(test_dataframe_const);
RUN_TEST(test_dataframe_copy);
RUN_TEST(test_dataframe_raw_data);
RUN_TEST(test_dataframe_report);
RUN_TEST(test_dataframe_echo);
RUN_TEST(test_transport);
UNITY_END();
return UNITY_END();
}

Loading…
Cancel
Save