Skip to content

Commit 0d39c5c

Browse files
authored
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
1 parent d9ae0f5 commit 0d39c5c

24 files changed

+253
-258
lines changed

README.md

+43-20
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,9 @@ The following diagram visualises how the framework's modular components fit toge
350350

351351
#### Stateful service
352352

353-
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:
353+
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.
354+
355+
Here is a simple example of a state class and a StatefulService to manage it:
354356

355357
```cpp
356358
class LightState {
@@ -369,15 +371,16 @@ You may listen for changes to state by registering an update handler callback. I
369371
// register an update handler
370372
update_handler_id_t myUpdateHandler = lightStateService.addUpdateHandler(
371373
[&](const String& originId) {
372-
Serial.println("The light's state has been updated");
374+
Serial.print("The light's state has been updated by: ");
375+
Serial.println(originId);
373376
}
374377
);
375378
376379
// remove the update handler
377380
lightStateService.removeUpdateHandler(myUpdateHandler);
378381
```
379382

380-
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:
383+
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:
381384

382385
Origin | Description
383386
--------------------- | -----------
@@ -393,17 +396,35 @@ lightStateService.read([&](LightState& state) {
393396
});
394397
```
395398

396-
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":
399+
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:
397400

398401
```cpp
399402
lightStateService.update([&](LightState& state) {
400-
state.on = true; // turn on the lights!
403+
if (state.on) {
404+
return StateUpdateResult::UNCHANGED; // lights were already on, return UNCHANGED
405+
}
406+
state.on = true; // turn on the lights
407+
return StateUpdateResult::CHANGED; // notify StatefulService by returning CHANGED
401408
}, "timer");
402409
```
403410

411+
There are three possible return values for an update function which are as follows:
412+
413+
Origin | Description
414+
----------------------------- | ---------------------------------------------------------------------------
415+
StateUpdateResult::CHANGED | The update changed the state, propagation should take place if required
416+
StateUpdateResult::UNCHANGED | The state was unchanged, propagation should not take place
417+
StateUpdateResult::ERROR | There was an error updating the state, propagation should not take place
418+
404419
#### Serialization
405420

406-
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.
421+
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:
422+
423+
Callback | Signature | Purpose
424+
---------------- | -------------------------------------------------------- | ---------------------------------------------------------------------------------
425+
JsonStateReader | void read(T& settings, JsonObject& root) | Reading the state object into a JsonObject
426+
JsonStateUpdater | StateUpdateResult update(JsonObject& root, T& settings) | Updating the state from a JsonObject, returning the appropriate StateUpdateResult
427+
407428

408429
The static functions below can be used to facilitate the serialization/deserialization of the light state:
409430

@@ -413,32 +434,33 @@ class LightState {
413434
bool on = false;
414435
uint8_t brightness = 255;
415436

416-
static void serialize(LightState& state, JsonObject& root) {
437+
static void read(LightState& state, JsonObject& root) {
417438
root["on"] = state.on;
418439
root["brightness"] = state.brightness;
419440
}
420441

421-
static void deserialize(JsonObject& root, LightState& state) {
442+
static StateUpdateResult update(JsonObject& root, LightState& state) {
422443
state.on = root["on"] | false;
423444
state.brightness = root["brightness"] | 255;
445+
return StateUpdateResult::CHANGED;
424446
}
425447
};
426448
```
427449
428450
For convenience, the StatefulService class provides overloads of its `update` and `read` functions which utilize these functions.
429451
430-
Copy the state to a JsonObject using a serializer:
452+
Read the state to a JsonObject using a serializer:
431453
432454
```cpp
433455
JsonObject jsonObject = jsonDocument.to<JsonObject>();
434-
lightStateService->read(jsonObject, serializer);
456+
lightStateService->read(jsonObject, LightState::read);
435457
```
436458

437459
Update the state from a JsonObject using a deserializer:
438460

439461
```cpp
440462
JsonObject jsonObject = jsonDocument.as<JsonObject>();
441-
lightStateService->update(jsonObject, deserializer, "timer");
463+
lightStateService->update(jsonObject, LightState::update, "timer");
442464
```
443465
444466
#### Endpoints
@@ -451,7 +473,7 @@ The code below demonstrates how to extend the LightStateService class to provide
451473
class LightStateService : public StatefulService<LightState> {
452474
public:
453475
LightStateService(AsyncWebServer* server) :
454-
_httpEndpoint(LightState::serialize, LightState::deserialize, this, server, "/rest/lightState") {
476+
_httpEndpoint(LightState::read, LightState::update, this, server, "/rest/lightState") {
455477
}
456478
457479
private:
@@ -471,7 +493,7 @@ The code below demonstrates how to extend the LightStateService class to provide
471493
class LightStateService : public StatefulService<LightState> {
472494
public:
473495
LightStateService(FS* fs) :
474-
_fsPersistence(LightState::serialize, LightState::deserialize, this, fs, "/config/lightState.json") {
496+
_fsPersistence(LightState::read, LightState::update, this, fs, "/config/lightState.json") {
475497
}
476498

477499
private:
@@ -489,7 +511,7 @@ The code below demonstrates how to extend the LightStateService class to provide
489511
class LightStateService : public StatefulService<LightState> {
490512
public:
491513
LightStateService(AsyncWebServer* server) :
492-
_webSocket(LightState::serialize, LightState::deserialize, this, server, "/ws/lightState"), {
514+
_webSocket(LightState::read, LightState::update, this, server, "/ws/lightState"), {
493515
}
494516
495517
private:
@@ -508,15 +530,16 @@ The framework includes an MQTT client which can be configured via the UI. MQTT r
508530
The code below demonstrates how to extend the LightStateService class to interface with MQTT:
509531

510532
```cpp
533+
511534
class LightStateService : public StatefulService<LightState> {
512535
public:
513536
LightStateService(AsyncMqttClient* mqttClient) :
514-
_mqttPubSub(LightState::serialize,
515-
LightState::deserialize,
516-
this,
517-
mqttClient,
518-
"homeassistant/light/my_light/set",
519-
"homeassistant/light/my_light/state") {
537+
_mqttPubSub(LightState::read,
538+
LightState::update,
539+
this,
540+
mqttClient,
541+
"homeassistant/light/my_light/set",
542+
"homeassistant/light/my_light/state") {
520543
}
521544

522545
private:

lib/framework/APSettingsService.cpp

+2-7
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
11
#include <APSettingsService.h>
22

33
APSettingsService::APSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) :
4-
_httpEndpoint(APSettings::serialize,
5-
APSettings::deserialize,
6-
this,
7-
server,
8-
AP_SETTINGS_SERVICE_PATH,
9-
securityManager),
10-
_fsPersistence(APSettings::serialize, APSettings::deserialize, this, fs, AP_SETTINGS_FILE),
4+
_httpEndpoint(APSettings::read, APSettings::update, this, server, AP_SETTINGS_SERVICE_PATH, securityManager),
5+
_fsPersistence(APSettings::read, APSettings::update, this, fs, AP_SETTINGS_FILE),
116
_dnsServer(nullptr),
127
_lastManaged(0) {
138
addUpdateHandler([&](const String& originId) { reconfigureAP(); }, false);

lib/framework/APSettingsService.h

+3-2
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,13 @@ class APSettings {
3636
String ssid;
3737
String password;
3838

39-
static void serialize(APSettings& settings, JsonObject& root) {
39+
static void read(APSettings& settings, JsonObject& root) {
4040
root["provision_mode"] = settings.provisionMode;
4141
root["ssid"] = settings.ssid;
4242
root["password"] = settings.password;
4343
}
4444

45-
static void deserialize(JsonObject& root, APSettings& settings) {
45+
static StateUpdateResult update(JsonObject& root, APSettings& settings) {
4646
settings.provisionMode = root["provision_mode"] | FACTORY_AP_PROVISION_MODE;
4747
switch (settings.provisionMode) {
4848
case AP_MODE_ALWAYS:
@@ -54,6 +54,7 @@ class APSettings {
5454
}
5555
settings.ssid = root["ssid"] | FACTORY_AP_SSID;
5656
settings.password = root["password"] | FACTORY_AP_PASSWORD;
57+
return StateUpdateResult::CHANGED;
5758
}
5859
};
5960

lib/framework/FSPersistence.h

+11-13
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,19 @@
22
#define FSPersistence_h
33

44
#include <StatefulService.h>
5-
#include <JsonSerializer.h>
6-
#include <JsonDeserializer.h>
75
#include <FS.h>
86

97
template <class T>
108
class FSPersistence {
119
public:
12-
FSPersistence(JsonSerializer<T> jsonSerializer,
13-
JsonDeserializer<T> jsonDeserializer,
10+
FSPersistence(JsonStateReader<T> stateReader,
11+
JsonStateUpdater<T> stateUpdater,
1412
StatefulService<T>* statefulService,
1513
FS* fs,
1614
char const* filePath,
1715
size_t bufferSize = DEFAULT_BUFFER_SIZE) :
18-
_jsonSerializer(jsonSerializer),
19-
_jsonDeserializer(jsonDeserializer),
16+
_stateReader(stateReader),
17+
_stateUpdater(stateUpdater),
2018
_statefulService(statefulService),
2119
_fs(fs),
2220
_filePath(filePath),
@@ -33,7 +31,7 @@ class FSPersistence {
3331
DeserializationError error = deserializeJson(jsonDocument, settingsFile);
3432
if (error == DeserializationError::Ok && jsonDocument.is<JsonObject>()) {
3533
JsonObject jsonObject = jsonDocument.as<JsonObject>();
36-
_statefulService->updateWithoutPropagation(jsonObject, _jsonDeserializer);
34+
_statefulService->updateWithoutPropagation(jsonObject, _stateUpdater);
3735
settingsFile.close();
3836
return;
3937
}
@@ -49,7 +47,7 @@ class FSPersistence {
4947
// create and populate a new json object
5048
DynamicJsonDocument jsonDocument = DynamicJsonDocument(_bufferSize);
5149
JsonObject jsonObject = jsonDocument.to<JsonObject>();
52-
_statefulService->read(jsonObject, _jsonSerializer);
50+
_statefulService->read(jsonObject, _stateReader);
5351

5452
// serialize it to filesystem
5553
File settingsFile = _fs->open(_filePath, "w");
@@ -79,21 +77,21 @@ class FSPersistence {
7977
}
8078

8179
private:
82-
JsonSerializer<T> _jsonSerializer;
83-
JsonDeserializer<T> _jsonDeserializer;
80+
JsonStateReader<T> _stateReader;
81+
JsonStateUpdater<T> _stateUpdater;
8482
StatefulService<T>* _statefulService;
85-
FS* _fs;
83+
FS* _fs;
8684
char const* _filePath;
8785
size_t _bufferSize;
8886
update_handler_id_t _updateHandlerId;
8987

9088
protected:
91-
// We assume the deserializer supplies sensible defaults if an empty object
89+
// We assume the updater supplies sensible defaults if an empty object
9290
// is supplied, this virtual function allows that to be changed.
9391
virtual void applyDefaults() {
9492
DynamicJsonDocument jsonDocument = DynamicJsonDocument(_bufferSize);
9593
JsonObject jsonObject = jsonDocument.as<JsonObject>();
96-
_statefulService->updateWithoutPropagation(jsonObject, _jsonDeserializer);
94+
_statefulService->updateWithoutPropagation(jsonObject, _stateUpdater);
9795
}
9896
};
9997

0 commit comments

Comments
 (0)