Adds callback based event bus

This commit is contained in:
Rune Harlyk
2026-02-09 22:45:25 +01:00
parent eba00f98cd
commit 5863598fbc
37 changed files with 671 additions and 816 deletions
+8 -6
View File
@@ -1,8 +1,7 @@
#pragma once
#include <template/stateful_service.h>
#include <template/stateful_proto_endpoint.h>
#include <template/stateful_persistence.h>
#include <esp_http_server.h>
#include <eventbus.hpp>
#include <settings/ap_settings.h>
#include <utils/timing.h>
#include <wifi/wifi_idf.h>
@@ -10,7 +9,9 @@
#include <esp_timer.h>
#include <string>
class APService : public StatefulService<APSettings> {
class WebServer;
class APService {
public:
APService();
~APService();
@@ -23,10 +24,11 @@ class APService : public StatefulService<APSettings> {
void statusProto(api_APStatus &proto);
APNetworkStatus getAPNetworkStatus();
StatefulProtoEndpoint<APSettings, api_APSettings> protoEndpoint;
void registerRoutes(WebServer &server);
private:
FSPersistencePB<APSettings> _persistence;
APSettings _settings {};
SubscriptionHandle _settingsHandle;
DNSServer *_dnsServer;
volatile unsigned long _lastManaged;
+7 -2
View File
@@ -8,6 +8,7 @@
#include <map>
#include <type_traits>
#include <communication/proto_helpers.h>
#include <eventbus.hpp>
class CommAdapterBase {
public:
@@ -35,6 +36,11 @@ class CommAdapterBase {
decoder_.on<T>(handler);
}
template <typename T>
void forward() {
decoder_.on<T>([](const T& data, int) { EventBus::instance().publish(data); });
}
template <typename T>
void emit(const T& data, int clientId = -1) {
constexpr pb_size_t tag = MessageTraits<T>::tag;
@@ -47,8 +53,7 @@ class CommAdapterBase {
size_t out_size;
pb_get_encoded_size(&out_size, socket_message_Message_fields, &msg_);
uint8_t* buffer = pb_heap_enc_buf;
if (out_size > sizeof(pb_heap_enc_buf)) { // If the encoded size exceeds our buffer size, we needs to malloc a
// buffer of a proper size
if (out_size > sizeof(pb_heap_enc_buf)) {
buffer = (uint8_t*)malloc(out_size);
}
+46 -11
View File
@@ -15,7 +15,7 @@
#include <pb_encode.h>
#include <pb_decode.h>
#include <platform_shared/api.pb.h>
#include <freertos/semphr.h>
#include <eventbus.hpp>
using HttpGetHandler = std::function<esp_err_t(httpd_req_t*)>;
using HttpPostHandler = std::function<esp_err_t(httpd_req_t*, api_Request*)>;
@@ -23,16 +23,21 @@ using WsFrameHandler = std::function<esp_err_t(httpd_req_t*, httpd_ws_frame_t*)>
using WsOpenHandler = std::function<void(httpd_req_t*)>;
using WsCloseHandler = std::function<void(int)>;
// Macro to register a proto endpoint that extracts a specific payload type
// Usage: STAITC_PROTO_POST_ENDPOINT(server, "/api/files/delete", file_delete_request, FileSystem::handleDelete)
// Handler signature: esp_err_t handleDelete(httpd_req_t* req, const api_FileDeleteRequest& payload)
#define STAITC_PROTO_POST_ENDPOINT(server_ref, uri, payload_type, handler) \
(server_ref).on(uri, HTTP_POST, [&](httpd_req_t *request, api_Request *protoReq) { \
if (protoReq->which_payload != api_Request_##payload_type##_tag) { \
return WebServer::sendError(request, 400, "Invalid request payload"); \
} \
return handler(request, protoReq->payload.payload_type); \
})
#define PROTO_ROUTE(server_ref, uri, field_name, proto_type) \
(server_ref) \
.protoRoute<proto_type>( \
uri, \
[](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; \
}, \
[](api_Response& res, const proto_type& data) { \
res.which_payload = api_Response_##field_name##_tag; \
res.payload.field_name = data; \
})
struct HttpRoute {
std::string uri;
@@ -54,6 +59,36 @@ class WebServer {
void on(const char* uri, httpd_method_t method, HttpGetHandler handler);
void on(const char* uri, httpd_method_t method, HttpPostHandler handler);
template <typename ProtoT>
void protoRoute(const char* uri, std::function<bool(const api_Request&, ProtoT&)> extractor,
std::function<void(api_Response&, const ProtoT&)> assigner) {
on(uri, HTTP_GET, [assigner](httpd_req_t* req) {
auto* res = new api_Response();
*res = api_Response_init_zero;
res->status_code = 200;
ProtoT current = EventBus::instance().peek<ProtoT>();
assigner(*res, current);
esp_err_t ret = WebServer::send(req, 200, *res, api_Response_fields);
delete res;
return ret;
});
on(uri, HTTP_POST, [extractor, assigner](httpd_req_t* req, api_Request* protoReq) {
ProtoT msg = {};
if (!extractor(*protoReq, msg)) {
return sendError(req, 400, "Invalid request type");
}
EventBus::instance().publish(msg);
auto* res = new api_Response();
*res = api_Response_init_zero;
res->status_code = 200;
ProtoT current = EventBus::instance().peek<ProtoT>();
assigner(*res, current);
esp_err_t ret = WebServer::send(req, 200, *res, api_Response_fields);
delete res;
return ret;
});
}
void onWsFrame(WsFrameHandler handler);
void onWsOpen(WsOpenHandler handler);
void onWsClose(WsCloseHandler handler);
+50
View File
@@ -0,0 +1,50 @@
#pragma once
#include <proto_event_storage.hpp>
#include <platform_shared/api.pb.h>
#include <peripherals/servo_controller.h>
#include <settings/wifi_settings.h>
#include <settings/ap_settings.h>
#include <settings/mdns_settings.h>
#include <settings/peripherals_settings.h>
#include <settings/camera_settings.h>
#include <features.h>
class EventStorageManager {
public:
void initialize() {
_servoStorage.begin();
_wifiStorage.begin();
_apStorage.begin();
_peripheralStorage.begin();
#if FT_ENABLED(USE_MDNS)
_mdnsStorage.begin();
#endif
#if FT_ENABLED(USE_CAMERA) && USE_DVP_CAMERA
_cameraStorage.begin();
#endif
}
private:
ProtoEventStorage<api_ServoSettings, ServoSettings_defaults> _servoStorage {
SERVO_SETTINGS_FILE, api_ServoSettings_fields, api_ServoSettings_size, 1000};
ProtoEventStorage<api_WifiSettings, WiFiSettings_defaults> _wifiStorage {
WIFI_SETTINGS_FILE, api_WifiSettings_fields, api_WifiSettings_size, 1000};
ProtoEventStorage<api_APSettings, APSettings_defaults> _apStorage {AP_SETTINGS_FILE, api_APSettings_fields,
api_APSettings_size, 1000};
ProtoEventStorage<api_PeripheralSettings, PeripheralsConfiguration_defaults> _peripheralStorage {
PERIPHERAL_SETTINGS_FILE, api_PeripheralSettings_fields, api_PeripheralSettings_size, 500};
#if FT_ENABLED(USE_MDNS)
ProtoEventStorage<api_MDNSSettings, MDNSSettings_defaults> _mdnsStorage {
MDNS_SETTINGS_FILE, api_MDNSSettings_fields, api_MDNSSettings_size, 1000};
#endif
#if FT_ENABLED(USE_CAMERA) && USE_DVP_CAMERA
ProtoEventStorage<api_CameraSettings, CameraSettings_defaults> _cameraStorage {
CAMERA_SETTINGS_FILE, api_CameraSettings_fields, api_CameraSettings_size, 1000};
#endif
};
+5
View File
@@ -0,0 +1,5 @@
#pragma once
#include <cstdint>
struct SystemReadyEvent {};
+166
View File
@@ -0,0 +1,166 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <functional>
#include <list>
#include <map>
#include <cstdint>
class EventBus;
class SubscriptionHandle {
public:
SubscriptionHandle() = default;
~SubscriptionHandle() { unsubscribe(); }
SubscriptionHandle(SubscriptionHandle&& other) noexcept
: typeId_(other.typeId_), handlerId_(other.handlerId_), bus_(other.bus_) {
other.bus_ = nullptr;
}
SubscriptionHandle& operator=(SubscriptionHandle&& other) noexcept {
if (this != &other) {
unsubscribe();
typeId_ = other.typeId_;
handlerId_ = other.handlerId_;
bus_ = other.bus_;
other.bus_ = nullptr;
}
return *this;
}
SubscriptionHandle(const SubscriptionHandle&) = delete;
SubscriptionHandle& operator=(const SubscriptionHandle&) = delete;
void unsubscribe();
private:
friend class EventBus;
using TypeId = const void*;
SubscriptionHandle(TypeId typeId, uint32_t handlerId, EventBus* bus)
: typeId_(typeId), handlerId_(handlerId), bus_(bus) {}
TypeId typeId_ = nullptr;
uint32_t handlerId_ = 0;
EventBus* bus_ = nullptr;
};
class EventBus {
public:
using TypeId = const void*;
template <typename T>
static TypeId typeId() {
static const char id = 0;
return &id;
}
static EventBus& instance() {
static EventBus bus;
return bus;
}
template <typename T>
SubscriptionHandle subscribe(std::function<void(const T&)> callback) {
xSemaphoreTake(mutex_, portMAX_DELAY);
auto id = typeId<T>();
uint32_t hid = nextHandlerId_++;
handlers_[id].push_back(
{hid, [cb = std::move(callback)](const void* data) { cb(*static_cast<const T*>(data)); }});
xSemaphoreGive(mutex_);
return SubscriptionHandle(id, hid, this);
}
template <typename T>
void publish(const T& event) {
xSemaphoreTake(mutex_, portMAX_DELAY);
auto id = typeId<T>();
auto& entry = cache_[id];
if (entry.data) {
*static_cast<T*>(entry.data) = event;
} else {
entry.data = new T(event);
entry.deleter = [](void* p) { delete static_cast<T*>(p); };
}
auto it = handlers_.find(id);
if (it == handlers_.end()) {
xSemaphoreGive(mutex_);
return;
}
auto snapshot = it->second;
xSemaphoreGive(mutex_);
for (auto& handler : snapshot) {
handler.callback(&event);
}
}
template <typename T>
bool has() {
xSemaphoreTake(mutex_, portMAX_DELAY);
auto it = cache_.find(typeId<T>());
bool result = it != cache_.end() && it->second.data != nullptr;
xSemaphoreGive(mutex_);
return result;
}
template <typename T>
T peek() {
xSemaphoreTake(mutex_, portMAX_DELAY);
auto it = cache_.find(typeId<T>());
T result = (it != cache_.end() && it->second.data) ? *static_cast<const T*>(it->second.data) : T {};
xSemaphoreGive(mutex_);
return result;
}
private:
friend class SubscriptionHandle;
EventBus() { mutex_ = xSemaphoreCreateMutex(); }
~EventBus() {
for (auto& [id, entry] : cache_) {
if (entry.deleter) entry.deleter(entry.data);
}
vSemaphoreDelete(mutex_);
}
EventBus(const EventBus&) = delete;
EventBus& operator=(const EventBus&) = delete;
void removeHandler(TypeId typeId, uint32_t handlerId) {
xSemaphoreTake(mutex_, portMAX_DELAY);
auto it = handlers_.find(typeId);
if (it != handlers_.end()) {
it->second.remove_if([handlerId](const Handler& h) { return h.id == handlerId; });
}
xSemaphoreGive(mutex_);
}
struct Handler {
uint32_t id;
std::function<void(const void*)> callback;
};
struct CacheEntry {
void* data = nullptr;
void (*deleter)(void*) = nullptr;
};
SemaphoreHandle_t mutex_;
uint32_t nextHandlerId_ = 1;
std::map<TypeId, std::list<Handler>> handlers_;
std::map<TypeId, CacheEntry> cache_;
};
inline void SubscriptionHandle::unsubscribe() {
if (bus_) {
bus_->removeHandler(typeId_, handlerId_);
bus_ = nullptr;
}
}
+4
View File
@@ -9,6 +9,8 @@
#include <cstdio>
#include <platform_shared/api.pb.h>
class WebServer;
#define MOUNT_POINT "/littlefs"
#define FS_CONFIG_DIRECTORY MOUNT_POINT "/config"
@@ -35,6 +37,8 @@ bool writeFile(const char *filename, const char *content);
bool writeFile(const char *filename, const uint8_t *content, size_t size);
bool mkdirRecursive(const char *path);
void registerRoutes(WebServer &server);
esp_err_t getFilesProto(httpd_req_t *request);
esp_err_t getFiles(httpd_req_t *request);
esp_err_t getConfigFile(httpd_req_t *request);
+4
View File
@@ -1,6 +1,7 @@
#pragma once
#include <platform_shared/message.pb.h>
#include <eventbus.hpp>
#include <filesystem.h>
#include <map>
#include <string>
@@ -45,6 +46,7 @@ class FileSystemHandler {
public:
FileSystemHandler();
void begin();
void setSendCallbacks(SendMetadataCallback sendMetadata, SendCallback sendData, SendCompleteCallback sendComplete,
SendUploadCompleteCallback sendUploadComplete);
@@ -74,6 +76,8 @@ class FileSystemHandler {
bool deleteRecursive(const std::string& path);
bool sendNextDownloadChunk(uint32_t transferId);
void finalizeUpload(uint32_t transferId, bool success, const std::string& error = "");
SubscriptionHandle uploadHandle_;
};
extern FileSystemHandler fsHandler;
+7 -6
View File
@@ -2,13 +2,13 @@
#include <esp_http_server.h>
#include <mdns.h>
#include <template/stateful_service.h>
#include <template/stateful_proto_endpoint.h>
#include <template/stateful_persistence.h>
#include <eventbus.hpp>
#include <settings/mdns_settings.h>
#include <utils/timing.h>
class MDNSService : public StatefulService<MDNSSettings> {
class WebServer;
class MDNSService {
public:
MDNSService();
~MDNSService();
@@ -18,10 +18,11 @@ class MDNSService : public StatefulService<MDNSSettings> {
esp_err_t getStatus(httpd_req_t *request);
esp_err_t queryServices(httpd_req_t *request, api_Request *protoReq);
StatefulProtoEndpoint<MDNSSettings, api_MDNSSettings> protoEndpoint;
void registerRoutes(WebServer &server);
private:
FSPersistencePB<MDNSSettings> _persistence;
MDNSSettings _settings {};
SubscriptionHandle _settingsHandle;
bool _started {false};
void reconfigureMDNS();
+5
View File
@@ -14,6 +14,7 @@
#include <motion_states/stand_state.h>
#include <motion_states/rest_state.h>
#include <message_types.h>
#include <eventbus.hpp>
enum class MOTION_STATE { DEACTIVATED, IDLE, CALIBRATION, REST, STAND, WALK };
@@ -62,6 +63,10 @@ class MotionService {
float dir[12] = {1, -1, -1, -1, -1, -1, 1, -1, -1, -1, -1, -1};
int64_t lastUpdate = esp_timer_get_time();
SubscriptionHandle controllerHandle_;
SubscriptionHandle walkGaitHandle_;
SubscriptionHandle anglesHandle_;
};
#endif
+8 -11
View File
@@ -3,12 +3,12 @@
#include <esp_http_server.h>
#include <features.h>
#include <template/stateful_service.h>
#include <template/stateful_proto_endpoint.h>
#include <template/stateful_persistence.h>
#include <eventbus.hpp>
#include <settings/camera_settings.h>
class WebServer;
namespace Camera {
#define USE_DVP_CAMERA (USE_CAMERA && !CONFIG_IDF_TARGET_ESP32P4)
@@ -25,11 +25,7 @@ void safe_sensor_return();
#define PART_BOUNDARY "frame"
class CameraService
#if USE_DVP_CAMERA
: public StatefulService<CameraSettings>
#endif
{
class CameraService {
public:
CameraService();
@@ -38,11 +34,12 @@ class CameraService
esp_err_t cameraStill(httpd_req_t *request);
esp_err_t cameraStream(httpd_req_t *request);
#if USE_DVP_CAMERA
StatefulProtoEndpoint<CameraSettings, api_CameraSettings> protoEndpoint;
void registerRoutes(WebServer &server);
#if USE_DVP_CAMERA
private:
FSPersistencePB<CameraSettings> _persistence;
CameraSettings _settings {};
SubscriptionHandle _settingsHandle;
void updateCamera();
#endif
};
+5 -19
View File
@@ -1,8 +1,6 @@
#pragma once
#include <template/stateful_persistence.h>
#include <template/stateful_service.h>
#include <template/stateful_proto_endpoint.h>
#include <eventbus.hpp>
#include <utils/math_utils.h>
#include <utils/timing.h>
#include <filesystem.h>
@@ -21,12 +19,9 @@
#include <peripherals/barometer.h>
#include <peripherals/gesture.h>
/*
* Ultrasonic Sensor Settings
*/
#define MAX_DISTANCE 200
class Peripherals : public StatefulService<PeripheralsConfiguration> {
class Peripherals {
public:
Peripherals();
@@ -42,21 +37,14 @@ class Peripherals : public StatefulService<PeripheralsConfiguration> {
void getIMUProto(socket_message_IMUData &data);
void getSettingsProto(socket_message_PeripheralSettingsData &data);
/* IMU FUNCTIONS */
bool readImu();
bool readMag();
bool readBMP();
bool readGesture();
void readSonar();
float angleX();
float angleY();
float angleZ();
gesture_t takeGesture();
@@ -66,14 +54,12 @@ class Peripherals : public StatefulService<PeripheralsConfiguration> {
bool calibrateIMU();
StatefulProtoEndpoint<PeripheralsConfiguration, api_PeripheralSettings> protoEndpoint;
private:
FSPersistencePB<PeripheralsConfiguration> _persistence;
PeripheralsConfiguration _settings {};
SubscriptionHandle _settingsHandle;
SemaphoreHandle_t _accessMutex;
inline void beginTransaction() { xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY); }
inline void endTransaction() { xSemaphoreGiveRecursive(_accessMutex); }
#if FT_ENABLED(USE_MPU6050 || USE_BNO055)
@@ -97,4 +83,4 @@ class Peripherals : public StatefulService<PeripheralsConfiguration> {
std::list<uint8_t> addressList;
bool i2c_active = false;
};
};
+17 -23
View File
@@ -2,11 +2,10 @@
#define ServoController_h
#include <peripherals/drivers/pca9685.h>
#include <template/stateful_persistence.h>
#include <template/stateful_proto_endpoint.h>
#include <template/stateful_service.h>
#include <eventbus.hpp>
#include <utils/math_utils.h>
#include <platform_shared/api.pb.h>
#include <platform_shared/message.pb.h>
#ifndef FACTORY_SERVO_PWM_FREQUENCY
#define FACTORY_SERVO_PWM_FREQUENCY 50
@@ -34,25 +33,18 @@ inline ServoSettings ServoSettings_defaults() {
return settings;
}
inline void ServoSettings_read(const ServoSettings &settings, ServoSettings &proto) { proto = settings; }
inline StateUpdateResult ServoSettings_update(const ServoSettings &proto, ServoSettings &settings) {
settings = proto;
return StateUpdateResult::CHANGED;
}
class ServoController : public StatefulService<ServoSettings> {
class ServoController {
public:
ServoController()
: protoEndpoint(ServoSettings_read, ServoSettings_update, this,
API_REQUEST_EXTRACTOR(servo_settings, ServoSettings),
API_RESPONSE_ASSIGNER(servo_settings, ServoSettings)),
_persistence(ServoSettings_read, ServoSettings_update, this, SERVO_SETTINGS_FILE, api_ServoSettings_fields,
api_ServoSettings_size, ServoSettings_defaults()) {}
void begin() {
_persistence.readFromFS();
_settings = EventBus::instance().peek<ServoSettings>();
initializePCA();
_settingsHandle =
EventBus::instance().subscribe<ServoSettings>([this](const ServoSettings& s) { _settings = s; });
servoPwmHandle_ = EventBus::instance().subscribe<socket_message_ServoPWMData>(
[this](const socket_message_ServoPWMData& data) { setServoPWM(data.servo_id, data.servo_pwm); });
servoStateHandle_ = EventBus::instance().subscribe<socket_message_ServoStateData>(
[this](const socket_message_ServoStateData& data) { data.active ? activate() : deactivate(); });
}
void pcaWrite(int index, int value) {
@@ -103,7 +95,7 @@ class ServoController : public StatefulService<ServoSettings> {
uint16_t pwms[12];
for (int i = 0; i < 12; i++) {
angles[i] = lerp(angles[i], target_angles[i], 0.1);
auto &servo = state().servos[i];
auto& servo = _settings.servos[i];
float angle = servo.direction * angles[i] + servo.center_angle;
uint16_t pwm = angle * servo.conversion + servo.center_pwm;
pwms[i] = pwm = std::clamp<uint16_t>(pwm, 125, 600);
@@ -115,8 +107,6 @@ class ServoController : public StatefulService<ServoSettings> {
if (control_state == SERVO_CONTROL_STATE::ANGLE) calculatePWM();
}
StatefulProtoEndpoint<ServoSettings, ServoSettings> protoEndpoint;
private:
void initializePCA() {
_pca.begin();
@@ -124,8 +114,8 @@ class ServoController : public StatefulService<ServoSettings> {
_pca.setPWMFreq(FACTORY_SERVO_PWM_FREQUENCY);
_pca.sleep();
}
FSPersistencePB<ServoSettings> _persistence;
ServoSettings _settings {};
PCA9685Driver _pca;
SERVO_CONTROL_STATE control_state = SERVO_CONTROL_STATE::DEACTIVATED;
@@ -133,6 +123,10 @@ class ServoController : public StatefulService<ServoSettings> {
bool is_active {false};
float angles[12] = {0, 90, -145, 0, 90, -145, 0, 90, -145, 0, 90, -145};
float target_angles[12] = {0, 90, -145, 0, 90, -145, 0, 90, -145, 0, 90, -145};
SubscriptionHandle _settingsHandle;
SubscriptionHandle servoPwmHandle_;
SubscriptionHandle servoStateHandle_;
};
#endif
+113
View File
@@ -0,0 +1,113 @@
#pragma once
#include <eventbus.hpp>
#include <pb_encode.h>
#include <pb_decode.h>
#include <esp_timer.h>
#include <esp_log.h>
#include <cstdio>
#include <memory>
template <typename ProtoMsg, ProtoMsg (*DefaultsFn)()>
class ProtoEventStorage {
public:
ProtoEventStorage(const char* filename, const pb_msgdesc_t* descriptor, size_t maxSize, uint32_t debounceMs = 1000)
: _filename(filename), _descriptor(descriptor), _maxSize(maxSize), _debounceMs(debounceMs) {}
void begin() {
ProtoMsg loaded {};
loadOrDefault(loaded);
EventBus::instance().publish(loaded);
esp_timer_create_args_t timerArgs {};
timerArgs.callback = timerCallback;
timerArgs.arg = this;
timerArgs.dispatch_method = ESP_TIMER_TASK;
timerArgs.name = _filename;
esp_timer_create(&timerArgs, &_timer);
_handle = EventBus::instance().subscribe<ProtoMsg>([this](const ProtoMsg& msg) {
_pending = msg;
esp_timer_stop(_timer);
esp_timer_start_once(_timer, static_cast<uint64_t>(_debounceMs) * 1000);
});
}
private:
const char* _filename;
const pb_msgdesc_t* _descriptor;
size_t _maxSize;
uint32_t _debounceMs;
SubscriptionHandle _handle;
ProtoMsg _pending {};
esp_timer_handle_t _timer = nullptr;
static void timerCallback(void* arg) {
auto* self = static_cast<ProtoEventStorage*>(arg);
self->save(self->_pending);
}
void loadOrDefault(ProtoMsg& outMsg) {
FILE* file = fopen(_filename, "rb");
if (!file) {
ESP_LOGI("ProtoStorage", "No file %s, using defaults", _filename);
outMsg = DefaultsFn();
return;
}
fseek(file, 0, SEEK_END);
long tellResult = ftell(file);
fseek(file, 0, SEEK_SET);
if (tellResult < 0) {
ESP_LOGE("ProtoStorage", "ftell failed for %s", _filename);
fclose(file);
outMsg = DefaultsFn();
return;
}
size_t size = static_cast<size_t>(tellResult);
if (size == 0 || size > _maxSize) {
ESP_LOGW("ProtoStorage", "Invalid size %zu for %s (max %zu)", size, _filename, _maxSize);
fclose(file);
outMsg = DefaultsFn();
return;
}
auto buffer = std::make_unique<uint8_t[]>(size);
size_t read = fread(buffer.get(), 1, size, file);
fclose(file);
if (read != size) {
ESP_LOGE("ProtoStorage", "Read %zu/%zu from %s", read, size, _filename);
outMsg = DefaultsFn();
return;
}
pb_istream_t stream = pb_istream_from_buffer(buffer.get(), size);
if (!pb_decode(&stream, _descriptor, &outMsg)) {
ESP_LOGE("ProtoStorage", "Decode failed for %s", _filename);
outMsg = DefaultsFn();
}
}
void save(const ProtoMsg& msg) {
auto buffer = std::make_unique<uint8_t[]>(_maxSize);
pb_ostream_t stream = pb_ostream_from_buffer(buffer.get(), _maxSize);
if (!pb_encode(&stream, _descriptor, &msg)) {
ESP_LOGE("ProtoStorage", "Encode failed for %s", _filename);
return;
}
FILE* file = fopen(_filename, "wb");
if (!file) {
ESP_LOGE("ProtoStorage", "Open write failed for %s", _filename);
return;
}
size_t written = fwrite(buffer.get(), 1, stream.bytes_written, file);
fclose(file);
if (written != stream.bytes_written) {
ESP_LOGE("ProtoStorage", "Write failed for %s (%zu/%zu)", _filename, written, stream.bytes_written);
}
}
};
-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;
}
-10
View File
@@ -50,14 +50,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
-16
View File
@@ -1,6 +1,5 @@
#pragma once
#include <template/state_result.h>
#include <platform_shared/api.pb.h>
#include <string>
#include <cstring>
@@ -13,45 +12,30 @@
#define FACTORY_MDNS_INSTANCE "ESP32 Device"
#endif
// Use proto types directly
using MDNSTxtRecord = api_MDNSTxtRecord;
using MDNSServiceDef = api_MDNSServiceDef;
using MDNSSettings = api_MDNSSettings;
using MDNSStatus = api_MDNSStatus;
// Default factory settings
inline MDNSSettings MDNSSettings_defaults() {
MDNSSettings settings = api_MDNSSettings_init_zero;
strncpy(settings.hostname, FACTORY_MDNS_HOSTNAME, sizeof(settings.hostname) - 1);
strncpy(settings.instance, FACTORY_MDNS_INSTANCE, sizeof(settings.instance) - 1);
// Default HTTP service
settings.services_count = 2;
strncpy(settings.services[0].service, "http", sizeof(settings.services[0].service) - 1);
strncpy(settings.services[0].protocol, "tcp", sizeof(settings.services[0].protocol) - 1);
settings.services[0].port = 80;
settings.services[0].txt_records_count = 0;
// Default WS service
strncpy(settings.services[1].service, "ws", sizeof(settings.services[1].service) - 1);
strncpy(settings.services[1].protocol, "tcp", sizeof(settings.services[1].protocol) - 1);
settings.services[1].port = 80;
settings.services[1].txt_records_count = 0;
// Default global txt record
settings.global_txt_records_count = 1;
strncpy(settings.global_txt_records[0].key, "Firmware Version", sizeof(settings.global_txt_records[0].key) - 1);
strncpy(settings.global_txt_records[0].value, APP_VERSION, sizeof(settings.global_txt_records[0].value) - 1);
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;
}
+1 -3
View File
@@ -1,4 +1,3 @@
#include <template/state_result.h>
#include <ArduinoJson.h>
#include <string>
@@ -32,11 +31,10 @@ class NTPSettings {
root["tz_format"] = settings.tzFormat.c_str();
}
static StateUpdateResult update(JsonVariant &root, NTPSettings &settings) {
static void 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,15 +1,12 @@
#pragma once
#include <template/state_result.h>
#include <sdkconfig.h>
#include <platform_shared/api.pb.h>
#include <global.h>
// Use proto types directly
using PinConfig = api_PinConfig;
using PeripheralsConfiguration = api_PeripheralSettings;
// Default factory settings
inline PeripheralsConfiguration PeripheralsConfiguration_defaults() {
PeripheralsConfiguration settings = api_PeripheralSettings_init_zero;
settings.sda = SDA_PIN;
@@ -18,14 +15,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;
}
+4
View File
@@ -12,11 +12,15 @@
#include "platform_shared/message.pb.h"
class WebServer;
namespace system_service {
esp_err_t handleReset(httpd_req_t *request);
esp_err_t handleRestart(httpd_req_t *request);
esp_err_t handleSleep(httpd_req_t *request);
void registerRoutes(WebServer &server);
void reset();
void restart();
void sleep();
-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,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 <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_;
};
+8 -7
View File
@@ -7,16 +7,16 @@
#include <filesystem.h>
#include <utils/timing.h>
#include <template/stateful_service.h>
#include <template/stateful_persistence.h>
#include <template/stateful_proto_endpoint.h>
#include <eventbus.hpp>
#include <settings/wifi_settings.h>
class WebServer;
#define WIFI_EVENT_STA_DISCONNECTED_IDF WIFI_EVENT_STA_DISCONNECTED
#define WIFI_EVENT_STA_STOP_IDF WIFI_EVENT_STA_STOP
#define IP_EVENT_STA_GOT_IP_IDF 1000
class WiFiService : public StatefulService<WiFiSettings> {
class WiFiService {
public:
WiFiService();
~WiFiService();
@@ -27,20 +27,21 @@ class WiFiService : public StatefulService<WiFiSettings> {
void setupMDNS(const char *hostname);
void selectNetwork(uint32_t index);
const char *getHostname() { return state().hostname; }
const char *getHostname() { return _settings.hostname; }
static esp_err_t handleScan(httpd_req_t *request);
static esp_err_t getNetworks(httpd_req_t *request);
static esp_err_t getNetworkStatus(httpd_req_t *request);
StatefulProtoEndpoint<WiFiSettings, api_WifiSettings> protoEndpoint;
void registerRoutes(WebServer &server);
private:
void onStationModeDisconnected(int32_t event, void *event_data);
void onStationModeStop(int32_t event, void *event_data);
static void onStationModeGotIP(int32_t event, void *event_data);
FSPersistencePB<WiFiSettings> _persistence;
WiFiSettings _settings {};
SubscriptionHandle _settingsHandle;
void reconfigureWiFiConnection();
void manageSTA();