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:
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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
|
@ -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_
|
@ -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_
|
@ -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;
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
103
lib/framework/FSPersistence.h
Normal file
103
lib/framework/FSPersistence.h
Normal 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
|
167
lib/framework/HttpEndpoint.h
Normal file
167
lib/framework/HttpEndpoint.h
Normal 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
|
9
lib/framework/JsonDeserializer.h
Normal file
9
lib/framework/JsonDeserializer.h
Normal 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
|
9
lib/framework/JsonSerializer.h
Normal file
9
lib/framework/JsonSerializer.h
Normal 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
17
lib/framework/JsonUtils.h
Normal 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
161
lib/framework/MqttPubSub.h
Normal 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
|
155
lib/framework/MqttSettingsService.cpp
Normal file
155
lib/framework/MqttSettingsService.cpp
Normal 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();
|
||||
}
|
||||
}
|
125
lib/framework/MqttSettingsService.h
Normal file
125
lib/framework/MqttSettingsService.h
Normal 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
|
24
lib/framework/MqttStatus.cpp
Normal file
24
lib/framework/MqttStatus.cpp
Normal 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);
|
||||
}
|
31
lib/framework/MqttStatus.h
Normal file
31
lib/framework/MqttStatus.h
Normal 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
|
@ -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();
|
||||
|
@ -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();
|
||||
};
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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
|
@ -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
|
137
lib/framework/StatefulService.h
Normal file
137
lib/framework/StatefulService.h
Normal 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
|
242
lib/framework/WebSocketTxRx.h
Normal file
242
lib/framework/WebSocketTxRx.h
Normal 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
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
};
|
||||
|
Reference in New Issue
Block a user