Merge pull request #169 from technyon/gpio

Make GPIOs user-configurable
This commit is contained in:
Jan-Ole Schümann
2023-04-08 08:25:29 +02:00
committed by GitHub
14 changed files with 658 additions and 92 deletions

View File

@@ -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

292
Gpio.cpp
View File

@@ -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<uint8_t>& 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<PinEntry> &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<PinEntry> &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<PinEntry>& 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<PinRole>& Gpio::getAllRoles() const
{
return _allRoles;
}
void Gpio::notify(const GpioAction &action)
{
for(auto& callback : _callbacks)
{
callback(action);
}
}
void Gpio::addCallback(std::function<void(const GpioAction&)> 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);
}

91
Gpio.h
View File

@@ -1,22 +1,103 @@
#pragma once
#include <functional>
#include <Preferences.h>
#include <vector>
#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<void(const GpioAction&)> callback);
void loadPinConfiguration();
void savePinConfiguration(const std::vector<PinEntry>& pinConfiguration);
const std::vector<uint8_t>& availablePins() const;
const std::vector<PinEntry>& pinConfiguration() const;
String getRoleDescription(PinRole role) const;
void getConfigurationText(String& text, const std::vector<PinEntry>& pinConfiguration, const String& linebreak = "\n") const;
const std::vector<PinRole>& getAllRoles() const;
void setPinOutput(const uint8_t& pin, const uint8_t& state);
private:
void notify(const GpioAction& action);
const std::vector<uint8_t> _availablePins = { 2, 4, 5, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 32, 33 };
const std::vector<PinRole> _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<PinEntry> _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<std::function<void(const GpioAction&)>> _callbacks;
static Gpio* _inst;
static NukiWrapper* _nuki;
static unsigned long _lockedTs;
static unsigned long _debounceTs;
Preferences* _preferences = nullptr;
};

View File

@@ -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"));

View File

@@ -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;
}
}
}

View File

@@ -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

View File

@@ -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;
}
}
}

View File

@@ -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

View File

@@ -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<char*> _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

View File

@@ -198,11 +198,10 @@ W5x00 SCK to GPIO18<br>
W5x00 MISO to GPIOGPIO19<br>
W5x00 MOSI to GPIO23<br>
W5x00 CS/SS to GPIO5
- Additionally connect:<br>
- Optionally connect:<br>
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.<br>
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.<br>
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.

View File

@@ -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:

View File

@@ -7,11 +7,12 @@
#include "RestartReason.h"
#include <esp_task_wdt.h>
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<PinEntry> 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("<BR><BR><h3>Credentials</h3>");
buildNavigationButton(response, "Edit", "/cred", _pinsConfigured ? "" : "<font color=\"#f07000\"><em>(!) Please configure PIN</em></font>");
response.concat("<BR><BR><h3>GPIO Configuration</h3>");
buildNavigationButton(response, "Edit", "/gpiocfg");
response.concat("<BR><BR><h3>Firmware update</h3>");
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("</table>");
@@ -780,6 +823,29 @@ void WebCfgServer::buildNukiConfigHtml(String &response)
response.concat("</BODY></HTML>");
}
void WebCfgServer::buildGpioConfigHtml(String &response)
{
buildHtmlHeader(response);
response.concat("<FORM ACTION=savegpiocfg method='POST'>");
response.concat("<h3>GPIO Configuration</h3>");
response.concat("<table>");
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("</table>");
response.concat("<br><INPUT TYPE=SUBMIT NAME=\"submit\" VALUE=\"Save\">");
response.concat("</FORM>");
response.concat("</BODY></HTML>");
}
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<std::pair<String, String>> WebCfgServer::getNetworkDetectionOp
std::vector<std::pair<String, String>> 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<std::pair<String, String>> WebCfgServer::getNetworkDetectionOp
return options;
}
const std::vector<std::pair<String, String>> WebCfgServer::getNetworkGpioOptions() const
const std::vector<std::pair<String, String>> WebCfgServer::getGpioOptions() const
{
std::vector<std::pair<String, String>> 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<PinEntry>& pinConfiguration = _gpio->pinConfiguration();
for(const auto& entry : pinConfiguration)
{
if(pin == entry.pin)
{
return String((int8_t)entry.role);
}
}
return String((int8_t)PinRole::Disabled);
}

View File

@@ -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<std::pair<String, String>> getNetworkDetectionOptions() const;
const std::vector<std::pair<String, String>> getNetworkGpioOptions() const;
const std::vector<std::pair<String, String>> 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;

View File

@@ -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);