From c7a95bf53f51d096e8fdcc53e3d0c7add75d62ae Mon Sep 17 00:00:00 2001 From: Max Prokhorov Date: Thu, 24 Sep 2020 07:51:36 +0300 Subject: [PATCH] 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 --- README.md | 1 + code/espurna/board.cpp | 4 - code/espurna/button.cpp | 338 +++++++++++++++++++++-------- code/espurna/button_config.h | 39 ++++ code/espurna/config/defaults.h | 50 +++++ code/espurna/config/dependencies.h | 17 +- code/espurna/config/general.h | 37 +++- code/espurna/config/hardware.h | 10 +- code/espurna/config/types.h | 9 +- code/espurna/gpio.cpp | 22 +- code/espurna/gpio.h | 13 -- code/espurna/gpio_pin.h | 39 ++++ code/espurna/libs/BasePin.h | 31 ++- code/espurna/mcp23s08.cpp | 16 -- code/espurna/mcp23s08.h | 20 -- code/espurna/mcp23s08_pin.h | 43 ++++ code/espurna/relay.cpp | 22 +- code/espurna/terminal.cpp | 56 +++-- code/test/build/nondefault.h | 3 + 19 files changed, 555 insertions(+), 215 deletions(-) create mode 100644 code/espurna/gpio_pin.h create mode 100644 code/espurna/mcp23s08_pin.h diff --git a/README.md b/README.md index 89bd8842..8a0fdc4a 100644 --- a/README.md +++ b/README.md @@ -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) * Switch management * 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) * 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) diff --git a/code/espurna/board.cpp b/code/espurna/board.cpp index 2aaf9966..ccceadc6 100644 --- a/code/espurna/board.cpp +++ b/code/espurna/board.cpp @@ -21,11 +21,7 @@ PROGMEM const char espurna_modules[] = "BROKER " #endif #if BUTTON_SUPPORT - #if BUTTON_EVENTS_SOURCE == BUTTON_EVENTS_SOURCE_GENERIC "BUTTON " - #else - "BUTTON_DUAL " - #endif #endif #if DEBUG_SERIAL_SUPPORT "DEBUG_SERIAL " diff --git a/code/espurna/button.cpp b/code/espurna/button.cpp index da8acc5b..c99308fa 100644 --- a/code/espurna/button.cpp +++ b/code/espurna/button.cpp @@ -21,11 +21,13 @@ Copyright (C) 2016-2019 by Xose Pérez #include "relay.h" #include "light.h" #include "ws.h" -#include "mcp23s08.h" - -#include "button_config.h" +#include "libs/BasePin.h" #include "libs/DebounceEvent.h" +#include "gpio_pin.h" +#include "mcp23s08_pin.h" + +#include "button_config.h" 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)); return { 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) { root["btnRepDel"] = getSetting("btnRepDel", _buttonRepeatDelay()); @@ -301,20 +294,20 @@ void _buttonWebSocketOnConnected(JsonObject& root) { 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` // TODO: schema names are uppercase to easily match settings? // TODO: schema name->type map to generate WebUI elements? JsonArray& schema = module.createNestedArray("_schema"); + schema.add("Prov"); + schema.add("GPIO"); schema.add("Mode"); schema.add("DefVal"); schema.add("PinMode"); - schema.add("Relay"); - schema.add("Press"); schema.add("Click"); schema.add("Dclk"); @@ -327,10 +320,14 @@ void _buttonWebSocketOnConnected(JsonObject& root) { schema.add("LclkDel"); 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"); @@ -338,9 +335,10 @@ void _buttonWebSocketOnConnected(JsonObject& root) { JsonArray& button = buttons.createNestedArray(); // TODO: configure PIN object instead of button specifically, link PIN<->BUTTON + button.add(getSetting({"btnProv", index}, _buttonProvider(index))); if (_buttons[i].getPin()) { button.add(getSetting({"btnGPIO", index}, _buttonPin(index))); - const auto config = _buttonConfig(index); + const auto config = _buttonRuntimeConfig(index); button.add(static_cast(config.mode)); button.add(static_cast(config.default_value)); button.add(static_cast(config.pin_mode)); @@ -352,8 +350,6 @@ void _buttonWebSocketOnConnected(JsonObject& root) { button.add(0); } - button.add(_buttons[i].relayID); - button.add(_buttons[i].actions.pressed); button.add(_buttons[i].actions.click); 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.lnglngclick); +#if RELAY_SUPPORT + button.add(_buttons[i].relayID); +#endif + // 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 // 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); } -#endif +#endif // WEB_SUPPORT bool buttonState(unsigned char id) { if (id >= _buttons.size()) return false; @@ -570,7 +568,7 @@ void _buttonLoopSonoffDual() { 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 // 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); @@ -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() { - #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(); - #else - #warning "Unknown value for BUTTON_EVENTS_SOURCE" #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 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::pins; + +#endif // BUTTON_PROVIDER_ANALOG_SUPPORT + +std::shared_ptr _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(new GpioPin(pin)); + +#if BUTTON_PROVIDER_MCP23S08_SUPPORT + case BUTTON_PROVIDER_MCP23S08: + if (!mcpGpioValid(pin)) { + break; + } + return std::shared_ptr(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(new AnalogPin(pin, level)); + } +#endif + + default: + break; + } + + return {}; +} + void buttonSetup() { // Backwards compatibility moveSetting("btnDelay", "btnRepDel"); // 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; #if BUTTON1_RELAY != RELAY_NONE ++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)); - #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)); @@ -753,15 +880,36 @@ void buttonSetup() { getSetting({"btnTclk", index}, _buttonTripleClick(index)) }; - const auto config = _buttonConfig(index); + const auto config = _buttonRuntimeConfig(index); _buttons.emplace_back( - std::make_shared(pin), config, + managed_pin, config, 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(); diff --git a/code/espurna/button_config.h b/code/espurna/button_config.h index 2660c635..66e07bd9 100644 --- a/code/espurna/button_config.h +++ b/code/espurna/button_config.h @@ -230,3 +230,42 @@ constexpr const bool _buttonMqttRetain(unsigned char index) { (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)) + ); +} diff --git a/code/espurna/config/defaults.h b/code/espurna/config/defaults.h index 8c2e5adc..f4a3c2f6 100644 --- a/code/espurna/config/defaults.h +++ b/code/espurna/config/defaults.h @@ -397,6 +397,56 @@ #define BUTTON8_MQTT_RETAIN BUTTON_MQTT_RETAIN #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 // ----------------------------------------------------------------------------- diff --git a/code/espurna/config/dependencies.h b/code/espurna/config/dependencies.h index f3f52d83..a97a5fe7 100644 --- a/code/espurna/config/dependencies.h +++ b/code/espurna/config/dependencies.h @@ -185,12 +185,13 @@ #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 -#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 #define DEBUG_SERIAL_SUPPORT 0 #endif @@ -232,3 +233,11 @@ #undef WEB_SUPPORT #define WEB_SUPPORT 1 #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 diff --git a/code/espurna/config/general.h b/code/espurna/config/general.h index fe1ecc34..158b59fc 100644 --- a/code/espurna/config/general.h +++ b/code/espurna/config/general.h @@ -393,19 +393,42 @@ #ifndef BUTTON_MQTT_SEND_ALL_EVENTS #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 #ifndef BUTTON_MQTT_RETAIN #define BUTTON_MQTT_RETAIN 0 #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 //------------------------------------------------------------------------------ diff --git a/code/espurna/config/hardware.h b/code/espurna/config/hardware.h index 25ddcebb..2ff8b2db 100644 --- a/code/espurna/config/hardware.h +++ b/code/espurna/config/hardware.h @@ -566,7 +566,7 @@ #define BUTTON2_RELAY 2 #define BUTTON3_RELAY 1 - #define BUTTON_EVENTS_SOURCE BUTTON_EVENTS_SOURCE_ITEAD_SONOFF_DUAL + #define BUTTON_PROVIDER_ITEAD_SONOFF_DUAL_SUPPORT 1 // LEDs #define LED1_PIN 13 @@ -4266,7 +4266,7 @@ #define BUTTON3_RELAY 2 #define BUTTON4_RELAY 1 - #define BUTTON_EVENTS_SOURCE BUTTON_EVENTS_SOURCE_FOXEL_LIGHTFOX_DUAL + #define BUTTON_PROVIDER_FOXEL_LIGHTFOX_DUAL_SUPPORT 1 // ----------------------------------------------------------------------------- // Teckin SP20 @@ -4869,15 +4869,19 @@ #define RELAY4_PIN 7 // Buttons + #define BUTTON1_PROVIDER BUTTON_PROVIDER_MCP23S08 #define BUTTON1_CONFIG BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH #define BUTTON1_PIN 0 + #define BUTTON2_PROVIDER BUTTON_PROVIDER_MCP23S08 #define BUTTON2_CONFIG BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH #define BUTTON2_PIN 1 + #define BUTTON3_PROVIDER BUTTON_PROVIDER_MCP23S08 #define BUTTON3_CONFIG BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH #define BUTTON3_PIN 2 + #define BUTTON4_PROVIDER BUTTON_PROVIDER_MCP23S08 #define BUTTON4_CONFIG BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH #define BUTTON4_PIN 3 @@ -4886,8 +4890,6 @@ #define BUTTON3_RELAY 3 #define BUTTON4_RELAY 4 - #define BUTTON_EVENTS_SOURCE BUTTON_EVENTS_SOURCE_MCP23S08 - // LEDs #define LED1_PIN 2 #define LED1_PIN_INVERSE 1 diff --git a/code/espurna/config/types.h b/code/espurna/config/types.h index 952f866b..c7c31da8 100644 --- a/code/espurna/config/types.h +++ b/code/espurna/config/types.h @@ -57,11 +57,10 @@ #define BUTTON_SET_PULLUP ButtonMask::SetPullup #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 diff --git a/code/espurna/gpio.cpp b/code/espurna/gpio.cpp index 14ba9f12..060995b6 100644 --- a/code/espurna/gpio.cpp +++ b/code/espurna/gpio.cpp @@ -8,28 +8,10 @@ Copyright (C) 2017-2019 by Xose Pérez #include "espurna.h" -#include - -// 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 + std::bitset _gpio_locked; std::bitset _gpio_available; diff --git a/code/espurna/gpio.h b/code/espurna/gpio.h index 0fb01684..158eb09b 100644 --- a/code/espurna/gpio.h +++ b/code/espurna/gpio.h @@ -8,24 +8,11 @@ Copyright (C) 2017-2019 by Xose Pérez #pragma once -#include - #include "espurna.h" #include "libs/BasePin.h" 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 gpioGetLock(unsigned char gpio); bool gpioReleaseLock(unsigned char gpio); diff --git a/code/espurna/gpio_pin.h b/code/espurna/gpio_pin.h new file mode 100644 index 00000000..31699d67 --- /dev/null +++ b/code/espurna/gpio_pin.h @@ -0,0 +1,39 @@ +/* + +Part of the GPIO MODULE + +Copyright (C) 2017-2019 by Xose Pérez + +*/ + +#pragma once + +#include "gpio.h" + +#include + +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(pin)); + return desc; + } + + int digitalRead() { + return ::digitalRead(this->pin); + } +}; + diff --git a/code/espurna/libs/BasePin.h b/code/espurna/libs/BasePin.h index 7614fa52..1c46b221 100644 --- a/code/espurna/libs/BasePin.h +++ b/code/espurna/libs/BasePin.h @@ -1,6 +1,6 @@ /* -Part of BUTTON module +Generic digital pin interface Copyright (C) 2020 by Maxim Prokhorov @@ -8,15 +8,37 @@ Copyright (C) 2020 by Maxim Prokhorov #pragma once +#include + #include #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) : pin(pin) {} + virtual ~BasePin() { + } + virtual operator bool() { return GPIO_NONE != pin; } @@ -24,6 +46,7 @@ struct BasePin { 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; + const unsigned char pin { GPIO_NONE }; }; diff --git a/code/espurna/mcp23s08.cpp b/code/espurna/mcp23s08.cpp index a0bbaad4..5eb93d1c 100644 --- a/code/espurna/mcp23s08.cpp +++ b/code/espurna/mcp23s08.cpp @@ -42,22 +42,6 @@ Copyright (C) 2016 Plamen Kovandjiev & Dimitar A static uint8_t _mcp23s08TxData[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() { DEBUG_MSG_P(PSTR("[MCP23S08] Initialize SPI bus\n")); diff --git a/code/espurna/mcp23s08.h b/code/espurna/mcp23s08.h index b32e3de8..ad4263a9 100644 --- a/code/espurna/mcp23s08.h +++ b/code/espurna/mcp23s08.h @@ -13,26 +13,10 @@ Copyright (C) 2016 Plamen Kovandjiev & Dimitar A #pragma once -#ifndef MCP23S08_H -#define MCP23S08_H - #include "espurna.h" -#include "libs/BasePin.h" - -#if MCP23S08_SUPPORT 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(); uint8_t MCP23S08ReadRegister(uint8_t address); @@ -43,7 +27,3 @@ void MCP23S08SetPin(uint8_t pinNumber, bool state); bool MCP23S08GetPin(uint8_t pinNumber); bool mcpGpioValid(unsigned char gpio); - -#endif // MCP23S08_SUPPORT == 1 - -#endif diff --git a/code/espurna/mcp23s08_pin.h b/code/espurna/mcp23s08_pin.h new file mode 100644 index 00000000..7d4dce3d --- /dev/null +++ b/code/espurna/mcp23s08_pin.h @@ -0,0 +1,43 @@ +/* + +Part of the MCP23S08 MODULE + +Copyright (C) 2020 by Eddi De Pieri + +Adapted from https://github.com/kmpelectronics/Arduino +Copyright (C) 2016 Plamen Kovandjiev & Dimitar Antonov + +(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(pin)); + return desc; + } +}; + diff --git a/code/espurna/relay.cpp b/code/espurna/relay.cpp index 7dfadf16..28a54a75 100644 --- a/code/espurna/relay.cpp +++ b/code/espurna/relay.cpp @@ -28,20 +28,32 @@ Copyright (C) 2016-2019 by Xose Pérez #include "tuya.h" #include "utils.h" #include "ws.h" -#include "mcp23s08.h" #include "libs/BasePin.h" +#include "gpio_pin.h" +#include "mcp23s08_pin.h" #include "relay_config.h" -struct DummyPin final : public BasePin { +class DummyPin final : public BasePin { +public: DummyPin(unsigned char 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 { diff --git a/code/espurna/terminal.cpp b/code/espurna/terminal.cpp index 6ee7a5d3..253737e7 100644 --- a/code/espurna/terminal.cpp +++ b/code/espurna/terminal.cpp @@ -293,31 +293,51 @@ void _terminalInitCommands() { *((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) { - 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&) { diff --git a/code/test/build/nondefault.h b/code/test/build/nondefault.h index d3b1a453..dc73ef00 100644 --- a/code/test/build/nondefault.h +++ b/code/test/build/nondefault.h @@ -15,3 +15,6 @@ #define PROMETHEUS_SUPPORT 1 #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