diff --git a/interface/src/project/EMSESPSettingsController.tsx b/interface/src/project/EMSESPSettingsController.tsx index 2e8fe2af7..53b666efa 100644 --- a/interface/src/project/EMSESPSettingsController.tsx +++ b/interface/src/project/EMSESPSettingsController.tsx @@ -117,6 +117,22 @@ function EMSESPSettingsControllerForm(props: EMSESPSettingsControllerFormProps) margin="normal" />

+ + External Button + + +

Dallas Sensor diff --git a/interface/src/project/EMSESPtypes.ts b/interface/src/project/EMSESPtypes.ts index d1f67b953..8fddcb7db 100644 --- a/interface/src/project/EMSESPtypes.ts +++ b/interface/src/project/EMSESPtypes.ts @@ -17,6 +17,7 @@ export interface EMSESPSettings { hide_led: boolean; api_enabled: boolean; analog_enabled: boolean; + pbutton_gpio: number; trace_raw: boolean; } diff --git a/lib/PButton/PButon.cpp b/lib/PButton/PButon.cpp new file mode 100644 index 000000000..27b23514a --- /dev/null +++ b/lib/PButton/PButon.cpp @@ -0,0 +1,187 @@ +/* + * Paul's Button Library + * + * Handles recognition of button presses, short, long, double-click and special sequences + * Used to send specific commands to the ESP8 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "PButton.h" + +// Constructor +PButton::PButton() { + // Initialization of default properties + Debounce_ = 40; // Debounce period to prevent flickering when pressing or releasing the button (in ms) + DblClickDelay_ = 250; // Max period between clicks for a double click event (in ms) + LongPressDelay_ = 750; // Hold period for a long press event (in ms) + VLongPressDelay_ = 3000; // Hold period for a very long press event (in ms) + + cb_onClick = nullptr; + cb_onDblClick = nullptr; + cb_onLongPress = nullptr; + cb_onVLongPress = nullptr; + + // Initialization of variables + state_ = true; // Value read from button + lastState_ = true; // Last value of button state + dblClickWaiting_ = false; // whether we're waiting for a double click (down) + dblClickOnNextUp_ = false; // whether to register a double click on next release, or whether to wait and click + singleClickOK_ = true; // whether it's OK to do a single click (default is true) + downTime_ = -1; // time the button was pressed down + upTime_ = -1; // time the button was released + ignoreUP_ = true; // whether to ignore the button release because the click+hold was triggered, changed from false for ESP32 + waitForUP_ = false; // when held, whether to wait for the up event + longPressHappened_ = false; // whether or not the hold event happened already + vLongPressHappened_ = false; // whether or not the long hold event happened already + buttonBusy_ = false; // idle +} + +bool PButton::init(uint8_t pin, bool pullMode) { + pin_ = pin; + pullMode_ = pullMode; // 1=HIGH (pullup) 0=LOW (pulldown) + +#if defined(ESP8266) + pinMode(pin_, pullMode ? INPUT_PULLUP : INPUT_PULLDOWN); + enabled_ = (digitalRead(pin_) != pullMode); // see if a button is connected +#else + // ESP32 + pinMode(pin_, INPUT); + enabled_ = (digitalRead(pin_) == pullMode); // see if a button is connected +#endif + + return enabled_; +} + +void PButton::onClick(uint16_t t, buttonEventHandler handler) { + Debounce_ = t; // Debounce period to prevent flickering when pressing or releasing the button (in ms) + cb_onClick = handler; +} + +void PButton::onDblClick(uint16_t t, buttonEventHandler handler) { + DblClickDelay_ = t; // Max period between clicks for a double click event (in ms) + cb_onDblClick = handler; +} + +void PButton::onLongPress(uint16_t t, buttonEventHandler handler) { + LongPressDelay_ = t; // Hold period for a long press event (in ms) + cb_onLongPress = handler; +} + +void PButton::onVLongPress(uint16_t t, buttonEventHandler handler) { + VLongPressDelay_ = t; // Hold period for a very long press event (in ms) + cb_onVLongPress = handler; +} + +bool PButton::check(void) { + if (!enabled_) { + return false; + } + + int resultEvent = 0; + long millisRes = millis(); + + state_ = digitalRead(pin_) == pullMode_; + + // Button pressed down + if (state_ != pullMode_ && lastState_ == pullMode_ && (millisRes - upTime_) > Debounce_) { + // Serial.println("*pressed*"); + downTime_ = millisRes; + ignoreUP_ = false; + waitForUP_ = false; + singleClickOK_ = true; + longPressHappened_ = false; + vLongPressHappened_ = false; + if ((millisRes - upTime_) < DblClickDelay_ && dblClickOnNextUp_ == false && dblClickWaiting_ == true) { + dblClickOnNextUp_ = true; + // Serial.println("*double up next*"); + } else { + dblClickOnNextUp_ = false; + } + dblClickWaiting_ = false; + + buttonBusy_ = true; // something is happening so we'll wait and see what action it is + } + + // Button released + else if (state_ == pullMode_ && lastState_ != pullMode_ && (millisRes - downTime_) > Debounce_) { + if (ignoreUP_ == false) { + upTime_ = millisRes; + // Serial.println("*released*"); + if (dblClickOnNextUp_ == false) { + dblClickWaiting_ = true; + } else { + resultEvent = 2; + // Serial.println("*double*"); + dblClickOnNextUp_ = false; + dblClickWaiting_ = false; + singleClickOK_ = false; + } + } + } + + // Test for normal click event: DblClickDelay expired + if (state_ == pullMode_ && (millisRes - upTime_) >= DblClickDelay_ && dblClickWaiting_ == true && dblClickOnNextUp_ == false && singleClickOK_ == true && resultEvent != 2) { + // Serial.println("*single click pressed*"); + resultEvent = 1; + dblClickWaiting_ = false; + } + + // added code: raise OnLongPress event when only when the button is released + if (state_ == pullMode_ && longPressHappened_ && !vLongPressHappened_) { + resultEvent = 3; + longPressHappened_ = false; + } + + // Test for hold + if (state_ != pullMode_ && (millisRes - downTime_) >= LongPressDelay_) { + // Trigger "normal" hold + if (longPressHappened_ == false) { + // resultEvent = 3; + waitForUP_ = true; + ignoreUP_ = true; + dblClickOnNextUp_ = false; + dblClickWaiting_ = false; + // _downTime = millis(); + longPressHappened_ = true; + } + // Trigger "long" hold + if ((millisRes - downTime_) >= VLongPressDelay_) { + if (vLongPressHappened_ == false) { + resultEvent = 4; + vLongPressHappened_ = true; + // _longPressHappened = false; + } + } + } + + lastState_ = state_; + + if (resultEvent == 1 && cb_onClick) { + cb_onClick(*this); + } else if (resultEvent == 2 && cb_onDblClick) { + cb_onDblClick(*this); + } else if (resultEvent == 3 && cb_onLongPress) { + cb_onLongPress(*this); + } else if (resultEvent == 4 && cb_onVLongPress) { + cb_onVLongPress(*this); + } + + // if any action has been prefromed we can stop waiting, and become idle + if (resultEvent >= 1 && resultEvent <= 4) { + buttonBusy_ = false; + } + + return buttonBusy_; +} diff --git a/lib/PButton/PButton.h b/lib/PButton/PButton.h new file mode 100644 index 000000000..16899ff49 --- /dev/null +++ b/lib/PButton/PButton.h @@ -0,0 +1,69 @@ +/* + * Paul's Button Library + * + * Handles recognition of button presses, short, long, double-click and special sequences + * Used to send specific commands to the ESP8 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef _PButton_H_ +#define _PButton_H_ + +#include + +class PButton { + public: + PButton(); + ~PButton() = default; + + typedef void (*buttonEventHandler)(PButton &); + + void onClick(uint16_t t, buttonEventHandler handler); + void onDblClick(uint16_t, buttonEventHandler handler); + void onLongPress(uint16_t t, buttonEventHandler handler); + void onVLongPress(uint16_t, buttonEventHandler handler); + bool init(uint8_t pin, bool pullMode); + bool check(void); + + private: + uint16_t Debounce_; // Debounce period to prevent flickering when pressing or releasing the button (in ms) + uint16_t DblClickDelay_; // Max period between clicks for a double click event (in ms) + uint16_t LongPressDelay_; // Hold period for a long press event (in ms) + uint16_t VLongPressDelay_; // Hold period for a very long press event (in ms) + + uint8_t pin_; + bool pullMode_; + bool enabled_; + + bool state_; // Value read from button + bool lastState_; // Last value of button state + bool dblClickWaiting_; // whether we're waiting for a double click (down) + bool dblClickOnNextUp_; // whether to register a double click on next release, or whether to wait and click + bool singleClickOK_; // whether it's OK to do a single click + + uint32_t downTime_; // time the button was pressed down + uint32_t upTime_; // time the button was released + + bool ignoreUP_; // whether to ignore the button release because the click+hold was triggered + bool waitForUP_; // when held, whether to wait for the up event + bool longPressHappened_; // whether or not the hold event happened already + bool vLongPressHappened_; // whether or not the long hold event happened already + + bool buttonBusy_; // false if idle + + buttonEventHandler cb_onClick, cb_onDblClick, cb_onLongPress, cb_onVLongPress; +}; + +#endif diff --git a/makefile b/makefile index 4f01c490a..477397c40 100644 --- a/makefile +++ b/makefile @@ -17,8 +17,8 @@ MAKEFLAGS+="j " #TARGET := $(notdir $(CURDIR)) TARGET := emsesp BUILD := build -SOURCES := src lib_standalone lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src src/devices lib/ArduinoJson/src src/test -INCLUDES := lib/ArduinoJson/src lib_standalone lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/uuid-telnet/src lib/uuid-syslog/src src/devices lib src +SOURCES := src lib_standalone lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src src/devices lib/ArduinoJson/src lib/PButton src/test +INCLUDES := lib/ArduinoJson/src lib_standalone lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/uuid-telnet/src lib/uuid-syslog/src lib/PButton src/devices lib src LIBRARIES := CPPCHECK = cppcheck diff --git a/src/WebSettingsService.cpp b/src/WebSettingsService.cpp index f14aa7ce9..be60557e7 100644 --- a/src/WebSettingsService.cpp +++ b/src/WebSettingsService.cpp @@ -48,6 +48,7 @@ void WebSettings::read(WebSettings & settings, JsonObject & root) { root["hide_led"] = settings.hide_led; root["api_enabled"] = settings.api_enabled; root["analog_enabled"] = settings.analog_enabled; + root["pbutton_gpio"] = settings.pbutton_gpio; } StateUpdateResult WebSettings::update(JsonObject & root, WebSettings & settings) { @@ -67,36 +68,32 @@ StateUpdateResult WebSettings::update(JsonObject & root, WebSettings & settings) } // syslog - snprintf_P(&crc_before[0], - crc_before.capacity() + 1, - PSTR("%d%d%d%s"), - settings.syslog_enabled, - settings.syslog_level, - settings.syslog_mark_interval, - settings.syslog_host.c_str()); + snprintf_P(&crc_before[0], crc_before.capacity() + 1, PSTR("%d%d%d%s"), settings.syslog_enabled, settings.syslog_level, settings.syslog_mark_interval, settings.syslog_host.c_str()); settings.syslog_enabled = root["syslog_enabled"] | EMSESP_DEFAULT_SYSLOG_ENABLED; settings.syslog_level = root["syslog_level"] | EMSESP_DEFAULT_SYSLOG_LEVEL; settings.syslog_mark_interval = root["syslog_mark_interval"] | EMSESP_DEFAULT_SYSLOG_MARK_INTERVAL; settings.syslog_host = root["syslog_host"] | EMSESP_DEFAULT_SYSLOG_HOST; settings.trace_raw = root["trace_raw"] | EMSESP_DEFAULT_TRACELOG_RAW; EMSESP::trace_raw(settings.trace_raw); - snprintf_P(&crc_after[0], - crc_after.capacity() + 1, - PSTR("%d%d%d%s"), - settings.syslog_enabled, - settings.syslog_level, - settings.syslog_mark_interval, - settings.syslog_host.c_str()); + snprintf_P(&crc_after[0], crc_after.capacity() + 1, PSTR("%d%d%d%s"), settings.syslog_enabled, settings.syslog_level, settings.syslog_mark_interval, settings.syslog_host.c_str()); if (crc_before != crc_after) { add_flags(ChangeFlags::SYSLOG); } - // other + // adc snprintf_P(&crc_before[0], crc_before.capacity() + 1, PSTR("%d"), settings.analog_enabled); settings.analog_enabled = root["analog_enabled"] | EMSESP_DEFAULT_ANALOG_ENABLED; snprintf_P(&crc_after[0], crc_after.capacity() + 1, PSTR("%d"), settings.analog_enabled); if (crc_before != crc_after) { - add_flags(ChangeFlags::OTHER); + add_flags(ChangeFlags::ADC); + } + + // button + snprintf_P(&crc_before[0], crc_before.capacity() + 1, PSTR("%d"), settings.pbutton_gpio); + settings.pbutton_gpio = root["pbutton_gpio"] | EMSESP_DEFAULT_PBUTTON_GPIO; + snprintf_P(&crc_after[0], crc_after.capacity() + 1, PSTR("%d"), settings.pbutton_gpio); + if (crc_before != crc_after) { + add_flags(ChangeFlags::BUTTON); } // dallas @@ -152,15 +149,19 @@ void WebSettingsService::onUpdate() { } if (WebSettings::has_flags(WebSettings::ChangeFlags::SYSLOG)) { - System::syslog_init(); + EMSESP::system_.syslog_init(true); // reload settings } - if (WebSettings::has_flags(WebSettings::ChangeFlags::OTHER)) { - System::other_init(); + if (WebSettings::has_flags(WebSettings::ChangeFlags::ADC)) { + EMSESP::system_.adc_init(true); // reload settings + } + + if (WebSettings::has_flags(WebSettings::ChangeFlags::BUTTON)) { + EMSESP::system_.button_init(true); // reload settings } if (WebSettings::has_flags(WebSettings::ChangeFlags::LED)) { - System::led_init(); + EMSESP::system_.led_init(true); // reload settings } } diff --git a/src/WebSettingsService.h b/src/WebSettingsService.h index e096a110f..2898ca2c1 100644 --- a/src/WebSettingsService.h +++ b/src/WebSettingsService.h @@ -36,13 +36,7 @@ #define EMSESP_DEFAULT_MASTER_THERMOSTAT 0 // not set #define EMSESP_DEFAULT_SHOWER_TIMER false #define EMSESP_DEFAULT_SHOWER_ALERT false - -#if defined(ESP32) #define EMSESP_DEFAULT_HIDE_LED true -#else -#define EMSESP_DEFAULT_HIDE_LED false -#endif - #define EMSESP_DEFAULT_DALLAS_PARASITE false #define EMSESP_DEFAULT_API_ENABLED false // turn off, because its insecure #define EMSESP_DEFAULT_BOOL_FORMAT 1 // on/off @@ -54,12 +48,14 @@ #define EMSESP_DEFAULT_TX_GPIO 5 // D8 on Wemos D1-32, OR 16 for UART2 on Lolin D32 #define EMSESP_DEFAULT_DALLAS_GPIO 18 // 18 on Wemos D1-32, 14 on LOLIN D32 #define EMSESP_DEFAULT_LED_GPIO 2 // 2 on Wemos D1-32, 5 on LOLIN D32 +#define EMSESP_DEFAULT_PBUTTON_GPIO 0 // default GPIO is 0 (off) #else // for standalone #define EMSESP_DEFAULT_RX_GPIO 0 #define EMSESP_DEFAULT_TX_GPIO 0 #define EMSESP_DEFAULT_DALLAS_GPIO 0 #define EMSESP_DEFAULT_LED_GPIO 0 +#define EMSESP_DEFAULT_PBUTTON_GPIO 0 #endif namespace emsesp { @@ -85,6 +81,7 @@ class WebSettings { bool hide_led; bool api_enabled; bool analog_enabled; + uint8_t pbutton_gpio; static void read(WebSettings & settings, JsonObject & root); static StateUpdateResult update(JsonObject & root, WebSettings & settings); @@ -94,10 +91,11 @@ class WebSettings { NONE = 0, UART = (1 << 0), SYSLOG = (1 << 1), - OTHER = (1 << 2), + ADC = (1 << 2), DALLAS = (1 << 3), SHOWER = (1 << 4), - LED = (1 << 5) + LED = (1 << 5), + BUTTON = (1 << 6) }; diff --git a/src/system.cpp b/src/system.cpp index e71f9a4ac..4155eb095 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -19,8 +19,6 @@ #include "system.h" #include "emsesp.h" // for send_raw_telegram() command -#include "version.h" // firmware version of EMS-ESP - #if defined(EMSESP_TEST) #include "test/test.h" #endif @@ -34,15 +32,8 @@ uuid::syslog::SyslogService System::syslog_; #endif // init statics -uint32_t System::heap_start_ = 1; // avoid using 0 to divide-by-zero later -bool System::upload_status_ = false; -bool System::hide_led_ = false; -uint8_t System::led_gpio_ = 0; -uint16_t System::analog_ = 0; -bool System::analog_enabled_ = false; -bool System::syslog_enabled_ = false; -std::string System::hostname_; -bool System::ethernet_connected_ = false; +uint32_t System::heap_start_ = 1; // avoid using 0 to divide-by-zero later +PButton System::myPButton_; // send on/off to a gpio pin // value: true = HIGH, false = LOW @@ -106,6 +97,7 @@ void System::restart() { void System::wifi_reconnect() { LOG_INFO(F("Wifi reconnecting...")); Shell::loop_all(); + EMSESP::console_.loop(); delay(1000); // wait a second EMSESP::webSettingsService.save(); // local settings EMSESP::esp8266React.getNetworkSettingsService()->callUpdateHandlers("local"); // in case we've changed ssid or password @@ -127,21 +119,12 @@ void System::format(uuid::console::Shell & shell) { System::restart(); } -void System::syslog_init() { - int8_t syslog_level_; - uint32_t syslog_mark_interval_; - String syslog_host_; - - // fetch settings - EMSESP::webSettingsService.read([&](WebSettings & settings) { - syslog_enabled_ = settings.syslog_enabled; - syslog_level_ = settings.syslog_level; - syslog_mark_interval_ = settings.syslog_mark_interval; - syslog_host_ = settings.syslog_host; - }); +void System::syslog_init(bool refresh) { + if (refresh) { + get_settings(); + } #ifndef EMSESP_STANDALONE - // check for empty hostname IPAddress addr; if (!addr.fromString(syslog_host_.c_str())) { @@ -161,137 +144,150 @@ void System::syslog_init() { syslog_.log_level((uuid::log::Level)syslog_level_); syslog_.mark_interval(syslog_mark_interval_); syslog_.destination(addr); - EMSESP::esp8266React.getNetworkSettingsService()->read([&](NetworkSettings & networkSettings) { syslog_.hostname(networkSettings.hostname.c_str()); }); + syslog_.hostname(hostname_.c_str()); EMSESP::logger().info(F("Syslog started")); #endif } +// read all the settings from the config files and store locally +void System::get_settings() { + EMSESP::webSettingsService.read([&](WebSettings & settings) { + // BUTTON + pbutton_gpio_ = settings.pbutton_gpio; + + // ADC + analog_enabled_ = settings.analog_enabled; + + // SYSLOG + syslog_enabled_ = settings.syslog_enabled; + syslog_level_ = settings.syslog_level; + syslog_mark_interval_ = settings.syslog_mark_interval; + syslog_host_ = settings.syslog_host; + + // LED + hide_led_ = settings.hide_led; + led_gpio_ = settings.led_gpio; + }); + + EMSESP::esp8266React.getNetworkSettingsService()->read([&](NetworkSettings & networkSettings) { + hostname(networkSettings.hostname.c_str()); + LOG_INFO(F("System %s booted (EMS-ESP version %s)"), networkSettings.hostname.c_str(), EMSESP_APP_VERSION); // print boot message + ethernet_profile_ = networkSettings.ethernet_profile; + }); +} + // first call. Sets memory and starts up the UART Serial bridge void System::start(uint32_t heap_start) { +#if defined(EMSESP_DEBUG) + show_mem("Startup"); +#endif + // set the inital free mem, only on first boot if (heap_start_ < 2) { heap_start_ = heap_start; } -#if defined(EMSESP_DEBUG) - show_mem("Startup"); -#endif + // load in all the settings first + get_settings(); - uint8_t ethernet_profile; - EMSESP::esp8266React.getNetworkSettingsService()->read([&](NetworkSettings & networkSettings) { - LOG_INFO(F("System %s booted (EMS-ESP version %s)"), networkSettings.hostname.c_str(), EMSESP_APP_VERSION); // print boot message - ethernet_profile = networkSettings.ethernet_profile; - }); - - // these commands respond to the topic "system" and take a payload like {cmd:"", data:"", id:""} - EMSESP::webSettingsService.read([&](WebSettings & settings) { - Command::add(EMSdevice::DeviceType::SYSTEM, F_(pin), System::command_pin); - Command::add(EMSdevice::DeviceType::SYSTEM, F_(send), System::command_send); - Command::add(EMSdevice::DeviceType::SYSTEM, F_(publish), System::command_publish); - Command::add(EMSdevice::DeviceType::SYSTEM, F_(fetch), System::command_fetch); - Command::add_with_json(EMSdevice::DeviceType::SYSTEM, F_(info), System::command_info); - Command::add_with_json(EMSdevice::DeviceType::SYSTEM, F_(settings), System::command_settings); - -#if defined(EMSESP_TEST) - Command::add(EMSdevice::DeviceType::SYSTEM, F("test"), System::command_test); -#endif - }); - - // start other services first - init(); - - // check ethernet profile, if we're using exclusive Ethernet then disabled wifi and AP/captive portal - if (ethernet_profile == 0) { - return; - } - - uint8_t phy_addr; // I²C-address of Ethernet PHY (0 or 1 for LAN8720, 31 for TLK110) - int power; // Pin# of the enable signal for the external crystal oscillator (-1 to disable for internal APLL source) - int mdc; // Pin# of the I²C clock signal for the Ethernet PHY - int mdio; // Pin# of the I²C IO signal for the Ethernet PHY - eth_phy_type_t type; // Type of the Ethernet PHY (LAN8720 or TLK110) - eth_clock_mode_t clock_mode; // ETH_CLOCK_GPIO0_IN or ETH_CLOCK_GPIO0_OUT, ETH_CLOCK_GPIO16_OUT, ETH_CLOCK_GPIO17_OUT for 50Hz inverted clock - - if (ethernet_profile == 1) { - // LAN8720 - phy_addr = 0; - power = -1; - mdc = 23; - mdio = 18; - type = ETH_PHY_LAN8720; - clock_mode = ETH_CLOCK_GPIO0_IN; - } else if (ethernet_profile == 2) { - // TLK110 - phy_addr = 31; - power = -1; - mdc = 23; - mdio = 18; - type = ETH_PHY_TLK110; - clock_mode = ETH_CLOCK_GPIO0_IN; - } - -#ifndef EMSESP_STANDALONE - if (ETH.begin(phy_addr, power, mdc, mdio, type, clock_mode)) { - // disable ssid and AP - EMSESP::esp8266React.getNetworkSettingsService()->update( - [&](NetworkSettings & settings) { - settings.ssid == ""; // remove SSID - return StateUpdateResult::CHANGED; - }, - "local"); - - EMSESP::esp8266React.getAPSettingsService()->update( - [&](APSettings & settings) { - settings.provisionMode = AP_MODE_NEVER; - return StateUpdateResult::CHANGED; - }, - "local"); - } -#endif + commands_init(); // console & api commands + button_init(false); // the special button + led_init(false); // init LED + syslog_init(false); // init SysLog + adc_init(false); // analog ADC + network_init(); // network + EMSESP::init_tx(); // start UART } -void System::other_init() { - // set the boolean format used for rendering booleans - EMSESP::webSettingsService.read([&](WebSettings & settings) { analog_enabled_ = settings.analog_enabled; }); -} - -// init stuff. This is called when settings are changed in the web -void System::init() { - led_init(); // init LED - - other_init(); // boolean format and analog setting - - syslog_init(); // init SysLog - - EMSESP::esp8266React.getNetworkSettingsService()->read([&](NetworkSettings & settings) { hostname(settings.hostname.c_str()); }); - +// adc and bluetooth +void System::adc_init(bool refresh) { + if (refresh) { + get_settings(); + } #ifndef EMSESP_STANDALONE // setCpuFrequencyMhz(160); // default is 240 // disable bluetooth btStop(); esp_bt_controller_disable(); - - // turn off ADC to save power if not needed if (!analog_enabled_) { - adc_power_off(); + adc_power_off(); // turn off ADC to save power if not needed } #endif +} - EMSESP::init_tx(); // start UART +// button single click +void System::button_OnClick(PButton & b) { + LOG_DEBUG(F("Button pressed - single click")); +} + +// button double click +void System::button_OnDblClick(PButton & b) { + LOG_DEBUG(F("Button pressed - double click - reconnect")); + EMSESP::system_.wifi_reconnect(); +} + +// button long press +void System::button_OnLongPress(PButton & b) { + LOG_DEBUG(F("Button pressed - long press")); +} + +// button indefinite press +void System::button_OnVLongPress(PButton & b) { + LOG_DEBUG(F("Button pressed - very long press")); + LOG_WARNING(F("Performing factory reset...")); + EMSESP::console_.loop(); + +#ifndef EMSESP_STANDALONE + // remove all files under config + File root = LITTLEFS.open(FS_CONFIG_DIRECTORY); + File file; + Serial.printf("Removing files: ", file.name()); + while (file = root.openNextFile()) { + Serial.printf("%s ", file.name()); + LITTLEFS.remove(file.name()); + } + Serial.println(); + + // restart + WiFi.disconnect(true); + delay(500); + ESP.restart(); +#endif +} + +// push button +void System::button_init(bool refresh) { + if (refresh) { + get_settings(); + } + + if (pbutton_gpio_) { + if (!myPButton_.init(pbutton_gpio_, HIGH)) { + LOG_INFO(F("External multi-functional button not detected")); + } else { + LOG_INFO(F("External multi-functional button enabled")); + } + + + myPButton_.onClick(BUTTON_Debounce, button_OnClick); + myPButton_.onDblClick(BUTTON_DblClickDelay, button_OnDblClick); + myPButton_.onLongPress(BUTTON_LongPressDelay, button_OnLongPress); + myPButton_.onVLongPress(BUTTON_VLongPressDelay, button_OnVLongPress); + } } // set the LED to on or off when in normal operating mode -void System::led_init() { - EMSESP::webSettingsService.read([&](WebSettings & settings) { - hide_led_ = settings.hide_led; - led_gpio_ = settings.led_gpio; - if (led_gpio_) { - pinMode(led_gpio_, OUTPUT); // 0 means disabled - digitalWrite(led_gpio_, hide_led_ ? !LED_ON : LED_ON); // LED on, for ever - } - }); +void System::led_init(bool refresh) { + if (refresh) { + get_settings(); + } + + if (led_gpio_) { + pinMode(led_gpio_, OUTPUT); // 0 means disabled + digitalWrite(led_gpio_, hide_led_ ? !LED_ON : LED_ON); // LED on, for ever + } } // returns true if OTA is uploading @@ -314,6 +310,7 @@ void System::upload_status(bool in_progress) { // checks system health and handles LED flashing wizardry void System::loop() { #ifndef EMSESP_STANDALONE + myPButton_.check(); // check button press if (syslog_enabled_) { syslog_.loop(); @@ -429,7 +426,57 @@ void System::set_led_speed(uint32_t speed) { led_monitor(); } -void System::init_network() { +// initializes network +void System::network_init() { + // check ethernet profile, if we're using exclusive Ethernet then disabled wifi and AP/captive portal + if (ethernet_profile_ == 0) { + return; + } + + uint8_t phy_addr; // I²C-address of Ethernet PHY (0 or 1 for LAN8720, 31 for TLK110) + int power; // Pin# of the enable signal for the external crystal oscillator (-1 to disable for internal APLL source) + int mdc; // Pin# of the I²C clock signal for the Ethernet PHY + int mdio; // Pin# of the I²C IO signal for the Ethernet PHY + eth_phy_type_t type; // Type of the Ethernet PHY (LAN8720 or TLK110) + eth_clock_mode_t clock_mode; // ETH_CLOCK_GPIO0_IN or ETH_CLOCK_GPIO0_OUT, ETH_CLOCK_GPIO16_OUT, ETH_CLOCK_GPIO17_OUT for 50Hz inverted clock + + if (ethernet_profile_ == 1) { + // LAN8720 + phy_addr = 0; + power = -1; + mdc = 23; + mdio = 18; + type = ETH_PHY_LAN8720; + clock_mode = ETH_CLOCK_GPIO0_IN; + } else if (ethernet_profile_ == 2) { + // TLK110 + phy_addr = 31; + power = -1; + mdc = 23; + mdio = 18; + type = ETH_PHY_TLK110; + clock_mode = ETH_CLOCK_GPIO0_IN; + } + +#ifndef EMSESP_STANDALONE + if (ETH.begin(phy_addr, power, mdc, mdio, type, clock_mode)) { + // disable ssid and AP + EMSESP::esp8266React.getNetworkSettingsService()->update( + [&](NetworkSettings & settings) { + settings.ssid == ""; // remove SSID + return StateUpdateResult::CHANGED; + }, + "local"); + + EMSESP::esp8266React.getAPSettingsService()->update( + [&](APSettings & settings) { + settings.provisionMode = AP_MODE_NEVER; + return StateUpdateResult::CHANGED; + }, + "local"); + } +#endif + last_system_check_ = 0; // force the LED to go from fast flash to pulse send_heartbeat(); } @@ -467,6 +514,20 @@ void System::system_check() { } } +// commands - takes static function pointers +void System::commands_init() { + // these commands respond to the topic "system" and take a payload like {cmd:"", data:"", id:""} + Command::add(EMSdevice::DeviceType::SYSTEM, F_(pin), System::command_pin); + Command::add(EMSdevice::DeviceType::SYSTEM, F_(send), System::command_send); + Command::add(EMSdevice::DeviceType::SYSTEM, F_(publish), System::command_publish); + Command::add(EMSdevice::DeviceType::SYSTEM, F_(fetch), System::command_fetch); + Command::add_with_json(EMSdevice::DeviceType::SYSTEM, F_(info), System::command_info); + Command::add_with_json(EMSdevice::DeviceType::SYSTEM, F_(settings), System::command_settings); +#if defined(EMSESP_TEST) + Command::add(EMSdevice::DeviceType::SYSTEM, F("test"), System::command_test); +#endif +} + // flashes the LED void System::led_monitor() { if (!led_gpio_) { @@ -579,7 +640,7 @@ void System::show_system(uuid::console::Shell & shell) { shell.println(); // show Ethernet - if (ethernet_connected()) { + if (ethernet_connected_) { shell.printfln(F("Ethernet: Connected")); shell.printfln(F("MAC address: %s"), ETH.macAddress().c_str()); shell.printfln(F("Hostname: %s"), ETH.getHostname()); @@ -590,21 +651,18 @@ void System::show_system(uuid::console::Shell & shell) { shell.printfln(F("Ethernet: disconnected")); } - EMSESP::webSettingsService.read([&](WebSettings & settings) { - shell.println(); - - if (!settings.syslog_enabled) { - shell.printfln(F("Syslog: disabled")); - } else { - shell.printfln(F("Syslog:")); - shell.print(F(" ")); - shell.printfln(F_(host_fmt), !settings.syslog_host.isEmpty() ? settings.syslog_host.c_str() : uuid::read_flash_string(F_(unset)).c_str()); - shell.print(F(" ")); - shell.printfln(F_(log_level_fmt), uuid::log::format_level_lowercase(static_cast(settings.syslog_level))); - shell.print(F(" ")); - shell.printfln(F_(mark_interval_fmt), settings.syslog_mark_interval); - } - }); + shell.println(); + if (!syslog_enabled_) { + shell.printfln(F("Syslog: disabled")); + } else { + shell.printfln(F("Syslog:")); + shell.print(F(" ")); + shell.printfln(F_(host_fmt), !syslog_host_.isEmpty() ? syslog_host_.c_str() : uuid::read_flash_string(F_(unset)).c_str()); + shell.print(F(" ")); + shell.printfln(F_(log_level_fmt), uuid::log::format_level_lowercase(static_cast(syslog_level_))); + shell.print(F(" ")); + shell.printfln(F_(mark_interval_fmt), syslog_mark_interval_); + } #endif } @@ -614,19 +672,19 @@ void System::console_commands(Shell & shell, unsigned int context) { EMSESPShell::commands->add_command(ShellContext::SYSTEM, CommandFlags::ADMIN, flash_string_vector{F_(restart)}, - [](Shell & shell __attribute__((unused)), const std::vector & arguments __attribute__((unused))) { restart(); }); + [](Shell & shell __attribute__((unused)), const std::vector & arguments __attribute__((unused))) { EMSESP::system_.restart(); }); EMSESPShell::commands->add_command(ShellContext::SYSTEM, CommandFlags::ADMIN, flash_string_vector{F_(wifi), F_(reconnect)}, - [](Shell & shell __attribute__((unused)), const std::vector & arguments __attribute__((unused))) { wifi_reconnect(); }); + [](Shell & shell __attribute__((unused)), const std::vector & arguments __attribute__((unused))) { EMSESP::system_.wifi_reconnect(); }); EMSESPShell::commands->add_command(ShellContext::SYSTEM, CommandFlags::ADMIN, flash_string_vector{F_(format)}, [](Shell & shell, const std::vector & arguments __attribute__((unused))) { shell.enter_password(F_(password_prompt), [=](Shell & shell, bool completed, const std::string & password) { if (completed) { EMSESP::esp8266React.getSecuritySettingsService()->read([&](SecuritySettings & securitySettings) { if (securitySettings.jwtSecret.equals(password.c_str())) { - format(shell); + EMSESP::system_.format(shell); } else { shell.println(F("incorrect password")); } @@ -658,7 +716,7 @@ void System::console_commands(Shell & shell, unsigned int context) { }); EMSESPShell::commands->add_command(ShellContext::SYSTEM, CommandFlags::USER, flash_string_vector{F_(show)}, [=](Shell & shell, const std::vector & arguments __attribute__((unused))) { - show_system(shell); + EMSESP::system_.show_system(shell); shell.println(); }); @@ -725,7 +783,7 @@ void System::console_commands(Shell & shell, unsigned int context) { }); EMSESPShell::commands->add_command(ShellContext::SYSTEM, CommandFlags::ADMIN, flash_string_vector{F_(show), F_(users)}, [](Shell & shell, const std::vector & arguments __attribute__((unused))) { - System::show_users(shell); + EMSESP::system_.show_users(shell); }); // enter the context @@ -825,6 +883,7 @@ bool System::command_settings(const char * value, const int8_t id, JsonObject & node["hide_led"] = settings.hide_led; node["api_enabled"] = settings.api_enabled; node["analog_enabled"] = settings.analog_enabled; + node["pbutton_gpio"] = settings.pbutton_gpio; }); return true; @@ -894,4 +953,5 @@ bool System::command_test(const char * value, const int8_t id) { } #endif + } // namespace emsesp diff --git a/src/system.h b/src/system.h index 6d527ad4e..32cdeb263 100644 --- a/src/system.h +++ b/src/system.h @@ -36,6 +36,7 @@ #endif #include +#include using uuid::console::Shell; @@ -47,92 +48,103 @@ class System { void loop(); // commands - static void restart(); - static void format(uuid::console::Shell & shell); - static void console_commands(Shell & shell, unsigned int context); - static bool command_pin(const char * value, const int8_t id); static bool command_send(const char * value, const int8_t id); static bool command_publish(const char * value, const int8_t id); static bool command_fetch(const char * value, const int8_t id); static bool command_info(const char * value, const int8_t id, JsonObject & json); static bool command_settings(const char * value, const int8_t id, JsonObject & json); - #if defined(EMSESP_TEST) static bool command_test(const char * value, const int8_t id); #endif - static void upload_status(bool in_progress); - static bool upload_status(); - static void show_mem(const char * note); - void init_network(); - static void init(); - static void led_init(); - static void syslog_init(); - static void other_init(); - + void restart(); + void format(uuid::console::Shell & shell); + void upload_status(bool in_progress); + bool upload_status(); + void show_mem(const char * note); + void get_settings(); + void led_init(bool refresh); + void syslog_init(bool refresh); + void adc_init(bool refresh); + void network_init(); + void commands_init(); + void button_init(bool refresh); bool check_upgrade(); void send_heartbeat(); - static std::string hostname() { + std::string hostname() { return hostname_; } - static void hostname(std::string hostname) { + void hostname(std::string hostname) { hostname_ = hostname; } - static bool ethernet_connected() { + bool ethernet_connected() { return ethernet_connected_; } - static void ethernet_connected(bool b) { + void ethernet_connected(bool b) { ethernet_connected_ = b; } private: static uuid::log::Logger logger_; + static uint32_t heap_start_; -#ifndef EMSESP_STANDALONE - static uuid::syslog::SyslogService syslog_; -#endif + // button + static PButton myPButton_; // PButton instance + static void button_OnClick(PButton & b); + static void button_OnDblClick(PButton & b); + static void button_OnLongPress(PButton & b); + static void button_OnVLongPress(PButton & b); + static constexpr uint32_t BUTTON_Debounce = 40; // Debounce period to prevent flickering when pressing or releasing the button (in ms) + static constexpr uint32_t BUTTON_DblClickDelay = 250; // Max period between clicks for a double click event (in ms) + static constexpr uint32_t BUTTON_LongPressDelay = 750; // Hold period for a long press event (in ms) + static constexpr uint32_t BUTTON_VLongPressDelay = 9000; // Hold period for a very long press event (in ms) static constexpr uint32_t SYSTEM_CHECK_FREQUENCY = 5000; // check every 5 seconds static constexpr uint32_t LED_WARNING_BLINK = 1000; // pulse to show no connection, 1 sec static constexpr uint32_t LED_WARNING_BLINK_FAST = 100; // flash quickly for boot up sequence static constexpr uint32_t SYSTEM_HEARTBEAT_INTERVAL = 60000; // in milliseconds, how often the MQTT heartbeat is sent (1 min) static constexpr uint32_t SYSTEM_MEASURE_ANALOG_INTERVAL = 500; + static constexpr uint8_t LED_ON = LOW; // internal LED - // internal LED - static constexpr uint8_t LED_ON = LOW; +#ifndef EMSESP_STANDALONE + static uuid::syslog::SyslogService syslog_; +#endif void led_monitor(); void set_led_speed(uint32_t speed); void system_check(); void measure_analog(); - static void show_system(uuid::console::Shell & shell); - static void show_users(uuid::console::Shell & shell); - static void wifi_reconnect(); - static int8_t wifi_quality(); + void show_system(uuid::console::Shell & shell); + void show_users(uuid::console::Shell & shell); + void wifi_reconnect(); + int8_t wifi_quality(); bool system_healthy_ = false; uint32_t led_flash_speed_ = LED_WARNING_BLINK_FAST; // default boot flashes quickly uint32_t last_heartbeat_ = 0; uint32_t last_system_check_ = 0; - - static bool upload_status_; // true if we're in the middle of a OTA firmware upload - static uint32_t heap_start_; - static uint16_t analog_; - static bool ethernet_connected_; + bool upload_status_ = false; // true if we're in the middle of a OTA firmware upload + bool ethernet_connected_; + uint16_t analog_; // settings - static std::string hostname_; - static bool hide_led_; - static uint8_t led_gpio_; - static bool syslog_enabled_; - static bool analog_enabled_; + std::string hostname_; + bool hide_led_; + uint8_t led_gpio_; + bool syslog_enabled_; + bool analog_enabled_; + uint8_t ethernet_profile_; + uint8_t pbutton_gpio_; + int8_t syslog_level_; + uint32_t syslog_mark_interval_; + String syslog_host_; }; } // namespace emsesp