📦 Moves templates
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
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
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <PsychicHttp.h>
|
||||
#include <template/stateful_endpoint.h>
|
||||
|
||||
#include <functional>
|
||||
|
||||
#define HTTP_ENDPOINT_ORIGIN_ID "http"
|
||||
#define HTTPS_ENDPOINT_ORIGIN_ID "https"
|
||||
|
||||
template <class T>
|
||||
class StatefulHttpEndpoint {
|
||||
protected:
|
||||
JsonStateReader<T> _stateReader;
|
||||
JsonStateUpdater<T> _stateUpdater;
|
||||
StatefulService<T> *_statefulService;
|
||||
|
||||
public:
|
||||
StatefulHttpEndpoint(JsonStateReader<T> stateReader, JsonStateUpdater<T> stateUpdater,
|
||||
StatefulService<T> *statefulService)
|
||||
: _stateReader(stateReader), _stateUpdater(stateUpdater), _statefulService(statefulService) {}
|
||||
|
||||
esp_err_t handleStateUpdate(PsychicRequest *request, JsonVariant &json) {
|
||||
if (!json.is<JsonObject>()) {
|
||||
return request->reply(400);
|
||||
}
|
||||
|
||||
JsonObject jsonObject = json.as<JsonObject>();
|
||||
StateUpdateResult outcome = _statefulService->updateWithoutPropagation(jsonObject, _stateUpdater);
|
||||
|
||||
if (outcome == StateUpdateResult::ERROR) {
|
||||
return request->reply(400);
|
||||
} else if ((outcome == StateUpdateResult::CHANGED)) {
|
||||
// persist the changes to the FS
|
||||
_statefulService->callUpdateHandlers(HTTP_ENDPOINT_ORIGIN_ID);
|
||||
}
|
||||
|
||||
PsychicJsonResponse response = PsychicJsonResponse(request, false);
|
||||
jsonObject = response.getRoot();
|
||||
|
||||
_statefulService->read(jsonObject, _stateReader);
|
||||
|
||||
return response.send();
|
||||
}
|
||||
|
||||
esp_err_t getState(PsychicRequest *request) {
|
||||
PsychicJsonResponse response = PsychicJsonResponse(request, false);
|
||||
JsonObject jsonObject = response.getRoot();
|
||||
_statefulService->read(jsonObject, _stateReader);
|
||||
return response.send();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,129 @@
|
||||
#ifndef FSPersistence_h
|
||||
#define FSPersistence_h
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
* Copyright (C) 2024 runeharlyk
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <FS.h>
|
||||
#include <template/stateful_service.h>
|
||||
#include <filesystem.h>
|
||||
|
||||
template <class T>
|
||||
class FSPersistence {
|
||||
public:
|
||||
FSPersistence(JsonStateReader<T> stateReader, JsonStateUpdater<T> stateUpdater, StatefulService<T> *statefulService,
|
||||
const char *filePath)
|
||||
: _stateReader(stateReader),
|
||||
_stateUpdater(stateUpdater),
|
||||
_statefulService(statefulService),
|
||||
_filePath(filePath),
|
||||
_updateHandlerId(0) {
|
||||
enableUpdateHandler();
|
||||
}
|
||||
|
||||
void readFromFS() {
|
||||
File settingsFile = _fs->open(_filePath, "r");
|
||||
|
||||
if (settingsFile) {
|
||||
JsonDocument jsonDocument;
|
||||
DeserializationError error = deserializeJson(jsonDocument, settingsFile);
|
||||
if (error == DeserializationError::Ok && jsonDocument.is<JsonObject>()) {
|
||||
JsonObject jsonObject = jsonDocument.as<JsonObject>();
|
||||
_statefulService->updateWithoutPropagation(jsonObject, _stateUpdater);
|
||||
settingsFile.close();
|
||||
return;
|
||||
}
|
||||
settingsFile.close();
|
||||
}
|
||||
|
||||
// If we reach here we have not been successful in loading the config
|
||||
// and hard-coded defaults are now applied. The settings are then
|
||||
// written back to the file system so the defaults persist between
|
||||
// resets. This last step is required as in some cases defaults contain
|
||||
// randomly generated values which would otherwise be modified on reset.
|
||||
applyDefaults();
|
||||
writeToFS();
|
||||
}
|
||||
|
||||
bool writeToFS() {
|
||||
// create and populate a new json object
|
||||
JsonDocument jsonDocument;
|
||||
JsonObject jsonObject = jsonDocument.to<JsonObject>();
|
||||
_statefulService->read(jsonObject, _stateReader);
|
||||
|
||||
// make directories if required
|
||||
mkdirs();
|
||||
|
||||
// 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([&](const String &originId) { writeToFS(); });
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
JsonStateReader<T> _stateReader;
|
||||
JsonStateUpdater<T> _stateUpdater;
|
||||
StatefulService<T> *_statefulService;
|
||||
FS *_fs {&ESPFS};
|
||||
const char *_filePath;
|
||||
size_t _bufferSize;
|
||||
update_handler_id_t _updateHandlerId;
|
||||
|
||||
// We assume we have a _filePath with format
|
||||
// "/directory1/directory2/filename" We create a directory for each missing
|
||||
// parent
|
||||
void mkdirs() {
|
||||
String path(_filePath);
|
||||
int index = 0;
|
||||
while ((index = path.indexOf('/', index + 1)) != -1) {
|
||||
String segment = path.substring(0, index);
|
||||
if (!_fs->exists(segment)) {
|
||||
_fs->mkdir(segment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
// We assume the updater supplies sensible defaults if an empty object
|
||||
// is supplied, this virtual function allows that to be changed.
|
||||
virtual void applyDefaults() {
|
||||
JsonDocument jsonDocument;
|
||||
JsonObject jsonObject = jsonDocument.as<JsonObject>();
|
||||
_statefulService->updateWithoutPropagation(jsonObject, _stateUpdater);
|
||||
}
|
||||
};
|
||||
|
||||
#endif // end FSPersistence
|
||||
@@ -0,0 +1,176 @@
|
||||
#ifndef StatefulService_h
|
||||
#define StatefulService_h
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
* Copyright (C) 2024 runeharlyk
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
#include <list>
|
||||
#include <functional>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
|
||||
#include <template/state_result.h>
|
||||
|
||||
template <typename T>
|
||||
using JsonStateUpdater = std::function<StateUpdateResult(JsonObject &root, T &settings)>;
|
||||
|
||||
template <typename T>
|
||||
using JsonStateReader = std::function<void(T &settings, JsonObject &root)>;
|
||||
|
||||
typedef size_t update_handler_id_t;
|
||||
typedef size_t hook_handler_id_t;
|
||||
typedef std::function<void(const String &originId)> StateUpdateCallback;
|
||||
typedef std::function<void(const String &originId, StateUpdateResult &result)> StateHookCallback;
|
||||
|
||||
typedef struct StateUpdateHandlerInfo {
|
||||
static inline update_handler_id_t currentUpdatedHandlerId = 0;
|
||||
update_handler_id_t _id;
|
||||
StateUpdateCallback _cb;
|
||||
bool _allowRemove;
|
||||
StateUpdateHandlerInfo(StateUpdateCallback cb, bool allowRemove)
|
||||
: _id(++currentUpdatedHandlerId), _cb(cb), _allowRemove(allowRemove) {};
|
||||
} StateUpdateHandlerInfo_t;
|
||||
|
||||
typedef struct StateHookHandlerInfo {
|
||||
static inline hook_handler_id_t currentHookHandlerId = 0;
|
||||
hook_handler_id_t _id;
|
||||
StateHookCallback _cb;
|
||||
bool _allowRemove;
|
||||
StateHookHandlerInfo(StateHookCallback cb, bool allowRemove)
|
||||
: _id(++currentHookHandlerId), _cb(cb), _allowRemove(allowRemove) {};
|
||||
} StateHookHandlerInfo_t;
|
||||
|
||||
template <class T>
|
||||
class StatefulService {
|
||||
public:
|
||||
template <typename... Args>
|
||||
StatefulService(Args &&...args)
|
||||
: _state(std::forward<Args>(args)...), _accessMutex(xSemaphoreCreateRecursiveMutex()) {}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hook_handler_id_t addHookHandler(StateHookCallback cb, bool allowRemove = true) {
|
||||
if (!cb) {
|
||||
return 0;
|
||||
}
|
||||
StateHookHandlerInfo_t hookHandler(cb, allowRemove);
|
||||
_hookHandlers.push_back(hookHandler);
|
||||
return hookHandler._id;
|
||||
}
|
||||
|
||||
void removeHookHandler(hook_handler_id_t id) {
|
||||
for (auto i = _hookHandlers.begin(); i != _hookHandlers.end();) {
|
||||
if ((*i)._allowRemove && (*i)._id == id) {
|
||||
i = _hookHandlers.erase(i);
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StateUpdateResult update(std::function<StateUpdateResult(T &)> stateUpdater, const String &originId) {
|
||||
beginTransaction();
|
||||
StateUpdateResult result = stateUpdater(_state);
|
||||
endTransaction();
|
||||
callHookHandlers(originId, result);
|
||||
if (result == StateUpdateResult::CHANGED) {
|
||||
callUpdateHandlers(originId);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
StateUpdateResult updateWithoutPropagation(std::function<StateUpdateResult(T &)> stateUpdater) {
|
||||
beginTransaction();
|
||||
StateUpdateResult result = stateUpdater(_state);
|
||||
endTransaction();
|
||||
return result;
|
||||
}
|
||||
|
||||
StateUpdateResult update(JsonObject &jsonObject, JsonStateUpdater<T> stateUpdater, const String &originId) {
|
||||
beginTransaction();
|
||||
StateUpdateResult result = stateUpdater(jsonObject, _state);
|
||||
endTransaction();
|
||||
callHookHandlers(originId, result);
|
||||
if (result == StateUpdateResult::CHANGED) {
|
||||
callUpdateHandlers(originId);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
StateUpdateResult updateWithoutPropagation(JsonObject &jsonObject, JsonStateUpdater<T> stateUpdater) {
|
||||
beginTransaction();
|
||||
StateUpdateResult result = stateUpdater(jsonObject, _state);
|
||||
endTransaction();
|
||||
return result;
|
||||
}
|
||||
|
||||
void read(std::function<void(T &)> stateReader) {
|
||||
beginTransaction();
|
||||
stateReader(_state);
|
||||
endTransaction();
|
||||
}
|
||||
|
||||
void read(JsonObject &jsonObject, JsonStateReader<T> stateReader) {
|
||||
beginTransaction();
|
||||
stateReader(_state, jsonObject);
|
||||
endTransaction();
|
||||
}
|
||||
|
||||
void callUpdateHandlers(const String &originId) {
|
||||
for (const StateUpdateHandlerInfo_t &updateHandler : _updateHandlers) {
|
||||
updateHandler._cb(originId);
|
||||
}
|
||||
}
|
||||
|
||||
void callHookHandlers(const String &originId, StateUpdateResult &result) {
|
||||
for (const StateHookHandlerInfo_t &hookHandler : _hookHandlers) {
|
||||
hookHandler._cb(originId, result);
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
T _state;
|
||||
|
||||
inline void beginTransaction() { xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY); }
|
||||
|
||||
inline void endTransaction() { xSemaphoreGiveRecursive(_accessMutex); }
|
||||
|
||||
private:
|
||||
SemaphoreHandle_t _accessMutex;
|
||||
std::list<StateUpdateHandlerInfo_t> _updateHandlers;
|
||||
std::list<StateHookHandlerInfo_t> _hookHandlers;
|
||||
};
|
||||
|
||||
#endif // end StatefulService_h
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <PsychicHttp.h>
|
||||
|
||||
#include <event_socket.h>
|
||||
#include <template/stateful_service.h>
|
||||
|
||||
template <class T>
|
||||
class EventEndpoint {
|
||||
public:
|
||||
EventEndpoint(JsonStateReader<T> stateReader, JsonStateUpdater<T> stateUpdater, StatefulService<T> *statefulService,
|
||||
const char *event)
|
||||
: _stateReader(stateReader), _stateUpdater(stateUpdater), _statefulService(statefulService), _event(event) {
|
||||
_statefulService->addUpdateHandler([&](const String &originId) { syncState(originId); }, false);
|
||||
}
|
||||
|
||||
void begin() {
|
||||
socket.onEvent(_event,
|
||||
std::bind(&EventEndpoint::updateState, this, std::placeholders::_1, std::placeholders::_2));
|
||||
socket.onSubscribe(_event,
|
||||
std::bind(&EventEndpoint::syncState, this, std::placeholders::_1, std::placeholders::_2));
|
||||
}
|
||||
|
||||
private:
|
||||
JsonStateReader<T> _stateReader;
|
||||
JsonStateUpdater<T> _stateUpdater;
|
||||
StatefulService<T> *_statefulService;
|
||||
const char *_event;
|
||||
|
||||
void updateState(JsonObject &root, int originId) {
|
||||
_statefulService->update(root, _stateUpdater, String(originId));
|
||||
}
|
||||
|
||||
void syncState(const String &originId, bool sync = false) {
|
||||
JsonDocument jsonDocument;
|
||||
JsonObject root = jsonDocument.to<JsonObject>();
|
||||
String output;
|
||||
_statefulService->read(root, _stateReader);
|
||||
serializeJson(root, output);
|
||||
ESP_LOGV("EventEndpoint", "Syncing state: %s", output.c_str());
|
||||
socket.emit(_event, output.c_str(), originId.c_str(), sync);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user