diff --git a/Config.h b/Config.h index 36f9084..36f828a 100644 --- a/Config.h +++ b/Config.h @@ -1,6 +1,6 @@ #pragma once -#define NUKI_HUB_VERSION "8.21" +#define NUKI_HUB_VERSION "8.22-pre-1" #define MQTT_QOS_LEVEL 1 #define MQTT_CLEAN_SESSIONS false diff --git a/Gpio.cpp b/Gpio.cpp index db790f4..e3680cc 100644 --- a/Gpio.cpp +++ b/Gpio.cpp @@ -3,42 +3,296 @@ #include "Arduino.h" #include "Pins.h" #include "Logger.h" +#include "PreferencesKeys.h" +#include "RestartReason.h" Gpio* Gpio::_inst = nullptr; -NukiWrapper* Gpio::_nuki = nullptr; -unsigned long Gpio::_lockedTs = 0; +unsigned long Gpio::_debounceTs = 0; const uint Gpio::_debounceTime = 1000; -void Gpio::init(NukiWrapper* nuki) +Gpio::Gpio(Preferences* preferences) +: _preferences(preferences) { - _nuki = nuki; + _inst = this; + loadPinConfiguration(); - pinMode(TRIGGER_LOCK_PIN, INPUT_PULLUP); - pinMode(TRIGGER_UNLOCK_PIN, INPUT_PULLUP); - pinMode(TRIGGER_UNLATCH_PIN, INPUT_PULLUP); + if(_preferences->getBool(preference_gpio_locking_enabled)) + { + migrateObsoleteSetting(); + } - attachInterrupt(TRIGGER_LOCK_PIN, isrLock, FALLING); - attachInterrupt(TRIGGER_UNLOCK_PIN, isrUnlock, FALLING); - attachInterrupt(TRIGGER_UNLATCH_PIN, isrUnlatch, FALLING); + _inst->init(); +} + +void Gpio::init() +{ + for(const auto& entry : _inst->_pinConfiguration) + { + const auto it = std::find(_inst->availablePins().begin(), _inst->availablePins().end(), entry.pin); + + if(it == _inst->availablePins().end()) + { + continue; + } + + switch(entry.role) + { + case PinRole::InputLock: + pinMode(entry.pin, INPUT_PULLUP); + attachInterrupt(entry.pin, isrLock, FALLING); + break; + case PinRole::InputUnlock: + pinMode(entry.pin, INPUT_PULLUP); + attachInterrupt(entry.pin, isrUnlock, FALLING); + break; + case PinRole::InputUnlatch: + pinMode(entry.pin, INPUT_PULLUP); + attachInterrupt(entry.pin, isrUnlatch, FALLING); + break; + case PinRole::InputElectricStrikeActuation: + pinMode(entry.pin, INPUT_PULLUP); + attachInterrupt(entry.pin, isrElectricStrikeActuation, FALLING); + break; + case PinRole::InputActivateRTO: + pinMode(entry.pin, INPUT_PULLUP); + attachInterrupt(entry.pin, isrActivateRTO, FALLING); + break; + case PinRole::InputActivateCM: + pinMode(entry.pin, INPUT_PULLUP); + attachInterrupt(entry.pin, isrActivateCM, FALLING); + break; + case PinRole::InputDeactivateRtoCm: + pinMode(entry.pin, INPUT_PULLUP); + attachInterrupt(entry.pin, isrDeactivateRtoCm, FALLING); + break; + default: + pinMode(entry.pin, OUTPUT); + break; + } + } +} + +const std::vector& Gpio::availablePins() const +{ + return _availablePins; +} + +void Gpio::loadPinConfiguration() +{ + size_t storedLength = _preferences->getBytesLength(preference_gpio_configuration); + if(storedLength == 0) + { + return; + } + + uint8_t serialized[storedLength]; + memset(serialized, 0, sizeof(serialized)); + + size_t size = _preferences->getBytes(preference_gpio_configuration, serialized, sizeof(serialized)); + + if(size == 0) + { + return; + } + + size_t numEntries = size / 2; + + _pinConfiguration.clear(); + _pinConfiguration.reserve(numEntries); + + for(int i=0; i < numEntries; i++) + { + PinEntry entry; + entry.pin = serialized[i * 2]; + entry.role = (PinRole) serialized[(i * 2 + 1)]; + if(entry.role != PinRole::Disabled) + { + _pinConfiguration.push_back(entry); + } + } +} + +void Gpio::savePinConfiguration(const std::vector &pinConfiguration) +{ + int8_t serialized[std::max(pinConfiguration.size() * 2, _preferences->getBytesLength(preference_gpio_configuration))]; + memset(serialized, 0, sizeof(serialized)); + + int len = pinConfiguration.size(); + for(int i=0; i < len; i++) + { + const auto& entry = pinConfiguration[i]; + + if(entry.role != PinRole::Disabled) + { + serialized[i * 2] = entry.pin; + serialized[i * 2 + 1] = (int8_t) entry.role; + } + } + + _preferences->putBytes(preference_gpio_configuration, serialized, sizeof(serialized)); +} + +const std::vector &Gpio::pinConfiguration() const +{ + return _pinConfiguration; +} + +String Gpio::getRoleDescription(PinRole role) const +{ + switch(role) + { + case PinRole::Disabled: + return "Disabled"; + case PinRole::InputLock: + return "Input: Lock"; + case PinRole::InputUnlock: + return "Input: Unlock"; + case PinRole::InputUnlatch: + return "Input: Unlatch"; + case PinRole::InputElectricStrikeActuation: + return "Input: Electric strike actuation"; + case PinRole::InputActivateRTO: + return "Input: Activate RTO"; + case PinRole::InputActivateCM: + return "Input: Activate CM"; + case PinRole::InputDeactivateRtoCm: + return "Input: Deactivate RTO/CM"; + case PinRole::OutputHighLocked: + return "Output: High when locked"; + case PinRole::OutputHighUnlocked: + return "Output: High when unlocked"; + case PinRole::OutputHighMotorBlocked: + return "Output: High when motor blocked"; + case PinRole::OutputHighRtoActive: + return "Output: High when RTO active"; + case PinRole::OutputHighCmActive: + return "Output: High when CM active"; + case PinRole::OutputHighRtoOrCmActive: + return "Output: High when RTO or CM active"; + default: + return "Unknown"; + } +} + +void Gpio::getConfigurationText(String& text, const std::vector& pinConfiguration, const String& linebreak) const +{ + for(const auto& entry : pinConfiguration) + { + if(entry.role != PinRole::Disabled) + { + text.concat("GPIO "); + text.concat(entry.pin); + if(entry.pin < 10) + { + text.concat(' '); + } + text.concat(": "); + text.concat(getRoleDescription(entry.role)); + text.concat(linebreak); + } + } +} + +const std::vector& Gpio::getAllRoles() const +{ + return _allRoles; +} + +void Gpio::notify(const GpioAction &action) +{ + for(auto& callback : _callbacks) + { + callback(action); + } +} + +void Gpio::addCallback(std::function callback) +{ + _callbacks.push_back(callback); } void Gpio::isrLock() { - if(millis() < _lockedTs) return; - _nuki->lock(); - _lockedTs = millis() + _debounceTime; + if(millis() < _debounceTs) return; + _inst->notify(GpioAction::Lock); + _debounceTs = millis() + _debounceTime; } void Gpio::isrUnlock() { - if(millis() < _lockedTs) return; - _nuki->unlock(); - _lockedTs = millis() + _debounceTime; + if(millis() < _debounceTs) return; + _inst->notify(GpioAction::Unlock); + _debounceTs = millis() + _debounceTime; } void Gpio::isrUnlatch() { - if(millis() < _lockedTs) return; - _nuki->unlatch(); - _lockedTs = millis() + _debounceTime; + if(millis() < _debounceTs) return; + _inst->notify(GpioAction::Unlatch); + _debounceTs = millis() + _debounceTime; +} + +void Gpio::isrElectricStrikeActuation() +{ + if(millis() < _debounceTs) return; + _inst->notify(GpioAction::ElectricStrikeActuation); + _debounceTs = millis() + _debounceTime; +} + +void Gpio::isrActivateRTO() +{ + if(millis() < _debounceTs) return; + _inst->notify(GpioAction::ActivateRTO); + _debounceTs = millis() + _debounceTime; +} + +void Gpio::isrActivateCM() +{ + if(millis() < _debounceTs) return; + _inst->notify(GpioAction::ActivateCM); + _debounceTs = millis() + _debounceTime; +} + +void Gpio::isrDeactivateRtoCm() +{ + if(millis() < _debounceTs) return; + _inst->notify(GpioAction::DeactivateRtoCm); + _debounceTs = millis() + _debounceTime; +} + +void Gpio::setPinOutput(const uint8_t& pin, const uint8_t& state) +{ + digitalWrite(pin, state); +} + +#define TRIGGER_LOCK_PIN 32 +#define TRIGGER_UNLOCK_PIN 33 +#define TRIGGER_UNLATCH_PIN 27 + +void Gpio::migrateObsoleteSetting() +{ + _pinConfiguration.clear(); + + PinEntry entry1; + entry1.pin = 27; + entry1.role = PinRole::InputUnlatch; + + PinEntry entry2; + entry2.pin = 32; + entry2.role = PinRole::InputLock; + + PinEntry entry3; + entry3.pin = 33; + entry3.role = PinRole::InputUnlock; + + _pinConfiguration.push_back(entry1); + _pinConfiguration.push_back(entry2); + _pinConfiguration.push_back(entry3); + + savePinConfiguration(_pinConfiguration); + + _preferences->remove(preference_gpio_locking_enabled); + Log->println("Migrated gpio control setting"); + delay(200); + restartEsp(RestartReason::GpioConfigurationUpdated); } diff --git a/Gpio.h b/Gpio.h index 6d4383e..768d717 100644 --- a/Gpio.h +++ b/Gpio.h @@ -1,22 +1,103 @@ #pragma once +#include +#include +#include -#include "NukiWrapper.h" +enum class PinRole +{ + Disabled, + InputLock, + InputUnlock, + InputUnlatch, + InputElectricStrikeActuation, + InputActivateRTO, + InputActivateCM, + InputDeactivateRtoCm, + OutputHighLocked, + OutputHighUnlocked, + OutputHighMotorBlocked, + OutputHighRtoActive, + OutputHighCmActive, + OutputHighRtoOrCmActive +}; + +enum class GpioAction +{ + Lock, + Unlock, + Unlatch, + ElectricStrikeActuation, + ActivateRTO, + ActivateCM, + DeactivateRtoCm +}; + +struct PinEntry +{ + uint8_t pin = 0; + PinRole role = PinRole::Disabled; +}; class Gpio { public: - Gpio() = delete; - static void init(NukiWrapper* nuki); + Gpio(Preferences* preferences); + static void init(); + + void migrateObsoleteSetting(); + + void addCallback(std::function callback); + + void loadPinConfiguration(); + void savePinConfiguration(const std::vector& pinConfiguration); + + const std::vector& availablePins() const; + const std::vector& pinConfiguration() const; + + String getRoleDescription(PinRole role) const; + void getConfigurationText(String& text, const std::vector& pinConfiguration, const String& linebreak = "\n") const; + + const std::vector& getAllRoles() const; + + void setPinOutput(const uint8_t& pin, const uint8_t& state); private: + void notify(const GpioAction& action); + + const std::vector _availablePins = { 2, 4, 5, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 32, 33 }; + const std::vector _allRoles = + { + PinRole::Disabled, + PinRole::InputLock, + PinRole::InputUnlock, + PinRole::InputUnlatch, + PinRole::InputElectricStrikeActuation, + PinRole::InputActivateRTO, + PinRole::InputActivateCM, + PinRole::InputDeactivateRtoCm, + PinRole::OutputHighLocked, + PinRole::OutputHighUnlocked, + PinRole::OutputHighRtoActive, + PinRole::OutputHighCmActive, + PinRole::OutputHighRtoOrCmActive, + }; + + std::vector _pinConfiguration; static const uint _debounceTime; static void IRAM_ATTR isrLock(); static void IRAM_ATTR isrUnlock(); static void IRAM_ATTR isrUnlatch(); + static void IRAM_ATTR isrElectricStrikeActuation(); + static void IRAM_ATTR isrActivateRTO(); + static void IRAM_ATTR isrActivateCM(); + static void IRAM_ATTR isrDeactivateRtoCm(); + + std::vector> _callbacks; static Gpio* _inst; - static NukiWrapper* _nuki; - static unsigned long _lockedTs; + static unsigned long _debounceTs; + + Preferences* _preferences = nullptr; }; diff --git a/Network.cpp b/Network.cpp index 690ee56..4156a88 100644 --- a/Network.cpp +++ b/Network.cpp @@ -46,21 +46,14 @@ void Network::setupDevice() _ipConfiguration = new IPConfiguration(_preferences); int hardwareDetect = _preferences->getInt(preference_network_hardware); - int hardwareDetectGpio = _preferences->getInt(preference_network_hardware_gpio); Log->print(F("Hardware detect : ")); Log->println(hardwareDetect); - Log->print(F("Hardware detect GPIO: ")); Log->println(hardwareDetectGpio); if(hardwareDetect == 0) { - hardwareDetect = 2; + hardwareDetect = 1; _preferences->putInt(preference_network_hardware, hardwareDetect); } - if(hardwareDetectGpio == 0) - { - hardwareDetectGpio = 26; - _preferences->putInt(preference_network_hardware_gpio, hardwareDetectGpio); - } if(strcmp(WiFi_fallbackDetect, "wifi_fallback") == 0) { @@ -77,12 +70,8 @@ void Network::setupDevice() _networkDeviceType = NetworkDeviceType::WiFi; break; case 2: - Log->print(F("Using PIN ")); - Log->print(hardwareDetectGpio); - Log->println(F(" for network device selection")); - - pinMode(hardwareDetectGpio, INPUT_PULLUP); - _networkDeviceType = digitalRead(hardwareDetectGpio) == HIGH ? NetworkDeviceType::WiFi : NetworkDeviceType::W5500; + Log->print(F("Generic W5500")); + _networkDeviceType = NetworkDeviceType::W5500; break; case 3: Log->println(F("W5500 on M5Stack Atom POE")); diff --git a/NukiOpenerWrapper.cpp b/NukiOpenerWrapper.cpp index cc33c25..df4df28 100644 --- a/NukiOpenerWrapper.cpp +++ b/NukiOpenerWrapper.cpp @@ -8,11 +8,12 @@ NukiOpenerWrapper* nukiOpenerInst; -NukiOpenerWrapper::NukiOpenerWrapper(const std::string& deviceName, uint32_t id, BleScanner::Scanner* scanner, NetworkOpener* network, Preferences* preferences) +NukiOpenerWrapper::NukiOpenerWrapper(const std::string& deviceName, uint32_t id, BleScanner::Scanner* scanner, NetworkOpener* network, Gpio* gpio, Preferences* preferences) : _deviceName(deviceName), _nukiOpener(deviceName, id), _bleScanner(scanner), _network(network), + _gpio(gpio), _preferences(preferences) { nukiOpenerInst = this; @@ -26,6 +27,8 @@ NukiOpenerWrapper::NukiOpenerWrapper(const std::string& deviceName, uint32_t id, network->setLockActionReceivedCallback(nukiOpenerInst->onLockActionReceivedCallback); network->setConfigUpdateReceivedCallback(nukiOpenerInst->onConfigUpdateReceivedCallback); network->setKeypadCommandReceivedCallback(nukiOpenerInst->onKeypadCommandReceivedCallback); + + _gpio->addCallback(NukiOpenerWrapper::gpioActionCallback); } @@ -252,6 +255,36 @@ void NukiOpenerWrapper::update() memcpy(&_lastKeyTurnerState, &_keyTurnerState, sizeof(NukiOpener::OpenerState)); } + +void NukiOpenerWrapper::electricStrikeActuation() +{ + _nextLockAction = NukiOpener::LockAction::ElectricStrikeActuation; +} + +void NukiOpenerWrapper::activateRTO() +{ + _nextLockAction = NukiOpener::LockAction::ActivateRTO; +} + +void NukiOpenerWrapper::activateCM() +{ + _nextLockAction = NukiOpener::LockAction::ActivateCM; +} + +void NukiOpenerWrapper::deactivateRtoCm() +{ + if(_keyTurnerState.nukiState == NukiOpener::State::ContinuousMode) + { + _nextLockAction = NukiOpener::LockAction::DeactivateCM; + return; + } + + if(_keyTurnerState.lockState == NukiOpener::LockState::RTOactive) + { + _nextLockAction = NukiOpener::LockAction::DeactivateRTO; + } +} + bool NukiOpenerWrapper::isPinSet() { return _nukiOpener.getSecurityPincode() != 0; @@ -301,6 +334,7 @@ void NukiOpenerWrapper::updateKeyTurnerState() else { _network->publishKeyTurnerState(_keyTurnerState, _lastKeyTurnerState); + updateGpioOutputs(); if(_keyTurnerState.nukiState == NukiOpener::State::ContinuousMode) { @@ -448,6 +482,25 @@ void NukiOpenerWrapper::onKeypadCommandReceivedCallback(const char *command, con nukiOpenerInst->onKeypadCommandReceived(command, id, name, code, enabled); } +void NukiOpenerWrapper::gpioActionCallback(const GpioAction &action) +{ + switch(action) + { + case GpioAction::ElectricStrikeActuation: + nukiOpenerInst->electricStrikeActuation(); + break; + case GpioAction::ActivateRTO: + nukiOpenerInst->activateRTO(); + break; + case GpioAction::ActivateCM: + nukiOpenerInst->activateCM(); + break; + case GpioAction::DeactivateRtoCm: + nukiOpenerInst->deactivateRtoCm(); + break; + } +} + void NukiOpenerWrapper::onConfigUpdateReceived(const char *topic, const char *value) { if(strcmp(topic, mqtt_topic_config_button_enabled) == 0) @@ -692,3 +745,31 @@ void NukiOpenerWrapper::disableWatchdog() { _restartBeaconTimeout = -1; } + +void NukiOpenerWrapper::updateGpioOutputs() +{ + using namespace NukiOpener; + + const auto& pinConfiguration = _gpio->pinConfiguration(); + + const LockState& lockState = _keyTurnerState.lockState; + + bool rtoActive = _keyTurnerState.lockState == LockState::RTOactive; + bool cmActive = _keyTurnerState.nukiState == State::ContinuousMode; + + for(const auto& entry : pinConfiguration) + { + switch(entry.role) + { + case PinRole::OutputHighRtoActive: + _gpio->setPinOutput(entry.pin, rtoActive ? HIGH : LOW); + break; + case PinRole::OutputHighCmActive: + _gpio->setPinOutput(entry.pin, cmActive ? HIGH : LOW); + break; + case PinRole::OutputHighRtoOrCmActive: + _gpio->setPinOutput(entry.pin, rtoActive || cmActive ? HIGH : LOW); + break; + } + } +} \ No newline at end of file diff --git a/NukiOpenerWrapper.h b/NukiOpenerWrapper.h index 01a903a..da80621 100644 --- a/NukiOpenerWrapper.h +++ b/NukiOpenerWrapper.h @@ -5,16 +5,22 @@ #include "NukiOpenerConstants.h" #include "NukiDataTypes.h" #include "BleScanner.h" +#include "Gpio.h" class NukiOpenerWrapper : public NukiOpener::SmartlockEventHandler { public: - NukiOpenerWrapper(const std::string& deviceName, uint32_t id, BleScanner::Scanner* scanner, NetworkOpener* network, Preferences* preferences); + NukiOpenerWrapper(const std::string& deviceName, uint32_t id, BleScanner::Scanner* scanner, NetworkOpener* network, Gpio* gpio, Preferences* preferences); virtual ~NukiOpenerWrapper(); void initialize(); void update(); + void electricStrikeActuation(); + void activateRTO(); + void activateCM(); + void deactivateRtoCm(); + bool isPinSet(); void setPin(const uint16_t pin); @@ -40,6 +46,7 @@ private: static bool onLockActionReceivedCallback(const char* value); static void onConfigUpdateReceivedCallback(const char* topic, const char* value); static void onKeypadCommandReceivedCallback(const char* command, const uint& id, const String& name, const String& code, const int& enabled); + static void gpioActionCallback(const GpioAction& action); void onConfigUpdateReceived(const char* topic, const char* value); void onKeypadCommandReceived(const char* command, const uint& id, const String& name, const String& code, const int& enabled); @@ -50,6 +57,8 @@ private: void updateKeypad(); void postponeBleWatchdog(); + void updateGpioOutputs(); + void readConfig(); void readAdvancedConfig(); @@ -61,9 +70,10 @@ private: std::string _deviceName; NukiOpener::NukiOpener _nukiOpener; - BleScanner::Scanner* _bleScanner; - NetworkOpener* _network; - Preferences* _preferences; + BleScanner::Scanner* _bleScanner = nullptr; + NetworkOpener* _network = nullptr; + Gpio* _gpio = nullptr; + Preferences* _preferences = nullptr; int _intervalLockstate = 0; // seconds int _intervalBattery = 0; // seconds int _intervalConfig = 60 * 60; // seconds diff --git a/NukiWrapper.cpp b/NukiWrapper.cpp index 24ac52a..8b527f8 100644 --- a/NukiWrapper.cpp +++ b/NukiWrapper.cpp @@ -8,11 +8,12 @@ NukiWrapper* nukiInst; -NukiWrapper::NukiWrapper(const std::string& deviceName, uint32_t id, BleScanner::Scanner* scanner, NetworkLock* network, Preferences* preferences) +NukiWrapper::NukiWrapper(const std::string& deviceName, uint32_t id, BleScanner::Scanner* scanner, NetworkLock* network, Gpio* gpio, Preferences* preferences) : _deviceName(deviceName), _bleScanner(scanner), _nukiLock(deviceName, id), _network(network), + _gpio(gpio), _preferences(preferences) { nukiInst = this; @@ -26,6 +27,8 @@ NukiWrapper::NukiWrapper(const std::string& deviceName, uint32_t id, BleScanner: network->setLockActionReceivedCallback(nukiInst->onLockActionReceivedCallback); network->setConfigUpdateReceivedCallback(nukiInst->onConfigUpdateReceivedCallback); network->setKeypadCommandReceivedCallback(nukiInst->onKeypadCommandReceivedCallback); + + _gpio->addCallback(NukiWrapper::gpioActionCallback); } @@ -307,6 +310,7 @@ void NukiWrapper::updateKeyTurnerState() _retryLockstateCount = 0; _network->publishKeyTurnerState(_keyTurnerState, _lastKeyTurnerState); + updateGpioOutputs(); char lockStateStr[20]; lockstateToString(_keyTurnerState.lockState, lockStateStr); @@ -446,6 +450,22 @@ void NukiWrapper::onKeypadCommandReceivedCallback(const char *command, const uin nukiInst->onKeypadCommandReceived(command, id, name, code, enabled); } +void NukiWrapper::gpioActionCallback(const GpioAction &action) +{ + switch(action) + { + case GpioAction::Lock: + nukiInst->lock(); + break; + case GpioAction::Unlock: + nukiInst->unlock(); + break; + case GpioAction::Unlatch: + nukiInst->unlatch(); + break; + } +} + void NukiWrapper::onConfigUpdateReceived(const char *topic, const char *value) { if(strcmp(topic, mqtt_topic_config_button_enabled) == 0) @@ -719,3 +739,29 @@ void NukiWrapper::disableWatchdog() { _restartBeaconTimeout = -1; } + +void NukiWrapper::updateGpioOutputs() +{ + using namespace NukiLock; + + const auto& pinConfiguration = _gpio->pinConfiguration(); + + const LockState& lockState = _keyTurnerState.lockState; + + for(const auto& entry : pinConfiguration) + { + switch(entry.role) + { + case PinRole::OutputHighLocked: + _gpio->setPinOutput(entry.pin, lockState == LockState::Locked || lockState == LockState::Locking ? HIGH : LOW); + break; + case PinRole::OutputHighUnlocked: + _gpio->setPinOutput(entry.pin, lockState == LockState::Locked || lockState == LockState::Locking ? LOW : HIGH); + break; + case PinRole::OutputHighMotorBlocked: + _gpio->setPinOutput(entry.pin, lockState == LockState::MotorBlocked ? HIGH : LOW); + break; + } + } +} + diff --git a/NukiWrapper.h b/NukiWrapper.h index 081530f..e553f15 100644 --- a/NukiWrapper.h +++ b/NukiWrapper.h @@ -5,11 +5,12 @@ #include "NukiDataTypes.h" #include "BleScanner.h" #include "NukiLock.h" +#include "Gpio.h" class NukiWrapper : public Nuki::SmartlockEventHandler { public: - NukiWrapper(const std::string& deviceName, uint32_t id, BleScanner::Scanner* scanner, NetworkLock* network, Preferences* preferences); + NukiWrapper(const std::string& deviceName, uint32_t id, BleScanner::Scanner* scanner, NetworkLock* network, Gpio* gpio, Preferences* preferences); virtual ~NukiWrapper(); void initialize(const bool& firstStart); @@ -42,6 +43,8 @@ private: static bool onLockActionReceivedCallback(const char* value); static void onConfigUpdateReceivedCallback(const char* topic, const char* value); static void onKeypadCommandReceivedCallback(const char* command, const uint& id, const String& name, const String& code, const int& enabled); + static void gpioActionCallback(const GpioAction& action); + void onConfigUpdateReceived(const char* topic, const char* value); void onKeypadCommandReceived(const char* command, const uint& id, const String& name, const String& code, const int& enabled); @@ -52,6 +55,8 @@ private: void updateKeypad(); void postponeBleWatchdog(); + void updateGpioOutputs(); + void readConfig(); void readAdvancedConfig(); @@ -63,8 +68,9 @@ private: std::string _deviceName; NukiLock::NukiLock _nukiLock; - BleScanner::Scanner* _bleScanner; - NetworkLock* _network; + BleScanner::Scanner* _bleScanner = nullptr; + NetworkLock* _network = nullptr; + Gpio* _gpio = nullptr; Preferences* _preferences; int _intervalLockstate = 0; // seconds int _intervalBattery = 0; // seconds diff --git a/PreferencesKeys.h b/PreferencesKeys.h index 4aed7cb..81009e1 100644 --- a/PreferencesKeys.h +++ b/PreferencesKeys.h @@ -25,7 +25,6 @@ #define preference_ip_gateway "ipgtw" #define preference_ip_dns_server "dnssrv" #define preference_network_hardware "nwhw" -#define preference_network_hardware_gpio "nwhwdt" #define preference_rssi_publish_interval "rssipb" #define preference_hostname "hostname" #define preference_network_timeout "nettmout" @@ -44,6 +43,7 @@ #define preference_cred_password "crdpass" #define preference_publish_authdata "pubauth" #define preference_gpio_locking_enabled "gpiolck" +#define preference_gpio_configuration "gpiocfg" #define preference_publish_debug_info "pubdbg" #define preference_presence_detection_timeout "prdtimeout" #define preference_has_mac_saved "hasmac" @@ -62,13 +62,13 @@ private: preference_lock_max_keypad_code_count, preference_opener_max_keypad_code_count, preference_mqtt_ca, preference_mqtt_crt, preference_mqtt_key, preference_mqtt_hass_discovery, preference_ip_dhcp_enabled, preference_ip_address, preference_ip_subnet, preference_ip_gateway, preference_ip_dns_server, - preference_network_hardware, preference_network_hardware_gpio, preference_rssi_publish_interval, + preference_network_hardware, preference_rssi_publish_interval, preference_hostname, preference_network_timeout, preference_restart_on_disconnect, preference_restart_timer, preference_restart_ble_beacon_lost, preference_query_interval_lockstate, preference_query_interval_configuration, preference_query_interval_battery, preference_query_interval_keypad, preference_keypad_control_enabled, preference_register_as_app, preference_command_nr_of_retries, preference_command_retry_delay, preference_cred_user, preference_cred_password, preference_publish_authdata, - preference_gpio_locking_enabled, preference_publish_debug_info, preference_presence_detection_timeout, + preference_publish_debug_info, preference_presence_detection_timeout, preference_has_mac_saved, preference_has_mac_byte_0, preference_has_mac_byte_1, preference_has_mac_byte_2, }; std::vector _redact = @@ -81,7 +81,7 @@ private: { preference_started_before, preference_mqtt_log_enabled, preference_lock_enabled, preference_opener_enabled, preference_restart_on_disconnect, preference_keypad_control_enabled, preference_register_as_app, preference_ip_dhcp_enabled, - preference_publish_authdata, preference_gpio_locking_enabled, preference_has_mac_saved, preference_publish_debug_info + preference_publish_authdata, preference_has_mac_saved, preference_publish_debug_info }; const bool isRedacted(const char* key) const diff --git a/README.md b/README.md index 58b1506..9b411d2 100644 --- a/README.md +++ b/README.md @@ -198,11 +198,10 @@ W5x00 SCK to GPIO18
W5x00 MISO to GPIOGPIO19
W5x00 MOSI to GPIO23
W5x00 CS/SS to GPIO5 -- Additionally connect:
+- Optionally connect:
W5x00 reset to GPIO33 -- Last but not least, on the ESP32 bridge GPIO26 and GND. This let's the firmware know that a LAN Module is connected -Wifi is now disabled, and the module doesn't boot into WifiManager anymore.
+Now connect via Wifi and change the network hardware to "Generic W5500". If the W5500 hwardware isn't detected, Wifi is used as a fallback.
Note: Encrypted MQTT is only available for Wifi and LAN8720 modules, W5x00 modules don't support encryption (that leaves Olimex, WT32-ETH01 and M5Stack PoESP32 Unit if encryption is desired). If encryption is needed, Olimex is the easiest option, since it has USB for flashing onboard. diff --git a/RestartReason.h b/RestartReason.h index 4542da3..17752a0 100644 --- a/RestartReason.h +++ b/RestartReason.h @@ -12,6 +12,7 @@ enum class RestartReason ReconfigureLAN8720, NetworkDeviceCriticalFailure, ConfigurationUpdated, + GpioConfigurationUpdated, RestartTimer, OTACompleted, OTATimeout, @@ -69,6 +70,8 @@ inline static String getRestartReason() return "NetworkDeviceCriticalFailure"; case RestartReason::ConfigurationUpdated: return "ConfigurationUpdated"; + case RestartReason::GpioConfigurationUpdated: + return "GpioConfigurationUpdated"; case RestartReason::RestartTimer: return "RestartTimer"; case RestartReason::OTACompleted: diff --git a/WebCfgServer.cpp b/WebCfgServer.cpp index d002db4..e6ca370 100644 --- a/WebCfgServer.cpp +++ b/WebCfgServer.cpp @@ -7,11 +7,12 @@ #include "RestartReason.h" #include -WebCfgServer::WebCfgServer(NukiWrapper* nuki, NukiOpenerWrapper* nukiOpener, Network* network, EthServer* ethServer, Preferences* preferences, bool allowRestartToPortal) +WebCfgServer::WebCfgServer(NukiWrapper* nuki, NukiOpenerWrapper* nukiOpener, Network* network, Gpio* gpio, EthServer* ethServer, Preferences* preferences, bool allowRestartToPortal) : _server(ethServer), _nuki(nuki), _nukiOpener(nukiOpener), _network(network), + _gpio(gpio), _preferences(preferences), _allowRestartToPortal(allowRestartToPortal) { @@ -35,13 +36,13 @@ WebCfgServer::WebCfgServer(NukiWrapper* nuki, NukiOpenerWrapper* nukiOpener, Net _pinsConfigured = true; - if(_nuki != nullptr) + if(_nuki != nullptr && !_nuki->isPinSet()) { - _pinsConfigured = _pinsConfigured && _nuki->isPinSet(); + _pinsConfigured = false; } - if(_nukiOpener != nullptr) + if(_nukiOpener != nullptr && !_nukiOpener->isPinSet()) { - _pinsConfigured = _pinsConfigured && _nukiOpener->isPinSet(); + _pinsConfigured = false; } _brokerConfigured = _preferences->getString(preference_mqtt_broker).length() > 0 && _preferences->getInt(preference_mqtt_broker_port) > 0; @@ -93,6 +94,14 @@ void WebCfgServer::initialize() buildNukiConfigHtml(response); _server.send(200, "text/html", response); }); + _server.on("/gpiocfg", [&]() { + if (_hasCredentials && !_server.authenticate(_credUser, _credPassword)) { + return _server.requestAuthentication(); + } + String response = ""; + buildGpioConfigHtml(response); + _server.send(200, "text/html", response); + }); _server.on("/wifi", [&]() { if (_hasCredentials && !_server.authenticate(_credUser, _credPassword)) { return _server.requestAuthentication(); @@ -128,7 +137,8 @@ void WebCfgServer::initialize() _network->reconfigureDevice(); } }); - _server.on("/savecfg", [&]() { + _server.on("/savecfg", [&]() + { if (_hasCredentials && !_server.authenticate(_credUser, _credPassword)) { return _server.requestAuthentication(); } @@ -152,6 +162,22 @@ void WebCfgServer::initialize() waitAndProcess(false, 1000); } }); + _server.on("/savegpiocfg", [&]() + { + if (_hasCredentials && !_server.authenticate(_credUser, _credPassword)) { + return _server.requestAuthentication(); + } + processGpioArgs(); + + String response = ""; + buildConfirmHtml(response, ""); + _server.send(200, "text/html", response); + Log->println(F("Restarting")); + + waitAndProcess(true, 1000); + restartEsp(RestartReason::GpioConfigurationUpdated); + }); + _server.on("/ota", [&]() { if (_hasCredentials && !_server.authenticate(_credUser, _credPassword)) { return _server.requestAuthentication(); @@ -298,11 +324,6 @@ bool WebCfgServer::processArgs(String& message) _preferences->putInt(preference_network_hardware, value.toInt()); configChanged = true; } - else if(key == "NWHWDT") - { - _preferences->putInt(preference_network_hardware_gpio, value.toInt()); - configChanged = true; - } else if(key == "RSSI") { _preferences->putInt(preference_rssi_publish_interval, value.toInt()); @@ -425,11 +446,6 @@ bool WebCfgServer::processArgs(String& message) _preferences->putBool(preference_publish_authdata, (value == "1")); configChanged = true; } - else if(key == "GPLCK") - { - _preferences->putBool(preference_gpio_locking_enabled, (value == "1")); - configChanged = true; - } else if(key == "REGAPP") { _preferences->putBool(preference_register_as_app, (value == "1")); @@ -523,6 +539,32 @@ bool WebCfgServer::processArgs(String& message) return configChanged; } + +void WebCfgServer::processGpioArgs() +{ + int count = _server.args(); + + std::vector pinConfiguration; + + for(int index = 0; index < count; index++) + { + String key = _server.argName(index); + String value = _server.arg(index); + + PinRole role = (PinRole)value.toInt(); + if(role != PinRole::Disabled) + { + PinEntry entry; + entry.pin = key.toInt(); + entry.role = role; + pinConfiguration.push_back(entry); + } + } + + _gpio->savePinConfiguration(pinConfiguration); +} + + void WebCfgServer::update() { if(_otaStartTs > 0 && (millis() - _otaStartTs) > 120000) @@ -575,6 +617,9 @@ void WebCfgServer::buildHtml(String& response) response.concat("

Credentials

"); buildNavigationButton(response, "Edit", "/cred", _pinsConfigured ? "" : "(!) Please configure PIN"); + response.concat("

GPIO Configuration

"); + buildNavigationButton(response, "Edit", "/gpiocfg"); + response.concat("

Firmware update

"); buildNavigationButton(response, "Open", "/ota"); @@ -713,7 +758,6 @@ void WebCfgServer::buildMqttConfigHtml(String &response) printTextarea(response, "MQTTCRT", "MQTT SSL Client Certificate (*, optional)", _preferences->getString(preference_mqtt_crt).c_str(), TLS_CERT_MAX_SIZE, _network->encryptionSupported(), true); printTextarea(response, "MQTTKEY", "MQTT SSL Client Key (*, optional)", _preferences->getString(preference_mqtt_key).c_str(), TLS_KEY_MAX_SIZE, _network->encryptionSupported(), true); printDropDown(response, "NWHW", "Network hardware", String(_preferences->getInt(preference_network_hardware)), getNetworkDetectionOptions()); - printDropDown(response, "NWHWDT", "Network hardware detection", String(_preferences->getInt(preference_network_hardware_gpio)), getNetworkGpioOptions()); printInputField(response, "RSSI", "RSSI Publish interval (seconds; -1 to disable)", _preferences->getInt(preference_rssi_publish_interval), 6); printInputField(response, "NETTIMEOUT", "Network Timeout until restart (seconds; -1 to disable)", _preferences->getInt(preference_network_timeout), 5); printCheckBox(response, "RSTDISC", "Restart on disconnect", _preferences->getBool(preference_restart_on_disconnect)); @@ -771,7 +815,6 @@ void WebCfgServer::buildNukiConfigHtml(String &response) printInputField(response, "NRTRY", "Number of retries if command failed", _preferences->getInt(preference_command_nr_of_retries), 10); printInputField(response, "TRYDLY", "Delay between retries (milliseconds)", _preferences->getInt(preference_command_retry_delay), 10); printCheckBox(response, "PUBAUTH", "Publish auth data (May reduce battery life)", _preferences->getBool(preference_publish_authdata)); - printCheckBox(response, "GPLCK", "Enable control via GPIO", _preferences->getBool(preference_gpio_locking_enabled)); printInputField(response, "PRDTMO", "Presence detection timeout (seconds; -1 to disable)", _preferences->getInt(preference_presence_detection_timeout), 10); printInputField(response, "RSBC", "Restart if bluetooth beacons not received (seconds; -1 to disable)", _preferences->getInt(preference_restart_ble_beacon_lost), 10); response.concat(""); @@ -780,6 +823,29 @@ void WebCfgServer::buildNukiConfigHtml(String &response) response.concat(""); } +void WebCfgServer::buildGpioConfigHtml(String &response) +{ + buildHtmlHeader(response); + + response.concat("
"); + response.concat("

GPIO Configuration

"); + response.concat(""); + + const auto& availablePins = _gpio->availablePins(); + for(const auto& pin : availablePins) + { + String pinStr = String(pin); + String pinDesc = "Gpio " + pinStr; + + printDropDown(response, pinStr.c_str(), pinDesc.c_str(), getPreselectionForGpio(pin), getGpioOptions()); + } + + response.concat("
"); + response.concat("
"); + response.concat("
"); + response.concat(""); +} + void WebCfgServer::buildConfirmHtml(String &response, const String &message, uint32_t redirectDelay) { String delay(redirectDelay); @@ -872,6 +938,8 @@ void WebCfgServer::buildInfoHtml(String &response) response.concat(uxTaskGetStackHighWaterMark(presenceDetectionTaskHandle)); response.concat("\n"); + _gpio->getConfigurationText(response, _gpio->pinConfiguration()); + response.concat("Restart reason FW: "); response.concat(getRestartReason()); response.concat( "\n"); @@ -1203,7 +1271,7 @@ const std::vector> WebCfgServer::getNetworkDetectionOp std::vector> options; options.push_back(std::make_pair("1", "Wifi only")); - options.push_back(std::make_pair("2", "Detect W5500 (GPIO CS=5; SCK=18; MISO=19; MOSI=23; RST=33)")); + options.push_back(std::make_pair("2", "Generic W5500")); options.push_back(std::make_pair("3", "M5Stack Atom POE (W5500)")); options.push_back(std::make_pair("4", "Olimex ESP32-POE / ESP-POE-ISO")); options.push_back(std::make_pair("5", "WT32-ETH01")); @@ -1213,16 +1281,31 @@ const std::vector> WebCfgServer::getNetworkDetectionOp return options; } -const std::vector> WebCfgServer::getNetworkGpioOptions() const +const std::vector> WebCfgServer::getGpioOptions() const { std::vector> options; - for(int i=16; i <= 33; i++) + const auto& roles = _gpio->getAllRoles(); + + for(const auto& role : roles) { - String txt = "Detect W5500 via GPIO "; - txt.concat(i); - options.push_back(std::make_pair(String(i), txt)); + options.push_back( std::make_pair(String((int)role), _gpio->getRoleDescription(role))); } return options; } + +String WebCfgServer::getPreselectionForGpio(const uint8_t &pin) +{ + const std::vector& pinConfiguration = _gpio->pinConfiguration(); + + for(const auto& entry : pinConfiguration) + { + if(pin == entry.pin) + { + return String((int8_t)entry.role); + } + } + + return String((int8_t)PinRole::Disabled); +} diff --git a/WebCfgServer.h b/WebCfgServer.h index 6356280..4a3b5d7 100644 --- a/WebCfgServer.h +++ b/WebCfgServer.h @@ -6,6 +6,7 @@ #include "NetworkLock.h" #include "NukiOpenerWrapper.h" #include "Ota.h" +#include "Gpio.h" extern TaskHandle_t networkTaskHandle; extern TaskHandle_t nukiTaskHandle; @@ -26,7 +27,7 @@ enum class TokenType class WebCfgServer { public: - WebCfgServer(NukiWrapper* nuki, NukiOpenerWrapper* nukiOpener, Network* network, EthServer* ethServer, Preferences* preferences, bool allowRestartToPortal); + WebCfgServer(NukiWrapper* nuki, NukiOpenerWrapper* nukiOpener, Network* network, Gpio* gpio, EthServer* ethServer, Preferences* preferences, bool allowRestartToPortal); ~WebCfgServer() = default; void initialize(); @@ -34,12 +35,14 @@ public: private: bool processArgs(String& message); + void processGpioArgs(); void buildHtml(String& response); void buildCredHtml(String& response); void buildOtaHtml(String& response, bool errored); void buildOtaCompletedHtml(String& response); void buildMqttConfigHtml(String& response); void buildNukiConfigHtml(String& response); + void buildGpioConfigHtml(String& response); void buildConfirmHtml(String& response, const String &message, uint32_t redirectDelay = 5); void buildConfigureWifiHtml(String& response); void buildInfoHtml(String& response); @@ -56,7 +59,8 @@ private: void buildNavigationButton(String& response, const char* caption, const char* targetPath, const char* labelText = ""); const std::vector> getNetworkDetectionOptions() const; - const std::vector> getNetworkGpioOptions() const; + const std::vector> getGpioOptions() const; + String getPreselectionForGpio(const uint8_t& pin); void printParameter(String& response, const char* description, const char* value, const char *link = ""); @@ -65,10 +69,11 @@ private: void handleOtaUpload(); WebServer _server; - NukiWrapper* _nuki; - NukiOpenerWrapper* _nukiOpener; - Network* _network; - Preferences* _preferences; + NukiWrapper* _nuki = nullptr; + NukiOpenerWrapper* _nukiOpener = nullptr; + Network* _network = nullptr; + Gpio* _gpio = nullptr; + Preferences* _preferences = nullptr; Ota _ota; bool _hasCredentials = false; diff --git a/main.cpp b/main.cpp index 7fe8644..97dc63b 100644 --- a/main.cpp +++ b/main.cpp @@ -25,6 +25,7 @@ NukiOpenerWrapper* nukiOpener = nullptr; PresenceDetection* presenceDetection = nullptr; Preferences* preferences = nullptr; EthServer* ethServer = nullptr; +Gpio* gpio = nullptr; bool lockEnabled = false; bool openerEnabled = false; @@ -169,6 +170,7 @@ void setup() Log->print(F("NUKI Hub version ")); Log->println(NUKI_HUB_VERSION); bool firstStart = initPreferences(); + initializeRestartReason(); CharBuffer::initialize(); @@ -207,26 +209,33 @@ void setup() bleScanner->initialize("NukiHub"); bleScanner->setScanDuration(10); + gpio = new Gpio(preferences); + String gpioDesc; + gpio->getConfigurationText(gpioDesc, gpio->pinConfiguration(), "\n\r"); + Serial.print(gpioDesc.c_str()); + Log->println(lockEnabled ? F("NUKI Lock enabled") : F("NUKI Lock disabled")); if(lockEnabled) { - nuki = new NukiWrapper("NukiHub", deviceId, bleScanner, networkLock, preferences); + nuki = new NukiWrapper("NukiHub", deviceId, bleScanner, networkLock, gpio, preferences); nuki->initialize(firstStart); - if(preferences->getBool(preference_gpio_locking_enabled)) - { - Gpio::init(nuki); - } + + +// if(preferences->getBool(preference_gpio_locking_enabled)) +// { +// Gpio::init(nuki); +// } } Log->println(openerEnabled ? F("NUKI Opener enabled") : F("NUKI Opener disabled")); if(openerEnabled) { - nukiOpener = new NukiOpenerWrapper("NukiHub", deviceId, bleScanner, networkOpener, preferences); + nukiOpener = new NukiOpenerWrapper("NukiHub", deviceId, bleScanner, networkOpener, gpio, preferences); nukiOpener->initialize(); } - webCfgServer = new WebCfgServer(nuki, nukiOpener, network, ethServer, preferences, network->networkDeviceType() == NetworkDeviceType::WiFi); + webCfgServer = new WebCfgServer(nuki, nukiOpener, network, gpio, ethServer, preferences, network->networkDeviceType() == NetworkDeviceType::WiFi); webCfgServer->initialize(); presenceDetection = new PresenceDetection(preferences, bleScanner, network, CharBuffer::get(), CHAR_BUFFER_SIZE);