Rework backend add MQTT and WebSocket support

* Update back end to add MQTT and WebSocket support
* Update demo project to demonstrate MQTT and WebSockets
* Update documentation to describe newly added and modified functionallity
* Introduce separate MQTT pub/sub, HTTP get/post and WebSocket rx/tx classes
* Significant reanaming - more accurate class names
* Use PROGMEM_WWW as default
* Update README documenting PROGMEM_WWW as default
* Update README with API changes
This commit is contained in:
rjwats
2020-05-14 23:23:45 +01:00
committed by GitHub
parent c47ea49a5d
commit a1f4e57a21
77 changed files with 2907 additions and 1192 deletions

View File

@ -1,14 +1,18 @@
#include <APSettingsService.h>
APSettingsService::APSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) :
AdminSettingsService(server, fs, securityManager, AP_SETTINGS_SERVICE_PATH, AP_SETTINGS_FILE) {
}
APSettingsService::~APSettingsService() {
_httpEndpoint(APSettings::serialize,
APSettings::deserialize,
this,
server,
AP_SETTINGS_SERVICE_PATH,
securityManager),
_fsPersistence(APSettings::serialize, APSettings::deserialize, this, fs, AP_SETTINGS_FILE) {
addUpdateHandler([&](String originId) { reconfigureAP(); }, false);
}
void APSettingsService::begin() {
SettingsService::begin();
_fsPersistence.readFromFS();
reconfigureAP();
}
@ -28,8 +32,8 @@ void APSettingsService::loop() {
void APSettingsService::manageAP() {
WiFiMode_t currentWiFiMode = WiFi.getMode();
if (_settings.provisionMode == AP_MODE_ALWAYS ||
(_settings.provisionMode == AP_MODE_DISCONNECTED && WiFi.status() != WL_CONNECTED)) {
if (_state.provisionMode == AP_MODE_ALWAYS ||
(_state.provisionMode == AP_MODE_DISCONNECTED && WiFi.status() != WL_CONNECTED)) {
if (currentWiFiMode == WIFI_OFF || currentWiFiMode == WIFI_STA) {
startAP();
}
@ -42,7 +46,7 @@ void APSettingsService::manageAP() {
void APSettingsService::startAP() {
Serial.println("Starting software access point");
WiFi.softAP(_settings.ssid.c_str(), _settings.password.c_str());
WiFi.softAP(_state.ssid.c_str(), _state.password.c_str());
if (!_dnsServer) {
IPAddress apIp = WiFi.softAPIP();
Serial.print("Starting captive portal on ");
@ -68,27 +72,3 @@ void APSettingsService::handleDNS() {
_dnsServer->processNextRequest();
}
}
void APSettingsService::readFromJsonObject(JsonObject& root) {
_settings.provisionMode = root["provision_mode"] | AP_MODE_ALWAYS;
switch (_settings.provisionMode) {
case AP_MODE_ALWAYS:
case AP_MODE_DISCONNECTED:
case AP_MODE_NEVER:
break;
default:
_settings.provisionMode = AP_MODE_ALWAYS;
}
_settings.ssid = root["ssid"] | AP_DEFAULT_SSID;
_settings.password = root["password"] | AP_DEFAULT_PASSWORD;
}
void APSettingsService::writeToJsonObject(JsonObject& root) {
root["provision_mode"] = _settings.provisionMode;
root["ssid"] = _settings.ssid;
root["password"] = _settings.password;
}
void APSettingsService::onConfigUpdated() {
reconfigureAP();
}

View File

@ -1,7 +1,9 @@
#ifndef APSettingsConfig_h
#define APSettingsConfig_h
#include <AdminSettingsService.h>
#include <HttpEndpoint.h>
#include <FSPersistence.h>
#include <DNSServer.h>
#include <IPAddress.h>
@ -24,22 +26,39 @@ class APSettings {
uint8_t provisionMode;
String ssid;
String password;
static void serialize(APSettings& settings, JsonObject& root) {
root["provision_mode"] = settings.provisionMode;
root["ssid"] = settings.ssid;
root["password"] = settings.password;
}
static void deserialize(JsonObject& root, APSettings& settings) {
settings.provisionMode = root["provision_mode"] | AP_MODE_ALWAYS;
switch (settings.provisionMode) {
case AP_MODE_ALWAYS:
case AP_MODE_DISCONNECTED:
case AP_MODE_NEVER:
break;
default:
settings.provisionMode = AP_MODE_ALWAYS;
}
settings.ssid = root["ssid"] | AP_DEFAULT_SSID;
settings.password = root["password"] | AP_DEFAULT_PASSWORD;
}
};
class APSettingsService : public AdminSettingsService<APSettings> {
class APSettingsService : public StatefulService<APSettings> {
public:
APSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager);
~APSettingsService();
void begin();
void loop();
protected:
void readFromJsonObject(JsonObject& root);
void writeToJsonObject(JsonObject& root);
void onConfigUpdated();
private:
HttpEndpoint<APSettings> _httpEndpoint;
FSPersistence<APSettings> _fsPersistence;
// for the mangement delay loop
unsigned long _lastManaged;

View File

@ -1,50 +0,0 @@
#ifndef AdminSettingsService_h
#define AdminSettingsService_h
#include <SettingsService.h>
template <class T>
class AdminSettingsService : public SettingsService<T> {
public:
AdminSettingsService(AsyncWebServer* server,
FS* fs,
SecurityManager* securityManager,
char const* servicePath,
char const* filePath) :
SettingsService<T>(server, fs, servicePath, filePath),
_securityManager(securityManager) {
}
protected:
// will validate the requests with the security manager
SecurityManager* _securityManager;
void fetchConfig(AsyncWebServerRequest* request) {
// verify the request against the predicate
Authentication authentication = _securityManager->authenticateRequest(request);
if (!getAuthenticationPredicate()(authentication)) {
request->send(401);
return;
}
// delegate to underlying implemetation
SettingsService<T>::fetchConfig(request);
}
void updateConfig(AsyncWebServerRequest* request, JsonDocument& jsonDocument) {
// verify the request against the predicate
Authentication authentication = _securityManager->authenticateRequest(request);
if (!getAuthenticationPredicate()(authentication)) {
request->send(401);
return;
}
// delegate to underlying implemetation
SettingsService<T>::updateConfig(request, jsonDocument);
}
// override this to replace the default authentication predicate, IS_ADMIN
AuthenticationPredicate getAuthenticationPredicate() {
return AuthenticationPredicates::IS_ADMIN;
}
};
#endif // end AdminSettingsService

View File

@ -1,33 +0,0 @@
#ifndef _AsyncJsonCallbackResponse_H_
#define _AsyncJsonCallbackResponse_H_
#include <AsyncJson.h>
#include <ESPAsyncWebServer.h>
/*
* Listens for a response being destroyed and calls a callback during said distruction.
* used so we can take action after the response has been rendered to the client.
*
* Avoids having to fork ESPAsyncWebServer with a callback feature, but not nice!
*/
typedef std::function<void()> AsyncJsonCallback;
class AsyncJsonCallbackResponse : public AsyncJsonResponse {
private:
AsyncJsonCallback _callback;
public:
AsyncJsonCallbackResponse(AsyncJsonCallback callback,
bool isArray = false,
size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE) :
AsyncJsonResponse(isArray, maxJsonBufferSize),
_callback{callback} {
}
~AsyncJsonCallbackResponse() {
_callback();
}
};
#endif // end _AsyncJsonCallbackResponse_H_

View File

@ -1,131 +0,0 @@
#ifndef Async_Json_Request_Web_Handler_H_
#define Async_Json_Request_Web_Handler_H_
#include <ArduinoJson.h>
#include <ESPAsyncWebServer.h>
#define ASYNC_JSON_REQUEST_DEFAULT_MAX_SIZE 1024
#define ASYNC_JSON_REQUEST_MIMETYPE "application/json"
/*
* Handy little utility for dealing with small JSON request body payloads.
*
* Need to be careful using this as we are somewhat limited by RAM.
*
* Really only of use where there is a determinate payload size.
*/
typedef std::function<void(AsyncWebServerRequest* request, JsonDocument& jsonDocument)> JsonRequestCallback;
class AsyncJsonWebHandler : public AsyncWebHandler {
private:
WebRequestMethodComposite _method;
JsonRequestCallback _onRequest;
size_t _maxContentLength;
protected:
String _uri;
public:
AsyncJsonWebHandler() :
_method(HTTP_POST | HTTP_PUT | HTTP_PATCH),
_onRequest(nullptr),
_maxContentLength(ASYNC_JSON_REQUEST_DEFAULT_MAX_SIZE),
_uri() {
}
~AsyncJsonWebHandler() {
}
void setUri(const String& uri) {
_uri = uri;
}
void setMethod(WebRequestMethodComposite method) {
_method = method;
}
void setMaxContentLength(size_t maxContentLength) {
_maxContentLength = maxContentLength;
}
void onRequest(JsonRequestCallback fn) {
_onRequest = fn;
}
virtual bool canHandle(AsyncWebServerRequest* request) override final {
if (!_onRequest)
return false;
if (!(_method & request->method()))
return false;
if (_uri.length() && (_uri != request->url() && !request->url().startsWith(_uri + "/")))
return false;
if (!request->contentType().equalsIgnoreCase(ASYNC_JSON_REQUEST_MIMETYPE))
return false;
request->addInterestingHeader("ANY");
return true;
}
virtual void handleRequest(AsyncWebServerRequest* request) override final {
// no request configured
if (!_onRequest) {
Serial.print("No request callback was configured for endpoint: ");
Serial.println(_uri);
request->send(500);
return;
}
// we have been handed too much data, return a 413 (payload too large)
if (request->contentLength() > _maxContentLength) {
request->send(413);
return;
}
// parse JSON and if possible handle the request
if (request->_tempObject) {
DynamicJsonDocument jsonDocument(_maxContentLength);
DeserializationError error = deserializeJson(jsonDocument, (uint8_t*)request->_tempObject);
if (error == DeserializationError::Ok) {
_onRequest(request, jsonDocument);
} else {
request->send(400);
}
return;
}
// fallthrough, we have a null pointer, return 500.
// this can be due to running out of memory or never receiving body data.
request->send(500);
}
virtual void handleBody(AsyncWebServerRequest* request,
uint8_t* data,
size_t len,
size_t index,
size_t total) override final {
if (_onRequest) {
// don't allocate if data is too large
if (total > _maxContentLength) {
return;
}
// try to allocate memory on first call
// NB: the memory allocated here is freed by ~AsyncWebServerRequest
if (index == 0 && !request->_tempObject) {
request->_tempObject = malloc(total);
}
// copy the data into the buffer, if we have a buffer!
if (request->_tempObject) {
memcpy((uint8_t*)request->_tempObject + index, data, len);
}
}
}
virtual bool isRequestHandlerTrivial() override final {
return _onRequest ? false : true;
}
};
#endif // end Async_Json_Request_Web_Handler_H_

View File

@ -1,21 +1,17 @@
#include <AuthenticationService.h>
AuthenticationService::AuthenticationService(AsyncWebServer* server, SecurityManager* securityManager) :
_securityManager(securityManager) {
_securityManager(securityManager),
_signInHandler(SIGN_IN_PATH,
std::bind(&AuthenticationService::signIn, this, std::placeholders::_1, std::placeholders::_2)) {
server->on(VERIFY_AUTHORIZATION_PATH,
HTTP_GET,
std::bind(&AuthenticationService::verifyAuthorization, this, std::placeholders::_1));
_signInHandler.setUri(SIGN_IN_PATH);
_signInHandler.setMethod(HTTP_POST);
_signInHandler.setMaxContentLength(MAX_AUTHENTICATION_SIZE);
_signInHandler.onRequest(
std::bind(&AuthenticationService::signIn, this, std::placeholders::_1, std::placeholders::_2));
server->addHandler(&_signInHandler);
}
AuthenticationService::~AuthenticationService() {
}
/**
* Verifys that the request supplied a valid JWT.
*/
@ -28,10 +24,10 @@ void AuthenticationService::verifyAuthorization(AsyncWebServerRequest* request)
* Signs in a user if the username and password match. Provides a JWT to be used in the Authorization header in
* subsequent requests.
*/
void AuthenticationService::signIn(AsyncWebServerRequest* request, JsonDocument& jsonDocument) {
if (jsonDocument.is<JsonObject>()) {
String username = jsonDocument["username"];
String password = jsonDocument["password"];
void AuthenticationService::signIn(AsyncWebServerRequest* request, JsonVariant& json) {
if (json.is<JsonObject>()) {
String username = json["username"];
String password = json["password"];
Authentication authentication = _securityManager->authenticate(username, password);
if (authentication.authenticated) {
User* user = authentication.user;

View File

@ -2,7 +2,6 @@
#define AuthenticationService_H_
#include <AsyncJson.h>
#include <AsyncJsonWebHandler.h>
#include <ESPAsyncWebServer.h>
#include <SecurityManager.h>
@ -14,14 +13,13 @@
class AuthenticationService {
public:
AuthenticationService(AsyncWebServer* server, SecurityManager* securityManager);
~AuthenticationService();
private:
SecurityManager* _securityManager;
AsyncJsonWebHandler _signInHandler;
AsyncCallbackJsonWebHandler _signInHandler;
// endpoint functions
void signIn(AsyncWebServerRequest* request, JsonDocument& jsonDocument);
void signIn(AsyncWebServerRequest* request, JsonVariant& json);
void verifyAuthorization(AsyncWebServerRequest* request);
};

View File

@ -6,12 +6,14 @@ ESP8266React::ESP8266React(AsyncWebServer* server, FS* fs) :
_apSettingsService(server, fs, &_securitySettingsService),
_ntpSettingsService(server, fs, &_securitySettingsService),
_otaSettingsService(server, fs, &_securitySettingsService),
_mqttSettingsService(server, fs, &_securitySettingsService),
_restartService(server, &_securitySettingsService),
_authenticationService(server, &_securitySettingsService),
_wifiScanner(server, &_securitySettingsService),
_wifiStatus(server, &_securitySettingsService),
_ntpStatus(server, &_securitySettingsService),
_apStatus(server, &_securitySettingsService),
_mqttStatus(server, &_mqttSettingsService, &_securitySettingsService),
_systemStatus(server, &_securitySettingsService) {
#ifdef PROGMEM_WWW
// Serve static resources from PROGMEM
@ -71,11 +73,12 @@ void ESP8266React::begin() {
_apSettingsService.begin();
_ntpSettingsService.begin();
_otaSettingsService.begin();
_mqttSettingsService.begin();
}
void ESP8266React::loop() {
_wifiSettingsService.loop();
_apSettingsService.loop();
_ntpSettingsService.loop();
_otaSettingsService.loop();
_mqttSettingsService.loop();
}

View File

@ -16,6 +16,8 @@
#include <APSettingsService.h>
#include <APStatus.h>
#include <AuthenticationService.h>
#include <MqttSettingsService.h>
#include <MqttStatus.h>
#include <NTPSettingsService.h>
#include <NTPStatus.h>
#include <OTASettingsService.h>
@ -41,32 +43,41 @@ class ESP8266React {
return &_securitySettingsService;
}
SettingsService<SecuritySettings>* getSecuritySettingsService() {
StatefulService<SecuritySettings>* getSecuritySettingsService() {
return &_securitySettingsService;
}
SettingsService<WiFiSettings>* getWiFiSettingsService() {
StatefulService<WiFiSettings>* getWiFiSettingsService() {
return &_wifiSettingsService;
}
SettingsService<APSettings>* getAPSettingsService() {
StatefulService<APSettings>* getAPSettingsService() {
return &_apSettingsService;
}
SettingsService<NTPSettings>* getNTPSettingsService() {
StatefulService<NTPSettings>* getNTPSettingsService() {
return &_ntpSettingsService;
}
SettingsService<OTASettings>* getOTASettingsService() {
StatefulService<OTASettings>* getOTASettingsService() {
return &_otaSettingsService;
}
StatefulService<MqttSettings>* getMqttSettingsService() {
return &_mqttSettingsService;
}
AsyncMqttClient* getMqttClient() {
return _mqttSettingsService.getMqttClient();
}
private:
SecuritySettingsService _securitySettingsService;
WiFiSettingsService _wifiSettingsService;
APSettingsService _apSettingsService;
NTPSettingsService _ntpSettingsService;
OTASettingsService _otaSettingsService;
MqttSettingsService _mqttSettingsService;
RestartService _restartService;
AuthenticationService _authenticationService;
@ -75,6 +86,7 @@ class ESP8266React {
WiFiStatus _wifiStatus;
NTPStatus _ntpStatus;
APStatus _apStatus;
MqttStatus _mqttStatus;
SystemStatus _systemStatus;
};

View File

@ -0,0 +1,103 @@
#ifndef FSPersistence_h
#define FSPersistence_h
#include <StatefulService.h>
#include <JsonSerializer.h>
#include <JsonDeserializer.h>
#include <FS.h>
#define MAX_FILE_SIZE 1024
template <class T>
class FSPersistence {
public:
FSPersistence(JsonSerializer<T> jsonSerializer,
JsonDeserializer<T> jsonDeserializer,
StatefulService<T>* statefulService,
FS* fs,
char const* filePath) :
_jsonSerializer(jsonSerializer),
_jsonDeserializer(jsonDeserializer),
_statefulService(statefulService),
_fs(fs),
_filePath(filePath) {
enableUpdateHandler();
}
void readFromFS() {
File settingsFile = _fs->open(_filePath, "r");
if (settingsFile) {
if (settingsFile.size() <= MAX_FILE_SIZE) {
DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_FILE_SIZE);
DeserializationError error = deserializeJson(jsonDocument, settingsFile);
if (error == DeserializationError::Ok && jsonDocument.is<JsonObject>()) {
updateSettings(jsonDocument.as<JsonObject>());
settingsFile.close();
return;
}
}
settingsFile.close();
}
// If we reach here we have not been successful in loading the config,
// hard-coded emergency defaults are now applied.
applyDefaults();
}
bool writeToFS() {
// create and populate a new json object
DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_FILE_SIZE);
JsonObject jsonObject = jsonDocument.to<JsonObject>();
_statefulService->read(jsonObject, _jsonSerializer);
// serialize it to filesystem
File settingsFile = _fs->open(_filePath, "w");
// failed to open file, return false
if (!settingsFile) {
return false;
}
// serialize the data to the file
serializeJson(jsonDocument, settingsFile);
settingsFile.close();
return true;
}
void disableUpdateHandler() {
if (_updateHandlerId) {
_statefulService->removeUpdateHandler(_updateHandlerId);
_updateHandlerId = 0;
}
}
void enableUpdateHandler() {
if (!_updateHandlerId) {
_updateHandlerId = _statefulService->addUpdateHandler([&](String originId) { writeToFS(); });
}
}
private:
JsonSerializer<T> _jsonSerializer;
JsonDeserializer<T> _jsonDeserializer;
StatefulService<T>* _statefulService;
FS* _fs;
char const* _filePath;
update_handler_id_t _updateHandlerId = 0;
// update the settings, but do not call propogate
void updateSettings(JsonObject root) {
_statefulService->updateWithoutPropagation(root, _jsonDeserializer);
}
protected:
// We assume the deserializer supplies sensible defaults if an empty object
// is supplied, this virtual function allows that to be changed.
virtual void applyDefaults() {
DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_FILE_SIZE);
updateSettings(jsonDocument.to<JsonObject>());
}
};
#endif // end FSPersistence

View File

@ -0,0 +1,167 @@
#ifndef HttpEndpoint_h
#define HttpEndpoint_h
#include <functional>
#include <AsyncJson.h>
#include <ESPAsyncWebServer.h>
#include <SecurityManager.h>
#include <StatefulService.h>
#include <JsonSerializer.h>
#include <JsonDeserializer.h>
#define MAX_CONTENT_LENGTH 1024
#define HTTP_ENDPOINT_ORIGIN_ID "http"
template <class T>
class HttpGetEndpoint {
public:
HttpGetEndpoint(JsonSerializer<T> jsonSerializer,
StatefulService<T>* statefulService,
AsyncWebServer* server,
const String& servicePath,
SecurityManager* securityManager,
AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) :
_jsonSerializer(jsonSerializer), _statefulService(statefulService) {
server->on(servicePath.c_str(),
HTTP_GET,
securityManager->wrapRequest(std::bind(&HttpGetEndpoint::fetchSettings, this, std::placeholders::_1),
authenticationPredicate));
}
HttpGetEndpoint(JsonSerializer<T> jsonSerializer,
StatefulService<T>* statefulService,
AsyncWebServer* server,
const String& servicePath) :
_jsonSerializer(jsonSerializer), _statefulService(statefulService) {
server->on(servicePath.c_str(), HTTP_GET, std::bind(&HttpGetEndpoint::fetchSettings, this, std::placeholders::_1));
}
protected:
JsonSerializer<T> _jsonSerializer;
StatefulService<T>* _statefulService;
void fetchSettings(AsyncWebServerRequest* request) {
AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_CONTENT_LENGTH);
JsonObject jsonObject = response->getRoot().to<JsonObject>();
_statefulService->read(jsonObject, _jsonSerializer);
response->setLength();
request->send(response);
}
};
template <class T>
class HttpPostEndpoint {
public:
HttpPostEndpoint(JsonSerializer<T> jsonSerializer,
JsonDeserializer<T> jsonDeserializer,
StatefulService<T>* statefulService,
AsyncWebServer* server,
const String& servicePath,
SecurityManager* securityManager,
AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) :
_jsonSerializer(jsonSerializer),
_jsonDeserializer(jsonDeserializer),
_statefulService(statefulService),
_updateHandler(
servicePath,
securityManager->wrapCallback(
std::bind(&HttpPostEndpoint::updateSettings, this, std::placeholders::_1, std::placeholders::_2),
authenticationPredicate)) {
_updateHandler.setMethod(HTTP_POST);
_updateHandler.setMaxContentLength(MAX_CONTENT_LENGTH);
server->addHandler(&_updateHandler);
}
HttpPostEndpoint(JsonSerializer<T> jsonSerializer,
JsonDeserializer<T> jsonDeserializer,
StatefulService<T>* statefulService,
AsyncWebServer* server,
const String& servicePath) :
_jsonSerializer(jsonSerializer),
_jsonDeserializer(jsonDeserializer),
_statefulService(statefulService),
_updateHandler(servicePath,
std::bind(&HttpPostEndpoint::updateSettings, this, std::placeholders::_1, std::placeholders::_2)) {
_updateHandler.setMethod(HTTP_POST);
_updateHandler.setMaxContentLength(MAX_CONTENT_LENGTH);
server->addHandler(&_updateHandler);
}
protected:
JsonSerializer<T> _jsonSerializer;
JsonDeserializer<T> _jsonDeserializer;
StatefulService<T>* _statefulService;
AsyncCallbackJsonWebHandler _updateHandler;
void fetchSettings(AsyncWebServerRequest* request) {
AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_CONTENT_LENGTH);
JsonObject jsonObject = response->getRoot().to<JsonObject>();
_statefulService->read(jsonObject, _jsonSerializer);
response->setLength();
request->send(response);
}
void updateSettings(AsyncWebServerRequest* request, JsonVariant& json) {
if (json.is<JsonObject>()) {
AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_CONTENT_LENGTH);
// use callback to update the settings once the response is complete
request->onDisconnect([this]() { _statefulService->callUpdateHandlers(HTTP_ENDPOINT_ORIGIN_ID); });
// update the settings, deferring the call to the update handlers to when the response is complete
_statefulService->updateWithoutPropagation([&](T& settings) {
JsonObject jsonObject = json.as<JsonObject>();
_jsonDeserializer(jsonObject, settings);
jsonObject = response->getRoot().to<JsonObject>();
_jsonSerializer(settings, jsonObject);
});
// write the response to the client
response->setLength();
request->send(response);
} else {
request->send(400);
}
}
};
template <class T>
class HttpEndpoint : public HttpGetEndpoint<T>, public HttpPostEndpoint<T> {
public:
HttpEndpoint(JsonSerializer<T> jsonSerializer,
JsonDeserializer<T> jsonDeserializer,
StatefulService<T>* statefulService,
AsyncWebServer* server,
const String& servicePath,
SecurityManager* securityManager,
AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) :
HttpGetEndpoint<T>(jsonSerializer,
statefulService,
server,
servicePath,
securityManager,
authenticationPredicate),
HttpPostEndpoint<T>(jsonSerializer,
jsonDeserializer,
statefulService,
server,
servicePath,
securityManager,
authenticationPredicate) {
}
HttpEndpoint(JsonSerializer<T> jsonSerializer,
JsonDeserializer<T> jsonDeserializer,
StatefulService<T>* statefulService,
AsyncWebServer* server,
const String& servicePath) :
HttpGetEndpoint<T>(jsonSerializer, statefulService, server, servicePath),
HttpPostEndpoint<T>(jsonSerializer, jsonDeserializer, statefulService, server, servicePath) {
}
};
#endif // end HttpEndpoint

View File

@ -0,0 +1,9 @@
#ifndef JsonDeserializer_h
#define JsonDeserializer_h
#include <ArduinoJson.h>
template <class T>
using JsonDeserializer = void (*)(JsonObject& root, T& settings);
#endif // end JsonDeserializer

View File

@ -0,0 +1,9 @@
#ifndef JsonSerializer_h
#define JsonSerializer_h
#include <ArduinoJson.h>
template <class T>
using JsonSerializer = void (*)(T& settings, JsonObject& root);
#endif // end JsonSerializer

17
lib/framework/JsonUtils.h Normal file
View File

@ -0,0 +1,17 @@
#include <Arduino.h>
#include <IPAddress.h>
#include <ArduinoJson.h>
class JsonUtils {
public:
static void readIP(JsonObject& root, String key, IPAddress& _ip) {
if (!root[key].is<String>() || !_ip.fromString(root[key].as<String>())) {
_ip = INADDR_NONE;
}
}
static void writeIP(JsonObject& root, String key, IPAddress& _ip) {
if (_ip != INADDR_NONE) {
root[key] = _ip.toString();
}
}
};

161
lib/framework/MqttPubSub.h Normal file
View File

@ -0,0 +1,161 @@
#ifndef MqttPubSub_h
#define MqttPubSub_h
#include <StatefulService.h>
#include <JsonSerializer.h>
#include <JsonDeserializer.h>
#include <AsyncMqttClient.h>
#define MAX_MESSAGE_SIZE 1024
#define MQTT_ORIGIN_ID "mqtt"
template <class T>
class MqttConnector {
protected:
StatefulService<T>* _statefulService;
AsyncMqttClient* _mqttClient;
MqttConnector(StatefulService<T>* statefulService, AsyncMqttClient* mqttClient) :
_statefulService(statefulService), _mqttClient(mqttClient) {
_mqttClient->onConnect(std::bind(&MqttConnector::onConnect, this));
}
virtual void onConnect() = 0;
};
template <class T>
class MqttPub : virtual public MqttConnector<T> {
public:
MqttPub(JsonSerializer<T> jsonSerializer,
StatefulService<T>* statefulService,
AsyncMqttClient* mqttClient,
String pubTopic = "") :
MqttConnector<T>(statefulService, mqttClient), _jsonSerializer(jsonSerializer), _pubTopic(pubTopic) {
MqttConnector<T>::_statefulService->addUpdateHandler([&](String originId) { publish(); }, false);
}
void setPubTopic(String pubTopic) {
_pubTopic = pubTopic;
publish();
}
protected:
virtual void onConnect() {
publish();
}
private:
JsonSerializer<T> _jsonSerializer;
String _pubTopic;
void publish() {
if (_pubTopic.length() > 0 && MqttConnector<T>::_mqttClient->connected()) {
// serialize to json doc
DynamicJsonDocument json(MAX_MESSAGE_SIZE);
JsonObject jsonObject = json.to<JsonObject>();
MqttConnector<T>::_statefulService->read(jsonObject, _jsonSerializer);
// serialize to string
String payload;
serializeJson(json, payload);
// publish the payload
MqttConnector<T>::_mqttClient->publish(_pubTopic.c_str(), 0, false, payload.c_str());
}
}
};
template <class T>
class MqttSub : virtual public MqttConnector<T> {
public:
MqttSub(JsonDeserializer<T> jsonDeserializer,
StatefulService<T>* statefulService,
AsyncMqttClient* mqttClient,
String subTopic = "") :
MqttConnector<T>(statefulService, mqttClient), _jsonDeserializer(jsonDeserializer), _subTopic(subTopic) {
MqttConnector<T>::_mqttClient->onMessage(std::bind(&MqttSub::onMqttMessage,
this,
std::placeholders::_1,
std::placeholders::_2,
std::placeholders::_3,
std::placeholders::_4,
std::placeholders::_5,
std::placeholders::_6));
}
void setSubTopic(String subTopic) {
if (!_subTopic.equals(subTopic)) {
// unsubscribe from the existing topic if one was set
if (_subTopic.length() > 0) {
MqttConnector<T>::_mqttClient->unsubscribe(_subTopic.c_str());
}
// set the new topic and re-configure the subscription
_subTopic = subTopic;
subscribe();
}
}
protected:
virtual void onConnect() {
subscribe();
}
private:
JsonDeserializer<T> _jsonDeserializer;
String _subTopic;
void subscribe() {
if (_subTopic.length() > 0) {
MqttConnector<T>::_mqttClient->subscribe(_subTopic.c_str(), 2);
}
}
void onMqttMessage(char* topic,
char* payload,
AsyncMqttClientMessageProperties properties,
size_t len,
size_t index,
size_t total) {
// we only care about the topic we are watching in this class
if (strcmp(_subTopic.c_str(), topic)) {
return;
}
// deserialize from string
DynamicJsonDocument json(MAX_MESSAGE_SIZE);
DeserializationError error = deserializeJson(json, payload, len);
if (!error && json.is<JsonObject>()) {
JsonObject jsonObject = json.as<JsonObject>();
MqttConnector<T>::_statefulService->update(jsonObject, _jsonDeserializer, MQTT_ORIGIN_ID);
}
}
};
template <class T>
class MqttPubSub : public MqttPub<T>, public MqttSub<T> {
public:
MqttPubSub(JsonSerializer<T> jsonSerializer,
JsonDeserializer<T> jsonDeserializer,
StatefulService<T>* statefulService,
AsyncMqttClient* mqttClient,
String pubTopic = "",
String subTopic = "") :
MqttConnector<T>(statefulService, mqttClient),
MqttPub<T>(jsonSerializer, statefulService, mqttClient, pubTopic = ""),
MqttSub<T>(jsonDeserializer, statefulService, mqttClient, subTopic = "") {
}
public:
void configureTopics(String pubTopic, String subTopic) {
MqttSub<T>::setSubTopic(subTopic);
MqttPub<T>::setPubTopic(pubTopic);
}
protected:
void onConnect() {
MqttSub<T>::onConnect();
MqttPub<T>::onConnect();
}
};
#endif // end MqttPubSub

View File

@ -0,0 +1,155 @@
#include <MqttSettingsService.h>
/**
* Retains a copy of the cstr provided in the pointer provided using dynamic allocation.
*
* Frees the pointer before allocation and leaves it as nullptr if cstr == nullptr.
*/
static char* retainCstr(const char* cstr, char** ptr) {
// free up previously retained value if exists
free(*ptr);
*ptr = nullptr;
// dynamically allocate and copy cstr (if non null)
if (cstr != nullptr) {
*ptr = (char*)malloc(strlen(cstr) + 1);
strcpy(*ptr, cstr);
}
// return reference to pointer for convenience
return *ptr;
}
MqttSettingsService::MqttSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) :
_httpEndpoint(MqttSettings::serialize,
MqttSettings::deserialize,
this,
server,
MQTT_SETTINGS_SERVICE_PATH,
securityManager),
_fsPersistence(MqttSettings::serialize, MqttSettings::deserialize, this, fs, MQTT_SETTINGS_FILE) {
#ifdef ESP32
WiFi.onEvent(
std::bind(&MqttSettingsService::onStationModeDisconnected, this, std::placeholders::_1, std::placeholders::_2),
WiFiEvent_t::SYSTEM_EVENT_STA_DISCONNECTED);
WiFi.onEvent(std::bind(&MqttSettingsService::onStationModeGotIP, this, std::placeholders::_1, std::placeholders::_2),
WiFiEvent_t::SYSTEM_EVENT_STA_GOT_IP);
#elif defined(ESP8266)
_onStationModeDisconnectedHandler = WiFi.onStationModeDisconnected(
std::bind(&MqttSettingsService::onStationModeDisconnected, this, std::placeholders::_1));
_onStationModeGotIPHandler =
WiFi.onStationModeGotIP(std::bind(&MqttSettingsService::onStationModeGotIP, this, std::placeholders::_1));
#endif
_mqttClient.onConnect(std::bind(&MqttSettingsService::onMqttConnect, this, std::placeholders::_1));
_mqttClient.onDisconnect(std::bind(&MqttSettingsService::onMqttDisconnect, this, std::placeholders::_1));
addUpdateHandler([&](String originId) { onConfigUpdated(); }, false);
}
MqttSettingsService::~MqttSettingsService() {
}
void MqttSettingsService::begin() {
_fsPersistence.readFromFS();
}
void MqttSettingsService::loop() {
if (_reconfigureMqtt || (_disconnectedAt && (unsigned long)(millis() - _disconnectedAt) >= MQTT_RECONNECTION_DELAY)) {
// reconfigure MQTT client
configureMqtt();
// clear the reconnection flags
_reconfigureMqtt = false;
_disconnectedAt = 0;
}
}
bool MqttSettingsService::isEnabled() {
return _state.enabled;
}
bool MqttSettingsService::isConnected() {
return _mqttClient.connected();
}
const char* MqttSettingsService::getClientId() {
return _mqttClient.getClientId();
}
AsyncMqttClientDisconnectReason MqttSettingsService::getDisconnectReason() {
return _disconnectReason;
}
AsyncMqttClient* MqttSettingsService::getMqttClient() {
return &_mqttClient;
}
void MqttSettingsService::onMqttConnect(bool sessionPresent) {
Serial.print("Connected to MQTT, ");
Serial.print(sessionPresent ? "with" : "without");
Serial.println(" persistent session");
}
void MqttSettingsService::onMqttDisconnect(AsyncMqttClientDisconnectReason reason) {
Serial.print("Disconnected from MQTT reason: ");
Serial.println((uint8_t)reason);
_disconnectReason = reason;
_disconnectedAt = millis();
}
void MqttSettingsService::onConfigUpdated() {
_reconfigureMqtt = true;
_disconnectedAt = 0;
}
#ifdef ESP32
void MqttSettingsService::onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info) {
if (_state.enabled) {
Serial.println("WiFi connection dropped, starting MQTT client.");
onConfigUpdated();
}
}
void MqttSettingsService::onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info) {
if (_state.enabled) {
Serial.println("WiFi connection dropped, stopping MQTT client.");
onConfigUpdated();
}
}
#elif defined(ESP8266)
void MqttSettingsService::onStationModeGotIP(const WiFiEventStationModeGotIP& event) {
if (_state.enabled) {
Serial.println("WiFi connection dropped, starting MQTT client.");
onConfigUpdated();
}
}
void MqttSettingsService::onStationModeDisconnected(const WiFiEventStationModeDisconnected& event) {
if (_state.enabled) {
Serial.println("WiFi connection dropped, stopping MQTT client.");
onConfigUpdated();
}
}
#endif
void MqttSettingsService::configureMqtt() {
// disconnect if currently connected
_mqttClient.disconnect();
// only connect if WiFi is connected and MQTT is enabled
if (_state.enabled && WiFi.isConnected()) {
Serial.println("Connecting to MQTT...");
_mqttClient.setServer(retainCstr(_state.host.c_str(), &_retainedHost), _state.port);
if (_state.username.length() > 0) {
_mqttClient.setCredentials(
retainCstr(_state.username.c_str(), &_retainedUsername),
retainCstr(_state.password.length() > 0 ? _state.password.c_str() : nullptr, &_retainedPassword));
} else {
_mqttClient.setCredentials(retainCstr(nullptr, &_retainedUsername), retainCstr(nullptr, &_retainedPassword));
}
_mqttClient.setClientId(retainCstr(_state.clientId.c_str(), &_retainedClientId));
_mqttClient.setKeepAlive(_state.keepAlive);
_mqttClient.setCleanSession(_state.cleanSession);
_mqttClient.setMaxTopicLength(_state.maxTopicLength);
_mqttClient.connect();
}
}

View File

@ -0,0 +1,125 @@
#ifndef MqttSettingsService_h
#define MqttSettingsService_h
#include <StatefulService.h>
#include <HttpEndpoint.h>
#include <FSPersistence.h>
#include <AsyncMqttClient.h>
#define MQTT_RECONNECTION_DELAY 5000
#define MQTT_SETTINGS_FILE "/config/mqttSettings.json"
#define MQTT_SETTINGS_SERVICE_PATH "/rest/mqttSettings"
#define MQTT_SETTINGS_SERVICE_DEFAULT_ENABLED false
#define MQTT_SETTINGS_SERVICE_DEFAULT_HOST "test.mosquitto.org"
#define MQTT_SETTINGS_SERVICE_DEFAULT_PORT 1883
#define MQTT_SETTINGS_SERVICE_DEFAULT_USERNAME ""
#define MQTT_SETTINGS_SERVICE_DEFAULT_PASSWORD ""
#define MQTT_SETTINGS_SERVICE_DEFAULT_CLIENT_ID generateClientId()
#define MQTT_SETTINGS_SERVICE_DEFAULT_KEEP_ALIVE 16
#define MQTT_SETTINGS_SERVICE_DEFAULT_CLEAN_SESSION true
#define MQTT_SETTINGS_SERVICE_DEFAULT_MAX_TOPIC_LENGTH 128
static String generateClientId() {
#ifdef ESP32
return "esp32-" + String((unsigned long)ESP.getEfuseMac(), HEX);
#elif defined(ESP8266)
return "esp8266-" + String(ESP.getChipId(), HEX);
#endif
}
class MqttSettings {
public:
// host and port - if enabled
bool enabled;
String host;
uint16_t port;
// username and password
String username;
String password;
// client id settings
String clientId;
// connection settings
uint16_t keepAlive;
bool cleanSession;
uint16_t maxTopicLength;
static void serialize(MqttSettings& settings, JsonObject& root) {
root["enabled"] = settings.enabled;
root["host"] = settings.host;
root["port"] = settings.port;
root["username"] = settings.username;
root["password"] = settings.password;
root["client_id"] = settings.clientId;
root["keep_alive"] = settings.keepAlive;
root["clean_session"] = settings.cleanSession;
root["max_topic_length"] = settings.maxTopicLength;
}
static void deserialize(JsonObject& root, MqttSettings& settings) {
settings.enabled = root["enabled"] | MQTT_SETTINGS_SERVICE_DEFAULT_ENABLED;
settings.host = root["host"] | MQTT_SETTINGS_SERVICE_DEFAULT_HOST;
settings.port = root["port"] | MQTT_SETTINGS_SERVICE_DEFAULT_PORT;
settings.username = root["username"] | MQTT_SETTINGS_SERVICE_DEFAULT_USERNAME;
settings.password = root["password"] | MQTT_SETTINGS_SERVICE_DEFAULT_PASSWORD;
settings.clientId = root["client_id"] | MQTT_SETTINGS_SERVICE_DEFAULT_CLIENT_ID;
settings.keepAlive = root["keep_alive"] | MQTT_SETTINGS_SERVICE_DEFAULT_KEEP_ALIVE;
settings.cleanSession = root["clean_session"] | MQTT_SETTINGS_SERVICE_DEFAULT_CLEAN_SESSION;
settings.maxTopicLength = root["max_topic_length"] | MQTT_SETTINGS_SERVICE_DEFAULT_MAX_TOPIC_LENGTH;
}
};
class MqttSettingsService : public StatefulService<MqttSettings> {
public:
MqttSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager);
~MqttSettingsService();
void begin();
void loop();
bool isEnabled();
bool isConnected();
const char* getClientId();
AsyncMqttClientDisconnectReason getDisconnectReason();
AsyncMqttClient* getMqttClient();
protected:
void onConfigUpdated();
private:
HttpEndpoint<MqttSettings> _httpEndpoint;
FSPersistence<MqttSettings> _fsPersistence;
// Pointers to hold retained copies of the mqtt client connection strings.
// Required as AsyncMqttClient holds refrences to the supplied connection strings.
char* _retainedHost = nullptr;
char* _retainedClientId = nullptr;
char* _retainedUsername = nullptr;
char* _retainedPassword = nullptr;
AsyncMqttClient _mqttClient;
bool _reconfigureMqtt;
unsigned long _disconnectedAt;
// connection status
AsyncMqttClientDisconnectReason _disconnectReason;
#ifdef ESP32
void onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info);
void onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info);
#elif defined(ESP8266)
WiFiEventHandler _onStationModeDisconnectedHandler;
WiFiEventHandler _onStationModeGotIPHandler;
void onStationModeGotIP(const WiFiEventStationModeGotIP& event);
void onStationModeDisconnected(const WiFiEventStationModeDisconnected& event);
#endif
void onMqttConnect(bool sessionPresent);
void onMqttDisconnect(AsyncMqttClientDisconnectReason reason);
void configureMqtt();
};
#endif // end MqttSettingsService_h

View File

@ -0,0 +1,24 @@
#include <MqttStatus.h>
MqttStatus::MqttStatus(AsyncWebServer* server,
MqttSettingsService* mqttSettingsService,
SecurityManager* securityManager) :
_mqttSettingsService(mqttSettingsService) {
server->on(MQTT_STATUS_SERVICE_PATH,
HTTP_GET,
securityManager->wrapRequest(std::bind(&MqttStatus::mqttStatus, this, std::placeholders::_1),
AuthenticationPredicates::IS_AUTHENTICATED));
}
void MqttStatus::mqttStatus(AsyncWebServerRequest* request) {
AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_MQTT_STATUS_SIZE);
JsonObject root = response->getRoot();
root["enabled"] = _mqttSettingsService->isEnabled();
root["connected"] = _mqttSettingsService->isConnected();
root["client_id"] = _mqttSettingsService->getClientId();
root["disconnect_reason"] = (uint8_t)_mqttSettingsService->getDisconnectReason();
response->setLength();
request->send(response);
}

View File

@ -0,0 +1,31 @@
#ifndef MqttStatus_h
#define MqttStatus_h
#ifdef ESP32
#include <WiFi.h>
#include <AsyncTCP.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#endif
#include <MqttSettingsService.h>
#include <ArduinoJson.h>
#include <AsyncJson.h>
#include <ESPAsyncWebServer.h>
#include <SecurityManager.h>
#define MAX_MQTT_STATUS_SIZE 1024
#define MQTT_STATUS_SERVICE_PATH "/rest/mqttStatus"
class MqttStatus {
public:
MqttStatus(AsyncWebServer* server, MqttSettingsService* mqttSettingsService, SecurityManager* securityManager);
private:
MqttSettingsService* _mqttSettingsService;
void mqttStatus(AsyncWebServerRequest* request);
};
#endif // end MqttStatus_h

View File

@ -1,7 +1,13 @@
#include <NTPSettingsService.h>
NTPSettingsService::NTPSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) :
AdminSettingsService(server, fs, securityManager, NTP_SETTINGS_SERVICE_PATH, NTP_SETTINGS_FILE) {
_httpEndpoint(NTPSettings::serialize,
NTPSettings::deserialize,
this,
server,
NTP_SETTINGS_SERVICE_PATH,
securityManager),
_fsPersistence(NTPSettings::serialize, NTPSettings::deserialize, this, fs, NTP_SETTINGS_FILE) {
#ifdef ESP32
WiFi.onEvent(
std::bind(&NTPSettingsService::onStationModeDisconnected, this, std::placeholders::_1, std::placeholders::_2),
@ -14,68 +20,43 @@ NTPSettingsService::NTPSettingsService(AsyncWebServer* server, FS* fs, SecurityM
_onStationModeGotIPHandler =
WiFi.onStationModeGotIP(std::bind(&NTPSettingsService::onStationModeGotIP, this, std::placeholders::_1));
#endif
addUpdateHandler([&](String originId) { configureNTP(); }, false);
}
NTPSettingsService::~NTPSettingsService() {
}
void NTPSettingsService::loop() {
// detect when we need to re-configure NTP and do it in the main loop
if (_reconfigureNTP) {
_reconfigureNTP = false;
configureNTP();
}
}
void NTPSettingsService::readFromJsonObject(JsonObject& root) {
_settings.enabled = root["enabled"] | NTP_SETTINGS_SERVICE_DEFAULT_ENABLED;
_settings.server = root["server"] | NTP_SETTINGS_SERVICE_DEFAULT_SERVER;
_settings.tzLabel = root["tz_label"] | NTP_SETTINGS_SERVICE_DEFAULT_TIME_ZONE_LABEL;
_settings.tzFormat = root["tz_format"] | NTP_SETTINGS_SERVICE_DEFAULT_TIME_ZONE_FORMAT;
}
void NTPSettingsService::writeToJsonObject(JsonObject& root) {
root["enabled"] = _settings.enabled;
root["server"] = _settings.server;
root["tz_label"] = _settings.tzLabel;
root["tz_format"] = _settings.tzFormat;
}
void NTPSettingsService::onConfigUpdated() {
_reconfigureNTP = true;
void NTPSettingsService::begin() {
_fsPersistence.readFromFS();
configureNTP();
}
#ifdef ESP32
void NTPSettingsService::onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info) {
Serial.println("Got IP address, starting NTP Synchronization");
_reconfigureNTP = true;
configureNTP();
}
void NTPSettingsService::onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info) {
Serial.println("WiFi connection dropped, stopping NTP.");
_reconfigureNTP = false;
sntp_stop();
configureNTP();
}
#elif defined(ESP8266)
void NTPSettingsService::onStationModeGotIP(const WiFiEventStationModeGotIP& event) {
Serial.println("Got IP address, starting NTP Synchronization");
_reconfigureNTP = true;
configureNTP();
}
void NTPSettingsService::onStationModeDisconnected(const WiFiEventStationModeDisconnected& event) {
Serial.println("WiFi connection dropped, stopping NTP.");
_reconfigureNTP = false;
sntp_stop();
configureNTP();
}
#endif
void NTPSettingsService::configureNTP() {
Serial.println("Configuring NTP...");
if (_settings.enabled) {
if (WiFi.isConnected() && _state.enabled) {
Serial.println("Starting NTP...");
#ifdef ESP32
configTzTime(_settings.tzFormat.c_str(), _settings.server.c_str());
configTzTime(_state.tzFormat.c_str(), _state.server.c_str());
#elif defined(ESP8266)
configTime(_settings.tzFormat.c_str(), _settings.server.c_str());
configTime(_state.tzFormat.c_str(), _state.server.c_str());
#endif
} else {
sntp_stop();

View File

@ -1,7 +1,8 @@
#ifndef NTPSettingsService_h
#define NTPSettingsService_h
#include <AdminSettingsService.h>
#include <HttpEndpoint.h>
#include <FSPersistence.h>
#include <time.h>
#ifdef ESP32
@ -16,10 +17,6 @@
#define NTP_SETTINGS_SERVICE_DEFAULT_TIME_ZONE_FORMAT "GMT0BST,M3.5.0/1,M10.5.0"
#define NTP_SETTINGS_SERVICE_DEFAULT_SERVER "time.google.com"
// min poll delay of 60 secs, max 1 day
#define NTP_SETTINGS_MIN_INTERVAL 60
#define NTP_SETTINGS_MAX_INTERVAL 86400
#define NTP_SETTINGS_FILE "/config/ntpSettings.json"
#define NTP_SETTINGS_SERVICE_PATH "/rest/ntpSettings"
@ -29,22 +26,31 @@ class NTPSettings {
String tzLabel;
String tzFormat;
String server;
static void serialize(NTPSettings& settings, JsonObject& root) {
root["enabled"] = settings.enabled;
root["server"] = settings.server;
root["tz_label"] = settings.tzLabel;
root["tz_format"] = settings.tzFormat;
}
static void deserialize(JsonObject& root, NTPSettings& settings) {
settings.enabled = root["enabled"] | NTP_SETTINGS_SERVICE_DEFAULT_ENABLED;
settings.server = root["server"] | NTP_SETTINGS_SERVICE_DEFAULT_SERVER;
settings.tzLabel = root["tz_label"] | NTP_SETTINGS_SERVICE_DEFAULT_TIME_ZONE_LABEL;
settings.tzFormat = root["tz_format"] | NTP_SETTINGS_SERVICE_DEFAULT_TIME_ZONE_FORMAT;
}
};
class NTPSettingsService : public AdminSettingsService<NTPSettings> {
class NTPSettingsService : public StatefulService<NTPSettings> {
public:
NTPSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager);
~NTPSettingsService();
void loop();
protected:
void readFromJsonObject(JsonObject& root);
void writeToJsonObject(JsonObject& root);
void onConfigUpdated();
void begin();
private:
bool _reconfigureNTP = false;
HttpEndpoint<NTPSettings> _httpEndpoint;
FSPersistence<NTPSettings> _fsPersistence;
#ifdef ESP32
void onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info);
@ -56,7 +62,6 @@ class NTPSettingsService : public AdminSettingsService<NTPSettings> {
void onStationModeGotIP(const WiFiEventStationModeGotIP& event);
void onStationModeDisconnected(const WiFiEventStationModeDisconnected& event);
#endif
void configureNTP();
};

View File

@ -1,7 +1,13 @@
#include <OTASettingsService.h>
OTASettingsService::OTASettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) :
AdminSettingsService(server, fs, securityManager, OTA_SETTINGS_SERVICE_PATH, OTA_SETTINGS_FILE) {
_httpEndpoint(OTASettings::serialize,
OTASettings::deserialize,
this,
server,
OTA_SETTINGS_SERVICE_PATH,
securityManager),
_fsPersistence(OTASettings::serialize, OTASettings::deserialize, this, fs, OTA_SETTINGS_FILE) {
#ifdef ESP32
WiFi.onEvent(std::bind(&OTASettingsService::onStationModeGotIP, this, std::placeholders::_1, std::placeholders::_2),
WiFiEvent_t::SYSTEM_EVENT_STA_GOT_IP);
@ -9,31 +15,18 @@ OTASettingsService::OTASettingsService(AsyncWebServer* server, FS* fs, SecurityM
_onStationModeGotIPHandler =
WiFi.onStationModeGotIP(std::bind(&OTASettingsService::onStationModeGotIP, this, std::placeholders::_1));
#endif
addUpdateHandler([&](String originId) { configureArduinoOTA(); }, false);
}
OTASettingsService::~OTASettingsService() {
}
void OTASettingsService::loop() {
if ( _settings.enabled && _arduinoOTA) {
_arduinoOTA->handle();
}
}
void OTASettingsService::onConfigUpdated() {
void OTASettingsService::begin() {
_fsPersistence.readFromFS();
configureArduinoOTA();
}
void OTASettingsService::readFromJsonObject(JsonObject& root) {
_settings.enabled = root["enabled"] | DEFAULT_OTA_ENABLED;
_settings.port = root["port"] | DEFAULT_OTA_PORT;
_settings.password = root["password"] | DEFAULT_OTA_PASSWORD;
}
void OTASettingsService::writeToJsonObject(JsonObject& root) {
root["enabled"] = _settings.enabled;
root["port"] = _settings.port;
root["password"] = _settings.password;
void OTASettingsService::loop() {
if (_state.enabled && _arduinoOTA) {
_arduinoOTA->handle();
}
}
void OTASettingsService::configureArduinoOTA() {
@ -44,11 +37,11 @@ void OTASettingsService::configureArduinoOTA() {
delete _arduinoOTA;
_arduinoOTA = nullptr;
}
if (_settings.enabled) {
Serial.println("Starting OTA Update Service");
if (_state.enabled) {
Serial.println("Starting OTA Update Service...");
_arduinoOTA = new ArduinoOTAClass;
_arduinoOTA->setPort(_settings.port);
_arduinoOTA->setPassword(_settings.password.c_str());
_arduinoOTA->setPort(_state.port);
_arduinoOTA->setPassword(_state.password.c_str());
_arduinoOTA->onStart([]() { Serial.println("Starting"); });
_arduinoOTA->onEnd([]() { Serial.println("\nEnd"); });
_arduinoOTA->onProgress([](unsigned int progress, unsigned int total) {
@ -70,6 +63,7 @@ void OTASettingsService::configureArduinoOTA() {
_arduinoOTA->begin();
}
}
#ifdef ESP32
void OTASettingsService::onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info) {
configureArduinoOTA();

View File

@ -1,7 +1,8 @@
#ifndef OTASettingsService_h
#define OTASettingsService_h
#include <AdminSettingsService.h>
#include <HttpEndpoint.h>
#include <FSPersistence.h>
#ifdef ESP32
#include <ESPmDNS.h>
@ -25,21 +26,30 @@ class OTASettings {
bool enabled;
int port;
String password;
static void serialize(OTASettings& settings, JsonObject& root) {
root["enabled"] = settings.enabled;
root["port"] = settings.port;
root["password"] = settings.password;
}
static void deserialize(JsonObject& root, OTASettings& settings) {
settings.enabled = root["enabled"] | DEFAULT_OTA_ENABLED;
settings.port = root["port"] | DEFAULT_OTA_PORT;
settings.password = root["password"] | DEFAULT_OTA_PASSWORD;
}
};
class OTASettingsService : public AdminSettingsService<OTASettings> {
class OTASettingsService : public StatefulService<OTASettings> {
public:
OTASettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager);
~OTASettingsService();
void begin();
void loop();
protected:
void onConfigUpdated();
void readFromJsonObject(JsonObject& root);
void writeToJsonObject(JsonObject& root);
private:
HttpEndpoint<OTASettings> _httpEndpoint;
FSPersistence<OTASettings> _fsPersistence;
ArduinoOTAClass* _arduinoOTA;
void configureArduinoOTA();

View File

@ -3,10 +3,13 @@
#include <ArduinoJsonJWT.h>
#include <ESPAsyncWebServer.h>
#include <AsyncJson.h>
#include <list>
#define DEFAULT_JWT_SECRET "esp8266-react"
#define ACCESS_TOKEN_PARAMATER "access_token"
#define AUTHORIZATION_HEADER "Authorization"
#define AUTHORIZATION_HEADER_PREFIX "Bearer "
#define AUTHORIZATION_HEADER_PREFIX_LEN 7
@ -59,7 +62,7 @@ class SecurityManager {
/*
* Authenticate, returning the user if found
*/
virtual Authentication authenticate(String username, String password) = 0;
virtual Authentication authenticate(String& username, String& password) = 0;
/*
* Check the request header for the Authorization token
@ -71,11 +74,22 @@ class SecurityManager {
*/
virtual String generateJWT(User* user) = 0;
/**
* Filter a request with the provided predicate, only returning true if the predicate matches.
*/
virtual ArRequestFilterFunction filterRequest(AuthenticationPredicate predicate) = 0;
/**
* Wrap the provided request to provide validation against an AuthenticationPredicate.
*/
virtual ArRequestHandlerFunction wrapRequest(ArRequestHandlerFunction onRequest,
AuthenticationPredicate predicate) = 0;
/**
* Wrap the provided json request callback to provide validation against an AuthenticationPredicate.
*/
virtual ArJsonRequestHandlerFunction wrapCallback(ArJsonRequestHandlerFunction callback,
AuthenticationPredicate predicate) = 0;
};
#endif // end SecurityManager_h

View File

@ -1,62 +1,48 @@
#include <SecuritySettingsService.h>
SecuritySettingsService::SecuritySettingsService(AsyncWebServer* server, FS* fs) :
AdminSettingsService(server, fs, this, SECURITY_SETTINGS_PATH, SECURITY_SETTINGS_FILE),
SecurityManager() {
}
SecuritySettingsService::~SecuritySettingsService() {
_httpEndpoint(SecuritySettings::serialize,
SecuritySettings::deserialize,
this,
server,
SECURITY_SETTINGS_PATH,
this),
_fsPersistence(SecuritySettings::serialize, SecuritySettings::deserialize, this, fs, SECURITY_SETTINGS_FILE) {
addUpdateHandler([&](String originId) { configureJWTHandler(); }, false);
}
void SecuritySettingsService::readFromJsonObject(JsonObject& root) {
// secret
_jwtHandler.setSecret(root["jwt_secret"] | DEFAULT_JWT_SECRET);
// users
_settings.users.clear();
if (root["users"].is<JsonArray>()) {
for (JsonVariant user : root["users"].as<JsonArray>()) {
_settings.users.push_back(User(user["username"], user["password"], user["admin"]));
}
} else {
_settings.users.push_back(User(DEFAULT_ADMIN_USERNAME, DEFAULT_ADMIN_USERNAME, true));
_settings.users.push_back(User(DEFAULT_GUEST_USERNAME, DEFAULT_GUEST_USERNAME, false));
}
void SecuritySettingsService::begin() {
_fsPersistence.readFromFS();
configureJWTHandler();
}
void SecuritySettingsService::writeToJsonObject(JsonObject& root) {
// secret
root["jwt_secret"] = _jwtHandler.getSecret();
// users
JsonArray users = root.createNestedArray("users");
for (User _user : _settings.users) {
JsonObject user = users.createNestedObject();
user["username"] = _user.username;
user["password"] = _user.password;
user["admin"] = _user.admin;
}
}
Authentication SecuritySettingsService::authenticateRequest(AsyncWebServerRequest *request) {
AsyncWebHeader *authorizationHeader = request->getHeader(AUTHORIZATION_HEADER);
Authentication SecuritySettingsService::authenticateRequest(AsyncWebServerRequest* request) {
AsyncWebHeader* authorizationHeader = request->getHeader(AUTHORIZATION_HEADER);
if (authorizationHeader) {
String value = authorizationHeader->value();
if (value.startsWith(AUTHORIZATION_HEADER_PREFIX)) {
value = value.substring(AUTHORIZATION_HEADER_PREFIX_LEN);
return authenticateJWT(value);
}
} else if (request->hasParam(ACCESS_TOKEN_PARAMATER)) {
AsyncWebParameter* tokenParamater = request->getParam(ACCESS_TOKEN_PARAMATER);
String value = tokenParamater->value();
return authenticateJWT(value);
}
return Authentication();
}
Authentication SecuritySettingsService::authenticateJWT(String jwt) {
void SecuritySettingsService::configureJWTHandler() {
_jwtHandler.setSecret(_state.jwtSecret);
}
Authentication SecuritySettingsService::authenticateJWT(String& jwt) {
DynamicJsonDocument payloadDocument(MAX_JWT_SIZE);
_jwtHandler.parseJWT(jwt, payloadDocument);
if (payloadDocument.is<JsonObject>()) {
JsonObject parsedPayload = payloadDocument.as<JsonObject>();
String username = parsedPayload["username"];
for (User _user : _settings.users) {
for (User _user : _state.users) {
if (_user.username == username && validatePayload(parsedPayload, &_user)) {
return Authentication(_user);
}
@ -65,8 +51,8 @@ Authentication SecuritySettingsService::authenticateJWT(String jwt) {
return Authentication();
}
Authentication SecuritySettingsService::authenticate(String username, String password) {
for (User _user : _settings.users) {
Authentication SecuritySettingsService::authenticate(String& username, String& password) {
for (User _user : _state.users) {
if (_user.username == username && _user.password == password) {
return Authentication(_user);
}
@ -74,28 +60,35 @@ Authentication SecuritySettingsService::authenticate(String username, String pas
return Authentication();
}
inline void populateJWTPayload(JsonObject &payload, User *user) {
inline void populateJWTPayload(JsonObject& payload, User* user) {
payload["username"] = user->username;
payload["admin"] = user->admin;
}
boolean SecuritySettingsService::validatePayload(JsonObject &parsedPayload, User *user) {
DynamicJsonDocument _jsonDocument(MAX_JWT_SIZE);
JsonObject payload = _jsonDocument.to<JsonObject>();
boolean SecuritySettingsService::validatePayload(JsonObject& parsedPayload, User* user) {
DynamicJsonDocument jsonDocument(MAX_JWT_SIZE);
JsonObject payload = jsonDocument.to<JsonObject>();
populateJWTPayload(payload, user);
return payload == parsedPayload;
}
String SecuritySettingsService::generateJWT(User *user) {
DynamicJsonDocument _jsonDocument(MAX_JWT_SIZE);
JsonObject payload = _jsonDocument.to<JsonObject>();
String SecuritySettingsService::generateJWT(User* user) {
DynamicJsonDocument jsonDocument(MAX_JWT_SIZE);
JsonObject payload = jsonDocument.to<JsonObject>();
populateJWTPayload(payload, user);
return _jwtHandler.buildJWT(payload);
}
ArRequestFilterFunction SecuritySettingsService::filterRequest(AuthenticationPredicate predicate) {
return [this, predicate](AsyncWebServerRequest* request) {
Authentication authentication = authenticateRequest(request);
return predicate(authentication);
};
}
ArRequestHandlerFunction SecuritySettingsService::wrapRequest(ArRequestHandlerFunction onRequest,
AuthenticationPredicate predicate) {
return [this, onRequest, predicate](AsyncWebServerRequest *request) {
AuthenticationPredicate predicate) {
return [this, onRequest, predicate](AsyncWebServerRequest* request) {
Authentication authentication = authenticateRequest(request);
if (!predicate(authentication)) {
request->send(401);
@ -104,3 +97,15 @@ ArRequestHandlerFunction SecuritySettingsService::wrapRequest(ArRequestHandlerFu
onRequest(request);
};
}
ArJsonRequestHandlerFunction SecuritySettingsService::wrapCallback(ArJsonRequestHandlerFunction callback,
AuthenticationPredicate predicate) {
return [this, callback, predicate](AsyncWebServerRequest* request, JsonVariant& json) {
Authentication authentication = authenticateRequest(request);
if (!predicate(authentication)) {
request->send(401);
return;
}
callback(request, json);
};
}

View File

@ -1,8 +1,9 @@
#ifndef SecuritySettingsService_h
#define SecuritySettingsService_h
#include <AdminSettingsService.h>
#include <SecurityManager.h>
#include <HttpEndpoint.h>
#include <FSPersistence.h>
#define DEFAULT_ADMIN_USERNAME "admin"
#define DEFAULT_GUEST_USERNAME "guest"
@ -14,30 +15,63 @@ class SecuritySettings {
public:
String jwtSecret;
std::list<User> users;
static void serialize(SecuritySettings& settings, JsonObject& root) {
// secret
root["jwt_secret"] = settings.jwtSecret;
// users
JsonArray users = root.createNestedArray("users");
for (User user : settings.users) {
JsonObject userRoot = users.createNestedObject();
userRoot["username"] = user.username;
userRoot["password"] = user.password;
userRoot["admin"] = user.admin;
}
}
static void deserialize(JsonObject& root, SecuritySettings& settings) {
// secret
settings.jwtSecret = root["jwt_secret"] | DEFAULT_JWT_SECRET;
// users
settings.users.clear();
if (root["users"].is<JsonArray>()) {
for (JsonVariant user : root["users"].as<JsonArray>()) {
settings.users.push_back(User(user["username"], user["password"], user["admin"]));
}
} else {
settings.users.push_back(User(DEFAULT_ADMIN_USERNAME, DEFAULT_ADMIN_USERNAME, true));
settings.users.push_back(User(DEFAULT_GUEST_USERNAME, DEFAULT_GUEST_USERNAME, false));
}
}
};
class SecuritySettingsService : public AdminSettingsService<SecuritySettings>, public SecurityManager {
class SecuritySettingsService : public StatefulService<SecuritySettings>, public SecurityManager {
public:
SecuritySettingsService(AsyncWebServer* server, FS* fs);
~SecuritySettingsService();
void begin();
// Functions to implement SecurityManager
Authentication authenticate(String username, String password);
Authentication authenticate(String& username, String& password);
Authentication authenticateRequest(AsyncWebServerRequest* request);
String generateJWT(User* user);
ArRequestFilterFunction filterRequest(AuthenticationPredicate predicate);
ArRequestHandlerFunction wrapRequest(ArRequestHandlerFunction onRequest, AuthenticationPredicate predicate);
protected:
void readFromJsonObject(JsonObject& root);
void writeToJsonObject(JsonObject& root);
ArJsonRequestHandlerFunction wrapCallback(ArJsonRequestHandlerFunction callback, AuthenticationPredicate predicate);
private:
HttpEndpoint<SecuritySettings> _httpEndpoint;
FSPersistence<SecuritySettings> _fsPersistence;
ArduinoJsonJWT _jwtHandler = ArduinoJsonJWT(DEFAULT_JWT_SECRET);
void configureJWTHandler();
/*
* Lookup the user by JWT
*/
Authentication authenticateJWT(String jwt);
Authentication authenticateJWT(String& jwt);
/*
* Verify the payload is correct

View File

@ -1,96 +0,0 @@
#ifndef SettingsPersistence_h
#define SettingsPersistence_h
#include <ArduinoJson.h>
#include <AsyncJson.h>
#include <AsyncJsonWebHandler.h>
#include <ESPAsyncWebServer.h>
#include <FS.h>
/**
* At the moment, not expecting settings service to have to deal with large JSON
* files this could be made configurable fairly simply, it's exposed on
* AsyncJsonWebHandler with a setter.
*/
#define MAX_SETTINGS_SIZE 1024
/*
* Mixin for classes which need to save settings to/from a file on the the file system as JSON.
*/
class SettingsPersistence {
protected:
// will store and retrieve config from the file system
FS* _fs;
// the file path our settings will be saved to
char const* _filePath;
bool writeToFS() {
// create and populate a new json object
DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_SETTINGS_SIZE);
JsonObject root = jsonDocument.to<JsonObject>();
writeToJsonObject(root);
// serialize it to filesystem
File configFile = _fs->open(_filePath, "w");
// failed to open file, return false
if (!configFile) {
return false;
}
serializeJson(jsonDocument, configFile);
configFile.close();
return true;
}
void readFromFS() {
File configFile = _fs->open(_filePath, "r");
// use defaults if no config found
if (configFile) {
// Protect against bad data uploaded to file system
// We never expect the config file to get very large, so cap it.
size_t size = configFile.size();
if (size <= MAX_SETTINGS_SIZE) {
DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_SETTINGS_SIZE);
DeserializationError error = deserializeJson(jsonDocument, configFile);
if (error == DeserializationError::Ok && jsonDocument.is<JsonObject>()) {
JsonObject root = jsonDocument.as<JsonObject>();
readFromJsonObject(root);
configFile.close();
return;
}
}
configFile.close();
}
// If we reach here we have not been successful in loading the config,
// hard-coded emergency defaults are now applied.
applyDefaultConfig();
}
// serialization routene, from local config to JsonObject
virtual void readFromJsonObject(JsonObject& root) {
}
virtual void writeToJsonObject(JsonObject& root) {
}
// We assume the readFromJsonObject supplies sensible defaults if an empty object
// is supplied, this virtual function allows that to be changed.
virtual void applyDefaultConfig() {
DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_SETTINGS_SIZE);
JsonObject root = jsonDocument.to<JsonObject>();
readFromJsonObject(root);
}
public:
SettingsPersistence(FS* fs, char const* filePath) : _fs(fs), _filePath(filePath) {
}
virtual ~SettingsPersistence() {
}
};
#endif // end SettingsPersistence

View File

@ -1,166 +0,0 @@
#ifndef SettingsService_h
#define SettingsService_h
#include <functional>
#ifdef ESP32
#include <WiFi.h>
#include <AsyncTCP.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#endif
#include <ArduinoJson.h>
#include <AsyncJson.h>
#include <AsyncJsonCallbackResponse.h>
#include <AsyncJsonWebHandler.h>
#include <ESPAsyncWebServer.h>
#include <SecurityManager.h>
#include <SettingsPersistence.h>
typedef size_t update_handler_id_t;
typedef std::function<void(void)> SettingsUpdateCallback;
static update_handler_id_t currentUpdateHandlerId;
typedef struct SettingsUpdateHandlerInfo {
update_handler_id_t _id;
SettingsUpdateCallback _cb;
bool _allowRemove;
SettingsUpdateHandlerInfo(SettingsUpdateCallback cb, bool allowRemove) :
_id(++currentUpdateHandlerId),
_cb(cb),
_allowRemove(allowRemove){};
} SettingsUpdateHandlerInfo_t;
/*
* Abstraction of a service which stores it's settings as JSON in a file system.
*/
template <class T>
class SettingsService : public SettingsPersistence {
public:
SettingsService(AsyncWebServer* server, FS* fs, char const* servicePath, char const* filePath) :
SettingsPersistence(fs, filePath),
_servicePath(servicePath) {
server->on(_servicePath, HTTP_GET, std::bind(&SettingsService::fetchConfig, this, std::placeholders::_1));
_updateHandler.setUri(servicePath);
_updateHandler.setMethod(HTTP_POST);
_updateHandler.setMaxContentLength(MAX_SETTINGS_SIZE);
_updateHandler.onRequest(
std::bind(&SettingsService::updateConfig, this, std::placeholders::_1, std::placeholders::_2));
server->addHandler(&_updateHandler);
}
virtual ~SettingsService() {
}
update_handler_id_t addUpdateHandler(SettingsUpdateCallback cb, bool allowRemove = true) {
if (!cb) {
return 0;
}
SettingsUpdateHandlerInfo_t updateHandler(cb, allowRemove);
_settingsUpdateHandlers.push_back(updateHandler);
return updateHandler._id;
}
void removeUpdateHandler(update_handler_id_t id) {
for (auto i = _settingsUpdateHandlers.begin(); i != _settingsUpdateHandlers.end();) {
if ((*i)._id == id) {
i = _settingsUpdateHandlers.erase(i);
} else {
++i;
}
}
}
T fetch() {
return _settings;
}
void update(T& settings) {
_settings = settings;
writeToFS();
callUpdateHandlers();
}
void fetchAsString(String& config) {
DynamicJsonDocument jsonDocument(MAX_SETTINGS_SIZE);
fetchAsDocument(jsonDocument);
serializeJson(jsonDocument, config);
}
void updateFromString(String& config) {
DynamicJsonDocument jsonDocument(MAX_SETTINGS_SIZE);
deserializeJson(jsonDocument, config);
updateFromDocument(jsonDocument);
}
void fetchAsDocument(JsonDocument& jsonDocument) {
JsonObject jsonObject = jsonDocument.to<JsonObject>();
writeToJsonObject(jsonObject);
}
void updateFromDocument(JsonDocument& jsonDocument) {
if (jsonDocument.is<JsonObject>()) {
JsonObject newConfig = jsonDocument.as<JsonObject>();
readFromJsonObject(newConfig);
writeToFS();
callUpdateHandlers();
}
}
void begin() {
// read the initial data from the file system
readFromFS();
}
protected:
T _settings;
char const* _servicePath;
AsyncJsonWebHandler _updateHandler;
std::list<SettingsUpdateHandlerInfo_t> _settingsUpdateHandlers;
virtual void fetchConfig(AsyncWebServerRequest* request) {
// handle the request
AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_SETTINGS_SIZE);
JsonObject jsonObject = response->getRoot();
writeToJsonObject(jsonObject);
response->setLength();
request->send(response);
}
virtual void updateConfig(AsyncWebServerRequest* request, JsonDocument& jsonDocument) {
// handle the request
if (jsonDocument.is<JsonObject>()) {
JsonObject newConfig = jsonDocument.as<JsonObject>();
readFromJsonObject(newConfig);
writeToFS();
// write settings back with a callback to reconfigure the wifi
AsyncJsonCallbackResponse* response =
new AsyncJsonCallbackResponse([this]() { callUpdateHandlers(); }, false, MAX_SETTINGS_SIZE);
JsonObject jsonObject = response->getRoot();
writeToJsonObject(jsonObject);
response->setLength();
request->send(response);
} else {
request->send(400);
}
}
void callUpdateHandlers() {
// call the classes own config update function
onConfigUpdated();
// call all setting update handlers
for (const SettingsUpdateHandlerInfo_t& handler : _settingsUpdateHandlers) {
handler._cb();
}
}
// implement to perform action when config has been updated
virtual void onConfigUpdated() {
}
};
#endif // end SettingsService

View File

@ -1,87 +0,0 @@
#ifndef Service_h
#define Service_h
#ifdef ESP32
#include <WiFi.h>
#include <AsyncTCP.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#endif
#include <ArduinoJson.h>
#include <AsyncJson.h>
#include <AsyncJsonCallbackResponse.h>
#include <AsyncJsonWebHandler.h>
#include <ESPAsyncWebServer.h>
/**
* At the moment, not expecting services to have to deal with large JSON
* files this could be made configurable fairly simply, it's exposed on
* AsyncJsonWebHandler with a setter.
*/
#define MAX_SETTINGS_SIZE 1024
/*
* Abstraction of a service which reads and writes data from an endpoint.
*
* Not currently used, but indended for use by features which do not
* require setting persistance.
*/
class SimpleService {
private:
AsyncJsonWebHandler _updateHandler;
void fetchConfig(AsyncWebServerRequest* request) {
AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_SETTINGS_SIZE);
JsonObject jsonObject = response->getRoot();
writeToJsonObject(jsonObject);
response->setLength();
request->send(response);
}
void updateConfig(AsyncWebServerRequest* request, JsonDocument& jsonDocument) {
if (jsonDocument.is<JsonObject>()) {
JsonObject newConfig = jsonDocument.as<JsonObject>();
readFromJsonObject(newConfig);
// write settings back with a callback to reconfigure the wifi
AsyncJsonCallbackResponse* response =
new AsyncJsonCallbackResponse([this]() { onConfigUpdated(); }, false, MAX_SETTINGS_SIZE);
JsonObject jsonObject = response->getRoot();
writeToJsonObject(jsonObject);
response->setLength();
request->send(response);
} else {
request->send(400);
}
}
protected:
// reads the local config from the
virtual void readFromJsonObject(JsonObject& root) {
}
virtual void writeToJsonObject(JsonObject& root) {
}
// implement to perform action when config has been updated
virtual void onConfigUpdated() {
}
public:
SimpleService(AsyncWebServer* server, char const* servicePath) {
server->on(servicePath, HTTP_GET, std::bind(&SimpleService::fetchConfig, this, std::placeholders::_1));
_updateHandler.setUri(servicePath);
_updateHandler.setMethod(HTTP_POST);
_updateHandler.setMaxContentLength(MAX_SETTINGS_SIZE);
_updateHandler.onRequest(
std::bind(&SimpleService::updateConfig, this, std::placeholders::_1, std::placeholders::_2));
server->addHandler(&_updateHandler);
}
virtual ~SimpleService() {
}
};
#endif // end SimpleService

View File

@ -0,0 +1,137 @@
#ifndef StatefulService_h
#define StatefulService_h
#include <Arduino.h>
#include <JsonDeserializer.h>
#include <JsonSerializer.h>
#include <list>
#include <functional>
#ifdef ESP32
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#endif
typedef size_t update_handler_id_t;
typedef std::function<void(String originId)> StateUpdateCallback;
static update_handler_id_t currentUpdatedHandlerId;
typedef struct StateUpdateHandlerInfo {
update_handler_id_t _id;
StateUpdateCallback _cb;
bool _allowRemove;
StateUpdateHandlerInfo(StateUpdateCallback cb, bool allowRemove) :
_id(++currentUpdatedHandlerId), _cb(cb), _allowRemove(allowRemove){};
} StateUpdateHandlerInfo_t;
template <class T>
class StatefulService {
public:
template <typename... Args>
#ifdef ESP32
StatefulService(Args&&... args) :
_state(std::forward<Args>(args)...), _accessMutex(xSemaphoreCreateRecursiveMutex()) {
}
#else
StatefulService(Args&&... args) : _state(std::forward<Args>(args)...) {
}
#endif
update_handler_id_t addUpdateHandler(StateUpdateCallback cb, bool allowRemove = true) {
if (!cb) {
return 0;
}
StateUpdateHandlerInfo_t updateHandler(cb, allowRemove);
_updateHandlers.push_back(updateHandler);
return updateHandler._id;
}
void removeUpdateHandler(update_handler_id_t id) {
for (auto i = _updateHandlers.begin(); i != _updateHandlers.end();) {
if ((*i)._allowRemove && (*i)._id == id) {
i = _updateHandlers.erase(i);
} else {
++i;
}
}
}
void updateWithoutPropagation(std::function<void(T&)> callback) {
#ifdef ESP32
xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY);
#endif
callback(_state);
#ifdef ESP32
xSemaphoreGiveRecursive(_accessMutex);
#endif
}
void updateWithoutPropagation(JsonObject& jsonObject, JsonDeserializer<T> deserializer) {
#ifdef ESP32
xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY);
#endif
deserializer(jsonObject, _state);
#ifdef ESP32
xSemaphoreGiveRecursive(_accessMutex);
#endif
}
void update(std::function<void(T&)> callback, String originId) {
#ifdef ESP32
xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY);
#endif
callback(_state);
callUpdateHandlers(originId);
#ifdef ESP32
xSemaphoreGiveRecursive(_accessMutex);
#endif
}
void update(JsonObject& jsonObject, JsonDeserializer<T> deserializer, String originId) {
#ifdef ESP32
xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY);
#endif
deserializer(jsonObject, _state);
callUpdateHandlers(originId);
#ifdef ESP32
xSemaphoreGiveRecursive(_accessMutex);
#endif
}
void read(std::function<void(T&)> callback) {
#ifdef ESP32
xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY);
#endif
callback(_state);
#ifdef ESP32
xSemaphoreGiveRecursive(_accessMutex);
#endif
}
void read(JsonObject& jsonObject, JsonSerializer<T> serializer) {
#ifdef ESP32
xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY);
#endif
serializer(_state, jsonObject);
#ifdef ESP32
xSemaphoreGiveRecursive(_accessMutex);
#endif
}
void callUpdateHandlers(String originId) {
for (const StateUpdateHandlerInfo_t& updateHandler : _updateHandlers) {
updateHandler._cb(originId);
}
}
protected:
T _state;
private:
#ifdef ESP32
SemaphoreHandle_t _accessMutex;
#endif
std::list<StateUpdateHandlerInfo_t> _updateHandlers;
};
#endif // end StatefulService_h

View File

@ -0,0 +1,242 @@
#ifndef WebSocketTxRx_h
#define WebSocketTxRx_h
#include <StatefulService.h>
#include <JsonSerializer.h>
#include <JsonDeserializer.h>
#include <ESPAsyncWebServer.h>
#define WEB_SOCKET_MSG_SIZE 1024
#define WEB_SOCKET_CLIENT_ID_MSG_SIZE 128
#define WEB_SOCKET_ORIGIN "websocket"
#define WEB_SOCKET_ORIGIN_CLIENT_ID_PREFIX "websocket:"
template <class T>
class WebSocketConnector {
protected:
StatefulService<T>* _statefulService;
AsyncWebServer* _server;
AsyncWebSocket _webSocket;
WebSocketConnector(StatefulService<T>* statefulService,
AsyncWebServer* server,
char const* webSocketPath,
SecurityManager* securityManager,
AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) :
_statefulService(statefulService), _server(server), _webSocket(webSocketPath) {
_webSocket.setFilter(securityManager->filterRequest(authenticationPredicate));
_webSocket.onEvent(std::bind(&WebSocketConnector::onWSEvent,
this,
std::placeholders::_1,
std::placeholders::_2,
std::placeholders::_3,
std::placeholders::_4,
std::placeholders::_5,
std::placeholders::_6));
_server->addHandler(&_webSocket);
_server->on(webSocketPath, HTTP_GET, std::bind(&WebSocketConnector::forbidden, this, std::placeholders::_1));
}
WebSocketConnector(StatefulService<T>* statefulService, AsyncWebServer* server, char const* webSocketPath) :
_statefulService(statefulService), _server(server), _webSocket(webSocketPath) {
_webSocket.onEvent(std::bind(&WebSocketConnector::onWSEvent,
this,
std::placeholders::_1,
std::placeholders::_2,
std::placeholders::_3,
std::placeholders::_4,
std::placeholders::_5,
std::placeholders::_6));
_server->addHandler(&_webSocket);
}
virtual void onWSEvent(AsyncWebSocket* server,
AsyncWebSocketClient* client,
AwsEventType type,
void* arg,
uint8_t* data,
size_t len) = 0;
String clientId(AsyncWebSocketClient* client) {
return WEB_SOCKET_ORIGIN_CLIENT_ID_PREFIX + String(client->id());
}
private:
void forbidden(AsyncWebServerRequest* request) {
request->send(403);
}
};
template <class T>
class WebSocketTx : virtual public WebSocketConnector<T> {
public:
WebSocketTx(JsonSerializer<T> jsonSerializer,
StatefulService<T>* statefulService,
AsyncWebServer* server,
char const* webSocketPath,
SecurityManager* securityManager,
AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) :
WebSocketConnector<T>(statefulService, server, webSocketPath, securityManager, authenticationPredicate),
_jsonSerializer(jsonSerializer) {
WebSocketConnector<T>::_statefulService->addUpdateHandler([&](String originId) { transmitData(nullptr, originId); },
false);
}
WebSocketTx(JsonSerializer<T> jsonSerializer,
StatefulService<T>* statefulService,
AsyncWebServer* server,
char const* webSocketPath) :
WebSocketConnector<T>(statefulService, server, webSocketPath), _jsonSerializer(jsonSerializer) {
WebSocketConnector<T>::_statefulService->addUpdateHandler([&](String originId) { transmitData(nullptr, originId); },
false);
}
protected:
virtual void onWSEvent(AsyncWebSocket* server,
AsyncWebSocketClient* client,
AwsEventType type,
void* arg,
uint8_t* data,
size_t len) {
if (type == WS_EVT_CONNECT) {
// when a client connects, we transmit it's id and the current payload
transmitId(client);
transmitData(client, WEB_SOCKET_ORIGIN);
}
}
private:
JsonSerializer<T> _jsonSerializer;
void transmitId(AsyncWebSocketClient* client) {
DynamicJsonDocument jsonDocument = DynamicJsonDocument(WEB_SOCKET_CLIENT_ID_MSG_SIZE);
JsonObject root = jsonDocument.to<JsonObject>();
root["type"] = "id";
root["id"] = WebSocketConnector<T>::clientId(client);
size_t len = measureJson(jsonDocument);
AsyncWebSocketMessageBuffer* buffer = WebSocketConnector<T>::_webSocket.makeBuffer(len);
if (buffer) {
serializeJson(jsonDocument, (char*)buffer->get(), len + 1);
client->text(buffer);
}
}
/**
* Broadcasts the payload to the destination, if provided. Otherwise broadcasts to all clients except the origin, if
* specified.
*
* Original implementation sent clients their own IDs so they could ignore updates they initiated. This approach
* simplifies the client and the server implementation but may not be sufficent for all use-cases.
*/
void transmitData(AsyncWebSocketClient* client, String originId) {
DynamicJsonDocument jsonDocument = DynamicJsonDocument(WEB_SOCKET_MSG_SIZE);
JsonObject root = jsonDocument.to<JsonObject>();
root["type"] = "payload";
root["origin_id"] = originId;
JsonObject payload = root.createNestedObject("payload");
WebSocketConnector<T>::_statefulService->read(payload, _jsonSerializer);
size_t len = measureJson(jsonDocument);
AsyncWebSocketMessageBuffer* buffer = WebSocketConnector<T>::_webSocket.makeBuffer(len);
if (buffer) {
serializeJson(jsonDocument, (char*)buffer->get(), len + 1);
if (client) {
client->text(buffer);
} else {
WebSocketConnector<T>::_webSocket.textAll(buffer);
}
}
}
};
template <class T>
class WebSocketRx : virtual public WebSocketConnector<T> {
public:
WebSocketRx(JsonDeserializer<T> jsonDeserializer,
StatefulService<T>* statefulService,
AsyncWebServer* server,
char const* webSocketPath,
SecurityManager* securityManager,
AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) :
WebSocketConnector<T>(statefulService, server, webSocketPath, securityManager, authenticationPredicate),
_jsonDeserializer(jsonDeserializer) {
}
WebSocketRx(JsonDeserializer<T> jsonDeserializer,
StatefulService<T>* statefulService,
AsyncWebServer* server,
char const* webSocketPath) :
WebSocketConnector<T>(statefulService, server, webSocketPath), _jsonDeserializer(jsonDeserializer) {
}
protected:
virtual void onWSEvent(AsyncWebSocket* server,
AsyncWebSocketClient* client,
AwsEventType type,
void* arg,
uint8_t* data,
size_t len) {
if (type == WS_EVT_DATA) {
AwsFrameInfo* info = (AwsFrameInfo*)arg;
if (info->final && info->index == 0 && info->len == len) {
if (info->opcode == WS_TEXT) {
DynamicJsonDocument jsonDocument = DynamicJsonDocument(WEB_SOCKET_MSG_SIZE);
DeserializationError error = deserializeJson(jsonDocument, (char*)data);
if (!error && jsonDocument.is<JsonObject>()) {
JsonObject jsonObject = jsonDocument.as<JsonObject>();
WebSocketConnector<T>::_statefulService->update(
jsonObject, _jsonDeserializer, WebSocketConnector<T>::clientId(client));
}
}
}
}
}
private:
JsonDeserializer<T> _jsonDeserializer;
};
template <class T>
class WebSocketTxRx : public WebSocketTx<T>, public WebSocketRx<T> {
public:
WebSocketTxRx(JsonSerializer<T> jsonSerializer,
JsonDeserializer<T> jsonDeserializer,
StatefulService<T>* statefulService,
AsyncWebServer* server,
char const* webSocketPath,
SecurityManager* securityManager,
AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) :
WebSocketConnector<T>(statefulService, server, webSocketPath, securityManager, authenticationPredicate),
WebSocketTx<T>(jsonSerializer, statefulService, server, webSocketPath, securityManager, authenticationPredicate),
WebSocketRx<T>(jsonDeserializer,
statefulService,
server,
webSocketPath,
securityManager,
authenticationPredicate) {
}
WebSocketTxRx(JsonSerializer<T> jsonSerializer,
JsonDeserializer<T> jsonDeserializer,
StatefulService<T>* statefulService,
AsyncWebServer* server,
char const* webSocketPath) :
WebSocketConnector<T>(statefulService, server, webSocketPath),
WebSocketTx<T>(jsonSerializer, statefulService, server, webSocketPath),
WebSocketRx<T>(jsonDeserializer, statefulService, server, webSocketPath) {
}
protected:
void onWSEvent(AsyncWebSocket* server,
AsyncWebSocketClient* client,
AwsEventType type,
void* arg,
uint8_t* data,
size_t len) {
WebSocketRx<T>::onWSEvent(server, client, type, arg, data, len);
WebSocketTx<T>::onWSEvent(server, client, type, arg, data, len);
}
};
#endif

View File

@ -1,7 +1,13 @@
#include <WiFiSettingsService.h>
WiFiSettingsService::WiFiSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) :
AdminSettingsService(server, fs, securityManager, WIFI_SETTINGS_SERVICE_PATH, WIFI_SETTINGS_FILE) {
_httpEndpoint(WiFiSettings::serialize,
WiFiSettings::deserialize,
this,
server,
WIFI_SETTINGS_SERVICE_PATH,
securityManager),
_fsPersistence(WiFiSettings::serialize, WiFiSettings::deserialize, this, fs, WIFI_SETTINGS_FILE) {
// We want the device to come up in opmode=0 (WIFI_OFF), when erasing the flash this is not the default.
// If needed, we save opmode=0 before disabling persistence so the device boots with WiFi disabled in the future.
if (WiFi.getMode() != WIFI_OFF) {
@ -24,60 +30,12 @@ WiFiSettingsService::WiFiSettingsService(AsyncWebServer* server, FS* fs, Securit
_onStationModeDisconnectedHandler = WiFi.onStationModeDisconnected(
std::bind(&WiFiSettingsService::onStationModeDisconnected, this, std::placeholders::_1));
#endif
}
WiFiSettingsService::~WiFiSettingsService() {
addUpdateHandler([&](String originId) { reconfigureWiFiConnection(); }, false);
}
void WiFiSettingsService::begin() {
SettingsService::begin();
reconfigureWiFiConnection();
}
void WiFiSettingsService::readFromJsonObject(JsonObject& root) {
_settings.ssid = root["ssid"] | "";
_settings.password = root["password"] | "";
_settings.hostname = root["hostname"] | "";
_settings.staticIPConfig = root["static_ip_config"] | false;
// extended settings
readIP(root, "local_ip", _settings.localIP);
readIP(root, "gateway_ip", _settings.gatewayIP);
readIP(root, "subnet_mask", _settings.subnetMask);
readIP(root, "dns_ip_1", _settings.dnsIP1);
readIP(root, "dns_ip_2", _settings.dnsIP2);
// Swap around the dns servers if 2 is populated but 1 is not
if (_settings.dnsIP1 == INADDR_NONE && _settings.dnsIP2 != INADDR_NONE) {
_settings.dnsIP1 = _settings.dnsIP2;
_settings.dnsIP2 = INADDR_NONE;
}
// Turning off static ip config if we don't meet the minimum requirements
// of ipAddress, gateway and subnet. This may change to static ip only
// as sensible defaults can be assumed for gateway and subnet
if (_settings.staticIPConfig &&
(_settings.localIP == INADDR_NONE || _settings.gatewayIP == INADDR_NONE || _settings.subnetMask == INADDR_NONE)) {
_settings.staticIPConfig = false;
}
}
void WiFiSettingsService::writeToJsonObject(JsonObject& root) {
// connection settings
root["ssid"] = _settings.ssid;
root["password"] = _settings.password;
root["hostname"] = _settings.hostname;
root["static_ip_config"] = _settings.staticIPConfig;
// extended settings
writeIP(root, "local_ip", _settings.localIP);
writeIP(root, "gateway_ip", _settings.gatewayIP);
writeIP(root, "subnet_mask", _settings.subnetMask);
writeIP(root, "dns_ip_1", _settings.dnsIP1);
writeIP(root, "dns_ip_2", _settings.dnsIP2);
}
void WiFiSettingsService::onConfigUpdated() {
_fsPersistence.readFromFS();
reconfigureWiFiConnection();
}
@ -95,18 +53,6 @@ void WiFiSettingsService::reconfigureWiFiConnection() {
#endif
}
void WiFiSettingsService::readIP(JsonObject& root, String key, IPAddress& _ip) {
if (!root[key].is<String>() || !_ip.fromString(root[key].as<String>())) {
_ip = INADDR_NONE;
}
}
void WiFiSettingsService::writeIP(JsonObject& root, String key, IPAddress& _ip) {
if (_ip != INADDR_NONE) {
root[key] = _ip.toString();
}
}
void WiFiSettingsService::loop() {
unsigned long currentMillis = millis();
if (!_lastConnectionAttempt || (unsigned long)(currentMillis - _lastConnectionAttempt) >= WIFI_RECONNECTION_DELAY) {
@ -117,27 +63,27 @@ void WiFiSettingsService::loop() {
void WiFiSettingsService::manageSTA() {
// Abort if already connected, or if we have no SSID
if (WiFi.isConnected() || _settings.ssid.length() == 0) {
if (WiFi.isConnected() || _state.ssid.length() == 0) {
return;
}
// Connect or reconnect as required
if ((WiFi.getMode() & WIFI_STA) == 0) {
Serial.println("Connecting to WiFi.");
if (_settings.staticIPConfig) {
if (_state.staticIPConfig) {
// configure for static IP
WiFi.config(_settings.localIP, _settings.gatewayIP, _settings.subnetMask, _settings.dnsIP1, _settings.dnsIP2);
WiFi.config(_state.localIP, _state.gatewayIP, _state.subnetMask, _state.dnsIP1, _state.dnsIP2);
} else {
// configure for DHCP
#ifdef ESP32
WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE);
WiFi.setHostname(_settings.hostname.c_str());
WiFi.setHostname(_state.hostname.c_str());
#elif defined(ESP8266)
WiFi.config(INADDR_ANY, INADDR_ANY, INADDR_ANY);
WiFi.hostname(_settings.hostname);
WiFi.hostname(_state.hostname);
#endif
}
// attempt to connect to the network
WiFi.begin(_settings.ssid.c_str(), _settings.password.c_str());
WiFi.begin(_state.ssid.c_str(), _state.password.c_str());
}
}

View File

@ -1,8 +1,10 @@
#ifndef WiFiSettingsService_h
#define WiFiSettingsService_h
#include <AdminSettingsService.h>
#include <IPAddress.h>
#include <StatefulService.h>
#include <FSPersistence.h>
#include <HttpEndpoint.h>
#include <JsonUtils.h>
#define WIFI_SETTINGS_FILE "/config/wifiSettings.json"
#define WIFI_SETTINGS_SERVICE_PATH "/rest/wifiSettings"
@ -22,22 +24,61 @@ class WiFiSettings {
IPAddress subnetMask;
IPAddress dnsIP1;
IPAddress dnsIP2;
static void serialize(WiFiSettings& settings, JsonObject& root) {
// connection settings
root["ssid"] = settings.ssid;
root["password"] = settings.password;
root["hostname"] = settings.hostname;
root["static_ip_config"] = settings.staticIPConfig;
// extended settings
JsonUtils::writeIP(root, "local_ip", settings.localIP);
JsonUtils::writeIP(root, "gateway_ip", settings.gatewayIP);
JsonUtils::writeIP(root, "subnet_mask", settings.subnetMask);
JsonUtils::writeIP(root, "dns_ip_1", settings.dnsIP1);
JsonUtils::writeIP(root, "dns_ip_2", settings.dnsIP2);
}
static void deserialize(JsonObject& root, WiFiSettings& settings) {
settings.ssid = root["ssid"] | "";
settings.password = root["password"] | "";
settings.hostname = root["hostname"] | "";
settings.staticIPConfig = root["static_ip_config"] | false;
// extended settings
JsonUtils::readIP(root, "local_ip", settings.localIP);
JsonUtils::readIP(root, "gateway_ip", settings.gatewayIP);
JsonUtils::readIP(root, "subnet_mask", settings.subnetMask);
JsonUtils::readIP(root, "dns_ip_1", settings.dnsIP1);
JsonUtils::readIP(root, "dns_ip_2", settings.dnsIP2);
// Swap around the dns servers if 2 is populated but 1 is not
if (settings.dnsIP1 == INADDR_NONE && settings.dnsIP2 != INADDR_NONE) {
settings.dnsIP1 = settings.dnsIP2;
settings.dnsIP2 = INADDR_NONE;
}
// Turning off static ip config if we don't meet the minimum requirements
// of ipAddress, gateway and subnet. This may change to static ip only
// as sensible defaults can be assumed for gateway and subnet
if (settings.staticIPConfig &&
(settings.localIP == INADDR_NONE || settings.gatewayIP == INADDR_NONE || settings.subnetMask == INADDR_NONE)) {
settings.staticIPConfig = false;
}
}
};
class WiFiSettingsService : public AdminSettingsService<WiFiSettings> {
class WiFiSettingsService : public StatefulService<WiFiSettings> {
public:
WiFiSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager);
~WiFiSettingsService();
void begin();
void loop();
protected:
void readFromJsonObject(JsonObject& root);
void writeToJsonObject(JsonObject& root);
void onConfigUpdated();
private:
HttpEndpoint<WiFiSettings> _httpEndpoint;
FSPersistence<WiFiSettings> _fsPersistence;
unsigned long _lastConnectionAttempt;
#ifdef ESP32
@ -49,8 +90,6 @@ class WiFiSettingsService : public AdminSettingsService<WiFiSettings> {
void onStationModeDisconnected(const WiFiEventStationModeDisconnected& event);
#endif
void readIP(JsonObject& root, String key, IPAddress& _ip);
void writeIP(JsonObject& root, String key, IPAddress& _ip);
void reconfigureWiFiConnection();
void manageSTA();
};