Browse Source

buttons: resistor ladder / analog buttons support (#2357)

- Buttons events source as button property instead of a global one
- Rename events source -> provider for all settings, consistent with the other things like relay and light providers
- AnalogPin to read between a certain analogRead() range
Trying to follow defaults here - analog 'press' is digital LOW, default value is HIGH, so no additional cfg entries are needed besides pin, level and changing evt source
- (debug) Refactor gpio command, add adc to show analogRead(pin)
- (debug) Add button command

Implemented based on:
https://gitter.im/tinkerman-cat/espurna?at=5f5d44c8df4af236f902e25d
https://gitter.im/tinkerman-cat/espurna?at=5f60e7f1f969413294e95370
mcspr-patch-1
Max Prokhorov 4 years ago
committed by GitHub
parent
commit
c7a95bf53f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 555 additions and 215 deletions
  1. +1
    -0
      README.md
  2. +0
    -4
      code/espurna/board.cpp
  3. +243
    -95
      code/espurna/button.cpp
  4. +39
    -0
      code/espurna/button_config.h
  5. +50
    -0
      code/espurna/config/defaults.h
  6. +13
    -4
      code/espurna/config/dependencies.h
  7. +30
    -7
      code/espurna/config/general.h
  8. +6
    -4
      code/espurna/config/hardware.h
  9. +4
    -5
      code/espurna/config/types.h
  10. +2
    -20
      code/espurna/gpio.cpp
  11. +0
    -13
      code/espurna/gpio.h
  12. +39
    -0
      code/espurna/gpio_pin.h
  13. +27
    -4
      code/espurna/libs/BasePin.h
  14. +0
    -16
      code/espurna/mcp23s08.cpp
  15. +0
    -20
      code/espurna/mcp23s08.h
  16. +43
    -0
      code/espurna/mcp23s08_pin.h
  17. +17
    -5
      code/espurna/relay.cpp
  18. +38
    -18
      code/espurna/terminal.cpp
  19. +3
    -0
      code/test/build/nondefault.h

+ 1
- 0
README.md View File

@ -59,6 +59,7 @@ Since November 2018, Max Prokhorov (**@mcspr**) is also actively working as a co
* Supports NetBIOS, LLMNR and Netbios (when built with Arduino Core >= 2.4.0) and SSDP (experimental) * Supports NetBIOS, LLMNR and Netbios (when built with Arduino Core >= 2.4.0) and SSDP (experimental)
* Switch management * Switch management
* Support for **push buttons** and **toggle switches** * Support for **push buttons** and **toggle switches**
* Support for **digital** and [**analog**](https://en.wikipedia.org/wiki/Resistor_ladder) buttons
* Configurable **status on boot** per switch (always ON, always OFF, same as before or toggle) * Configurable **status on boot** per switch (always ON, always OFF, same as before or toggle)
* Support for **pulse mode** per switch (normally ON or normally OFF) with configurable time * Support for **pulse mode** per switch (normally ON or normally OFF) with configurable time
* Support for **relay synchronization** (all equal, only one ON, one and only on ON) * Support for **relay synchronization** (all equal, only one ON, one and only on ON)


+ 0
- 4
code/espurna/board.cpp View File

@ -21,11 +21,7 @@ PROGMEM const char espurna_modules[] =
"BROKER " "BROKER "
#endif #endif
#if BUTTON_SUPPORT #if BUTTON_SUPPORT
#if BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_GENERIC
"BUTTON " "BUTTON "
#else
"BUTTON_DUAL "
#endif
#endif #endif
#if DEBUG_SERIAL_SUPPORT #if DEBUG_SERIAL_SUPPORT
"DEBUG_SERIAL " "DEBUG_SERIAL "


+ 243
- 95
code/espurna/button.cpp View File

@ -21,11 +21,13 @@ Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
#include "relay.h" #include "relay.h"
#include "light.h" #include "light.h"
#include "ws.h" #include "ws.h"
#include "mcp23s08.h"
#include "button_config.h"
#include "libs/BasePin.h"
#include "libs/DebounceEvent.h" #include "libs/DebounceEvent.h"
#include "gpio_pin.h"
#include "mcp23s08_pin.h"
#include "button_config.h"
BrokerBind(ButtonBroker); BrokerBind(ButtonBroker);
@ -170,7 +172,7 @@ button_actions_t _buttonConstructActions(unsigned char index) {
}; };
} }
debounce_event::types::Config _buttonConfig(unsigned char index) {
debounce_event::types::Config _buttonRuntimeConfig(unsigned char index) {
const auto config = _buttonDecodeConfigBitmask(_buttonConfigBitmask(index)); const auto config = _buttonDecodeConfigBitmask(_buttonConfigBitmask(index));
return { return {
getSetting({"btnMode", index}, config.mode), getSetting({"btnMode", index}, config.mode),
@ -282,15 +284,6 @@ void _buttonWebSocketOnVisible(JsonObject& root) {
} }
} }
#if (BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_ITEAD_SONOFF_DUAL) || \
(BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_FOXEL_LIGHTFOX_DUAL)
void _buttonWebSocketOnConnected(JsonObject& root) {
root["btnRepDel"] = getSetting("btnRepDel", _buttonRepeatDelay());
}
#else
void _buttonWebSocketOnConnected(JsonObject& root) { void _buttonWebSocketOnConnected(JsonObject& root) {
root["btnRepDel"] = getSetting("btnRepDel", _buttonRepeatDelay()); root["btnRepDel"] = getSetting("btnRepDel", _buttonRepeatDelay());
@ -301,20 +294,20 @@ void _buttonWebSocketOnConnected(JsonObject& root) {
JsonObject& module = root.createNestedObject("btn"); JsonObject& module = root.createNestedObject("btn");
// TODO: hardware can sometimes use a different event source
// TODO: hardware can sometimes use a different providers
// e.g. Sonoff Dual does not need `Pin`, `Mode` or any of `Del` // e.g. Sonoff Dual does not need `Pin`, `Mode` or any of `Del`
// TODO: schema names are uppercase to easily match settings? // TODO: schema names are uppercase to easily match settings?
// TODO: schema name->type map to generate WebUI elements? // TODO: schema name->type map to generate WebUI elements?
JsonArray& schema = module.createNestedArray("_schema"); JsonArray& schema = module.createNestedArray("_schema");
schema.add("Prov");
schema.add("GPIO"); schema.add("GPIO");
schema.add("Mode"); schema.add("Mode");
schema.add("DefVal"); schema.add("DefVal");
schema.add("PinMode"); schema.add("PinMode");
schema.add("Relay");
schema.add("Press"); schema.add("Press");
schema.add("Click"); schema.add("Click");
schema.add("Dclk"); schema.add("Dclk");
@ -327,10 +320,14 @@ void _buttonWebSocketOnConnected(JsonObject& root) {
schema.add("LclkDel"); schema.add("LclkDel");
schema.add("LLclkDel"); schema.add("LLclkDel");
#if MQTT_SUPPORT
schema.add("MqttSendAll");
schema.add("MqttRetain");
#endif
#if RELAY_SUPPORT
schema.add("Relay");
#endif
#if MQTT_SUPPORT
schema.add("MqttSendAll");
schema.add("MqttRetain");
#endif
JsonArray& buttons = module.createNestedArray("list"); JsonArray& buttons = module.createNestedArray("list");
@ -338,9 +335,10 @@ void _buttonWebSocketOnConnected(JsonObject& root) {
JsonArray& button = buttons.createNestedArray(); JsonArray& button = buttons.createNestedArray();
// TODO: configure PIN object instead of button specifically, link PIN<->BUTTON // 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].getPin()) {
button.add(getSetting({"btnGPIO", index}, _buttonPin(index))); button.add(getSetting({"btnGPIO", index}, _buttonPin(index)));
const auto config = _buttonConfig(index);
const auto config = _buttonRuntimeConfig(index);
button.add(static_cast<int>(config.mode)); button.add(static_cast<int>(config.mode));
button.add(static_cast<int>(config.default_value)); button.add(static_cast<int>(config.default_value));
button.add(static_cast<int>(config.pin_mode)); button.add(static_cast<int>(config.pin_mode));
@ -352,8 +350,6 @@ void _buttonWebSocketOnConnected(JsonObject& root) {
button.add(0); button.add(0);
} }
button.add(_buttons[i].relayID);
button.add(_buttons[i].actions.pressed); button.add(_buttons[i].actions.pressed);
button.add(_buttons[i].actions.click); button.add(_buttons[i].actions.click);
button.add(_buttons[i].actions.dblclick); button.add(_buttons[i].actions.dblclick);
@ -366,22 +362,24 @@ void _buttonWebSocketOnConnected(JsonObject& root) {
button.add(_buttons[i].event_delays.lngclick); button.add(_buttons[i].event_delays.lngclick);
button.add(_buttons[i].event_delays.lnglngclick); button.add(_buttons[i].event_delays.lnglngclick);
#if RELAY_SUPPORT
button.add(_buttons[i].relayID);
#endif
// TODO: send bitmask as number? // TODO: send bitmask as number?
#if MQTT_SUPPORT
button.add(_buttons_mqtt_send_all[i] ? 1 : 0);
button.add(_buttons_mqtt_retain[i] ? 1 : 0);
#endif
#if MQTT_SUPPORT
button.add(_buttons_mqtt_send_all[i] ? 1 : 0);
button.add(_buttons_mqtt_retain[i] ? 1 : 0);
#endif
} }
#endif #endif
} }
#endif // BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_GENERIC
bool _buttonWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
bool _buttonWebSocketOnKeyCheck(const char * key, JsonVariant&) {
return (strncmp(key, "btn", 3) == 0); return (strncmp(key, "btn", 3) == 0);
} }
#endif
#endif // WEB_SUPPORT
bool buttonState(unsigned char id) { bool buttonState(unsigned char id) {
if (id >= _buttons.size()) return false; if (id >= _buttons.size()) return false;
@ -570,7 +568,7 @@ void _buttonLoopSonoffDual() {
const unsigned char value [[gnu::unused]] = bytes[2]; const unsigned char value [[gnu::unused]] = bytes[2];
#if BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_ITEAD_SONOFF_DUAL
#if BUTTON_PROVIDER_ITEAD_SONOFF_DUAL_SUPPORT
// RELAYs and BUTTONs are synchonized in the SIL F330 // RELAYs and BUTTONs are synchonized in the SIL F330
// The on-board BUTTON2 should toggle RELAY0 value // The on-board BUTTON2 should toggle RELAY0 value
@ -599,7 +597,7 @@ void _buttonLoopSonoffDual() {
} }
#elif BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_FOXEL_LIGHTFOX_DUAL
#elif BUTTON_PROVIDER_FOXEL_LIGHTFOX_DUAL_SUPPORT
DEBUG_MSG_P(PSTR("[BUTTON] [LIGHTFOX] Received buttons mask: %u\n"), value); DEBUG_MSG_P(PSTR("[BUTTON] [LIGHTFOX] Received buttons mask: %u\n"), value);
@ -609,7 +607,7 @@ void _buttonLoopSonoffDual() {
} }
} }
#endif // BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_ITEAD_SONOFF_DUAL
#endif // BUTTON_PROVIDER_ITEAD_SONOFF_DUAL
} }
@ -624,26 +622,191 @@ void _buttonLoopGeneric() {
void buttonLoop() { void buttonLoop() {
#if (BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_GENERIC) || \
(BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_MCP23S08)
_buttonLoopGeneric();
#elif (BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_ITEAD_SONOFF_DUAL) || \
(BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_FOXEL_LIGHTFOX_DUAL)
_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(); _buttonLoopSonoffDual();
#else
#warning "Unknown value for BUTTON_EVENTS_SOURCE"
#endif #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!)
// - https://github.com/bxparks/AceButton/blob/develop/src/ace_button/LadderButtonConfig.cpp
// - https://github.com/dxinteractive/AnalogMultiButton
#if BUTTON_PROVIDER_ANALOG_SUPPORT
class AnalogPin final : public BasePin {
public:
static constexpr int RangeFrom { 0 };
static constexpr int RangeTo { 1023 };
AnalogPin() = delete;
AnalogPin(unsigned char) = delete;
AnalogPin(unsigned char pin_, int expected_) :
BasePin(pin_),
_expected(expected_)
{
pins.reserve(ButtonsPresetMax);
pins.push_back(this);
adjustPinRanges();
}
~AnalogPin() {
pins.erase(std::remove(pins.begin(), pins.end(), this), pins.end());
adjustPinRanges();
}
// Notice that 'static' method vars are shared between instances
// This way we will throttle every invocation (which should be safe to do, since we only read things through the button loop)
int analogRead() {
static unsigned long ts { ESP.getCycleCount() };
static int last { ::analogRead(pin) };
// Cannot hammer analogRead() all the time:
// https://github.com/esp8266/Arduino/issues/1634
if (ESP.getCycleCount() - ts >= _read_interval) {
ts = ESP.getCycleCount();
last = ::analogRead(pin);
}
return last;
}
// XXX: make static ctor and call this implicitly?
static bool checkExpectedLevel(int expected) {
if (expected > RangeTo) {
return false;
}
for (auto pin : pins) {
if (expected == pin->_expected) {
return false;
}
}
return true;
}
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
);
return String(buffer);
}
// Simulate LOW level when the range matches and HIGH when it does not
int digitalRead() override {
const auto reading = analogRead();
return !((_from < reading) && (reading < _to));
}
void pinMode(int8_t) override {
}
void digitalWrite(int8_t val) override {
}
private:
// ref. https://github.com/bxparks/AceButton/tree/develop/docs/resistor_ladder#level-matching-tolerance-range
// fuzzy matching instead of directly comparing with the `_expected` level and / or specifying tolerance manually
// for example, for pins with expected values 0, 327, 512 and 844 we match analogRead() when:
// - 0..163 for 0
// - 163..419 for 327
// - 419..678 for 512
// - 678..933 for 844
// - 933..1024 is ignored
static std::vector<AnalogPin*> pins;
unsigned long _read_interval { microsecondsToClockCycles(200u) };
int _expected { 0u };
int _from { RangeFrom };
int _to { RangeTo };
static void adjustPinRanges() {
std::sort(pins.begin(), pins.end(), [](const AnalogPin* lhs, const AnalogPin* rhs) -> bool {
return lhs->_expected < rhs->_expected;
});
AnalogPin* last { nullptr };
for (unsigned index = 0; index < pins.size(); ++index) {
int edge = (index + 1 != pins.size())
? pins[index + 1]->_expected
: RangeTo;
pins[index]->_from = last
? last->_to
: RangeFrom;
pins[index]->_to = (pins[index]->_expected + edge) / 2;
last = pins[index];
}
}
};
std::vector<AnalogPin*> AnalogPin::pins;
#endif // BUTTON_PROVIDER_ANALOG_SUPPORT
std::shared_ptr<BasePin> _buttonFromProvider([[gnu::unused]] unsigned char index, int provider, unsigned char pin) {
switch (provider) {
case BUTTON_PROVIDER_GENERIC:
if (!gpioValid(pin)) {
break;
}
return std::shared_ptr<BasePin>(new GpioPin(pin));
#if BUTTON_PROVIDER_MCP23S08_SUPPORT
case BUTTON_PROVIDER_MCP23S08:
if (!mcpGpioValid(pin)) {
break;
}
return std::shared_ptr<BasePin>(new McpGpioPin(pin));
#endif
#if BUTTON_PROVIDER_ANALOG_SUPPORT
case BUTTON_PROVIDER_ANALOG: {
if (A0 != pin) {
break;
}
const auto level = getSetting({"btnLevel", index}, _buttonAnalogLevel(index));
if (!AnalogPin::checkExpectedLevel(level)) {
break;
}
return std::shared_ptr<BasePin>(new AnalogPin(pin, level));
}
#endif
default:
break;
}
return {};
}
void buttonSetup() { void buttonSetup() {
// Backwards compatibility // Backwards compatibility
moveSetting("btnDelay", "btnRepDel"); moveSetting("btnDelay", "btnRepDel");
// Special hardware cases // Special hardware cases
#if (BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_ITEAD_SONOFF_DUAL) || \
(BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_FOXEL_LIGHTFOX_DUAL)
#if BUTTON_PROVIDER_ITEAD_SONOFF_DUAL_SUPPORT || BUTTON_PROVIDER_FOXEL_LIGHTFOX_DUAL
{
size_t buttons = 0; size_t buttons = 0;
#if BUTTON1_RELAY != RELAY_NONE #if BUTTON1_RELAY != RELAY_NONE
++buttons; ++buttons;
@ -680,59 +843,23 @@ void buttonSetup() {
); );
} }
}
#endif // BUTTON_PROVIDER_ITEAD_SONOFF_DUAL_SUPPORT || BUTTON_PROVIDER_FOXEL_LIGHTFOX_DUAL
// Generic GPIO input handlers
#elif (BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_GENERIC) || \
(BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_MCP23S08)
size_t buttons = 0;
#if BUTTON1_PIN != GPIO_NONE
++buttons;
#endif
#if BUTTON2_PIN != GPIO_NONE
++buttons;
#endif
#if BUTTON3_PIN != GPIO_NONE
++buttons;
#endif
#if BUTTON4_PIN != GPIO_NONE
++buttons;
#endif
#if BUTTON5_PIN != GPIO_NONE
++buttons;
#endif
#if BUTTON6_PIN != GPIO_NONE
++buttons;
#endif
#if BUTTON7_PIN != GPIO_NONE
++buttons;
#endif
#if BUTTON8_PIN != GPIO_NONE
++buttons;
#endif
_buttons.reserve(buttons);
#if BUTTON_PROVIDER_GENERIC_SUPPORT
// TODO: allow to change gpio pin type based on config?
#if (BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_GENERIC)
using gpio_type = GpioPin;
#elif (BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_MCP23S08)
using gpio_type = McpGpioPin;
#endif
// Generic GPIO input handlers
{
_buttons.reserve(_buttonPreconfiguredPins());
for (unsigned char index = 0; index < ButtonsMax; ++index) {
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)); const auto pin = getSetting({"btnGPIO", index}, _buttonPin(index));
#if (BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_GENERIC)
if (!gpioValid(pin)) {
break;
}
#elif (BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_MCP23S08)
if (!mcpGpioValid(pin)) {
break;
}
#endif
auto managed_pin = _buttonFromProvider(index, provider, pin);
if (!managed_pin) {
break;
}
const auto relayID = getSetting({"btnRelay", index}, _buttonRelay(index)); const auto relayID = getSetting({"btnRelay", index}, _buttonRelay(index));
@ -753,15 +880,36 @@ void buttonSetup() {
getSetting({"btnTclk", index}, _buttonTripleClick(index)) getSetting({"btnTclk", index}, _buttonTripleClick(index))
}; };
const auto config = _buttonConfig(index);
const auto config = _buttonRuntimeConfig(index);
_buttons.emplace_back( _buttons.emplace_back(
std::make_shared<gpio_type>(pin), config,
managed_pin, config,
relayID, actions, delays relayID, actions, delays
); );
} }
#endif
}
#endif
#if TERMINAL_SUPPORT
if (_buttons.size()) {
terminalRegisterCommand(F("BUTTON"), [](const terminal::CommandContext& ctx) {
unsigned index { 0u };
for (auto& button : _buttons) {
ctx.output.printf("%u - ", index++);
if (button.event_emitter) {
auto pin = button.event_emitter->getPin();
ctx.output.println(pin->description());
} else {
ctx.output.println(F("Virtual"));
}
}
terminalOK(ctx);
});
}
#endif
_buttonConfigure(); _buttonConfigure();


+ 39
- 0
code/espurna/button_config.h View File

@ -230,3 +230,42 @@ constexpr const bool _buttonMqttRetain(unsigned char index) {
(index == 7) ? (1 == BUTTON8_MQTT_RETAIN) : (1 == BUTTON_MQTT_RETAIN) (index == 7) ? (1 == BUTTON8_MQTT_RETAIN) : (1 == BUTTON_MQTT_RETAIN)
); );
} }
constexpr int _buttonProvider(unsigned char index) {
return (
(index == 0) ? (BUTTON1_PROVIDER) :
(index == 1) ? (BUTTON2_PROVIDER) :
(index == 2) ? (BUTTON3_PROVIDER) :
(index == 3) ? (BUTTON4_PROVIDER) :
(index == 4) ? (BUTTON5_PROVIDER) :
(index == 5) ? (BUTTON6_PROVIDER) :
(index == 6) ? (BUTTON7_PROVIDER) :
(index == 7) ? (BUTTON8_PROVIDER) : BUTTON_PROVIDER_GENERIC
);
}
constexpr int _buttonAnalogLevel(unsigned char index) {
return (
(index == 0) ? (BUTTON1_ANALOG_LEVEL) :
(index == 1) ? (BUTTON2_ANALOG_LEVEL) :
(index == 2) ? (BUTTON3_ANALOG_LEVEL) :
(index == 3) ? (BUTTON4_ANALOG_LEVEL) :
(index == 4) ? (BUTTON5_ANALOG_LEVEL) :
(index == 5) ? (BUTTON6_ANALOG_LEVEL) :
(index == 6) ? (BUTTON7_ANALOG_LEVEL) :
(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))
);
}

+ 50
- 0
code/espurna/config/defaults.h View File

@ -397,6 +397,56 @@
#define BUTTON8_MQTT_RETAIN BUTTON_MQTT_RETAIN #define BUTTON8_MQTT_RETAIN BUTTON_MQTT_RETAIN
#endif #endif
#ifndef BUTTON1_PROVIDER
#define BUTTON1_PROVIDER BUTTON_PROVIDER_GENERIC
#endif
#ifndef BUTTON2_PROVIDER
#define BUTTON2_PROVIDER BUTTON_PROVIDER_GENERIC
#endif
#ifndef BUTTON3_PROVIDER
#define BUTTON3_PROVIDER BUTTON_PROVIDER_GENERIC
#endif
#ifndef BUTTON4_PROVIDER
#define BUTTON4_PROVIDER BUTTON_PROVIDER_GENERIC
#endif
#ifndef BUTTON5_PROVIDER
#define BUTTON5_PROVIDER BUTTON_PROVIDER_GENERIC
#endif
#ifndef BUTTON6_PROVIDER
#define BUTTON6_PROVIDER BUTTON_PROVIDER_GENERIC
#endif
#ifndef BUTTON7_PROVIDER
#define BUTTON7_PROVIDER BUTTON_PROVIDER_GENERIC
#endif
#ifndef BUTTON8_PROVIDER
#define BUTTON8_PROVIDER BUTTON_PROVIDER_GENERIC
#endif
#ifndef BUTTON1_ANALOG_LEVEL
#define BUTTON1_ANALOG_LEVEL 0
#endif
#ifndef BUTTON2_ANALOG_LEVEL
#define BUTTON2_ANALOG_LEVEL 0
#endif
#ifndef BUTTON3_ANALOG_LEVEL
#define BUTTON3_ANALOG_LEVEL 0
#endif
#ifndef BUTTON4_ANALOG_LEVEL
#define BUTTON4_ANALOG_LEVEL 0
#endif
#ifndef BUTTON5_ANALOG_LEVEL
#define BUTTON5_ANALOG_LEVEL 0
#endif
#ifndef BUTTON6_ANALOG_LEVEL
#define BUTTON6_ANALOG_LEVEL 0
#endif
#ifndef BUTTON7_ANALOG_LEVEL
#define BUTTON7_ANALOG_LEVEL 0
#endif
#ifndef BUTTON8_ANALOG_LEVEL
#define BUTTON8_ANALOG_LEVEL 0
#endif
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Encoders // Encoders
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------


+ 13
- 4
code/espurna/config/dependencies.h View File

@ -185,12 +185,13 @@
#endif #endif
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// When using Dual / Lightfox Dual, notify that Serial should be used
// Remove serial debug support completely in case hardware does not support it
// TODO: provide runtime check as well?
#if (BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_ITEAD_SONOFF_DUAL) || \
(BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_FOXEL_LIGHTFOX_DUAL)
#if (BUTTON_PROVIDER_ITEAD_SONOFF_DUAL_SUPPORT) || \
(BUTTON_PROVIDER_FOXEL_LIGHTFOX_DUAL_SUPPORT)
#if DEBUG_SERIAL_SUPPORT #if DEBUG_SERIAL_SUPPORT
#warning "DEBUG_SERIAL_SUPPORT conflicts with the current BUTTON_EVENTS_SOURCE"
#warning "DEBUG_SERIAL_SUPPORT will be disabled because it conflicts with the BUTTON_PROVIDER_{ITEAD_SONOFF_DUAL,FOXEL_LIGHTFOX_DUAL}"
#undef DEBUG_SERIAL_SUPPORT #undef DEBUG_SERIAL_SUPPORT
#define DEBUG_SERIAL_SUPPORT 0 #define DEBUG_SERIAL_SUPPORT 0
#endif #endif
@ -232,3 +233,11 @@
#undef WEB_SUPPORT #undef WEB_SUPPORT
#define WEB_SUPPORT 1 #define WEB_SUPPORT 1
#endif #endif
//------------------------------------------------------------------------------
// Analog pin needs ADC_TOUT mode set up at compile time
#if BUTTON_PROVIDER_ANALOG_SUPPORT
#undef ADC_MODE_VALUE
#define ADC_MODE_VALUE ADC_TOUT
#endif

+ 30
- 7
code/espurna/config/general.h View File

@ -393,19 +393,42 @@
#ifndef BUTTON_MQTT_SEND_ALL_EVENTS #ifndef BUTTON_MQTT_SEND_ALL_EVENTS
#define BUTTON_MQTT_SEND_ALL_EVENTS 0 // 0 - to send only events the are bound to actions #define BUTTON_MQTT_SEND_ALL_EVENTS 0 // 0 - to send only events the are bound to actions
// 1 - to send all button events to MQTT
// 1 - to send all button events to MQTT
#endif #endif
#ifndef BUTTON_MQTT_RETAIN #ifndef BUTTON_MQTT_RETAIN
#define BUTTON_MQTT_RETAIN 0 #define BUTTON_MQTT_RETAIN 0
#endif #endif
#ifndef BUTTON_EVENTS_SOURCE
#define BUTTON_EVENTS_SOURCE BUTTON_EVENTS_SOURCE_GENERIC // Type of button event source. One of:
// BUTTON_EVENTS_SOURCE_GENERIC - GPIOs (virtual or real)
// BUTTON_EVENTS_SOURCE_SONOFF_DUAL - hardware specific, drive buttons through serial connection
// BUTTON_EVENTS_SOURCE_FOXEL_LIGHTFOX_DUAL - similar to Itead Sonoff Dual, hardware specific
// BUTTON_EVENTS_SOURCE_MCP23S08 - activate virtual button connected to gpio expander
// 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
#endif
// Resistor ladder support. Poll analog pin and return digital LOW when analog reading is in a certain range
// ref. https://github.com/bxparks/AceButton/tree/develop/docs/resistor_ladder
// Uses BUTTON#_ANALOG_LEVEL for the individual button level configuration
#ifndef BUTTON_PROVIDER_ANALOG_SUPPORT
#define BUTTON_PROVIDER_ANALOG_SUPPORT 0
#endif #endif
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------


+ 6
- 4
code/espurna/config/hardware.h View File

@ -566,7 +566,7 @@
#define BUTTON2_RELAY 2 #define BUTTON2_RELAY 2
#define BUTTON3_RELAY 1 #define BUTTON3_RELAY 1
#define BUTTON_EVENTS_SOURCE BUTTON_EVENTS_SOURCE_ITEAD_SONOFF_DUAL
#define BUTTON_PROVIDER_ITEAD_SONOFF_DUAL_SUPPORT 1
// LEDs // LEDs
#define LED1_PIN 13 #define LED1_PIN 13
@ -4266,7 +4266,7 @@
#define BUTTON3_RELAY 2 #define BUTTON3_RELAY 2
#define BUTTON4_RELAY 1 #define BUTTON4_RELAY 1
#define BUTTON_EVENTS_SOURCE BUTTON_EVENTS_SOURCE_FOXEL_LIGHTFOX_DUAL
#define BUTTON_PROVIDER_FOXEL_LIGHTFOX_DUAL_SUPPORT 1
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Teckin SP20 // Teckin SP20
@ -4869,15 +4869,19 @@
#define RELAY4_PIN 7 #define RELAY4_PIN 7
// Buttons // Buttons
#define BUTTON1_PROVIDER BUTTON_PROVIDER_MCP23S08
#define BUTTON1_CONFIG BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH #define BUTTON1_CONFIG BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON1_PIN 0 #define BUTTON1_PIN 0
#define BUTTON2_PROVIDER BUTTON_PROVIDER_MCP23S08
#define BUTTON2_CONFIG BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH #define BUTTON2_CONFIG BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON2_PIN 1 #define BUTTON2_PIN 1
#define BUTTON3_PROVIDER BUTTON_PROVIDER_MCP23S08
#define BUTTON3_CONFIG BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH #define BUTTON3_CONFIG BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON3_PIN 2 #define BUTTON3_PIN 2
#define BUTTON4_PROVIDER BUTTON_PROVIDER_MCP23S08
#define BUTTON4_CONFIG BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH #define BUTTON4_CONFIG BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
#define BUTTON4_PIN 3 #define BUTTON4_PIN 3
@ -4886,8 +4890,6 @@
#define BUTTON3_RELAY 3 #define BUTTON3_RELAY 3
#define BUTTON4_RELAY 4 #define BUTTON4_RELAY 4
#define BUTTON_EVENTS_SOURCE BUTTON_EVENTS_SOURCE_MCP23S08
// LEDs // LEDs
#define LED1_PIN 2 #define LED1_PIN 2
#define LED1_PIN_INVERSE 1 #define LED1_PIN_INVERSE 1


+ 4
- 5
code/espurna/config/types.h View File

@ -57,11 +57,10 @@
#define BUTTON_SET_PULLUP ButtonMask::SetPullup #define BUTTON_SET_PULLUP ButtonMask::SetPullup
#define BUTTON_SET_PULLDOWN ButtonMask::SetPulldown #define BUTTON_SET_PULLDOWN ButtonMask::SetPulldown
// configure which type of event emitter is used
#define BUTTON_EVENTS_SOURCE_GENERIC 0
#define BUTTON_EVENTS_SOURCE_ITEAD_SONOFF_DUAL 1
#define BUTTON_EVENTS_SOURCE_FOXEL_LIGHTFOX_DUAL 2
#define BUTTON_EVENTS_SOURCE_MCP23S08 3
// configure where do we get the button events
#define BUTTON_PROVIDER_GENERIC 0
#define BUTTON_PROVIDER_MCP23S08 1
#define BUTTON_PROVIDER_ANALOG 2
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// ENCODER // ENCODER


+ 2
- 20
code/espurna/gpio.cpp View File

@ -8,28 +8,10 @@ Copyright (C) 2017-2019 by Xose Pérez <xose dot perez at gmail dot com>
#include "espurna.h" #include "espurna.h"
#include <bitset>
// We need to explicitly call the constructor, because we need to set the const `pin`:
// https://isocpp.org/wiki/faq/multiple-inheritance#virtual-inheritance-ctors
GpioPin::GpioPin(unsigned char pin) :
BasePin(pin)
{}
inline void GpioPin::pinMode(int8_t mode) {
::pinMode(this->pin, mode);
}
inline void GpioPin::digitalWrite(int8_t val) {
::digitalWrite(this->pin, val);
}
inline int GpioPin::digitalRead() {
return ::digitalRead(this->pin);
}
// -------------------------------------------------------------------------- // --------------------------------------------------------------------------
#include <bitset>
std::bitset<GpioPins> _gpio_locked; std::bitset<GpioPins> _gpio_locked;
std::bitset<GpioPins> _gpio_available; std::bitset<GpioPins> _gpio_available;


+ 0
- 13
code/espurna/gpio.h View File

@ -8,24 +8,11 @@ Copyright (C) 2017-2019 by Xose Pérez <xose dot perez at gmail dot com>
#pragma once #pragma once
#include <cstdint>
#include "espurna.h" #include "espurna.h"
#include "libs/BasePin.h" #include "libs/BasePin.h"
constexpr const size_t GpioPins = 17; constexpr const size_t GpioPins = 17;
// real hardware pin
class GpioPin final : virtual public BasePin {
public:
explicit GpioPin(unsigned char pin);
void pinMode(int8_t mode);
void digitalWrite(int8_t val);
int digitalRead();
};
bool gpioValid(unsigned char gpio); bool gpioValid(unsigned char gpio);
bool gpioGetLock(unsigned char gpio); bool gpioGetLock(unsigned char gpio);
bool gpioReleaseLock(unsigned char gpio); bool gpioReleaseLock(unsigned char gpio);


+ 39
- 0
code/espurna/gpio_pin.h View File

@ -0,0 +1,39 @@
/*
Part of the GPIO MODULE
Copyright (C) 2017-2019 by Xose Pérez <xose dot perez at gmail dot com>
*/
#pragma once
#include "gpio.h"
#include <cstdint>
class GpioPin final : public BasePin {
public:
explicit GpioPin(unsigned char pin_) :
BasePin(pin_)
{}
void pinMode(int8_t mode) override {
::pinMode(this->pin, mode);
}
void digitalWrite(int8_t val) override {
::digitalWrite(this->pin, val);
}
String description() const override {
static String desc(String(F("GpioPin @ GPIO")) + static_cast<int>(pin));
return desc;
}
int digitalRead() {
return ::digitalRead(this->pin);
}
};

+ 27
- 4
code/espurna/libs/BasePin.h View File

@ -1,6 +1,6 @@
/* /*
Part of BUTTON module
Generic digital pin interface
Copyright (C) 2020 by Maxim Prokhorov <prokhorov dot max at outlook dot com> Copyright (C) 2020 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
@ -8,15 +8,37 @@ Copyright (C) 2020 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
#pragma once #pragma once
#include <Arduino.h>
#include <cstdint> #include <cstdint>
#include "../config/types.h" #include "../config/types.h"
// base interface for generic pin handler.
struct BasePin {
class BasePin {
public:
// TODO: we always need to explicitly call the constructor from the child
// class, because we need to set the const `pin` member on construction
// - https://isocpp.org/wiki/faq/multiple-inheritance#virtual-inheritance-ctors
// TODO: vtable anchoring? same applies to every implemented ..._pin.h
// not a problem when using single-source aka unity build (build with `env ESPURNA_BUILD_SINGLE_SOURCE=1`)
//
// Some sources:
// - https://llvm.org/docs/CodingStandards.html#provide-a-virtual-method-anchor-for-classes-in-headers
// > If a class is defined in a header file and has a vtable (either it has virtual methods or it derives from classes with virtual methods),
// > it must always have at least one out-of-line virtual method in the class. Without this, the compiler will copy the vtable and RTTI into
// > every .o file that #includes the header, bloating .o file sizes and increasing link times.
// - http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1263r0.pdf
// > 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) : explicit BasePin(unsigned char pin) :
pin(pin) pin(pin)
{} {}
virtual ~BasePin() {
}
virtual operator bool() { virtual operator bool() {
return GPIO_NONE != pin; return GPIO_NONE != pin;
} }
@ -24,6 +46,7 @@ struct BasePin {
virtual void pinMode(int8_t mode) = 0; virtual void pinMode(int8_t mode) = 0;
virtual void digitalWrite(int8_t val) = 0; virtual void digitalWrite(int8_t val) = 0;
virtual int digitalRead() = 0; virtual int digitalRead() = 0;
virtual String description() const = 0;
const unsigned char pin;
const unsigned char pin { GPIO_NONE };
}; };

+ 0
- 16
code/espurna/mcp23s08.cpp View File

@ -42,22 +42,6 @@ Copyright (C) 2016 Plamen Kovandjiev <p.kovandiev@kmpelectronics.eu> & Dimitar A
static uint8_t _mcp23s08TxData[16] __attribute__((aligned(4))); static uint8_t _mcp23s08TxData[16] __attribute__((aligned(4)));
static uint8_t _mcp23s08RxData[16] __attribute__((aligned(4))); static uint8_t _mcp23s08RxData[16] __attribute__((aligned(4)));
McpGpioPin::McpGpioPin(unsigned char pin) :
BasePin(pin)
{}
inline void McpGpioPin::pinMode(int8_t mode) {
::MCP23S08SetDirection(this->pin, mode);
}
inline void McpGpioPin::digitalWrite(int8_t val) {
::MCP23S08SetPin(this->pin, val);
}
inline int McpGpioPin::digitalRead() {
return ::MCP23S08GetPin(this->pin);
}
void MCP23S08Setup() void MCP23S08Setup()
{ {
DEBUG_MSG_P(PSTR("[MCP23S08] Initialize SPI bus\n")); DEBUG_MSG_P(PSTR("[MCP23S08] Initialize SPI bus\n"));


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

@ -13,26 +13,10 @@ Copyright (C) 2016 Plamen Kovandjiev <p.kovandiev@kmpelectronics.eu> & Dimitar A
#pragma once #pragma once
#ifndef MCP23S08_H
#define MCP23S08_H
#include "espurna.h" #include "espurna.h"
#include "libs/BasePin.h"
#if MCP23S08_SUPPORT
constexpr size_t McpGpioPins = 8; constexpr size_t McpGpioPins = 8;
// real hardware pin
class McpGpioPin final : public BasePin {
public:
explicit McpGpioPin(unsigned char pin);
void pinMode(int8_t mode);
void digitalWrite(int8_t val);
int digitalRead();
};
void MCP23S08Setup(); void MCP23S08Setup();
uint8_t MCP23S08ReadRegister(uint8_t address); uint8_t MCP23S08ReadRegister(uint8_t address);
@ -43,7 +27,3 @@ void MCP23S08SetPin(uint8_t pinNumber, bool state);
bool MCP23S08GetPin(uint8_t pinNumber); bool MCP23S08GetPin(uint8_t pinNumber);
bool mcpGpioValid(unsigned char gpio); bool mcpGpioValid(unsigned char gpio);
#endif // MCP23S08_SUPPORT == 1
#endif

+ 43
- 0
code/espurna/mcp23s08_pin.h View File

@ -0,0 +1,43 @@
/*
Part of the MCP23S08 MODULE
Copyright (C) 2020 by Eddi De Pieri <eddi at depieri dot com>
Adapted from https://github.com/kmpelectronics/Arduino
Copyright (C) 2016 Plamen Kovandjiev <p.kovandiev@kmpelectronics.eu> & Dimitar Antonov <d.antonov@kmpelectronics.eu>
(ref. https://github.com/kmpelectronics/Arduino/blob/master/ProDinoWiFiEsp/src/PRODINoESP8266/src/KMPDinoWiFiESP.cpp)
*/
#pragma once
#include "libs/BasePin.h"
#include "mcp23s08.h"
class McpGpioPin final : public BasePin {
public:
explicit McpGpioPin(unsigned char pin) :
BasePin(pin)
{}
void pinMode(int8_t mode) override {
::MCP23S08SetDirection(this->pin, mode);
}
void digitalWrite(int8_t val) override {
::MCP23S08SetPin(this->pin, val);
}
int digitalRead() override {
return ::MCP23S08GetPin(this->pin);
}
String description() const override {
static String desc(String(F("McpGpioPin @ GPIO")) + static_cast<int>(pin));
return desc;
}
};

+ 17
- 5
code/espurna/relay.cpp View File

@ -28,20 +28,32 @@ Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
#include "tuya.h" #include "tuya.h"
#include "utils.h" #include "utils.h"
#include "ws.h" #include "ws.h"
#include "mcp23s08.h"
#include "libs/BasePin.h" #include "libs/BasePin.h"
#include "gpio_pin.h"
#include "mcp23s08_pin.h"
#include "relay_config.h" #include "relay_config.h"
struct DummyPin final : public BasePin {
class DummyPin final : public BasePin {
public:
DummyPin(unsigned char pin) : DummyPin(unsigned char pin) :
BasePin(pin) BasePin(pin)
{} {}
void pinMode(int8_t) override {}
void digitalWrite(int8_t) override {}
int digitalRead() override { return 0; }
void pinMode(int8_t) override {
}
void digitalWrite(int8_t) override {
}
int digitalRead() override {
return 0;
}
String description() const override {
return F("DummyPin");
}
}; };
struct relay_t { struct relay_t {


+ 38
- 18
code/espurna/terminal.cpp View File

@ -293,31 +293,51 @@ void _terminalInitCommands() {
*((int*) 0) = 0; // see https://github.com/esp8266/Arduino/issues/1494 *((int*) 0) = 0; // see https://github.com/esp8266/Arduino/issues/1494
}); });
terminalRegisterCommand(F("ADC"), [](const terminal::CommandContext& ctx) {
const int pin = (ctx.argc == 2)
? ctx.argv[1].toInt()
: A0;
ctx.output.println(analogRead(pin));
terminalOK(ctx);
});
terminalRegisterCommand(F("GPIO"), [](const terminal::CommandContext& ctx) { terminalRegisterCommand(F("GPIO"), [](const terminal::CommandContext& ctx) {
int pin = -1;
if (ctx.argc < 2) {
DEBUG_MSG("Printing all GPIO pins:\n");
} else {
pin = ctx.argv[1].toInt();
if (!gpioValid(pin)) {
terminalError(F("Invalid GPIO pin"));
return;
}
const int pin = (ctx.argc >= 2)
? ctx.argv[1].toInt()
: -1;
if (ctx.argc > 2) {
bool state = String(ctx.argv[2]).toInt() == 1;
digitalWrite(pin, state);
}
if ((pin >= 0) && !gpioValid(pin)) {
terminalError(ctx, F("Invalid pin number"));
return;
} }
for (int i = 0; i <= 15; i++) {
if (gpioValid(i) && (pin == -1 || pin == i)) {
DEBUG_MSG_P(PSTR("GPIO %s pin %d is %s\n"), GPEP(i) ? "output" : "input", i, digitalRead(i) == HIGH ? "HIGH" : "LOW");
int start = 0;
int end = GpioPins;
switch (ctx.argc) {
case 3:
pinMode(pin, OUTPUT);
digitalWrite(pin, (1 == ctx.argv[2].toInt()));
break;
case 2:
start = pin;
end = pin + 1;
// fallthrough into print
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",
current,
(HIGH == digitalRead(current)) ? "HIGH" : "LOW"
);
}
} }
break;
} }
terminalOK();
terminalOK(ctx);
}); });
terminalRegisterCommand(F("HEAP"), [](const terminal::CommandContext&) { terminalRegisterCommand(F("HEAP"), [](const terminal::CommandContext&) {


+ 3
- 0
code/test/build/nondefault.h View File

@ -15,3 +15,6 @@
#define PROMETHEUS_SUPPORT 1 #define PROMETHEUS_SUPPORT 1
#define RFB_SUPPORT 1 #define RFB_SUPPORT 1
#define RFB_PROVIDER RFB_PROVIDER_RCSWITCH #define RFB_PROVIDER RFB_PROVIDER_RCSWITCH
#define MCP23S08_SUPPORT 1
#define BUTTON_PROVIDER_ANALOG_SUPPORT 1
#define BUTTON_PROVIDER_ITEAD_SONOFF_DUAL_SUPPORT 1

Loading…
Cancel
Save