Apply updates alternative (#135)

* Rename "serialize" and "deserialize" functions to "read" and "update" to reflect API in StatefulService
* Move new definitions to StatefulService.h so it is obvious it is not general purpose
* Update README
This commit is contained in:
rjwats 2020-05-29 20:18:43 +01:00 committed by GitHub
parent d9ae0f5cf9
commit 0d39c5ca00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 253 additions and 258 deletions

View File

@ -350,7 +350,9 @@ The following diagram visualises how the framework's modular components fit toge
#### Stateful service
The [StatefulService.h](lib/framework/StatefulService.h) class is a responsible for managing state and interfacing with code which wants to change or respond to changes in that state. You can define a data class to hold some state, then build a StatefulService class to manage its state:
The [StatefulService.h](lib/framework/StatefulService.h) class is responsible for managing state. It has an API which allows other code to update or respond to updates in the state it manages. You can define a data class to hold state, then build a StatefulService class to manage it. After that you may attach HTTP endpoints, WebSockets or MQTT topics to the StatefulService instance to provide commonly required features.
Here is a simple example of a state class and a StatefulService to manage it:
```cpp
class LightState {
@ -369,7 +371,8 @@ You may listen for changes to state by registering an update handler callback. I
// register an update handler
update_handler_id_t myUpdateHandler = lightStateService.addUpdateHandler(
[&](const String& originId) {
Serial.println("The light's state has been updated");
Serial.print("The light's state has been updated by: ");
Serial.println(originId);
}
);
@ -377,7 +380,7 @@ update_handler_id_t myUpdateHandler = lightStateService.addUpdateHandler(
lightStateService.removeUpdateHandler(myUpdateHandler);
```
An "originId" is passed to the update handler which may be used to identify the origin of the update. The default origin values the framework provides are:
An "originId" is passed to the update handler which may be used to identify the origin of an update. The default origin values the framework provides are:
Origin | Description
--------------------- | -----------
@ -393,17 +396,35 @@ lightStateService.read([&](LightState& state) {
});
```
StatefulService also exposes an update function which allows the caller to update the state with a callback. This approach automatically calls the registered update handlers when complete. The example below turns on the lights using the arbitrary origin "timer":
StatefulService also exposes an update function which allows the caller to update the state with a callback. This function automatically calls the registered update handlers if the state has been changed. The example below changes the state of the light (turns it on) using the arbitrary origin "timer" and returns the "CHANGED" state update result, indicating that a change was made:
```cpp
lightStateService.update([&](LightState& state) {
state.on = true; // turn on the lights!
if (state.on) {
return StateUpdateResult::UNCHANGED; // lights were already on, return UNCHANGED
}
state.on = true; // turn on the lights
return StateUpdateResult::CHANGED; // notify StatefulService by returning CHANGED
}, "timer");
```
There are three possible return values for an update function which are as follows:
Origin | Description
----------------------------- | ---------------------------------------------------------------------------
StateUpdateResult::CHANGED | The update changed the state, propagation should take place if required
StateUpdateResult::UNCHANGED | The state was unchanged, propagation should not take place
StateUpdateResult::ERROR | There was an error updating the state, propagation should not take place
#### Serialization
When transmitting state over HTTP, WebSockets, or MQTT it must to be marshalled into a serializable form (JSON). The framework uses ArduinoJson for serialization and the functions defined in [JsonSerializer.h](lib/framework/JsonSerializer.h) and [JsonDeserializer.h](lib/framework/JsonDeserializer.h) facilitate this.
When reading or updating state from an external source (HTTP, WebSockets, or MQTT for example) the state must be marshalled into a serializable form (JSON). SettingsService provides two callback patterns which facilitate this internally:
Callback | Signature | Purpose
---------------- | -------------------------------------------------------- | ---------------------------------------------------------------------------------
JsonStateReader | void read(T& settings, JsonObject& root) | Reading the state object into a JsonObject
JsonStateUpdater | StateUpdateResult update(JsonObject& root, T& settings) | Updating the state from a JsonObject, returning the appropriate StateUpdateResult
The static functions below can be used to facilitate the serialization/deserialization of the light state:
@ -413,32 +434,33 @@ class LightState {
bool on = false;
uint8_t brightness = 255;
static void serialize(LightState& state, JsonObject& root) {
static void read(LightState& state, JsonObject& root) {
root["on"] = state.on;
root["brightness"] = state.brightness;
}
static void deserialize(JsonObject& root, LightState& state) {
static StateUpdateResult update(JsonObject& root, LightState& state) {
state.on = root["on"] | false;
state.brightness = root["brightness"] | 255;
return StateUpdateResult::CHANGED;
}
};
```
For convenience, the StatefulService class provides overloads of its `update` and `read` functions which utilize these functions.
Copy the state to a JsonObject using a serializer:
Read the state to a JsonObject using a serializer:
```cpp
JsonObject jsonObject = jsonDocument.to<JsonObject>();
lightStateService->read(jsonObject, serializer);
lightStateService->read(jsonObject, LightState::read);
```
Update the state from a JsonObject using a deserializer:
```cpp
JsonObject jsonObject = jsonDocument.as<JsonObject>();
lightStateService->update(jsonObject, deserializer, "timer");
lightStateService->update(jsonObject, LightState::update, "timer");
```
#### Endpoints
@ -451,7 +473,7 @@ The code below demonstrates how to extend the LightStateService class to provide
class LightStateService : public StatefulService<LightState> {
public:
LightStateService(AsyncWebServer* server) :
_httpEndpoint(LightState::serialize, LightState::deserialize, this, server, "/rest/lightState") {
_httpEndpoint(LightState::read, LightState::update, this, server, "/rest/lightState") {
}
private:
@ -471,7 +493,7 @@ The code below demonstrates how to extend the LightStateService class to provide
class LightStateService : public StatefulService<LightState> {
public:
LightStateService(FS* fs) :
_fsPersistence(LightState::serialize, LightState::deserialize, this, fs, "/config/lightState.json") {
_fsPersistence(LightState::read, LightState::update, this, fs, "/config/lightState.json") {
}
private:
@ -489,7 +511,7 @@ The code below demonstrates how to extend the LightStateService class to provide
class LightStateService : public StatefulService<LightState> {
public:
LightStateService(AsyncWebServer* server) :
_webSocket(LightState::serialize, LightState::deserialize, this, server, "/ws/lightState"), {
_webSocket(LightState::read, LightState::update, this, server, "/ws/lightState"), {
}
private:
@ -508,15 +530,16 @@ The framework includes an MQTT client which can be configured via the UI. MQTT r
The code below demonstrates how to extend the LightStateService class to interface with MQTT:
```cpp
class LightStateService : public StatefulService<LightState> {
public:
LightStateService(AsyncMqttClient* mqttClient) :
_mqttPubSub(LightState::serialize,
LightState::deserialize,
this,
mqttClient,
"homeassistant/light/my_light/set",
"homeassistant/light/my_light/state") {
_mqttPubSub(LightState::read,
LightState::update,
this,
mqttClient,
"homeassistant/light/my_light/set",
"homeassistant/light/my_light/state") {
}
private:

View File

@ -1,13 +1,8 @@
#include <APSettingsService.h>
APSettingsService::APSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) :
_httpEndpoint(APSettings::serialize,
APSettings::deserialize,
this,
server,
AP_SETTINGS_SERVICE_PATH,
securityManager),
_fsPersistence(APSettings::serialize, APSettings::deserialize, this, fs, AP_SETTINGS_FILE),
_httpEndpoint(APSettings::read, APSettings::update, this, server, AP_SETTINGS_SERVICE_PATH, securityManager),
_fsPersistence(APSettings::read, APSettings::update, this, fs, AP_SETTINGS_FILE),
_dnsServer(nullptr),
_lastManaged(0) {
addUpdateHandler([&](const String& originId) { reconfigureAP(); }, false);

View File

@ -36,13 +36,13 @@ class APSettings {
String ssid;
String password;
static void serialize(APSettings& settings, JsonObject& root) {
static void read(APSettings& settings, JsonObject& root) {
root["provision_mode"] = settings.provisionMode;
root["ssid"] = settings.ssid;
root["password"] = settings.password;
}
static void deserialize(JsonObject& root, APSettings& settings) {
static StateUpdateResult update(JsonObject& root, APSettings& settings) {
settings.provisionMode = root["provision_mode"] | FACTORY_AP_PROVISION_MODE;
switch (settings.provisionMode) {
case AP_MODE_ALWAYS:
@ -54,6 +54,7 @@ class APSettings {
}
settings.ssid = root["ssid"] | FACTORY_AP_SSID;
settings.password = root["password"] | FACTORY_AP_PASSWORD;
return StateUpdateResult::CHANGED;
}
};

View File

@ -2,21 +2,19 @@
#define FSPersistence_h
#include <StatefulService.h>
#include <JsonSerializer.h>
#include <JsonDeserializer.h>
#include <FS.h>
template <class T>
class FSPersistence {
public:
FSPersistence(JsonSerializer<T> jsonSerializer,
JsonDeserializer<T> jsonDeserializer,
FSPersistence(JsonStateReader<T> stateReader,
JsonStateUpdater<T> stateUpdater,
StatefulService<T>* statefulService,
FS* fs,
char const* filePath,
size_t bufferSize = DEFAULT_BUFFER_SIZE) :
_jsonSerializer(jsonSerializer),
_jsonDeserializer(jsonDeserializer),
_stateReader(stateReader),
_stateUpdater(stateUpdater),
_statefulService(statefulService),
_fs(fs),
_filePath(filePath),
@ -33,7 +31,7 @@ class FSPersistence {
DeserializationError error = deserializeJson(jsonDocument, settingsFile);
if (error == DeserializationError::Ok && jsonDocument.is<JsonObject>()) {
JsonObject jsonObject = jsonDocument.as<JsonObject>();
_statefulService->updateWithoutPropagation(jsonObject, _jsonDeserializer);
_statefulService->updateWithoutPropagation(jsonObject, _stateUpdater);
settingsFile.close();
return;
}
@ -49,7 +47,7 @@ class FSPersistence {
// create and populate a new json object
DynamicJsonDocument jsonDocument = DynamicJsonDocument(_bufferSize);
JsonObject jsonObject = jsonDocument.to<JsonObject>();
_statefulService->read(jsonObject, _jsonSerializer);
_statefulService->read(jsonObject, _stateReader);
// serialize it to filesystem
File settingsFile = _fs->open(_filePath, "w");
@ -79,8 +77,8 @@ class FSPersistence {
}
private:
JsonSerializer<T> _jsonSerializer;
JsonDeserializer<T> _jsonDeserializer;
JsonStateReader<T> _stateReader;
JsonStateUpdater<T> _stateUpdater;
StatefulService<T>* _statefulService;
FS* _fs;
char const* _filePath;
@ -88,12 +86,12 @@ class FSPersistence {
update_handler_id_t _updateHandlerId;
protected:
// We assume the deserializer supplies sensible defaults if an empty object
// We assume the updater supplies sensible defaults if an empty object
// is supplied, this virtual function allows that to be changed.
virtual void applyDefaults() {
DynamicJsonDocument jsonDocument = DynamicJsonDocument(_bufferSize);
JsonObject jsonObject = jsonDocument.as<JsonObject>();
_statefulService->updateWithoutPropagation(jsonObject, _jsonDeserializer);
_statefulService->updateWithoutPropagation(jsonObject, _stateUpdater);
}
};

View File

@ -8,46 +8,44 @@
#include <SecurityManager.h>
#include <StatefulService.h>
#include <JsonSerializer.h>
#include <JsonDeserializer.h>
#define HTTP_ENDPOINT_ORIGIN_ID "http"
template <class T>
class HttpGetEndpoint {
public:
HttpGetEndpoint(JsonSerializer<T> jsonSerializer,
HttpGetEndpoint(JsonStateReader<T> stateReader,
StatefulService<T>* statefulService,
AsyncWebServer* server,
const String& servicePath,
SecurityManager* securityManager,
AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN,
size_t bufferSize = DEFAULT_BUFFER_SIZE) :
_jsonSerializer(jsonSerializer), _statefulService(statefulService), _bufferSize(bufferSize) {
_stateReader(stateReader), _statefulService(statefulService), _bufferSize(bufferSize) {
server->on(servicePath.c_str(),
HTTP_GET,
securityManager->wrapRequest(std::bind(&HttpGetEndpoint::fetchSettings, this, std::placeholders::_1),
authenticationPredicate));
}
HttpGetEndpoint(JsonSerializer<T> jsonSerializer,
HttpGetEndpoint(JsonStateReader<T> stateReader,
StatefulService<T>* statefulService,
AsyncWebServer* server,
const String& servicePath,
size_t bufferSize = DEFAULT_BUFFER_SIZE) :
_jsonSerializer(jsonSerializer), _statefulService(statefulService), _bufferSize(bufferSize) {
_stateReader(stateReader), _statefulService(statefulService), _bufferSize(bufferSize) {
server->on(servicePath.c_str(), HTTP_GET, std::bind(&HttpGetEndpoint::fetchSettings, this, std::placeholders::_1));
}
protected:
JsonSerializer<T> _jsonSerializer;
JsonStateReader<T> _stateReader;
StatefulService<T>* _statefulService;
size_t _bufferSize;
void fetchSettings(AsyncWebServerRequest* request) {
AsyncJsonResponse* response = new AsyncJsonResponse(false, _bufferSize);
JsonObject jsonObject = response->getRoot().to<JsonObject>();
_statefulService->read(jsonObject, _jsonSerializer);
_statefulService->read(jsonObject, _stateReader);
response->setLength();
request->send(response);
@ -57,16 +55,16 @@ class HttpGetEndpoint {
template <class T>
class HttpPostEndpoint {
public:
HttpPostEndpoint(JsonSerializer<T> jsonSerializer,
JsonDeserializer<T> jsonDeserializer,
HttpPostEndpoint(JsonStateReader<T> stateReader,
JsonStateUpdater<T> stateUpdater,
StatefulService<T>* statefulService,
AsyncWebServer* server,
const String& servicePath,
SecurityManager* securityManager,
AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN,
size_t bufferSize = DEFAULT_BUFFER_SIZE) :
_jsonSerializer(jsonSerializer),
_jsonDeserializer(jsonDeserializer),
_stateReader(stateReader),
_stateUpdater(stateUpdater),
_statefulService(statefulService),
_updateHandler(
servicePath,
@ -79,14 +77,14 @@ class HttpPostEndpoint {
server->addHandler(&_updateHandler);
}
HttpPostEndpoint(JsonSerializer<T> jsonSerializer,
JsonDeserializer<T> jsonDeserializer,
HttpPostEndpoint(JsonStateReader<T> stateReader,
JsonStateUpdater<T> stateUpdater,
StatefulService<T>* statefulService,
AsyncWebServer* server,
const String& servicePath,
size_t bufferSize = DEFAULT_BUFFER_SIZE) :
_jsonSerializer(jsonSerializer),
_jsonDeserializer(jsonDeserializer),
_stateReader(stateReader),
_stateUpdater(stateUpdater),
_statefulService(statefulService),
_updateHandler(servicePath,
std::bind(&HttpPostEndpoint::updateSettings, this, std::placeholders::_1, std::placeholders::_2),
@ -97,56 +95,54 @@ class HttpPostEndpoint {
}
protected:
JsonSerializer<T> _jsonSerializer;
JsonDeserializer<T> _jsonDeserializer;
JsonStateReader<T> _stateReader;
JsonStateUpdater<T> _stateUpdater;
StatefulService<T>* _statefulService;
AsyncCallbackJsonWebHandler _updateHandler;
size_t _bufferSize;
void updateSettings(AsyncWebServerRequest* request, JsonVariant& json) {
if (json.is<JsonObject>()) {
AsyncJsonResponse* response = new AsyncJsonResponse(false, _bufferSize);
// 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 {
if (!json.is<JsonObject>()) {
request->send(400);
return;
}
JsonObject jsonObject = json.as<JsonObject>();
StateUpdateResult outcome = _statefulService->updateWithoutPropagation(jsonObject, _stateUpdater);
if (outcome == StateUpdateResult::ERROR) {
request->send(400);
return;
}
if (outcome == StateUpdateResult::CHANGED) {
request->onDisconnect([this]() { _statefulService->callUpdateHandlers(HTTP_ENDPOINT_ORIGIN_ID); });
}
AsyncJsonResponse* response = new AsyncJsonResponse(false, _bufferSize);
jsonObject = response->getRoot().to<JsonObject>();
_statefulService->read(jsonObject, _stateReader);
response->setLength();
request->send(response);
}
};
template <class T>
class HttpEndpoint : public HttpGetEndpoint<T>, public HttpPostEndpoint<T> {
public:
HttpEndpoint(JsonSerializer<T> jsonSerializer,
JsonDeserializer<T> jsonDeserializer,
HttpEndpoint(JsonStateReader<T> stateReader,
JsonStateUpdater<T> stateUpdater,
StatefulService<T>* statefulService,
AsyncWebServer* server,
const String& servicePath,
SecurityManager* securityManager,
AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN,
size_t bufferSize = DEFAULT_BUFFER_SIZE) :
HttpGetEndpoint<T>(jsonSerializer,
HttpGetEndpoint<T>(stateReader,
statefulService,
server,
servicePath,
securityManager,
authenticationPredicate,
bufferSize),
HttpPostEndpoint<T>(jsonSerializer,
jsonDeserializer,
HttpPostEndpoint<T>(stateReader,
stateUpdater,
statefulService,
server,
servicePath,
@ -155,14 +151,14 @@ class HttpEndpoint : public HttpGetEndpoint<T>, public HttpPostEndpoint<T> {
bufferSize) {
}
HttpEndpoint(JsonSerializer<T> jsonSerializer,
JsonDeserializer<T> jsonDeserializer,
HttpEndpoint(JsonStateReader<T> stateReader,
JsonStateUpdater<T> stateUpdater,
StatefulService<T>* statefulService,
AsyncWebServer* server,
const String& servicePath,
size_t bufferSize = DEFAULT_BUFFER_SIZE) :
HttpGetEndpoint<T>(jsonSerializer, statefulService, server, servicePath, bufferSize),
HttpPostEndpoint<T>(jsonSerializer, jsonDeserializer, statefulService, server, servicePath, bufferSize) {
HttpGetEndpoint<T>(stateReader, statefulService, server, servicePath, bufferSize),
HttpPostEndpoint<T>(stateReader, stateUpdater, statefulService, server, servicePath, bufferSize) {
}
};

View File

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

View File

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

View File

@ -2,8 +2,6 @@
#define MqttPubSub_h
#include <StatefulService.h>
#include <JsonSerializer.h>
#include <JsonDeserializer.h>
#include <AsyncMqttClient.h>
#define MQTT_ORIGIN_ID "mqtt"
@ -31,12 +29,12 @@ class MqttConnector {
template <class T>
class MqttPub : virtual public MqttConnector<T> {
public:
MqttPub(JsonSerializer<T> jsonSerializer,
MqttPub(JsonStateReader<T> stateReader,
StatefulService<T>* statefulService,
AsyncMqttClient* mqttClient,
const String& pubTopic = "",
size_t bufferSize = DEFAULT_BUFFER_SIZE) :
MqttConnector<T>(statefulService, mqttClient, bufferSize), _jsonSerializer(jsonSerializer), _pubTopic(pubTopic) {
MqttConnector<T>(statefulService, mqttClient, bufferSize), _stateReader(stateReader), _pubTopic(pubTopic) {
MqttConnector<T>::_statefulService->addUpdateHandler([&](const String& originId) { publish(); }, false);
}
@ -51,7 +49,7 @@ class MqttPub : virtual public MqttConnector<T> {
}
private:
JsonSerializer<T> _jsonSerializer;
JsonStateReader<T> _stateReader;
String _pubTopic;
void publish() {
@ -59,7 +57,7 @@ class MqttPub : virtual public MqttConnector<T> {
// serialize to json doc
DynamicJsonDocument json(MqttConnector<T>::_bufferSize);
JsonObject jsonObject = json.to<JsonObject>();
MqttConnector<T>::_statefulService->read(jsonObject, _jsonSerializer);
MqttConnector<T>::_statefulService->read(jsonObject, _stateReader);
// serialize to string
String payload;
@ -74,14 +72,12 @@ class MqttPub : virtual public MqttConnector<T> {
template <class T>
class MqttSub : virtual public MqttConnector<T> {
public:
MqttSub(JsonDeserializer<T> jsonDeserializer,
MqttSub(JsonStateUpdater<T> stateUpdater,
StatefulService<T>* statefulService,
AsyncMqttClient* mqttClient,
const String& subTopic = "",
size_t bufferSize = DEFAULT_BUFFER_SIZE) :
MqttConnector<T>(statefulService, mqttClient, bufferSize),
_jsonDeserializer(jsonDeserializer),
_subTopic(subTopic) {
MqttConnector<T>(statefulService, mqttClient, bufferSize), _stateUpdater(stateUpdater), _subTopic(subTopic) {
MqttConnector<T>::_mqttClient->onMessage(std::bind(&MqttSub::onMqttMessage,
this,
std::placeholders::_1,
@ -110,7 +106,7 @@ class MqttSub : virtual public MqttConnector<T> {
}
private:
JsonDeserializer<T> _jsonDeserializer;
JsonStateUpdater<T> _stateUpdater;
String _subTopic;
void subscribe() {
@ -135,7 +131,7 @@ class MqttSub : virtual public MqttConnector<T> {
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);
MqttConnector<T>::_statefulService->update(jsonObject, _stateUpdater, MQTT_ORIGIN_ID);
}
}
};
@ -143,16 +139,16 @@ class MqttSub : virtual public MqttConnector<T> {
template <class T>
class MqttPubSub : public MqttPub<T>, public MqttSub<T> {
public:
MqttPubSub(JsonSerializer<T> jsonSerializer,
JsonDeserializer<T> jsonDeserializer,
MqttPubSub(JsonStateReader<T> stateReader,
JsonStateUpdater<T> stateUpdater,
StatefulService<T>* statefulService,
AsyncMqttClient* mqttClient,
const String& pubTopic = "",
const String& subTopic = "",
size_t bufferSize = DEFAULT_BUFFER_SIZE) :
MqttConnector<T>(statefulService, mqttClient, bufferSize),
MqttPub<T>(jsonSerializer, statefulService, mqttClient, pubTopic, bufferSize),
MqttSub<T>(jsonDeserializer, statefulService, mqttClient, subTopic, bufferSize) {
MqttPub<T>(stateReader, statefulService, mqttClient, pubTopic, bufferSize),
MqttSub<T>(stateUpdater, statefulService, mqttClient, subTopic, bufferSize) {
}
public:

View File

@ -21,13 +21,8 @@ static char* retainCstr(const char* cstr, char** 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),
_httpEndpoint(MqttSettings::read, MqttSettings::update, this, server, MQTT_SETTINGS_SERVICE_PATH, securityManager),
_fsPersistence(MqttSettings::read, MqttSettings::update, this, fs, MQTT_SETTINGS_FILE),
_retainedHost(nullptr),
_retainedClientId(nullptr),
_retainedUsername(nullptr),

View File

@ -75,7 +75,7 @@ class MqttSettings {
bool cleanSession;
uint16_t maxTopicLength;
static void serialize(MqttSettings& settings, JsonObject& root) {
static void read(MqttSettings& settings, JsonObject& root) {
root["enabled"] = settings.enabled;
root["host"] = settings.host;
root["port"] = settings.port;
@ -87,7 +87,7 @@ class MqttSettings {
root["max_topic_length"] = settings.maxTopicLength;
}
static void deserialize(JsonObject& root, MqttSettings& settings) {
static StateUpdateResult update(JsonObject& root, MqttSettings& settings) {
settings.enabled = root["enabled"] | FACTORY_MQTT_ENABLED;
settings.host = root["host"] | FACTORY_MQTT_HOST;
settings.port = root["port"] | FACTORY_MQTT_PORT;
@ -97,6 +97,7 @@ class MqttSettings {
settings.keepAlive = root["keep_alive"] | FACTORY_MQTT_KEEP_ALIVE;
settings.cleanSession = root["clean_session"] | FACTORY_MQTT_CLEAN_SESSION;
settings.maxTopicLength = root["max_topic_length"] | FACTORY_MQTT_MAX_TOPIC_LENGTH;
return StateUpdateResult::CHANGED;
}
};

View File

@ -1,13 +1,8 @@
#include <NTPSettingsService.h>
NTPSettingsService::NTPSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) :
_httpEndpoint(NTPSettings::serialize,
NTPSettings::deserialize,
this,
server,
NTP_SETTINGS_SERVICE_PATH,
securityManager),
_fsPersistence(NTPSettings::serialize, NTPSettings::deserialize, this, fs, NTP_SETTINGS_FILE) {
_httpEndpoint(NTPSettings::read, NTPSettings::update, this, server, NTP_SETTINGS_SERVICE_PATH, securityManager),
_fsPersistence(NTPSettings::read, NTPSettings::update, this, fs, NTP_SETTINGS_FILE) {
#ifdef ESP32
WiFi.onEvent(
std::bind(&NTPSettingsService::onStationModeDisconnected, this, std::placeholders::_1, std::placeholders::_2),

View File

@ -37,18 +37,19 @@ class NTPSettings {
String tzFormat;
String server;
static void serialize(NTPSettings& settings, JsonObject& root) {
static void read(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) {
static StateUpdateResult update(JsonObject& root, NTPSettings& settings) {
settings.enabled = root["enabled"] | FACTORY_NTP_ENABLED;
settings.server = root["server"] | FACTORY_NTP_SERVER;
settings.tzLabel = root["tz_label"] | FACTORY_NTP_TIME_ZONE_LABEL;
settings.tzFormat = root["tz_format"] | FACTORY_NTP_TIME_ZONE_FORMAT;
return StateUpdateResult::CHANGED;
}
};

View File

@ -1,13 +1,8 @@
#include <OTASettingsService.h>
OTASettingsService::OTASettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) :
_httpEndpoint(OTASettings::serialize,
OTASettings::deserialize,
this,
server,
OTA_SETTINGS_SERVICE_PATH,
securityManager),
_fsPersistence(OTASettings::serialize, OTASettings::deserialize, this, fs, OTA_SETTINGS_FILE),
_httpEndpoint(OTASettings::read, OTASettings::update, this, server, OTA_SETTINGS_SERVICE_PATH, securityManager),
_fsPersistence(OTASettings::read, OTASettings::update, this, fs, OTA_SETTINGS_FILE),
_arduinoOTA(nullptr) {
#ifdef ESP32
WiFi.onEvent(std::bind(&OTASettingsService::onStationModeGotIP, this, std::placeholders::_1, std::placeholders::_2),

View File

@ -34,16 +34,17 @@ class OTASettings {
int port;
String password;
static void serialize(OTASettings& settings, JsonObject& root) {
static void read(OTASettings& settings, JsonObject& root) {
root["enabled"] = settings.enabled;
root["port"] = settings.port;
root["password"] = settings.password;
}
static void deserialize(JsonObject& root, OTASettings& settings) {
static StateUpdateResult update(JsonObject& root, OTASettings& settings) {
settings.enabled = root["enabled"] | FACTORY_OTA_ENABLED;
settings.port = root["port"] | FACTORY_OTA_PORT;
settings.password = root["password"] | FACTORY_OTA_PASSWORD;
return StateUpdateResult::CHANGED;
}
};

View File

@ -1,13 +1,8 @@
#include <SecuritySettingsService.h>
SecuritySettingsService::SecuritySettingsService(AsyncWebServer* server, FS* fs) :
_httpEndpoint(SecuritySettings::serialize,
SecuritySettings::deserialize,
this,
server,
SECURITY_SETTINGS_PATH,
this),
_fsPersistence(SecuritySettings::serialize, SecuritySettings::deserialize, this, fs, SECURITY_SETTINGS_FILE),
_httpEndpoint(SecuritySettings::read, SecuritySettings::update, this, server, SECURITY_SETTINGS_PATH, this),
_fsPersistence(SecuritySettings::read, SecuritySettings::update, this, fs, SECURITY_SETTINGS_FILE),
_jwtHandler(FACTORY_JWT_SECRET) {
addUpdateHandler([&](const String& originId) { configureJWTHandler(); }, false);
}

View File

@ -29,7 +29,7 @@ class SecuritySettings {
String jwtSecret;
std::list<User> users;
static void serialize(SecuritySettings& settings, JsonObject& root) {
static void read(SecuritySettings& settings, JsonObject& root) {
// secret
root["jwt_secret"] = settings.jwtSecret;
@ -43,7 +43,7 @@ class SecuritySettings {
}
}
static void deserialize(JsonObject& root, SecuritySettings& settings) {
static StateUpdateResult update(JsonObject& root, SecuritySettings& settings) {
// secret
settings.jwtSecret = root["jwt_secret"] | FACTORY_JWT_SECRET;
@ -57,6 +57,7 @@ class SecuritySettings {
settings.users.push_back(User(FACTORY_ADMIN_USERNAME, FACTORY_ADMIN_PASSWORD, true));
settings.users.push_back(User(FACTORY_GUEST_USERNAME, FACTORY_GUEST_PASSWORD, false));
}
return StateUpdateResult::CHANGED;
}
};

View File

@ -2,8 +2,7 @@
#define StatefulService_h
#include <Arduino.h>
#include <JsonDeserializer.h>
#include <JsonSerializer.h>
#include <ArduinoJson.h>
#include <list>
#include <functional>
@ -16,6 +15,18 @@
#define DEFAULT_BUFFER_SIZE 1024
#endif
enum class StateUpdateResult {
CHANGED = 0, // The update changed the state and propagation should take place if required
UNCHANGED, // The state was unchanged, propagation should not take place
ERROR // There was a problem updating the state, propagation should not take place
};
template <class T>
using JsonStateUpdater = StateUpdateResult (*)(JsonObject& root, T& settings);
template <class T>
using JsonStateReader = void (*)(T& settings, JsonObject& root);
typedef size_t update_handler_id_t;
typedef std::function<void(const String& originId)> StateUpdateCallback;
@ -60,66 +71,50 @@ class StatefulService {
}
}
void updateWithoutPropagation(std::function<void(T&)> callback) {
#ifdef ESP32
xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY);
#endif
callback(_state);
#ifdef ESP32
xSemaphoreGiveRecursive(_accessMutex);
#endif
StateUpdateResult update(std::function<StateUpdateResult(T&)> stateUpdater, const String& originId) {
beginTransaction();
StateUpdateResult result = stateUpdater(_state);
endTransaction();
if (result == StateUpdateResult::CHANGED) {
callUpdateHandlers(originId);
}
return result;
}
void updateWithoutPropagation(JsonObject& jsonObject, JsonDeserializer<T> deserializer) {
#ifdef ESP32
xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY);
#endif
deserializer(jsonObject, _state);
#ifdef ESP32
xSemaphoreGiveRecursive(_accessMutex);
#endif
StateUpdateResult updateWithoutPropagation(std::function<StateUpdateResult(T&)> stateUpdater) {
beginTransaction();
StateUpdateResult result = stateUpdater(_state);
endTransaction();
return result;
}
void update(std::function<void(T&)> callback, const String& originId) {
#ifdef ESP32
xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY);
#endif
callback(_state);
callUpdateHandlers(originId);
#ifdef ESP32
xSemaphoreGiveRecursive(_accessMutex);
#endif
StateUpdateResult update(JsonObject& jsonObject, JsonStateUpdater<T> stateUpdater, const String& originId) {
beginTransaction();
StateUpdateResult result = stateUpdater(jsonObject, _state);
endTransaction();
if (result == StateUpdateResult::CHANGED) {
callUpdateHandlers(originId);
}
return result;
}
void update(JsonObject& jsonObject, JsonDeserializer<T> deserializer, const String& originId) {
#ifdef ESP32
xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY);
#endif
deserializer(jsonObject, _state);
callUpdateHandlers(originId);
#ifdef ESP32
xSemaphoreGiveRecursive(_accessMutex);
#endif
StateUpdateResult updateWithoutPropagation(JsonObject& jsonObject, JsonStateUpdater<T> stateUpdater) {
beginTransaction();
StateUpdateResult result = stateUpdater(jsonObject, _state);
endTransaction();
return result;
}
void read(std::function<void(T&)> callback) {
#ifdef ESP32
xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY);
#endif
callback(_state);
#ifdef ESP32
xSemaphoreGiveRecursive(_accessMutex);
#endif
void read(std::function<void(T&)> stateReader) {
beginTransaction();
stateReader(_state);
endTransaction();
}
void read(JsonObject& jsonObject, JsonSerializer<T> serializer) {
#ifdef ESP32
xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY);
#endif
serializer(_state, jsonObject);
#ifdef ESP32
xSemaphoreGiveRecursive(_accessMutex);
#endif
void read(JsonObject& jsonObject, JsonStateReader<T> stateReader) {
beginTransaction();
stateReader(_state, jsonObject);
endTransaction();
}
void callUpdateHandlers(const String& originId) {
@ -131,6 +126,18 @@ class StatefulService {
protected:
T _state;
inline void beginTransaction() {
#ifdef ESP32
xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY);
#endif
}
inline void endTransaction() {
#ifdef ESP32
xSemaphoreGiveRecursive(_accessMutex);
#endif
}
private:
#ifdef ESP32
SemaphoreHandle_t _accessMutex;

View File

@ -2,8 +2,6 @@
#define WebSocketTxRx_h
#include <StatefulService.h>
#include <JsonSerializer.h>
#include <JsonDeserializer.h>
#include <ESPAsyncWebServer.h>
#define WEB_SOCKET_CLIENT_ID_MSG_SIZE 128
@ -75,7 +73,7 @@ class WebSocketConnector {
template <class T>
class WebSocketTx : virtual public WebSocketConnector<T> {
public:
WebSocketTx(JsonSerializer<T> jsonSerializer,
WebSocketTx(JsonStateReader<T> stateReader,
StatefulService<T>* statefulService,
AsyncWebServer* server,
char const* webSocketPath,
@ -88,19 +86,19 @@ class WebSocketTx : virtual public WebSocketConnector<T> {
securityManager,
authenticationPredicate,
bufferSize),
_jsonSerializer(jsonSerializer) {
_stateReader(stateReader) {
WebSocketConnector<T>::_statefulService->addUpdateHandler(
[&](const String& originId) { transmitData(nullptr, originId); }, false);
}
WebSocketTx(JsonSerializer<T> jsonSerializer,
WebSocketTx(JsonStateReader<T> stateReader,
StatefulService<T>* statefulService,
AsyncWebServer* server,
char const* webSocketPath,
size_t bufferSize = DEFAULT_BUFFER_SIZE) :
WebSocketConnector<T>(statefulService, server, webSocketPath, bufferSize), _jsonSerializer(jsonSerializer) {
WebSocketConnector<T>::_statefulService->addUpdateHandler([&](const String& originId) { transmitData(nullptr, originId); },
false);
WebSocketConnector<T>(statefulService, server, webSocketPath, bufferSize), _stateReader(stateReader) {
WebSocketConnector<T>::_statefulService->addUpdateHandler(
[&](const String& originId) { transmitData(nullptr, originId); }, false);
}
protected:
@ -118,7 +116,7 @@ class WebSocketTx : virtual public WebSocketConnector<T> {
}
private:
JsonSerializer<T> _jsonSerializer;
JsonStateReader<T> _stateReader;
void transmitId(AsyncWebSocketClient* client) {
DynamicJsonDocument jsonDocument = DynamicJsonDocument(WEB_SOCKET_CLIENT_ID_MSG_SIZE);
@ -146,7 +144,7 @@ class WebSocketTx : virtual public WebSocketConnector<T> {
root["type"] = "payload";
root["origin_id"] = originId;
JsonObject payload = root.createNestedObject("payload");
WebSocketConnector<T>::_statefulService->read(payload, _jsonSerializer);
WebSocketConnector<T>::_statefulService->read(payload, _stateReader);
size_t len = measureJson(jsonDocument);
AsyncWebSocketMessageBuffer* buffer = WebSocketConnector<T>::_webSocket.makeBuffer(len);
@ -164,7 +162,7 @@ class WebSocketTx : virtual public WebSocketConnector<T> {
template <class T>
class WebSocketRx : virtual public WebSocketConnector<T> {
public:
WebSocketRx(JsonDeserializer<T> jsonDeserializer,
WebSocketRx(JsonStateUpdater<T> stateUpdater,
StatefulService<T>* statefulService,
AsyncWebServer* server,
char const* webSocketPath,
@ -177,15 +175,15 @@ class WebSocketRx : virtual public WebSocketConnector<T> {
securityManager,
authenticationPredicate,
bufferSize),
_jsonDeserializer(jsonDeserializer) {
_stateUpdater(stateUpdater) {
}
WebSocketRx(JsonDeserializer<T> jsonDeserializer,
WebSocketRx(JsonStateUpdater<T> stateUpdater,
StatefulService<T>* statefulService,
AsyncWebServer* server,
char const* webSocketPath,
size_t bufferSize = DEFAULT_BUFFER_SIZE) :
WebSocketConnector<T>(statefulService, server, webSocketPath, bufferSize), _jsonDeserializer(jsonDeserializer) {
WebSocketConnector<T>(statefulService, server, webSocketPath, bufferSize), _stateUpdater(stateUpdater) {
}
protected:
@ -204,7 +202,7 @@ class WebSocketRx : virtual public WebSocketConnector<T> {
if (!error && jsonDocument.is<JsonObject>()) {
JsonObject jsonObject = jsonDocument.as<JsonObject>();
WebSocketConnector<T>::_statefulService->update(
jsonObject, _jsonDeserializer, WebSocketConnector<T>::clientId(client));
jsonObject, _stateUpdater, WebSocketConnector<T>::clientId(client));
}
}
}
@ -212,14 +210,14 @@ class WebSocketRx : virtual public WebSocketConnector<T> {
}
private:
JsonDeserializer<T> _jsonDeserializer;
JsonStateUpdater<T> _stateUpdater;
};
template <class T>
class WebSocketTxRx : public WebSocketTx<T>, public WebSocketRx<T> {
public:
WebSocketTxRx(JsonSerializer<T> jsonSerializer,
JsonDeserializer<T> jsonDeserializer,
WebSocketTxRx(JsonStateReader<T> stateReader,
JsonStateUpdater<T> stateUpdater,
StatefulService<T>* statefulService,
AsyncWebServer* server,
char const* webSocketPath,
@ -232,14 +230,14 @@ class WebSocketTxRx : public WebSocketTx<T>, public WebSocketRx<T> {
securityManager,
authenticationPredicate,
bufferSize),
WebSocketTx<T>(jsonSerializer,
WebSocketTx<T>(stateReader,
statefulService,
server,
webSocketPath,
securityManager,
authenticationPredicate,
bufferSize),
WebSocketRx<T>(jsonDeserializer,
WebSocketRx<T>(stateUpdater,
statefulService,
server,
webSocketPath,
@ -248,15 +246,15 @@ class WebSocketTxRx : public WebSocketTx<T>, public WebSocketRx<T> {
bufferSize) {
}
WebSocketTxRx(JsonSerializer<T> jsonSerializer,
JsonDeserializer<T> jsonDeserializer,
WebSocketTxRx(JsonStateReader<T> stateReader,
JsonStateUpdater<T> stateUpdater,
StatefulService<T>* statefulService,
AsyncWebServer* server,
char const* webSocketPath,
size_t bufferSize = DEFAULT_BUFFER_SIZE) :
WebSocketConnector<T>(statefulService, server, webSocketPath, bufferSize),
WebSocketTx<T>(jsonSerializer, statefulService, server, webSocketPath, bufferSize),
WebSocketRx<T>(jsonDeserializer, statefulService, server, webSocketPath, bufferSize) {
WebSocketTx<T>(stateReader, statefulService, server, webSocketPath, bufferSize),
WebSocketRx<T>(stateUpdater, statefulService, server, webSocketPath, bufferSize) {
}
protected:

View File

@ -1,13 +1,8 @@
#include <WiFiSettingsService.h>
WiFiSettingsService::WiFiSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) :
_httpEndpoint(WiFiSettings::serialize,
WiFiSettings::deserialize,
this,
server,
WIFI_SETTINGS_SERVICE_PATH,
securityManager),
_fsPersistence(WiFiSettings::serialize, WiFiSettings::deserialize, this, fs, WIFI_SETTINGS_FILE),
_httpEndpoint(WiFiSettings::read, WiFiSettings::update, this, server, WIFI_SETTINGS_SERVICE_PATH, securityManager),
_fsPersistence(WiFiSettings::read, WiFiSettings::update, this, fs, WIFI_SETTINGS_FILE),
_lastConnectionAttempt(0) {
// 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.

View File

@ -37,7 +37,7 @@ class WiFiSettings {
IPAddress dnsIP1;
IPAddress dnsIP2;
static void serialize(WiFiSettings& settings, JsonObject& root) {
static void read(WiFiSettings& settings, JsonObject& root) {
// connection settings
root["ssid"] = settings.ssid;
root["password"] = settings.password;
@ -52,7 +52,7 @@ class WiFiSettings {
JsonUtils::writeIP(root, "dns_ip_2", settings.dnsIP2);
}
static void deserialize(JsonObject& root, WiFiSettings& settings) {
static StateUpdateResult update(JsonObject& root, WiFiSettings& settings) {
settings.ssid = root["ssid"] | FACTORY_WIFI_SSID;
settings.password = root["password"] | FACTORY_WIFI_PASSWORD;
settings.hostname = root["hostname"] | FACTORY_WIFI_HOSTNAME;
@ -78,6 +78,7 @@ class WiFiSettings {
(settings.localIP == INADDR_NONE || settings.gatewayIP == INADDR_NONE || settings.subnetMask == INADDR_NONE)) {
settings.staticIPConfig = false;
}
return StateUpdateResult::CHANGED;
}
};

View File

@ -1,14 +1,14 @@
#include <LightMqttSettingsService.h>
LightMqttSettingsService::LightMqttSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) :
_httpEndpoint(LightMqttSettings::serialize,
LightMqttSettings::deserialize,
_httpEndpoint(LightMqttSettings::read,
LightMqttSettings::update,
this,
server,
LIGHT_BROKER_SETTINGS_PATH,
securityManager,
AuthenticationPredicates::IS_AUTHENTICATED),
_fsPersistence(LightMqttSettings::serialize, LightMqttSettings::deserialize, this, fs, LIGHT_BROKER_SETTINGS_FILE) {
_fsPersistence(LightMqttSettings::read, LightMqttSettings::update, this, fs, LIGHT_BROKER_SETTINGS_FILE) {
}
void LightMqttSettingsService::begin() {

View File

@ -14,16 +14,17 @@ class LightMqttSettings {
String name;
String uniqueId;
static void serialize(LightMqttSettings& settings, JsonObject& root) {
static void read(LightMqttSettings& settings, JsonObject& root) {
root["mqtt_path"] = settings.mqttPath;
root["name"] = settings.name;
root["unique_id"] = settings.uniqueId;
}
static void deserialize(JsonObject& root, LightMqttSettings& settings) {
static StateUpdateResult update(JsonObject& root, LightMqttSettings& settings) {
settings.mqttPath = root["mqtt_path"] | ESPUtils::defaultDeviceValue("homeassistant/light/");
settings.name = root["name"] | ESPUtils::defaultDeviceValue("light-");
settings.uniqueId = root["unique_id"] | ESPUtils::defaultDeviceValue("light-");
return StateUpdateResult::CHANGED;
}
};

View File

@ -4,16 +4,16 @@ LightStateService::LightStateService(AsyncWebServer* server,
SecurityManager* securityManager,
AsyncMqttClient* mqttClient,
LightMqttSettingsService* lightMqttSettingsService) :
_httpEndpoint(LightState::serialize,
LightState::deserialize,
_httpEndpoint(LightState::read,
LightState::update,
this,
server,
LIGHT_SETTINGS_ENDPOINT_PATH,
securityManager,
AuthenticationPredicates::IS_AUTHENTICATED),
_mqttPubSub(LightState::haSerialize, LightState::haDeserialize, this, mqttClient),
_webSocket(LightState::serialize,
LightState::deserialize,
_mqttPubSub(LightState::haRead, LightState::haUpdate, this, mqttClient),
_webSocket(LightState::read,
LightState::update,
this,
server,
LIGHT_SETTINGS_SOCKET_PATH,
@ -40,6 +40,7 @@ void LightStateService::begin() {
}
void LightStateService::onConfigUpdated() {
Serial.printf_P(PSTR("The light is now: %s\r\n"), _state.ledOn ? "on" : "off");
digitalWrite(BLINK_LED, _state.ledOn ? LED_ON : LED_OFF);
}

View File

@ -31,21 +31,38 @@ class LightState {
public:
bool ledOn;
static void serialize(LightState& settings, JsonObject& root) {
static void read(LightState& settings, JsonObject& root) {
root["led_on"] = settings.ledOn;
}
static void deserialize(JsonObject& root, LightState& settings) {
settings.ledOn = root["led_on"] | DEFAULT_LED_STATE;
static StateUpdateResult update(JsonObject& root, LightState& lightState) {
boolean newState = root["led_on"] | DEFAULT_LED_STATE;
if (lightState.ledOn != newState) {
lightState.ledOn = newState;
return StateUpdateResult::CHANGED;
}
return StateUpdateResult::UNCHANGED;
}
static void haSerialize(LightState& settings, JsonObject& root) {
static void haRead(LightState& settings, JsonObject& root) {
root["state"] = settings.ledOn ? ON_STATE : OFF_STATE;
}
static void haDeserialize(JsonObject& root, LightState& settings) {
static StateUpdateResult haUpdate(JsonObject& root, LightState& lightState) {
String state = root["state"];
settings.ledOn = strcmp(ON_STATE, state.c_str()) ? false : true;
// parse new led state
boolean newState = false;
if (state.equals(ON_STATE)) {
newState = true;
} else if (!state.equals(OFF_STATE)) {
return StateUpdateResult::ERROR;
}
// change the new state, if required
if (lightState.ledOn != newState) {
lightState.ledOn = newState;
return StateUpdateResult::CHANGED;
}
return StateUpdateResult::UNCHANGED;
}
};