🔥 Removes old stateful services

This commit is contained in:
Rune Harlyk
2026-02-01 00:30:08 +01:00
parent 780d178e87
commit 57e0aac2aa
12 changed files with 0 additions and 651 deletions
-8
View File
@@ -2,7 +2,6 @@
#include <wifi/wifi_idf.h>
#include <wifi/dns_server.h>
#include <template/state_result.h>
#include <platform_shared/api.pb.h>
#include <cstring>
@@ -75,10 +74,3 @@ inline APSettings APSettings_defaults() {
settings.subnet_mask = parseIPv4(FACTORY_AP_SUBNET_MASK);
return settings;
}
inline void APSettings_read(const APSettings &settings, APSettings &proto) { proto = settings; }
inline StateUpdateResult APSettings_update(const APSettings &proto, APSettings &settings) {
settings = proto;
return StateUpdateResult::CHANGED;
}
-11
View File
@@ -1,6 +1,5 @@
#pragma once
#include <template/state_result.h>
#include <platform_shared/api.pb.h>
#include <esp_camera.h>
@@ -45,14 +44,4 @@ inline CameraSettings CameraSettings_defaults() {
return settings;
}
// Proto read/update are identity functions since type is the same
inline void CameraSettings_read(const CameraSettings& settings, CameraSettings& proto) {
proto = settings;
}
inline StateUpdateResult CameraSettings_update(const CameraSettings& proto, CameraSettings& settings) {
settings = proto;
return StateUpdateResult::CHANGED;
}
} // namespace Camera
-11
View File
@@ -1,6 +1,5 @@
#pragma once
#include <template/state_result.h>
#include <platform_shared/api.pb.h>
#include <string>
#include <cstring>
@@ -45,13 +44,3 @@ inline MDNSSettings MDNSSettings_defaults() {
return settings;
}
// Proto read/update are identity functions since type is the same
inline void MDNSSettings_read(const MDNSSettings& settings, MDNSSettings& proto) {
proto = settings;
}
inline StateUpdateResult MDNSSettings_update(const MDNSSettings& proto, MDNSSettings& settings) {
settings = proto;
return StateUpdateResult::CHANGED;
}
-42
View File
@@ -1,42 +0,0 @@
#include <template/state_result.h>
#include <ArduinoJson.h>
#include <string>
#ifndef FACTORY_NTP_ENABLED
#define FACTORY_NTP_ENABLED true
#endif
#ifndef FACTORY_NTP_TIME_ZONE_LABEL
#define FACTORY_NTP_TIME_ZONE_LABEL "Europe/London"
#endif
#ifndef FACTORY_NTP_TIME_ZONE_FORMAT
#define FACTORY_NTP_TIME_ZONE_FORMAT "GMT0BST,M3.5.0/1,M10.5.0"
#endif
#ifndef FACTORY_NTP_SERVER
#define FACTORY_NTP_SERVER "time.google.com"
#endif
class NTPSettings {
public:
bool enabled;
std::string tzLabel;
std::string tzFormat;
std::string server;
static void read(NTPSettings &settings, JsonVariant &root) {
root["enabled"] = settings.enabled;
root["server"] = settings.server.c_str();
root["tz_label"] = settings.tzLabel.c_str();
root["tz_format"] = settings.tzFormat.c_str();
}
static StateUpdateResult update(JsonVariant &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;
}
};
@@ -1,6 +1,5 @@
#pragma once
#include <template/state_result.h>
#include <platform_shared/api.pb.h>
/*
@@ -29,14 +28,3 @@ inline PeripheralsConfiguration PeripheralsConfiguration_defaults() {
settings.pins_count = 0;
return settings;
}
// Proto read/update are identity functions since type is the same
inline void PeripheralsConfiguration_read(const PeripheralsConfiguration& settings, PeripheralsConfiguration& proto) {
proto = settings;
}
inline StateUpdateResult PeripheralsConfiguration_update(const PeripheralsConfiguration& proto,
PeripheralsConfiguration& settings) {
settings = proto;
return StateUpdateResult::CHANGED;
}
-8
View File
@@ -1,7 +1,6 @@
#pragma once
#include <wifi/wifi_idf.h>
#include <template/state_result.h>
#include <platform_shared/api.pb.h>
#include <cstring>
@@ -49,10 +48,3 @@ inline WiFiSettings WiFiSettings_defaults() {
}
return settings;
}
inline void WiFiSettings_read(const WiFiSettings &settings, WiFiSettings &proto) { proto = settings; }
inline StateUpdateResult WiFiSettings_update(const WiFiSettings &proto, WiFiSettings &settings) {
settings = proto;
return StateUpdateResult::CHANGED;
}
-7
View File
@@ -1,7 +0,0 @@
#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
};
@@ -1,47 +0,0 @@
#pragma once
#include <esp_http_server.h>
#include <ArduinoJson.h>
#include <template/stateful_service.h>
#include <communication/webserver.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(httpd_req_t *request, JsonVariant &json) {
JsonVariant jsonObject = json.as<JsonVariant>();
StateUpdateResult outcome = _statefulService->updateWithoutPropagation(jsonObject, _stateUpdater);
if (outcome == StateUpdateResult::ERROR) {
return WebServer::sendError(request, 400, "Invalid state");
} else if ((outcome == StateUpdateResult::CHANGED)) {
_statefulService->callUpdateHandlers(HTTP_ENDPOINT_ORIGIN_ID);
}
JsonDocument doc;
JsonVariant root = doc.to<JsonVariant>();
_statefulService->read(root, _stateReader);
return WebServer::sendJson(request, 200, doc);
}
esp_err_t getState(httpd_req_t *request) {
JsonDocument doc;
JsonVariant root = doc.to<JsonVariant>();
_statefulService->read(root, _stateReader);
return WebServer::sendJson(request, 200, doc);
}
};
@@ -1,94 +0,0 @@
#ifndef FSPersistence_h
#define FSPersistence_h
#include <template/stateful_service.h>
#include <filesystem.h>
#include <ArduinoJson.h>
#include <cstdio>
#include <sys/stat.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() {
std::string content = FileSystem::readFile(_filePath);
if (!content.empty()) {
JsonDocument jsonDocument;
DeserializationError error = deserializeJson(jsonDocument, content);
if (error == DeserializationError::Ok) {
JsonVariant jsonObject = jsonDocument.as<JsonVariant>();
_statefulService->updateWithoutPropagation(jsonObject, _stateUpdater);
return;
}
}
applyDefaults();
writeToFS();
}
bool writeToFS() {
JsonDocument jsonDocument;
JsonVariant jsonObject = jsonDocument.to<JsonVariant>();
_statefulService->read(jsonObject, _stateReader);
mkdirs();
std::string content;
serializeJson(jsonDocument, content);
return FileSystem::writeFile(_filePath, content.c_str());
}
void disableUpdateHandler() {
if (_updateHandlerId) {
_statefulService->removeUpdateHandler(_updateHandlerId);
_updateHandlerId = 0;
}
}
void enableUpdateHandler() {
if (!_updateHandlerId) {
_updateHandlerId = _statefulService->addUpdateHandler([&](const std::string &originId) { writeToFS(); });
}
}
private:
JsonStateReader<T> _stateReader;
JsonStateUpdater<T> _stateUpdater;
StatefulService<T> *_statefulService;
const char *_filePath;
size_t _bufferSize;
HandlerId _updateHandlerId;
void mkdirs() {
std::string path(_filePath);
size_t index = 0;
while ((index = path.find('/', index + 1)) != std::string::npos) {
std::string segment = path.substr(0, index);
struct stat st;
if (stat(segment.c_str(), &st) != 0) {
FileSystem::mkdirRecursive(segment.c_str());
}
}
}
protected:
virtual void applyDefaults() {
JsonDocument jsonDocument;
JsonVariant jsonObject = jsonDocument.as<JsonVariant>();
_statefulService->updateWithoutPropagation(jsonObject, _stateUpdater);
}
};
#endif
@@ -1,141 +0,0 @@
#pragma once
#include <template/stateful_service.h>
#include <template/state_result.h>
#include <filesystem.h>
#include <pb_encode.h>
#include <pb_decode.h>
#include <cstdio>
#include <sys/stat.h>
#include <esp_log.h>
static const char *TAG_PERSISTENCE = "FSPersistencePB";
template <class T>
class FSPersistencePB {
public:
using ProtoStateReader = std::function<void(const T &, T &)>;
using ProtoStateUpdater = std::function<StateUpdateResult(const T &, T &)>;
FSPersistencePB(ProtoStateReader stateReader, ProtoStateUpdater stateUpdater, StatefulService<T> *statefulService,
const char *filePath, const pb_msgdesc_t *msgDescriptor, size_t maxSize, const T &defaultState)
: _stateReader(stateReader),
_stateUpdater(stateUpdater),
_statefulService(statefulService),
_filePath(filePath),
_msgDescriptor(msgDescriptor),
_maxSize(maxSize),
_defaultState(defaultState),
_updateHandlerId(0) {
enableUpdateHandler();
}
void readFromFS() {
FILE *file = fopen(_filePath, "rb");
if (file) {
fseek(file, 0, SEEK_END);
size_t fileSize = ftell(file);
fseek(file, 0, SEEK_SET);
if (fileSize > 0 && fileSize <= _maxSize) {
uint8_t *buffer = new uint8_t[fileSize];
size_t bytesRead = fread(buffer, 1, fileSize, file);
fclose(file);
if (bytesRead == fileSize) {
T *protoMsg = new T();
*protoMsg = {};
pb_istream_t stream = pb_istream_from_buffer(buffer, bytesRead);
if (pb_decode(&stream, _msgDescriptor, protoMsg)) {
_statefulService->updateWithoutPropagation(
[this, protoMsg](T &state) { return _stateUpdater(*protoMsg, state); });
delete protoMsg;
delete[] buffer;
return;
}
delete protoMsg;
}
delete[] buffer;
} else {
fclose(file);
}
}
applyDefaults();
writeToFS();
}
bool writeToFS() {
uint8_t *buffer = new uint8_t[_maxSize];
pb_ostream_t stream = pb_ostream_from_buffer(buffer, _maxSize);
T *protoMsg = new T();
*protoMsg = {};
_statefulService->read([this, protoMsg](const T &state) { _stateReader(state, *protoMsg); });
bool encodeSuccess = pb_encode(&stream, _msgDescriptor, protoMsg);
delete protoMsg;
if (!encodeSuccess) {
delete[] buffer;
return false;
}
mkdirs();
FILE *file = fopen(_filePath, "wb");
if (!file) {
ESP_LOGE(TAG_PERSISTENCE, "Failed to open file for writing: %s", _filePath);
delete[] buffer;
return false;
}
size_t written = fwrite(buffer, 1, stream.bytes_written, file);
fclose(file);
delete[] buffer;
return written == stream.bytes_written;
}
void disableUpdateHandler() {
if (_updateHandlerId) {
_statefulService->removeUpdateHandler(_updateHandlerId);
_updateHandlerId = 0;
}
}
void enableUpdateHandler() {
if (!_updateHandlerId) {
_updateHandlerId = _statefulService->addUpdateHandler([&](const std::string &originId) { writeToFS(); });
}
}
private:
ProtoStateReader _stateReader;
ProtoStateUpdater _stateUpdater;
StatefulService<T> *_statefulService;
const char *_filePath;
const pb_msgdesc_t *_msgDescriptor;
size_t _maxSize;
T _defaultState;
HandlerId _updateHandlerId;
void mkdirs() {
std::string path(_filePath);
size_t index = 0;
while ((index = path.find('/', index + 1)) != std::string::npos) {
std::string segment = path.substr(0, index);
struct stat st;
if (stat(segment.c_str(), &st) != 0) {
FileSystem::mkdirRecursive(segment.c_str());
}
}
}
protected:
void applyDefaults() {
_statefulService->updateWithoutPropagation([this](T &state) { return _stateUpdater(_defaultState, state); });
}
};
@@ -1,135 +0,0 @@
#pragma once
#include <memory>
#include <esp_http_server.h>
#include <template/stateful_service.h>
#include <communication/webserver.h>
#include <platform_shared/api.pb.h>
#include <pb_encode.h>
#include <pb_decode.h>
#include <functional>
#define PROTO_ENDPOINT_ORIGIN_ID "proto"
/**
* A stateful HTTP endpoint that uses protobuf encoding with api::Request/Response wrappers.
*
* @tparam T The internal state type (e.g., APSettings C++ class)
* @tparam ProtoT The protobuf message type within the oneof (e.g., api_APSettings)
*
* The endpoint receives api::Request, extracts the specific payload from the oneof,
* and returns api::Response with the updated state.
*/
template <class T, class ProtoT>
class StatefulProtoEndpoint {
public:
/** Converts internal state to protobuf message for responses */
// Formats are passed as referenced const (local variable) we want to read from, and a reference (proto) we write to
using ProtoStateReader = std::function<void(const T&, ProtoT&)>;
/** Converts incoming protobuf message to internal state */
// Formats are passed as referenced const (new object) we read from, and a reference to the local variable we write
// to
using ProtoStateUpdater = std::function<StateUpdateResult(const ProtoT&, T&)>;
/** Extracts the specific proto type from Request oneof */
using RequestExtractor = std::function<bool(const api_Request&, ProtoT&)>;
/** Assigns the specific proto type to Response oneof */
using ResponseAssigner = std::function<void(api_Response&, const ProtoT&)>;
protected:
ProtoStateReader _stateReader;
ProtoStateUpdater _stateUpdater;
StatefulService<T>* _statefulService;
RequestExtractor _requestExtractor;
ResponseAssigner _responseAssigner;
public:
/**
* Constructor for wrapped proto endpoint
* @param stateReader Converts internal state to proto
* @param stateUpdater Converts proto to internal state
* @param statefulService The stateful service to manage
* @param requestExtractor Extracts specific type from Request oneof
* @param responseAssigner Assigns specific type to Response oneof
*/
StatefulProtoEndpoint(ProtoStateReader stateReader, ProtoStateUpdater stateUpdater,
StatefulService<T>* statefulService, RequestExtractor requestExtractor,
ResponseAssigner responseAssigner)
: _stateReader(stateReader),
_stateUpdater(stateUpdater),
_statefulService(statefulService),
_requestExtractor(requestExtractor),
_responseAssigner(responseAssigner) {}
/**
* Handles POST requests: extracts payload from pre-decoded Request, updates state, returns Response
*/
esp_err_t handleStateUpdate(httpd_req_t* httpReq, api_Request* protoReq) {
ProtoT protoMsg = {};
if (!_requestExtractor(*protoReq, protoMsg)) {
return sendErrorResponse(httpReq, 400, "Invalid request type");
}
StateUpdateResult outcome = _statefulService->update(
[this, &protoMsg](T& settings) { return _stateUpdater(protoMsg, settings); }, PROTO_ENDPOINT_ORIGIN_ID);
if (outcome == StateUpdateResult::ERROR) {
return sendErrorResponse(httpReq, 400, "Invalid state");
}
return sendStateResponse(httpReq, 200);
}
/**
* Handles GET requests: reads current state and returns it as Response
*/
esp_err_t getState(httpd_req_t* request) { return sendStateResponse(request, 200); }
private:
/** Sends current state wrapped in Response */
esp_err_t sendStateResponse(httpd_req_t* request, uint32_t statusCode) {
api_Response res = api_Response_init_zero;
res.status_code = statusCode;
ProtoT protoState = {};
_statefulService->read([this, &protoState](const T& settings) { _stateReader(settings, protoState); });
_responseAssigner(res, protoState);
return WebServer::send(request, 200, res, api_Response_fields);
}
/** Sends error wrapped in Response */
esp_err_t sendErrorResponse(httpd_req_t* request, uint32_t statusCode, const char* message) {
api_Response res = api_Response_init_zero;
res.status_code = statusCode;
res.error_message = (char*)message;
return WebServer::send(request, statusCode == 200 ? 200 : 400, res, api_Response_fields);
}
};
// =============================================================================
// Helper macros for defining request extractors and response assigners
// =============================================================================
/**
* Creates a request extractor lambda for a specific payload type
* Usage: API_REQUEST_EXTRACTOR(ap_settings, api_APSettings)
*/
#define API_REQUEST_EXTRACTOR(field_name, proto_type) \
[](const api_Request& req, proto_type& out) -> bool { \
if (req.which_payload == api_Request_##field_name##_tag) { \
out = req.payload.field_name; \
return true; \
} \
return false; \
}
/**
* Creates a response assigner lambda for a specific payload type
* Usage: API_RESPONSE_ASSIGNER(ap_settings, api_APSettings)
*/
#define API_RESPONSE_ASSIGNER(field_name, proto_type) \
[](api_Response& res, const proto_type& data) { \
res.which_payload = api_Response_##field_name##_tag; \
res.payload.field_name = data; \
}
-135
View File
@@ -1,135 +0,0 @@
#pragma once
#include <list>
#include <functional>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <string>
#include <template/state_result.h>
using HandlerId = size_t;
using StateUpdateCallback = std::function<void(const std::string &originId)>;
using StateHookCallback = std::function<void(const std::string &originId, StateUpdateResult &result)>;
class HandlerBase {
protected:
static inline HandlerId nextId_ = 1;
HandlerId id_;
bool allowRemove_;
HandlerBase(bool allowRemove) : id_(nextId_++), allowRemove_(allowRemove) {}
public:
HandlerId getId() const { return id_; }
bool isRemovable() const { return allowRemove_; }
};
class UpdateHandler : public HandlerBase {
StateUpdateCallback callback_;
public:
UpdateHandler(StateUpdateCallback callback, bool allowRemove)
: HandlerBase(allowRemove), callback_(std::move(callback)) {}
void invoke(const std::string &originId) const { callback_(originId); }
};
class HookHandler : public HandlerBase {
StateHookCallback callback_;
public:
HookHandler(StateHookCallback callback, bool allowRemove)
: HandlerBase(allowRemove), callback_(std::move(callback)) {}
void invoke(const std::string &originId, StateUpdateResult &result) const { callback_(originId, result); }
};
template <class T>
class StatefulService {
public:
template <typename... Args>
StatefulService(Args &&...args) : state_(std::forward<Args>(args)...), mutex_(xSemaphoreCreateRecursiveMutex()) {}
HandlerId addUpdateHandler(StateUpdateCallback callback, bool allowRemove = true) {
if (!callback) return 0;
updateHandlers_.emplace_back(std::move(callback), allowRemove);
return updateHandlers_.back().getId();
}
void removeUpdateHandler(HandlerId id) {
updateHandlers_.remove_if(
[id](const UpdateHandler &handler) { return handler.isRemovable() && handler.getId() == id; });
}
HandlerId addHookHandler(StateHookCallback callback, bool allowRemove = true) {
if (!callback) return 0;
hookHandlers_.emplace_back(std::move(callback), allowRemove);
return hookHandlers_.back().getId();
}
void removeHookHandler(HandlerId id) {
hookHandlers_.remove_if(
[id](const HookHandler &handler) { return handler.isRemovable() && handler.getId() == id; });
}
StateUpdateResult update(std::function<StateUpdateResult(T &)> stateUpdater, const std::string &originId) {
lock();
StateUpdateResult result = stateUpdater(state_);
unlock();
notifyStateChange(originId, result);
return result;
}
StateUpdateResult updateWithoutPropagation(std::function<StateUpdateResult(T &)> stateUpdater) {
lock();
StateUpdateResult result = stateUpdater(state_);
unlock();
return result;
}
void read(std::function<void(T &)> stateReader) {
lock();
stateReader(state_);
unlock();
}
void read(std::function<void(const T &)> stateReader) const {
const_cast<StatefulService *>(this)->lock();
stateReader(state_);
const_cast<StatefulService *>(this)->unlock();
}
void callUpdateHandlers(const std::string &originId) {
for (const UpdateHandler &updateHandler : updateHandlers_) {
updateHandler.invoke(originId);
}
}
void callHookHandlers(const std::string &originId, StateUpdateResult &result) {
for (const HookHandler &hookHandler : hookHandlers_) {
hookHandler.invoke(originId, result);
}
}
T &state() { return state_; }
private:
T state_;
inline void lock() { xSemaphoreTakeRecursive(mutex_, portMAX_DELAY); }
inline void unlock() { xSemaphoreGiveRecursive(mutex_); }
void notifyStateChange(const std::string &originId, StateUpdateResult &result) {
callHookHandlers(originId, result);
if (result == StateUpdateResult::CHANGED) {
callUpdateHandlers(originId);
}
}
SemaphoreHandle_t mutex_;
std::list<UpdateHandler> updateHandlers_;
std::list<HookHandler> hookHandlers_;
};