4 Commits

Author SHA1 Message Date
Rune Harlyk 18ec2e618e Heap event bus 2026-02-01 01:09:46 +01:00
Rune Harlyk 0c0ef0ac40 Moves rest of events to event bus 2026-02-01 00:44:32 +01:00
Rune Harlyk 57e0aac2aa 🔥 Removes old stateful services 2026-02-01 00:30:16 +01:00
Rune Harlyk 780d178e87 Adds event bus 2026-02-01 00:26:02 +01:00
54 changed files with 1385 additions and 1389 deletions
+2 -2
View File
@@ -15,7 +15,7 @@ import type {
} from '$lib/platform_shared/filesystem'
import type { Result, DataResult, ListResult, ProgressCallback } from '$lib/types/models'
const MAX_CHUNK_SIZE = 1024 * 64 // 64KB - must match ESP32 FS_MAX_CHUNK_SIZE
const MAX_CHUNK_SIZE = 2 ** 14
type TimeoutId = ReturnType<typeof setTimeout>
type CleanupFn = (() => void) | null
@@ -51,7 +51,7 @@ export class FileSystemClient {
private downloadListenerCleanup: CleanupFn = null
private completeListenerCleanup: CleanupFn = null
private uploadCompleteListenerCleanup: CleanupFn = null
private transferTimeout = 300000
private transferTimeout = 60000
constructor() {
this.setupListeners()
-2
View File
@@ -153,8 +153,6 @@ function createWebSocket() {
}
const { tag, msg } = decodeMessage(frame.data)
const key: keyof Message = (MESSAGE_TAG_TO_KEY.get(tag) ?? "") as keyof Message;
console.log(key + ": ", msg[key])
if (msg.correlationResponse) {
const pending = pending_requests.get(msg.correlationResponse.correlationId)
if (pending) {
+1 -1
View File
@@ -21,7 +21,7 @@ export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://192.168.50.141/',
target: 'http://spot-micro.local/',
changeOrigin: true,
ws: true
}
+11 -6
View File
@@ -1,16 +1,15 @@
#pragma once
#include <template/stateful_service.h>
#include <template/stateful_proto_endpoint.h>
#include <template/stateful_persistence_pb.h>
#include <event_bus/event_bus.h>
#include <settings/ap_settings.h>
#include <utils/timing.h>
#include <wifi/wifi_idf.h>
#include <wifi/dns_server.h>
#include <esp_timer.h>
#include <esp_http_server.h>
#include <string>
class APService : public StatefulService<APSettings> {
class APService {
public:
APService();
~APService();
@@ -23,10 +22,16 @@ class APService : public StatefulService<APSettings> {
void statusProto(api_APStatus &proto);
APNetworkStatus getAPNetworkStatus();
StatefulProtoEndpoint<APSettings, api_APSettings> protoEndpoint;
esp_err_t getSettings(httpd_req_t *request);
esp_err_t updateSettings(httpd_req_t *request, api_Request *protoReq);
private:
FSPersistencePB<APSettings> _persistence;
static constexpr const char *TAG = "APService";
void onSettingsChanged(const api_APSettings &newSettings);
APSettings _settings = APSettings_defaults();
EventBus::Handle<api_APSettings> _settingsHandle;
DNSServer *_dnsServer;
volatile unsigned long _lastManaged;
+45 -11
View File
@@ -6,9 +6,11 @@
#include <functional>
#include <list>
#include <map>
#include <memory>
#include <vector>
#include <type_traits>
#include <communication/proto_helpers.h>
#include <utils/timing.h>
#include <event_bus/event_bus.h>
class CommAdapterBase {
public:
@@ -36,39 +38,63 @@ class CommAdapterBase {
decoder_.on<T>(handler);
}
template <typename T>
void onPublish(std::function<void(const T&, int)> handler = nullptr) {
decoder_.on<T>([this, handler](const T& data, int clientId) {
EventBus::publish(data);
if (handler) handler(data, clientId);
});
}
template <typename T>
void bridgeFromEventBus() {
eventBusHandles_.push_back(
std::make_unique<EventBusHandleStorage<T>>(EventBus::subscribe<T>([this](const T& data) { emit(data); })));
}
template <typename T>
void bridgeFromEventBus(uint32_t intervalMs) {
eventBusHandles_.push_back(std::make_unique<EventBusHandleStorage<T>>(
EventBus::subscribe<T>(intervalMs, [this](const T& data) { emit(data); })));
}
template <typename T>
void emit(const T& data, int clientId = -1) {
constexpr pb_size_t tag = MessageTraits<T>::tag;
if (clientId < 0 && !hasSubscribers(tag)) return;
xSemaphoreTake(mutex_, portMAX_DELAY);
msg_.which_message = tag;
MessageTraits<T>::assign(msg_, data);
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);
}
pb_ostream_t stream = pb_ostream_from_buffer(buffer, out_size);
if (!pb_encode(&stream, socket_message_Message_fields, &msg_)) {
ESP_LOGE("ProtoComm", "Failed to encode message (tag %d): %s (calc=%u, written=%u)",
(int)tag, PB_GET_ERROR(&stream), out_size, stream.bytes_written);
ESP_LOGE("ProtoComm", "Failed to encode message (tag %d), buffer too small?", (int)tag);
xSemaphoreGive(mutex_);
if (pb_heap_enc_buf != buffer) free(buffer);
return;
}
if (clientId >= 0) {
send(buffer, stream.bytes_written, clientId);
} else {
sendToSubscribers(tag, buffer, stream.bytes_written);
sendToSubscribersLocked(tag, buffer, stream.bytes_written);
}
if (pb_heap_enc_buf != buffer) {
free(buffer);
}
xSemaphoreGive(mutex_);
}
protected:
@@ -97,11 +123,9 @@ class CommAdapterBase {
}
void handleIncoming(const uint8_t* data, size_t len, int cid) {
TIME_IT(
if (!decoder_.decode(data, len, cid)) {
ESP_LOGE("ProtoComm", "Failed to decode incoming message from client %d", cid);
}
, INCOMING_DECODE)
}
void sendPong(int cid) {
@@ -120,12 +144,22 @@ class CommAdapterBase {
socket_message_Message msg_ = socket_message_Message_init_zero;
uint8_t pb_heap_enc_buf[PROTO_BUFFER_SIZE];
struct EventBusHandleBase {
virtual ~EventBusHandleBase() = default;
};
template <typename T>
struct EventBusHandleStorage : EventBusHandleBase {
EventBus::Handle<T> handle;
EventBusHandleStorage(EventBus::Handle<T>&& h) : handle(std::move(h)) {}
};
std::vector<std::unique_ptr<EventBusHandleBase>> eventBusHandles_;
private:
void sendToSubscribers(int32_t tag, const uint8_t* data, size_t len) {
xSemaphoreTake(mutex_, portMAX_DELAY);
void sendToSubscribersLocked(int32_t tag, const uint8_t* data, size_t len) {
for (int cid : client_subscriptions_[tag]) {
send(data, len, cid);
}
xSemaphoreGive(mutex_);
}
};
+6 -23
View File
@@ -3,7 +3,6 @@
#include <pb_encode.h>
#include <pb_decode.h>
#include <platform_shared/message.pb.h>
#include <esp_log.h>
#include <functional>
#include <map>
@@ -72,48 +71,32 @@ class ProtoDecoder {
bool decode(const uint8_t* data, size_t len, int clientId) {
pb_istream_t stream = pb_istream_from_buffer(data, len);
// Reset message before decoding (nanopb will malloc FT_POINTER fields)
msg_ = socket_message_Message_init_zero;
bool success = pb_decode(&stream, socket_message_Message_fields, &msg_);
if (!success) {
ESP_LOGE("ProtoHelpers", "Decode failed: %s (len=%u)", PB_GET_ERROR(&stream), len);
pb_release(socket_message_Message_fields, &msg_);
if (!pb_decode(&stream, socket_message_Message_fields, &msg_)) {
return false;
}
bool handled = false;
switch (msg_.which_message) {
case socket_message_Message_sub_notif_tag:
if (subscribeHandler_) subscribeHandler_(msg_.message.sub_notif.tag, clientId);
handled = true;
break;
return true;
case socket_message_Message_unsub_notif_tag:
if (unsubscribeHandler_) unsubscribeHandler_(msg_.message.unsub_notif.tag, clientId);
handled = true;
break;
return true;
case socket_message_Message_pingmsg_tag:
if (pingHandler_) pingHandler_(clientId);
handled = true;
break;
return true;
default: {
auto it = handlers_.find(msg_.which_message);
if (it != handlers_.end()) {
it->second(clientId);
handled = true;
return true;
}
break;
return false;
}
}
// Free any malloc'd FT_POINTER fields
pb_release(socket_message_Message_fields, &msg_);
return handled;
}
private:
@@ -0,0 +1,51 @@
#pragma once
#include <consumers/proto_event_storage.h>
#include <platform_shared/api.pb.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 <esp_log.h>
class EventStorageManager {
public:
void initialize() {
ESP_LOGI(TAG, "Loading settings from storage");
_wifiStorage.begin();
_apStorage.begin();
_mdnsStorage.begin();
_peripheralStorage.begin();
#if FT_ENABLED(USE_CAMERA)
_cameraStorage.begin();
#endif
ESP_LOGI(TAG, "Settings loaded and published");
}
private:
static constexpr const char* TAG = "StorageManager";
ProtoEventStorage<api_WifiSettings, WiFiSettings_defaults> _wifiStorage =
ProtoEventStorage<api_WifiSettings, WiFiSettings_defaults>("/config/wifiSettings.pb", api_WifiSettings_fields,
api_WifiSettings_size, 1000);
ProtoEventStorage<api_APSettings, APSettings_defaults> _apStorage =
ProtoEventStorage<api_APSettings, APSettings_defaults>("/config/apSettings.pb", api_APSettings_fields,
api_APSettings_size, 1000);
ProtoEventStorage<api_MDNSSettings, MDNSSettings_defaults> _mdnsStorage =
ProtoEventStorage<api_MDNSSettings, MDNSSettings_defaults>("/config/mdnsSettings.pb", api_MDNSSettings_fields,
api_MDNSSettings_size, 1000);
ProtoEventStorage<api_PeripheralSettings, PeripheralsConfiguration_defaults> _peripheralStorage =
ProtoEventStorage<api_PeripheralSettings, PeripheralsConfiguration_defaults>(
"/config/peripheralSettings.pb", api_PeripheralSettings_fields, api_PeripheralSettings_size, 500);
#if FT_ENABLED(USE_CAMERA)
ProtoEventStorage<api_CameraSettings, Camera::CameraSettings_defaults> _cameraStorage =
ProtoEventStorage<api_CameraSettings, Camera::CameraSettings_defaults>(
"/config/cameraSettings.pb", api_CameraSettings_fields, api_CameraSettings_size, 1000);
#endif
};
@@ -0,0 +1,72 @@
#pragma once
#include <event_bus/event_bus.h>
#include <pb_encode.h>
#include <pb_decode.h>
#include <esp_log.h>
#include <memory>
#include <cstdio>
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() {
auto loaded = std::unique_ptr<ProtoMsg>(new ProtoMsg {});
loadOrDefault(*loaded);
EventBus::publish(*loaded, "EventStorage");
ESP_LOGI(TAG, "Loaded %s", _filename);
_handle = EventBus::subscribe<ProtoMsg>(_debounceMs, [this](const ProtoMsg& msg) { save(msg); });
}
private:
static constexpr const char* TAG = "ProtoStorage";
const char* _filename;
const pb_msgdesc_t* _descriptor;
size_t _maxSize;
uint32_t _debounceMs;
typename EventBus::Handle<ProtoMsg> _handle;
void loadOrDefault(ProtoMsg& outMsg) {
FILE* file = fopen(_filename, "rb");
if (!file) {
outMsg = DefaultsFn();
return;
}
fseek(file, 0, SEEK_END);
size_t size = ftell(file);
fseek(file, 0, SEEK_SET);
if (size == 0 || size > _maxSize) {
fclose(file);
outMsg = DefaultsFn();
return;
}
auto buffer = std::make_unique<uint8_t[]>(size);
fread(buffer.get(), 1, size, file);
fclose(file);
pb_istream_t stream = pb_istream_from_buffer(buffer.get(), size);
if (!pb_decode(&stream, _descriptor, &outMsg)) {
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)) return;
FILE* file = fopen(_filename, "wb");
if (!file) return;
fwrite(buffer.get(), 1, stream.bytes_written, file);
fclose(file);
ESP_LOGD(TAG, "Saved %s", _filename);
}
};
+65
View File
@@ -0,0 +1,65 @@
#pragma once
#include <event_bus/typed_event_bus.h>
#include <event_bus/event_registry.h>
#include <event_bus/event_metadata.h>
#include <esp_timer.h>
#include <atomic>
class EventBus {
public:
template <typename Msg>
using Bus = TypedEventBus<Msg, EventBusConfig<Msg>::QueueDepth, EventBusConfig<Msg>::MaxSubs,
EventBusConfig<Msg>::BatchSize>;
template <typename Msg>
using Handle = typename Bus<Msg>::Handle;
template <typename Msg>
static bool publish(const Msg& msg, const char* source = nullptr) {
if (_hasGlobalListeners.load(std::memory_order_acquire)) {
notifyGlobalListeners(msg, source);
}
return Bus<Msg>::publish(msg);
}
template <typename Msg, typename Callback>
static auto subscribe(Callback&& callback) {
return Bus<Msg>::subscribe(std::forward<Callback>(callback));
}
template <typename Msg, typename Callback>
static auto subscribe(uint32_t intervalMs, Callback&& callback) {
return Bus<Msg>::subscribe(intervalMs, std::forward<Callback>(callback));
}
template <typename Msg>
static void publishISR(const Msg& msg, BaseType_t* higherPriorityTaskWoken = nullptr) {
Bus<Msg>::publishISR(msg, higherPriorityTaskWoken);
}
template <typename Msg>
static bool peek(Msg& out) {
return Bus<Msg>::peek(out);
}
template <typename Msg>
static bool take(Msg& out) {
return Bus<Msg>::take(out);
}
template <typename Msg>
static bool hasSubscribers() {
return Bus<Msg>::hasSubscribers();
}
using GlobalHandler = FixedFn<void(EventType, const void*, size_t, uint64_t), 64>;
static size_t subscribeGlobal(GlobalHandler&& handler);
static void unsubscribeGlobal(size_t id);
private:
static std::atomic<bool> _hasGlobalListeners;
template <typename Msg>
static void notifyGlobalListeners(const Msg& msg, const char* source);
};
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include <event_bus/event_types.h>
#include <cstdint>
struct EventMetadata {
EventType type;
uint64_t timestamp;
uint32_t sequence;
const char* source;
};
template <typename Msg>
struct EventEnvelope {
EventMetadata metadata;
Msg payload;
};
+63
View File
@@ -0,0 +1,63 @@
#pragma once
#include <event_bus/event_types.h>
#include <platform_shared/api.pb.h>
#include <platform_shared/message.pb.h>
template <typename T>
struct EventTypeTraits;
template <typename T>
struct EventBusConfig {
static constexpr size_t QueueDepth = 64;
static constexpr size_t MaxSubs = 8;
static constexpr size_t BatchSize = 16;
};
#define REGISTER_EVENT_TYPE(MsgType, EventTypeValue) \
template <> \
struct EventTypeTraits<MsgType> { \
static constexpr EventType type = EventTypeValue; \
static constexpr const char* name = #MsgType; \
};
#define REGISTER_SETTINGS_TYPE(MsgType, EventTypeValue) \
REGISTER_EVENT_TYPE(MsgType, EventTypeValue) \
template <> \
struct EventBusConfig<MsgType> { \
static constexpr size_t QueueDepth = 1; \
static constexpr size_t MaxSubs = 6; \
static constexpr size_t BatchSize = 1; \
};
REGISTER_SETTINGS_TYPE(api_WifiSettings, EventType::WIFI_SETTINGS)
REGISTER_SETTINGS_TYPE(api_APSettings, EventType::AP_SETTINGS)
REGISTER_SETTINGS_TYPE(api_MDNSSettings, EventType::MDNS_SETTINGS)
REGISTER_SETTINGS_TYPE(api_PeripheralSettings, EventType::PERIPHERAL_SETTINGS)
REGISTER_SETTINGS_TYPE(api_ServoSettings, EventType::SERVO_SETTINGS)
REGISTER_SETTINGS_TYPE(api_CameraSettings, EventType::CAMERA_SETTINGS)
#define REGISTER_COMMAND_TYPE(MsgType, EventTypeValue) \
REGISTER_EVENT_TYPE(MsgType, EventTypeValue) \
template <> \
struct EventBusConfig<MsgType> { \
static constexpr size_t QueueDepth = 1; \
static constexpr size_t MaxSubs = 3; \
static constexpr size_t BatchSize = 1; \
};
#define REGISTER_STREAM_TYPE(MsgType, EventTypeValue) \
REGISTER_EVENT_TYPE(MsgType, EventTypeValue) \
template <> \
struct EventBusConfig<MsgType> { \
static constexpr size_t QueueDepth = 4; \
static constexpr size_t MaxSubs = 3; \
static constexpr size_t BatchSize = 4; \
};
REGISTER_STREAM_TYPE(socket_message_IMUData, EventType::IMU_DATA)
REGISTER_COMMAND_TYPE(socket_message_ControllerData, EventType::MOTION_COMMAND)
REGISTER_COMMAND_TYPE(socket_message_ModeData, EventType::MOTION_MODE)
REGISTER_COMMAND_TYPE(socket_message_AnglesData, EventType::MOTION_ANGLES)
REGISTER_COMMAND_TYPE(socket_message_WalkGaitData, EventType::MOTION_WALK_GAIT)
REGISTER_COMMAND_TYPE(socket_message_ServoStateData, EventType::SERVO_STATE)
REGISTER_COMMAND_TYPE(socket_message_ServoPWMData, EventType::SERVO_PWM)
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include <cstdint>
enum class EventType : uint16_t {
WIFI_SETTINGS = 100,
AP_SETTINGS = 110,
MDNS_SETTINGS = 120,
PERIPHERAL_SETTINGS = 130,
SERVO_SETTINGS = 140,
CAMERA_SETTINGS = 150,
WIFI_STATUS = 101,
AP_STATUS = 111,
IMU_DATA = 131,
MOTION_COMMAND = 200,
MOTION_MODE = 201,
MOTION_ANGLES = 202,
MOTION_WALK_GAIT = 203,
SERVO_STATE = 141,
SERVO_PWM = 142,
SYSTEM_BOOT = 300,
STORAGE_HYDRATION_COMPLETE = 301,
};
const char* eventTypeName(EventType type);
bool isSettingsEvent(EventType type);
+55
View File
@@ -0,0 +1,55 @@
#pragma once
#include <event_bus/event_bus.h>
#include <event_bus/event_registry.h>
#include <communication/webserver.h>
#include <esp_http_server.h>
template <typename TMsg, pb_size_t RequestTag, pb_size_t ResponseTag>
class RestSettingsEndpoint {
public:
static esp_err_t getSettings(httpd_req_t *request) {
api_Response response = api_Response_init_zero;
response.status_code = 200;
response.which_payload = ResponseTag;
TMsg settings;
if (!EventBus::peek(settings)) {
return WebServer::sendError(request, 404, "Settings not found");
}
*reinterpret_cast<TMsg *>(&response.payload) = settings;
return WebServer::send(request, 200, response, api_Response_fields);
}
static esp_err_t updateSettings(httpd_req_t *request, api_Request *protoReq) {
if (protoReq->which_payload != RequestTag) {
return WebServer::sendError(request, 400, "Invalid payload type");
}
const TMsg &settings = *reinterpret_cast<const TMsg *>(&protoReq->payload);
EventBus::publish(settings, "HTTPEndpoint");
api_Response response = api_Response_init_zero;
response.status_code = 200;
response.which_payload = api_Response_empty_message_tag;
return WebServer::send(request, 200, response, api_Response_fields);
}
};
using WiFiSettingsEndpoint =
RestSettingsEndpoint<api_WifiSettings, api_Request_wifi_settings_tag, api_Response_wifi_settings_tag>;
using ServoSettingsEndpoint =
RestSettingsEndpoint<api_ServoSettings, api_Request_servo_settings_tag, api_Response_servo_settings_tag>;
using PeripheralSettingsEndpoint = RestSettingsEndpoint<api_PeripheralSettings, api_Request_peripheral_settings_tag,
api_Response_peripheral_settings_tag>;
using APSettingsEndpoint =
RestSettingsEndpoint<api_APSettings, api_Request_ap_settings_tag, api_Response_ap_settings_tag>;
using MDNSSettingsEndpoint =
RestSettingsEndpoint<api_MDNSSettings, api_Request_mdns_settings_tag, api_Response_mdns_settings_tag>;
using CameraSettingsEndpoint =
RestSettingsEndpoint<api_CameraSettings, api_Request_camera_settings_tag, api_Response_camera_settings_tag>;
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include <cstdint>
enum class SystemEventType : uint8_t {
STORAGE_HYDRATION_COMPLETE,
SYSTEM_BOOT_COMPLETE,
};
struct SystemEvent {
SystemEventType type;
};
+337
View File
@@ -0,0 +1,337 @@
#pragma once
#include <vector>
#include <functional>
#include <type_traits>
#include <cstddef>
#include <cstring>
#include <array>
#include <optional>
#include <atomic>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/queue.h>
#include <esp_heap_caps.h>
template <typename Sig, size_t MaxSize>
class FixedFn;
template <typename R, typename... A, size_t MaxSize>
class FixedFn<R(A...), MaxSize> {
alignas(void*) std::byte buf[MaxSize];
R (*call)(void*, A&&...) {};
void (*moveFn)(void*, void*) {};
void (*destroy)(void*) {};
public:
template <typename Fun>
void set(Fun&& f) {
static_assert(sizeof(Fun) <= MaxSize);
new (buf) Fun(std::forward<Fun>(f));
call = [](void* p, A&&... as) -> R { return (*reinterpret_cast<Fun*>(p))(std::forward<A>(as)...); };
moveFn = [](void* d, void* s) { new (d) Fun(std::move(*reinterpret_cast<Fun*>(s))); };
destroy = [](void* p) { reinterpret_cast<Fun*>(p)->~Fun(); };
}
R operator()(A... as) const {
return call(const_cast<void*>(static_cast<const void*>(buf)), std::forward<A>(as)...);
}
FixedFn() = default;
FixedFn(FixedFn&& o) noexcept {
if (o.moveFn) o.moveFn(buf, o.buf);
call = o.call;
moveFn = o.moveFn;
destroy = o.destroy;
o.call = nullptr;
o.moveFn = nullptr;
o.destroy = nullptr;
}
FixedFn(const FixedFn&) = delete;
FixedFn& operator=(const FixedFn&) = delete;
FixedFn& operator=(FixedFn&&) = delete;
~FixedFn() {
if (destroy) destroy(buf);
}
};
enum class EmitMode { Latest, Batch };
template <typename Msg, size_t QueueDepth = 64, size_t MaxSubs = 8, size_t BatchSize = 16>
class TypedEventBus {
struct Item {
Msg payload;
size_t exclude;
};
static constexpr size_t NO_EX = MaxSubs;
struct Sub {
FixedFn<void(const Msg*, size_t), 48> cb;
TickType_t interval;
TickType_t last;
EmitMode mode;
Msg* buf;
size_t cnt;
std::atomic<bool> enabled;
std::atomic<uint32_t> running;
Sub() : buf(nullptr), cnt(0), enabled(false), running(0) {
buf = static_cast<Msg*>(heap_caps_malloc(BatchSize * sizeof(Msg), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
if (!buf) buf = static_cast<Msg*>(malloc(BatchSize * sizeof(Msg)));
}
~Sub() {
if (buf) free(buf);
}
Sub(const Sub&) = delete;
Sub& operator=(const Sub&) = delete;
};
struct BusState {
QueueHandle_t queue;
Sub* subs[MaxSubs];
portMUX_TYPE mux;
Msg latest;
std::atomic<bool> hasLatest;
std::atomic<size_t> subCount;
std::atomic<bool> taskStarted;
BusState()
: queue(nullptr),
mux(portMUX_INITIALIZER_UNLOCKED),
latest {},
hasLatest(false),
subCount(0),
taskStarted(false) {
for (size_t i = 0; i < MaxSubs; ++i) subs[i] = nullptr;
}
};
static BusState& state() {
static BusState s;
return s;
}
static void ensureQueue() {
auto& s = state();
if (s.queue) return;
portENTER_CRITICAL(&s.mux);
if (!s.queue) {
s.queue = xQueueCreate(QueueDepth, sizeof(Item));
}
portEXIT_CRITICAL(&s.mux);
}
static void storeISR(const Msg& m) {
auto& s = state();
UBaseType_t saved = portSET_INTERRUPT_MASK_FROM_ISR();
s.latest = m;
s.hasLatest.store(true, std::memory_order_release);
portCLEAR_INTERRUPT_MASK_FROM_ISR(saved);
}
static void dispatch(const Msg& m, size_t ex) {
auto& s = state();
TickType_t now = xTaskGetTickCount();
Sub* ready[MaxSubs];
size_t readyCnt = 0;
portENTER_CRITICAL(&s.mux);
for (size_t i = 0; i < MaxSubs; ++i) {
Sub* sub = s.subs[i];
if (!sub || i == ex) continue;
if (!sub->enabled.load(std::memory_order_acquire)) continue;
TickType_t dt = now - sub->last;
if (sub->interval && dt < sub->interval) {
if (sub->mode == EmitMode::Batch) {
if (sub->cnt < BatchSize)
sub->buf[sub->cnt++] = m;
else
sub->buf[BatchSize - 1] = m;
} else {
sub->buf[0] = m;
sub->cnt = 1;
}
} else {
if (sub->cnt < BatchSize)
sub->buf[sub->cnt++] = m;
else
sub->buf[BatchSize - 1] = m;
sub->last = now;
sub->running.fetch_add(1, std::memory_order_acq_rel);
ready[readyCnt++] = sub;
}
}
portEXIT_CRITICAL(&s.mux);
for (size_t i = 0; i < readyCnt; ++i) {
Sub* sub = ready[i];
sub->cb(sub->buf, sub->cnt);
sub->cnt = 0;
sub->running.fetch_sub(1, std::memory_order_acq_rel);
}
}
static void worker(void*) {
auto& s = state();
Item it;
while (xQueueReceive(s.queue, &it, portMAX_DELAY) == pdTRUE) dispatch(it.payload, it.exclude);
}
static void ensureTask() {
auto& s = state();
if (!s.taskStarted.load(std::memory_order_acquire)) {
bool expected = false;
if (s.taskStarted.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) {
ensureQueue();
xTaskCreatePinnedToCore(worker, "evtbus", 4096, nullptr, 6, nullptr, 1);
}
}
}
static bool push(const Msg& m, size_t ex = NO_EX, TickType_t to = 0) {
ensureTask();
auto& s = state();
Item it {m, ex};
return xQueueSend(s.queue, &it, to) == pdTRUE;
}
public:
class Handle {
size_t idx {NO_EX};
friend class TypedEventBus;
explicit Handle(size_t i) : idx(i) {}
public:
Handle() = default;
Handle(const Handle&) = delete;
Handle& operator=(const Handle&) = delete;
Handle(Handle&& o) noexcept : idx(o.idx) { o.idx = NO_EX; }
Handle& operator=(Handle&& o) noexcept {
if (this != &o) {
unsubscribe();
idx = o.idx;
o.idx = NO_EX;
}
return *this;
}
~Handle() { unsubscribe(); }
void unsubscribe() {
if (idx < MaxSubs) {
auto& s = state();
Sub* sub = nullptr;
portENTER_CRITICAL(&s.mux);
sub = s.subs[idx];
if (sub) {
sub->enabled.store(false, std::memory_order_release);
}
portEXIT_CRITICAL(&s.mux);
if (sub) {
while (sub->running.load(std::memory_order_acquire) != 0) taskYIELD();
portENTER_CRITICAL(&s.mux);
s.subs[idx] = nullptr;
portEXIT_CRITICAL(&s.mux);
delete sub;
s.subCount.fetch_sub(1, std::memory_order_acq_rel);
}
idx = NO_EX;
}
}
bool valid() const { return idx < MaxSubs; }
};
static void store(const Msg& m) {
auto& s = state();
portENTER_CRITICAL(&s.mux);
s.latest = m;
s.hasLatest.store(true, std::memory_order_release);
portEXIT_CRITICAL(&s.mux);
}
template <typename C>
static void consume(C fn) {
static Handle h = subscribe(std::forward<C>(fn));
(void)h;
}
template <typename C>
static Handle subscribe(uint32_t ms, EmitMode mode, C fn) {
ensureTask();
auto& s = state();
portENTER_CRITICAL(&s.mux);
for (size_t i = 0; i < MaxSubs; ++i) {
if (!s.subs[i]) {
Sub* sub = new Sub();
sub->cb.set(std::move(fn));
sub->interval = pdMS_TO_TICKS(ms);
sub->last = xTaskGetTickCount() - sub->interval;
sub->mode = mode;
sub->cnt = 0;
sub->enabled.store(true, std::memory_order_release);
sub->running.store(0, std::memory_order_release);
s.subs[i] = sub;
s.subCount.fetch_add(1, std::memory_order_acq_rel);
portEXIT_CRITICAL(&s.mux);
return Handle(i);
}
}
portEXIT_CRITICAL(&s.mux);
return Handle(NO_EX);
}
template <typename C>
static Handle subscribe(C fn) {
if constexpr (std::is_invocable_v<C, const Msg*, size_t>)
return subscribe(0, EmitMode::Latest, std::move(fn));
else
return subscribe(0, EmitMode::Latest, [fn = std::move(fn)](const Msg* p, size_t n) {
for (size_t i = 0; i < n; ++i) fn(p[i]);
});
}
template <typename C>
static Handle subscribe(uint32_t ms, C fn) {
if constexpr (std::is_invocable_v<C, const Msg*, size_t>)
return subscribe(ms, EmitMode::Batch, std::move(fn));
else
return subscribe(ms, EmitMode::Batch, [fn = std::move(fn)](const Msg* p, size_t n) {
for (size_t i = 0; i < n; ++i) fn(p[i]);
});
}
static bool publish(const Msg& m) {
store(m);
return push(m);
}
static bool publish(const Msg& m, const Handle& h) {
store(m);
return push(m, h.valid() ? h.idx : NO_EX);
}
static void publishISR(const Msg& m, BaseType_t* hpw = nullptr) {
auto& s = state();
storeISR(m);
ensureQueue();
Item it {m, NO_EX};
xQueueSendFromISR(s.queue, &it, hpw);
}
static bool peek(Msg& out) {
auto& s = state();
if (!s.hasLatest.load(std::memory_order_acquire)) return false;
portENTER_CRITICAL(&s.mux);
out = s.latest;
portEXIT_CRITICAL(&s.mux);
return true;
}
static bool take(Msg& out) {
auto& s = state();
if (!s.hasLatest.load(std::memory_order_acquire)) return false;
portENTER_CRITICAL(&s.mux);
out = s.latest;
s.hasLatest.store(false, std::memory_order_release);
portEXIT_CRITICAL(&s.mux);
return true;
}
static bool hasSubscribers() { return state().subCount.load(std::memory_order_acquire) > 0; }
};
+9 -11
View File
@@ -9,18 +9,16 @@
#include <cstdio>
#include <platform_shared/api.pb.h>
#define MOUNT_POINT "/"
#define LITTLEFS_MOUNT_POINT "/littlefs"
#define SD_MOUNT_POINT "/sdcard"
#define MOUNT_POINT "/littlefs"
#define FS_CONFIG_DIRECTORY LITTLEFS_MOUNT_POINT "/config"
#define DEVICE_CONFIG_FILE LITTLEFS_MOUNT_POINT "/config/peripheral.pb"
#define CAMERA_SETTINGS_FILE LITTLEFS_MOUNT_POINT "/config/cameraSettings.pb"
#define AP_SETTINGS_FILE LITTLEFS_MOUNT_POINT "/config/apSettings.pb"
#define MDNS_SETTINGS_FILE LITTLEFS_MOUNT_POINT "/config/mdnsSettings.pb"
#define WIFI_SETTINGS_FILE LITTLEFS_MOUNT_POINT "/config/wifiSettings.pb"
#define PERIPHERAL_SETTINGS_FILE LITTLEFS_MOUNT_POINT "/config/peripheralSettings.pb"
#define SERVO_SETTINGS_FILE LITTLEFS_MOUNT_POINT "/config/servoSettings.pb"
#define FS_CONFIG_DIRECTORY MOUNT_POINT "/config"
#define DEVICE_CONFIG_FILE MOUNT_POINT "/config/peripheral.pb"
#define CAMERA_SETTINGS_FILE MOUNT_POINT "/config/cameraSettings.pb"
#define AP_SETTINGS_FILE MOUNT_POINT "/config/apSettings.pb"
#define MDNS_SETTINGS_FILE MOUNT_POINT "/config/mdnsSettings.pb"
#define WIFI_SETTINGS_FILE MOUNT_POINT "/config/wifiSettings.pb"
#define PERIPHERAL_SETTINGS_FILE MOUNT_POINT "/config/peripheralSettings.pb"
#define SERVO_SETTINGS_FILE MOUNT_POINT "/config/servoSettings.pb"
namespace FileSystem {
+1 -29
View File
@@ -6,16 +6,9 @@
#include <string>
#include <functional>
#include <cstdio>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <freertos/task.h>
#include <freertos/semphr.h>
#define FS_MAX_CHUNK_SIZE (1024 * 64)
#define FS_MAX_CHUNK_SIZE 16384
#define FS_TRANSFER_TIMEOUT_MS 30000
#define FS_WRITE_QUEUE_SIZE 4
#define FS_WRITER_TASK_STACK_SIZE 4096
#define FS_WRITER_TASK_PRIORITY 5
namespace FileSystemWS {
@@ -36,7 +29,6 @@ struct UploadState {
uint32_t fileSize;
uint32_t totalChunks;
uint32_t chunksReceived;
uint32_t chunksWritten;
uint32_t bytesReceived;
uint32_t lastActivityTime;
int clientId;
@@ -44,14 +36,6 @@ struct UploadState {
std::string errorMessage;
};
struct WriteRequest {
uint32_t transferId;
uint8_t* data;
size_t size;
uint32_t chunkIndex;
bool isLastChunk;
};
using SendMetadataCallback = std::function<void(const socket_message_FSDownloadMetadata&, int clientId)>;
using SendCallback = std::function<void(const socket_message_FSDownloadData&, int clientId)>;
using SendCompleteCallback = std::function<void(const socket_message_FSDownloadComplete&, int clientId)>;
@@ -60,10 +44,6 @@ using SendUploadCompleteCallback = std::function<void(const socket_message_FSUpl
class FileSystemHandler {
public:
FileSystemHandler();
~FileSystemHandler();
void startWriterTask();
void stopWriterTask();
void setSendCallbacks(SendMetadataCallback sendMetadata, SendCallback sendData, SendCompleteCallback sendComplete,
SendUploadCompleteCallback sendUploadComplete);
@@ -83,12 +63,6 @@ class FileSystemHandler {
std::map<uint32_t, UploadState> uploads_;
uint32_t transferIdCounter_;
// Async writer task
QueueHandle_t writeQueue_;
TaskHandle_t writerTaskHandle_;
SemaphoreHandle_t uploadsMutex_;
volatile bool writerTaskRunning_;
inline uint32_t generateTransferId() { return ++transferIdCounter_; }
SendMetadataCallback sendMetadataCallback_;
@@ -100,8 +74,6 @@ class FileSystemHandler {
bool deleteRecursive(const std::string& path);
bool sendNextDownloadChunk(uint32_t transferId);
void finalizeUpload(uint32_t transferId, bool success, const std::string& error = "");
void processWriteRequest(const WriteRequest& req);
static void writerTaskFunc(void* param);
};
extern FileSystemHandler fsHandler;
-13
View File
@@ -36,16 +36,3 @@
#ifndef I2C_FREQUENCY
#define I2C_FREQUENCY 100000UL
#endif
// Optional SD card mounting via SDMMC (1-bit mode for ESP32-S3-CAM)
// Pin definitions - override in build flags if needed
#ifndef SD_CMD_PIN
#define SD_CMD_PIN GPIO_NUM_38
#endif
#ifndef SD_CLK_PIN
#define SD_CLK_PIN GPIO_NUM_39
#endif
#ifndef SD_DATA_PIN
#define SD_DATA_PIN GPIO_NUM_40
#endif
+10 -6
View File
@@ -2,13 +2,11 @@
#include <esp_http_server.h>
#include <mdns.h>
#include <template/stateful_service.h>
#include <template/stateful_proto_endpoint.h>
#include <template/stateful_persistence_pb.h>
#include <event_bus/event_bus.h>
#include <settings/mdns_settings.h>
#include <utils/timing.h>
class MDNSService : public StatefulService<MDNSSettings> {
class MDNSService {
public:
MDNSService();
~MDNSService();
@@ -18,10 +16,16 @@ 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;
esp_err_t getSettings(httpd_req_t *request);
esp_err_t updateSettings(httpd_req_t *request, api_Request *protoReq);
private:
FSPersistencePB<MDNSSettings> _persistence;
static constexpr const char *TAG = "MDNSService";
void onSettingsChanged(const api_MDNSSettings &newSettings);
MDNSSettings _settings = MDNSSettings_defaults();
EventBus::Handle<api_MDNSSettings> _settingsHandle;
bool _started {false};
void reconfigureMDNS();
+14
View File
@@ -14,13 +14,18 @@
#include <motion_states/stand_state.h>
#include <motion_states/rest_state.h>
#include <message_types.h>
#include <event_bus/event_bus.h>
enum class MOTION_STATE { DEACTIVATED, IDLE, CALIBRATION, REST, STAND, WALK };
class MotionService {
public:
using ModeChangeCallback = std::function<void(bool active)>;
void begin();
void setModeChangeCallback(ModeChangeCallback callback) { modeChangeCallback_ = callback; }
void handleAngles(const socket_message_AnglesData& data);
void handleInput(const socket_message_ControllerData& data);
@@ -42,6 +47,8 @@ class MotionService {
inline bool isActive() { return state != nullptr; }
private:
void subscribeToEvents();
Kinematics kinematics;
CommandMsg command = {0, 0, 0, 0, 0, 0, 0};
@@ -62,6 +69,13 @@ class MotionService {
float dir[12] = {1, -1, -1, -1, -1, -1, 1, -1, -1, -1, -1, -1};
int64_t lastUpdate = esp_timer_get_time();
ModeChangeCallback modeChangeCallback_;
EventBus::Handle<socket_message_ControllerData> controllerHandle_;
EventBus::Handle<socket_message_ModeData> modeHandle_;
EventBus::Handle<socket_message_AnglesData> anglesHandle_;
EventBus::Handle<socket_message_WalkGaitData> walkGaitHandle_;
};
#endif
+10 -6
View File
@@ -3,9 +3,7 @@
#include <esp_http_server.h>
#include <features.h>
#include <template/stateful_service.h>
#include <template/stateful_proto_endpoint.h>
#include <template/stateful_persistence_pb.h>
#include <event_bus/event_bus.h>
#include <settings/camera_settings.h>
@@ -23,7 +21,7 @@ camera_fb_t *safe_camera_fb_get();
sensor_t *safe_sensor_get();
void safe_sensor_return();
class CameraService : public StatefulService<CameraSettings> {
class CameraService {
public:
CameraService();
@@ -32,10 +30,16 @@ class CameraService : public StatefulService<CameraSettings> {
esp_err_t cameraStill(httpd_req_t *request);
esp_err_t cameraStream(httpd_req_t *request);
StatefulProtoEndpoint<CameraSettings, api_CameraSettings> protoEndpoint;
esp_err_t getSettings(httpd_req_t *request);
esp_err_t updateSettings(httpd_req_t *request, api_Request *protoReq);
private:
FSPersistencePB<CameraSettings> _persistence;
static constexpr const char *TAG = "CameraService";
void onSettingsChanged(const api_CameraSettings &newSettings);
void updateCamera();
CameraSettings _settings = CameraSettings_defaults();
EventBus::Handle<api_CameraSettings> _settingsHandle;
};
} // namespace Camera
+11 -7
View File
@@ -1,14 +1,13 @@
#pragma once
#include <template/stateful_persistence_pb.h>
#include <template/stateful_service.h>
#include <template/stateful_proto_endpoint.h>
#include <event_bus/event_bus.h>
#include <utils/math_utils.h>
#include <utils/timing.h>
#include <filesystem.h>
#include <features.h>
#include <settings/peripherals_settings.h>
#include <platform_shared/message.pb.h>
#include <esp_http_server.h>
#include <list>
@@ -26,7 +25,7 @@
*/
#define MAX_DISTANCE 200
class Peripherals : public StatefulService<PeripheralsConfiguration> {
class Peripherals {
public:
Peripherals();
@@ -42,7 +41,6 @@ class Peripherals : public StatefulService<PeripheralsConfiguration> {
void getIMUProto(socket_message_IMUData &data);
void getSettingsProto(socket_message_PeripheralSettingsData &data);
/* IMU FUNCTIONS */
bool readImu();
bool readMag();
@@ -66,10 +64,16 @@ class Peripherals : public StatefulService<PeripheralsConfiguration> {
bool calibrateIMU();
StatefulProtoEndpoint<PeripheralsConfiguration, api_PeripheralSettings> protoEndpoint;
esp_err_t getSettings(httpd_req_t *request);
esp_err_t updateSettings(httpd_req_t *request, api_Request *protoReq);
private:
FSPersistencePB<PeripheralsConfiguration> _persistence;
static constexpr const char *TAG = "Peripherals";
void onSettingsChanged(const api_PeripheralSettings &newSettings);
PeripheralsConfiguration _settings = PeripheralsConfiguration_defaults();
EventBus::Handle<api_PeripheralSettings> _settingsHandle;
SemaphoreHandle_t _accessMutex;
inline void beginTransaction() { xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY); }
+35 -32
View File
@@ -2,11 +2,11 @@
#define ServoController_h
#include <peripherals/drivers/pca9685.h>
#include <template/stateful_persistence_pb.h>
#include <template/stateful_proto_endpoint.h>
#include <template/stateful_service.h>
#include <event_bus/event_bus.h>
#include <utils/math_utils.h>
#include <platform_shared/api.pb.h>
#include <platform_shared/message.pb.h>
#include <esp_http_server.h>
#ifndef FACTORY_SERVO_PWM_FREQUENCY
#define FACTORY_SERVO_PWM_FREQUENCY 50
@@ -24,43 +24,40 @@ inline ServoSettings ServoSettings_defaults() {
ServoSettings settings = {};
settings.servos_count = 12;
const api_Servo defaults[12] = {
{306, -1, 0, 2.0f, "Servo1"}, {306, 1, -45, 2.0f, "Servo2"},
{306, 1, 90, 2.0f, "Servo3"}, {306, -1, 0, 2.0f, "Servo4"},
{306, -1, 45, 2.0f, "Servo5"}, {306, -1, -90, 2.0f, "Servo6"},
{306, 1, 0, 2.0f, "Servo7"}, {306, 1, -45, 2.0f, "Servo8"},
{306, 1, 90, 2.0f, "Servo9"}, {306, 1, 0, 2.0f, "Servo10"},
{306, -1, 45, 2.0f, "Servo11"}, {306, -1, -90, 2.0f, "Servo12"}
};
{306, -1, 0, 2.0f, "Servo1"}, {306, 1, -45, 2.0f, "Servo2"}, {306, 1, 90, 2.0f, "Servo3"},
{306, -1, 0, 2.0f, "Servo4"}, {306, -1, 45, 2.0f, "Servo5"}, {306, -1, -90, 2.0f, "Servo6"},
{306, 1, 0, 2.0f, "Servo7"}, {306, 1, -45, 2.0f, "Servo8"}, {306, 1, 90, 2.0f, "Servo9"},
{306, 1, 0, 2.0f, "Servo10"}, {306, -1, 45, 2.0f, "Servo11"}, {306, -1, -90, 2.0f, "Servo12"}};
for (int i = 0; i < 12; i++) {
settings.servos[i] = defaults[i];
}
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()) {}
ServoController() {}
void begin() {
_persistence.readFromFS();
_settingsHandle = EventBus::subscribe<api_ServoSettings>(
[this](const api_ServoSettings &settings) { onSettingsChanged(settings); });
_pwmHandle = EventBus::subscribe<socket_message_ServoPWMData>(
[this](const socket_message_ServoPWMData &data) { setServoPWM(data.servo_id, data.servo_pwm); });
_stateHandle = EventBus::subscribe<socket_message_ServoStateData>(
[this](const socket_message_ServoStateData &data) { data.active ? activate() : deactivate(); });
api_ServoSettings initialSettings;
if (EventBus::peek(initialSettings)) {
onSettingsChanged(initialSettings);
}
initializePCA();
}
esp_err_t getSettings(httpd_req_t *request);
esp_err_t updateSettings(httpd_req_t *request, api_Request *protoReq);
void pcaWrite(int index, int value) {
if (value < 0 || value > 4096) {
ESP_LOGE("Peripherals", "Invalid PWM value %d for %d :: Valid range 0-4096", value, index);
@@ -109,7 +106,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);
@@ -121,16 +118,22 @@ class ServoController : public StatefulService<ServoSettings> {
if (control_state == SERVO_CONTROL_STATE::ANGLE) calculatePWM();
}
StatefulProtoEndpoint<ServoSettings, ServoSettings> protoEndpoint;
private:
static constexpr const char *TAG = "ServoController";
void onSettingsChanged(const api_ServoSettings &newSettings) { _settings = newSettings; }
void initializePCA() {
_pca.begin();
_pca.setOscillatorFrequency(FACTORY_SERVO_OSCILLATOR_FREQUENCY);
_pca.setPWMFreq(FACTORY_SERVO_PWM_FREQUENCY);
_pca.sleep();
}
FSPersistencePB<ServoSettings> _persistence;
api_ServoSettings _settings = ServoSettings_defaults();
EventBus::Handle<api_ServoSettings> _settingsHandle;
EventBus::Handle<socket_message_ServoPWMData> _pwmHandle;
EventBus::Handle<socket_message_ServoStateData> _stateHandle;
PCA9685Driver _pca;
-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 <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 -9
View File
@@ -16,18 +16,17 @@
} \
} while (0)
// Note: name must be a valid variable name too
#define TIME_IT(code, name) \
#define TIME_IT(code) \
{ \
uint64_t time_it_start##name = esp_timer_get_time(); \
uint64_t time_it_start = esp_timer_get_time(); \
code; \
uint64_t time_it_elapsed##name = esp_timer_get_time() - time_it_start##name; \
if (time_it_elapsed##name < 1000) { \
ESP_LOGI("Time It - " #name, "Time elapsed: %llu microseconds", time_it_elapsed##name); \
} else if (time_it_elapsed##name < 1000000) { \
ESP_LOGI("Time It - " #name, "Time elapsed: %llu milliseconds", time_it_elapsed##name / 1000); \
uint64_t time_it_elapsed = esp_timer_get_time() - time_it_start; \
if (time_it_elapsed < 1000) { \
ESP_LOGI("Time It", "Time elapsed: %llu microseconds", time_it_elapsed); \
} else if (time_it_elapsed < 1000000) { \
ESP_LOGI("Time It", "Time elapsed: %llu milliseconds", time_it_elapsed / 1000); \
} else { \
ESP_LOGI("Time It - " #name, "Time elapsed: %.2f seconds", time_it_elapsed##name / 1000000.0); \
ESP_LOGI("Time It", "Time elapsed: %.2f seconds", time_it_elapsed / 1000000.0); \
} \
}
+20 -11
View File
@@ -5,18 +5,15 @@
#include <mdns.h>
#include <string>
#include <filesystem.h>
#include <utils/timing.h>
#include <template/stateful_service.h>
#include <template/stateful_persistence_pb.h>
#include <template/stateful_proto_endpoint.h>
#include <event_bus/event_bus.h>
#include <settings/wifi_settings.h>
#include <utils/timing.h>
#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,27 +24,39 @@ class WiFiService : public StatefulService<WiFiSettings> {
void setupMDNS(const char *hostname);
void selectNetwork(uint32_t index);
const char *getHostname() { return state().hostname; }
const char *getHostname() {
static api_WifiSettings cached_settings;
EventBus::peek(cached_settings);
return cached_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;
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;
void onSettingsChanged(const api_WifiSettings &newSettings);
void reconfigureWiFiConnection();
void manageSTA();
void configureNetwork(WiFiNetwork &network);
void configureNetwork(const WiFiNetwork &network);
api_WifiSettings getSettings() const {
api_WifiSettings settings;
EventBus::peek(settings);
return settings;
}
EventBus::Handle<api_WifiSettings> _settingsHandle;
bool _initialized;
unsigned long _lastConnectionAttempt;
bool _stopping;
constexpr static uint16_t reconnectDelay {10000};
static constexpr const char *TAG = "WiFiService";
};
+1
View File
@@ -5,6 +5,7 @@ idf_component_register(
"peripherals"
"wifi"
"platform_shared"
"event_bus"
"../../submodules/nanopb"
INCLUDE_DIRS
"../include"
+52 -20
View File
@@ -1,19 +1,7 @@
#include <ap_service.h>
#include <communication/webserver.h>
static const char *TAG = "APService";
APService::APService()
: protoEndpoint(APSettings_read, APSettings_update, this, API_REQUEST_EXTRACTOR(ap_settings, api_APSettings),
API_RESPONSE_ASSIGNER(ap_settings, api_APSettings)),
_persistence(APSettings_read, APSettings_update, this, AP_SETTINGS_FILE, api_APSettings_fields,
api_APSettings_size, APSettings_defaults()),
_dnsServer(nullptr),
_lastManaged(0),
_reconfigureAp(false),
_recoveryMode(false) {
addUpdateHandler([&](const std::string &originId) { reconfigureAP(); }, false);
}
APService::APService() : _dnsServer(nullptr), _lastManaged(0), _reconfigureAp(false), _recoveryMode(false) {}
APService::~APService() {
if (_dnsServer) {
@@ -22,7 +10,30 @@ APService::~APService() {
}
}
void APService::begin() { _persistence.readFromFS(); }
void APService::begin() {
_settingsHandle =
EventBus::subscribe<api_APSettings>([this](const api_APSettings &settings) { onSettingsChanged(settings); });
api_APSettings initialSettings;
if (EventBus::peek(initialSettings)) {
onSettingsChanged(initialSettings);
}
}
void APService::onSettingsChanged(const api_APSettings &newSettings) {
strncpy(_settings.ssid, newSettings.ssid, sizeof(_settings.ssid) - 1);
_settings.ssid[sizeof(_settings.ssid) - 1] = '\0';
strncpy(_settings.password, newSettings.password, sizeof(_settings.password) - 1);
_settings.password[sizeof(_settings.password) - 1] = '\0';
_settings.local_ip = newSettings.local_ip;
_settings.gateway_ip = newSettings.gateway_ip;
_settings.subnet_mask = newSettings.subnet_mask;
_settings.channel = newSettings.channel;
_settings.ssid_hidden = newSettings.ssid_hidden;
_settings.max_clients = newSettings.max_clients;
_settings.provision_mode = newSettings.provision_mode;
reconfigureAP();
}
esp_err_t APService::getStatusProto(httpd_req_t *request) {
api_Response res = api_Response_init_zero;
@@ -44,7 +55,7 @@ void APService::statusProto(api_APStatus &proto) {
APNetworkStatus APService::getAPNetworkStatus() {
wifi_mode_t currentWiFiMode = WiFi.getMode();
bool apActive = currentWiFiMode == WIFI_MODE_AP || currentWiFiMode == WIFI_MODE_APSTA;
if (apActive && state().provision_mode != AP_MODE_ALWAYS && WiFi.status() == WL_CONNECTED) {
if (apActive && _settings.provision_mode != AP_MODE_ALWAYS && WiFi.status() == WL_CONNECTED) {
return LINGERING;
}
return apActive ? ACTIVE : INACTIVE;
@@ -70,8 +81,8 @@ void APService::loop() {
void APService::manageAP() {
wifi_mode_t currentWiFiMode = WiFi.getMode();
if (state().provision_mode == AP_MODE_ALWAYS ||
(state().provision_mode == AP_MODE_DISCONNECTED && WiFi.status() != WL_CONNECTED) || _recoveryMode) {
if (_settings.provision_mode == AP_MODE_ALWAYS ||
(_settings.provision_mode == AP_MODE_DISCONNECTED && WiFi.status() != WL_CONNECTED) || _recoveryMode) {
if (_reconfigureAp || currentWiFiMode == WIFI_MODE_NULL || currentWiFiMode == WIFI_MODE_STA) {
startAP();
}
@@ -83,9 +94,9 @@ void APService::manageAP() {
}
void APService::startAP() {
ESP_LOGI(TAG, "Starting software access point: %s", state().ssid);
WiFi.softAPConfig(IPAddress(state().local_ip), IPAddress(state().gateway_ip), IPAddress(state().subnet_mask));
WiFi.softAP(state().ssid, state().password, state().channel, state().ssid_hidden, state().max_clients);
ESP_LOGI(TAG, "Starting software access point: %s", _settings.ssid);
WiFi.softAPConfig(IPAddress(_settings.local_ip), IPAddress(_settings.gateway_ip), IPAddress(_settings.subnet_mask));
WiFi.softAP(_settings.ssid, _settings.password, _settings.channel, _settings.ssid_hidden, _settings.max_clients);
#if CONFIG_IDF_TARGET_ESP32C3
WiFi.setTxPower(8);
#endif
@@ -109,3 +120,24 @@ void APService::stopAP() {
}
void APService::handleDNS() {}
esp_err_t APService::getSettings(httpd_req_t *request) {
api_Response response = api_Response_init_zero;
response.status_code = 200;
response.which_payload = api_Response_ap_settings_tag;
response.payload.ap_settings = _settings;
return WebServer::send(request, 200, response, api_Response_fields);
}
esp_err_t APService::updateSettings(httpd_req_t *request, api_Request *protoReq) {
if (protoReq->which_payload != api_Request_ap_settings_tag) {
return ESP_FAIL;
}
EventBus::publish(protoReq->payload.ap_settings, "HTTPEndpoint");
api_Response response = api_Response_init_zero;
response.status_code = 200;
response.which_payload = api_Response_empty_message_tag;
return WebServer::send(request, 200, response, api_Response_fields);
}
+4 -29
View File
@@ -2,7 +2,6 @@
#include <esp_log.h>
#include <cstring>
#include <algorithm>
#include <utils/timing.h>
static const char* TAG = "WebServer";
@@ -122,10 +121,6 @@ esp_err_t WebServer::httpHandler(httpd_req_t* req) {
}
esp_err_t WebServer::wsHandler(httpd_req_t* req) {
esp_err_t result;
httpd_ws_frame_t frame;
esp_err_t ret;
TIME_IT(
WebServer* self = static_cast<WebServer*>(req->user_ctx);
if (req->method == HTTP_GET) {
@@ -138,21 +133,17 @@ esp_err_t WebServer::wsHandler(httpd_req_t* req) {
return ESP_OK;
}
httpd_ws_frame_t frame;
memset(&frame, 0, sizeof(httpd_ws_frame_t));
frame.type = HTTPD_WS_TYPE_BINARY;
TIME_IT(
ret = httpd_ws_recv_frame(req, &frame, 0);
esp_err_t ret = httpd_ws_recv_frame(req, &frame, 0);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to get frame len: %s", esp_err_to_name(ret));
return ret;
}
, FRAME_LEN)
if (frame.len > 0) {
TIME_IT(
frame.payload = (uint8_t*)malloc(frame.len);
if (!frame.payload) {
ESP_LOGE(TAG, "Failed to allocate frame payload");
@@ -165,7 +156,6 @@ esp_err_t WebServer::wsHandler(httpd_req_t* req) {
free(frame.payload);
return ret;
}
, FRAME_RECEIVE)
}
if (frame.type == HTTPD_WS_TYPE_CLOSE) {
@@ -179,18 +169,15 @@ esp_err_t WebServer::wsHandler(httpd_req_t* req) {
return ESP_OK;
}
result = ESP_OK;
esp_err_t result = ESP_OK;
if (self->wsFrameHandler_) {
TIME_IT(
result = self->wsFrameHandler_(req, &frame);
, FRAME_HANDLER)
}
if (frame.payload) {
free(frame.payload);
}
, WS_HANDLER)
return result;
}
@@ -278,19 +265,7 @@ esp_err_t WebServer::wsSend(int sockfd, const uint8_t* data, size_t len) {
.type = HTTPD_WS_TYPE_BINARY,
.payload = const_cast<uint8_t*>(data),
.len = len};
xSemaphoreTake(wsMutex_, portMAX_DELAY);
esp_err_t ret = httpd_ws_send_frame_async(server_, sockfd, &frame);
xSemaphoreGive(wsMutex_);
if (ret != ESP_OK) {
if (httpd_ws_get_fd_info(server_, sockfd) != HTTPD_WS_CLIENT_WEBSOCKET) {
ESP_LOGW(TAG, "Removing disconnected client %d", sockfd);
removeWsClient(sockfd);
return ESP_ERR_INVALID_STATE;
}
}
return ret;
return httpd_ws_send_frame_async(server_, sockfd, &frame);
}
esp_err_t WebServer::wsSendAll(const uint8_t* data, size_t len) {
+1 -19
View File
@@ -24,26 +24,8 @@ void Websocket::onWsClose(int sockfd) {
}
esp_err_t Websocket::onFrame(httpd_req_t* req, httpd_ws_frame_t* frame) {
// Handle PING - respond with PONG
if (frame->type == HTTPD_WS_TYPE_PING) {
httpd_ws_frame_t pong = {
.final = true,
.fragmented = false,
.type = HTTPD_WS_TYPE_PONG,
.payload = frame->payload,
.len = frame->len
};
return httpd_ws_send_frame(req, &pong);
}
// Ignore PONG frames
if (frame->type == HTTPD_WS_TYPE_PONG) {
return ESP_OK;
}
// Ignore other non-binary frames
if (frame->type != HTTPD_WS_TYPE_BINARY) {
ESP_LOGD(TAG, "Ignoring frame type %d", frame->type);
ESP_LOGW(TAG, "Expected binary frame, got type %d", frame->type);
return ESP_OK;
}
+61
View File
@@ -0,0 +1,61 @@
#include <event_bus/event_bus.h>
#include <vector>
#include <memory>
#include <mutex>
#include <algorithm>
std::atomic<bool> EventBus::_hasGlobalListeners {false};
struct GlobalSubscription {
size_t id;
std::unique_ptr<EventBus::GlobalHandler> handler;
};
static std::vector<GlobalSubscription> globalSubs;
static std::mutex globalMutex;
static size_t nextGlobalId = 1;
size_t EventBus::subscribeGlobal(GlobalHandler&& handler) {
std::lock_guard<std::mutex> lock(globalMutex);
size_t id = nextGlobalId++;
auto h = std::make_unique<GlobalHandler>(std::move(handler));
globalSubs.push_back({id, std::move(h)});
_hasGlobalListeners.store(true, std::memory_order_release);
return id;
}
void EventBus::unsubscribeGlobal(size_t id) {
std::lock_guard<std::mutex> lock(globalMutex);
globalSubs.erase(std::remove_if(globalSubs.begin(), globalSubs.end(), [id](const auto& s) { return s.id == id; }),
globalSubs.end());
_hasGlobalListeners.store(!globalSubs.empty(), std::memory_order_release);
}
template <typename Msg>
void EventBus::notifyGlobalListeners(const Msg& msg, const char* source) {
uint64_t timestamp = esp_timer_get_time();
EventType type = EventTypeTraits<Msg>::type;
std::lock_guard<std::mutex> lock(globalMutex);
for (auto& sub : globalSubs) {
(*sub.handler)(type, &msg, sizeof(Msg), timestamp);
}
}
template void EventBus::notifyGlobalListeners<api_WifiSettings>(const api_WifiSettings&, const char*);
template void EventBus::notifyGlobalListeners<api_APSettings>(const api_APSettings&, const char*);
template void EventBus::notifyGlobalListeners<api_MDNSSettings>(const api_MDNSSettings&, const char*);
template void EventBus::notifyGlobalListeners<api_PeripheralSettings>(const api_PeripheralSettings&, const char*);
template void EventBus::notifyGlobalListeners<api_ServoSettings>(const api_ServoSettings&, const char*);
template void EventBus::notifyGlobalListeners<api_CameraSettings>(const api_CameraSettings&, const char*);
template void EventBus::notifyGlobalListeners<socket_message_IMUData>(const socket_message_IMUData&, const char*);
template void EventBus::notifyGlobalListeners<socket_message_ControllerData>(const socket_message_ControllerData&,
const char*);
template void EventBus::notifyGlobalListeners<socket_message_ModeData>(const socket_message_ModeData&, const char*);
template void EventBus::notifyGlobalListeners<socket_message_AnglesData>(const socket_message_AnglesData&, const char*);
template void EventBus::notifyGlobalListeners<socket_message_WalkGaitData>(const socket_message_WalkGaitData&,
const char*);
template void EventBus::notifyGlobalListeners<socket_message_ServoStateData>(const socket_message_ServoStateData&,
const char*);
template void EventBus::notifyGlobalListeners<socket_message_ServoPWMData>(const socket_message_ServoPWMData&,
const char*);
+29
View File
@@ -0,0 +1,29 @@
#include <event_bus/event_types.h>
const char* eventTypeName(EventType type) {
switch (type) {
case EventType::WIFI_SETTINGS: return "WIFI_SETTINGS";
case EventType::AP_SETTINGS: return "AP_SETTINGS";
case EventType::MDNS_SETTINGS: return "MDNS_SETTINGS";
case EventType::PERIPHERAL_SETTINGS: return "PERIPHERAL_SETTINGS";
case EventType::SERVO_SETTINGS: return "SERVO_SETTINGS";
case EventType::CAMERA_SETTINGS: return "CAMERA_SETTINGS";
case EventType::WIFI_STATUS: return "WIFI_STATUS";
case EventType::AP_STATUS: return "AP_STATUS";
case EventType::IMU_DATA: return "IMU_DATA";
case EventType::MOTION_COMMAND: return "MOTION_COMMAND";
case EventType::MOTION_MODE: return "MOTION_MODE";
case EventType::MOTION_ANGLES: return "MOTION_ANGLES";
case EventType::MOTION_WALK_GAIT: return "MOTION_WALK_GAIT";
case EventType::SERVO_STATE: return "SERVO_STATE";
case EventType::SERVO_PWM: return "SERVO_PWM";
case EventType::SYSTEM_BOOT: return "SYSTEM_BOOT";
case EventType::STORAGE_HYDRATION_COMPLETE: return "STORAGE_HYDRATION_COMPLETE";
default: return "UNKNOWN";
}
}
bool isSettingsEvent(EventType type) {
return static_cast<uint16_t>(type) >= 100 && static_cast<uint16_t>(type) < 200 &&
static_cast<uint16_t>(type) % 10 == 0;
}
+6 -41
View File
@@ -6,9 +6,6 @@
#include <esp_log.h>
#include <pb_encode.h>
#include <pb_decode.h>
#include "esp_vfs_fat.h"
#include <sdmmc_cmd.h>
#include <driver/sdmmc_host.h>
static const char *TAG = "FileSystem";
@@ -82,7 +79,7 @@ void listFilesProto(const std::string &directory, api_FileEntry *entry) {
if (path.empty() || path[0] != '/') {
path = "/" + directory;
}
std::string fullPath = path;
std::string fullPath = std::string(MOUNT_POINT) + path;
listFilesProtoRecursive(fullPath, entry);
}
@@ -111,7 +108,7 @@ esp_err_t getFilesProto(httpd_req_t *request) {
bool init() {
esp_vfs_littlefs_conf_t conf = {
.base_path = LITTLEFS_MOUNT_POINT,
.base_path = MOUNT_POINT,
.partition_label = "spiffs",
.format_if_mount_failed = true,
.dont_mount = false,
@@ -137,38 +134,6 @@ bool init() {
mkdirRecursive(FS_CONFIG_DIRECTORY);
esp_vfs_fat_sdmmc_mount_config_t sd_mount_config = {
.format_if_mount_failed = false,
.max_files = 4,
.allocation_unit_size = 16 * 1024,
};
sdmmc_host_t host = SDMMC_HOST_DEFAULT();
host.flags = SDMMC_HOST_FLAG_1BIT; // Use 1-bit mode
host.max_freq_khz = SDMMC_FREQ_DEFAULT;
sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();
slot_config.width = 1; // 1-bit mode
slot_config.clk = SD_CLK_PIN;
slot_config.cmd = SD_CMD_PIN;
slot_config.d0 = SD_DATA_PIN;
slot_config.flags |= SDMMC_SLOT_FLAG_INTERNAL_PULLUP;
sdmmc_card_t *card = nullptr;
esp_err_t err = esp_vfs_fat_sdmmc_mount(SD_MOUNT_POINT, &host, &slot_config, &sd_mount_config, &card);
if (err != ESP_OK) {
if (err == ESP_FAIL) {
ESP_LOGW(TAG, "Failed to mount SD card filesystem");
} else {
ESP_LOGW(TAG, "SD card not present or failed to initialize (%s)", esp_err_to_name(err));
}
// Don't fail - SD card is optional
} else {
ESP_LOGI(TAG, "SD card mounted at %s", SD_MOUNT_POINT);
ESP_LOGI(TAG, "SD card: %s, %lluMB", card->cid.name,
((uint64_t)card->csd.capacity) * card->csd.sector_size / (1024 * 1024));
}
return true;
}
@@ -266,7 +231,7 @@ esp_err_t getFiles(httpd_req_t *request) {
esp_err_t getConfigFile(httpd_req_t *request) {
const char *uri = request->uri;
std::string path = std::string(LITTLEFS_MOUNT_POINT) + "/config" + std::string(uri).substr(11);
std::string path = std::string(MOUNT_POINT) + "/config" + std::string(uri).substr(11);
if (!fileExists(path.c_str())) {
return WebServer::sendError(request, 404, "File not found");
@@ -288,7 +253,7 @@ esp_err_t getConfigFile(httpd_req_t *request) {
}
esp_err_t handleDelete(httpd_req_t *request, const api_FileDeleteRequest &req) {
std::string fullPath = req.path;
std::string fullPath = std::string(MOUNT_POINT) + req.path;
ESP_LOGI(TAG, "Deleting file: %s", fullPath.c_str());
api_Response res = api_Response_init_zero;
@@ -302,7 +267,7 @@ esp_err_t handleDelete(httpd_req_t *request, const api_FileDeleteRequest &req) {
}
esp_err_t handleEdit(httpd_req_t *request, const api_FileEditRequest &req) {
std::string fullPath = req.path;
std::string fullPath = std::string(MOUNT_POINT) + req.path;
ESP_LOGI(TAG, "Editing file: %s", fullPath.c_str());
api_Response res = api_Response_init_zero;
@@ -361,7 +326,7 @@ bool editFile(const char *filename, const uint8_t *content, size_t size) { retur
bool editFile(const char *filename, const char *content) { return writeFile(filename, content); }
esp_err_t mkdir(httpd_req_t *request, const api_FileMkdirRequest &req) {
std::string fullPath = req.path;
std::string fullPath = std::string(MOUNT_POINT) + req.path;
ESP_LOGI(TAG, "Creating directory: %s", fullPath.c_str());
api_Response res = api_Response_init_zero;
+45 -278
View File
@@ -7,8 +7,6 @@
#include <dirent.h>
#include <sys/stat.h>
#include <cstring>
#include <cerrno>
#include <utils/timing.h>
static const char* TAG = "FileSystemWS";
@@ -16,137 +14,7 @@ namespace FileSystemWS {
FileSystemHandler fsHandler;
FileSystemHandler::FileSystemHandler()
: transferIdCounter_(0), writeQueue_(nullptr), writerTaskHandle_(nullptr), uploadsMutex_(nullptr),
writerTaskRunning_(false) {
uploadsMutex_ = xSemaphoreCreateMutex();
}
FileSystemHandler::~FileSystemHandler() {
stopWriterTask();
if (uploadsMutex_) {
vSemaphoreDelete(uploadsMutex_);
}
}
void FileSystemHandler::startWriterTask() {
if (writerTaskHandle_ != nullptr) {
return;
}
writeQueue_ = xQueueCreate(FS_WRITE_QUEUE_SIZE, sizeof(WriteRequest));
if (!writeQueue_) {
ESP_LOGE(TAG, "Failed to create write queue");
return;
}
writerTaskRunning_ = true;
BaseType_t result =
xTaskCreate(writerTaskFunc, "fs_writer", FS_WRITER_TASK_STACK_SIZE, this, FS_WRITER_TASK_PRIORITY, &writerTaskHandle_);
if (result != pdPASS) {
ESP_LOGE(TAG, "Failed to create writer task");
vQueueDelete(writeQueue_);
writeQueue_ = nullptr;
writerTaskRunning_ = false;
} else {
ESP_LOGI(TAG, "Writer task started");
}
}
void FileSystemHandler::stopWriterTask() {
if (!writerTaskRunning_) {
return;
}
writerTaskRunning_ = false;
// Send a poison pill to wake up the task
WriteRequest poison = {0, nullptr, 0, 0, false};
if (writeQueue_) {
xQueueSend(writeQueue_, &poison, portMAX_DELAY);
}
// Wait for task to finish
if (writerTaskHandle_) {
vTaskDelay(pdMS_TO_TICKS(100));
writerTaskHandle_ = nullptr;
}
if (writeQueue_) {
// Drain any remaining requests and free their data
WriteRequest req;
while (xQueueReceive(writeQueue_, &req, 0) == pdTRUE) {
if (req.data) {
free(req.data);
}
}
vQueueDelete(writeQueue_);
writeQueue_ = nullptr;
}
ESP_LOGI(TAG, "Writer task stopped");
}
void FileSystemHandler::writerTaskFunc(void* param) {
FileSystemHandler* self = static_cast<FileSystemHandler*>(param);
WriteRequest req;
while (self->writerTaskRunning_) {
if (xQueueReceive(self->writeQueue_, &req, pdMS_TO_TICKS(10)) == pdTRUE) {
if (req.data == nullptr) {
// Poison pill - exit
break;
}
self->processWriteRequest(req);
free(req.data);
}
}
vTaskDelete(nullptr);
}
void FileSystemHandler::processWriteRequest(const WriteRequest& req) {
xSemaphoreTake(uploadsMutex_, portMAX_DELAY);
auto it = uploads_.find(req.transferId);
if (it == uploads_.end()) {
xSemaphoreGive(uploadsMutex_);
return;
}
UploadState& state = it->second;
if (state.hasError) {
xSemaphoreGive(uploadsMutex_);
return;
}
size_t bytesWritten = fwrite(req.data, 1, req.size, state.file);
if (bytesWritten != req.size) {
state.hasError = true;
state.errorMessage = "Failed to write chunk";
xSemaphoreGive(uploadsMutex_);
finalizeUpload(req.transferId, false, state.errorMessage);
return;
}
state.chunksWritten++;
ESP_LOGD(TAG, "Async write chunk %u/%u: %u bytes", state.chunksWritten, state.totalChunks, bytesWritten);
// Periodic flush
if (state.chunksWritten > 0 && state.chunksWritten % 64 == 0) {
fflush(state.file);
}
bool shouldFinalize = req.isLastChunk;
xSemaphoreGive(uploadsMutex_);
if (shouldFinalize) {
finalizeUpload(req.transferId, true);
}
}
FileSystemHandler::FileSystemHandler() : transferIdCounter_(0) {}
void FileSystemHandler::setSendCallbacks(SendMetadataCallback sendMetadata, SendCallback sendData,
SendCompleteCallback sendComplete,
@@ -184,46 +52,35 @@ void FileSystemHandler::cleanupExpiredTransfers() {
}
}
xSemaphoreTake(uploadsMutex_, portMAX_DELAY);
auto ulIt = uploads_.begin();
while (ulIt != uploads_.end()) {
if (now - ulIt->second.lastActivityTime > FS_TRANSFER_TIMEOUT_MS) {
if (ulIt->second.file) {
fclose(ulIt->second.file);
ulIt->second.file = nullptr;
}
std::string path = ulIt->second.path;
uint32_t chunksReceived = ulIt->second.chunksReceived;
int clientId = ulIt->second.clientId;
uint32_t transferId = ulIt->first;
ulIt = uploads_.erase(ulIt);
xSemaphoreGive(uploadsMutex_);
remove(path.c_str());
ESP_LOGW(TAG, "Upload %u timed out, deleted partial file", transferId);
remove(ulIt->second.path.c_str());
ESP_LOGW(TAG, "Upload %u timed out, deleted partial file", ulIt->first);
if (sendUploadCompleteCallback_) {
socket_message_FSUploadComplete complete = socket_message_FSUploadComplete_init_zero;
complete.transfer_id = transferId;
complete.transfer_id = ulIt->first;
complete.success = false;
strncpy(complete.error, "Transfer timed out", sizeof(complete.error) - 1);
complete.chunks_received = chunksReceived;
sendUploadCompleteCallback_(complete, clientId);
complete.chunks_received = ulIt->second.chunksReceived;
sendUploadCompleteCallback_(complete, ulIt->second.clientId);
}
xSemaphoreTake(uploadsMutex_, portMAX_DELAY);
ulIt = uploads_.erase(ulIt);
} else {
++ulIt;
}
}
xSemaphoreGive(uploadsMutex_);
}
socket_message_FSDeleteResponse FileSystemHandler::handleDelete(const socket_message_FSDeleteRequest& req) {
socket_message_FSDeleteResponse response = socket_message_FSDeleteResponse_init_zero;
std::string path = req.path;
std::string path = std::string(MOUNT_POINT) + req.path;
ESP_LOGI(TAG, "Delete request: %s", path.c_str());
struct stat st;
@@ -272,7 +129,7 @@ bool FileSystemHandler::deleteRecursive(const std::string& path) {
socket_message_FSMkdirResponse FileSystemHandler::handleMkdir(const socket_message_FSMkdirRequest& req) {
socket_message_FSMkdirResponse response = socket_message_FSMkdirResponse_init_zero;
std::string path = req.path;
std::string path = std::string(MOUNT_POINT) + req.path;
ESP_LOGI(TAG, "Mkdir request: %s", path.c_str());
struct stat st;
@@ -293,15 +150,6 @@ socket_message_FSMkdirResponse FileSystemHandler::handleMkdir(const socket_messa
}
void FileSystemHandler::listDirectory(const std::string& path, socket_message_FSListResponse& response) {
// Root "/" is virtual - list mount points instead
if (strcmp(path.c_str(), "/") == 0) {
strncpy(response.directories[0].name, LITTLEFS_MOUNT_POINT + 1, sizeof(response.directories[0].name) - 1);
strncpy(response.directories[1].name, SD_MOUNT_POINT + 1, sizeof(response.directories[1].name) - 1);
response.directories_count = 2;
return;
}
DIR* dir = opendir(path.c_str());
if (!dir) {
return;
@@ -343,13 +191,15 @@ void FileSystemHandler::listDirectory(const std::string& path, socket_message_FS
socket_message_FSListResponse FileSystemHandler::handleList(const socket_message_FSListRequest& req) {
socket_message_FSListResponse response = socket_message_FSListResponse_init_zero;
std::string path = req.path;
std::string path = std::string(MOUNT_POINT);
if (strlen(req.path) > 0 && req.path[0] != '\0') {
path += req.path;
}
ESP_LOGI(TAG, "List request: %s", path.c_str());
struct stat st;
// Make sure that path exists, or that it is a root listing
if (strcmp(path.c_str(), "/") != 0 && stat(path.c_str(), &st) != 0) {
if (stat(path.c_str(), &st) != 0) {
response.success = false;
strncpy(response.error, "Path not found", sizeof(response.error) - 1);
return response;
@@ -362,7 +212,7 @@ socket_message_FSListResponse FileSystemHandler::handleList(const socket_message
}
void FileSystemHandler::handleDownloadRequest(const socket_message_FSDownloadRequest& req, int clientId) {
std::string path = req.path;
std::string path = std::string(MOUNT_POINT) + req.path;
ESP_LOGI(TAG, "Download request: %s", path.c_str());
struct stat st;
@@ -458,28 +308,8 @@ bool FileSystemHandler::sendNextDownloadChunk(uint32_t transferId) {
bytesToRead = state.fileSize - position;
}
// Allocate buffer for FT_POINTER data field
data->data = (pb_bytes_array_t*)malloc(PB_BYTES_ARRAY_T_ALLOCSIZE(bytesToRead));
if (!data->data) {
delete data;
if (sendCompleteCallback_) {
socket_message_FSDownloadComplete complete = socket_message_FSDownloadComplete_init_zero;
complete.transfer_id = transferId;
complete.success = false;
strncpy(complete.error, "Memory allocation failed", sizeof(complete.error) - 1);
complete.total_chunks = state.chunksSent;
complete.file_size = state.fileSize;
sendCompleteCallback_(complete, state.clientId);
}
fclose(state.file);
downloads_.erase(it);
ESP_LOGE(TAG, "Download failed - memory allocation: %u", transferId);
return false;
}
size_t bytesRead = fread(data->data->bytes, 1, bytesToRead, state.file);
size_t bytesRead = fread(data->data.bytes, 1, bytesToRead, state.file);
if (bytesRead == 0 && bytesToRead > 0) {
free(data->data);
delete data;
if (sendCompleteCallback_) {
socket_message_FSDownloadComplete complete = socket_message_FSDownloadComplete_init_zero;
@@ -496,13 +326,12 @@ bool FileSystemHandler::sendNextDownloadChunk(uint32_t transferId) {
ESP_LOGE(TAG, "Download failed - read error: %u", transferId);
return false;
}
data->data->size = bytesRead;
data->data.size = bytesRead;
if (sendDataCallback_) {
sendDataCallback_(*data, state.clientId);
}
free(data->data);
delete data;
state.chunksSent++;
ESP_LOGD(TAG, "Download chunk %u/%u sent: %u bytes", state.chunksSent, state.totalChunks, bytesRead);
@@ -514,22 +343,17 @@ socket_message_FSUploadStartResponse FileSystemHandler::handleUploadStart(const
int clientId) {
socket_message_FSUploadStartResponse response = socket_message_FSUploadStartResponse_init_zero;
std::string path = req.path;
std::string path = std::string(MOUNT_POINT) + req.path;
ESP_LOGI(TAG, "Upload start request: %s, size=%u, chunks=%u", path.c_str(), req.file_size, req.total_chunks);
// Check available space on the target filesystem
if (path.find(SD_MOUNT_POINT) != 0) {
// LittleFS path
size_t fs_total = 0, fs_used = 0;
esp_littlefs_info("spiffs", &fs_total, &fs_used);
size_t freeSpace = fs_total - fs_used;
if (freeSpace < req.file_size + 4096) {
response.success = false;
strncpy(response.error, "Insufficient storage space", sizeof(response.error) - 1);
return response;
}
size_t fs_total = 0, fs_used = 0;
esp_littlefs_info("spiffs", &fs_total, &fs_used);
size_t freeSpace = fs_total - fs_used;
if (freeSpace < req.file_size + 4096) {
response.success = false;
strncpy(response.error, "Insufficient storage space", sizeof(response.error) - 1);
return response;
}
// TODO: SD card space check skipped - FAT doesn't have a simple API for this
size_t lastSlash = path.find_last_of('/');
if (lastSlash != std::string::npos && lastSlash > 0) {
@@ -544,15 +368,11 @@ socket_message_FSUploadStartResponse FileSystemHandler::handleUploadStart(const
FILE* file = fopen(path.c_str(), "wb");
if (!file) {
ESP_LOGE(TAG, "fopen failed for '%s': %s (errno=%d)", path.c_str(), strerror(errno), errno);
response.success = false;
snprintf(response.error, sizeof(response.error) - 1, "Cannot open file: %s", strerror(errno));
strncpy(response.error, "Cannot open file for writing", sizeof(response.error) - 1);
return response;
}
// Set larger buffer for better write performance
setvbuf(file, nullptr, _IOFBF, 32 * 1024);
uint32_t transferId = generateTransferId();
UploadState state;
@@ -561,15 +381,12 @@ socket_message_FSUploadStartResponse FileSystemHandler::handleUploadStart(const
state.fileSize = req.file_size;
state.totalChunks = req.total_chunks;
state.chunksReceived = 0;
state.chunksWritten = 0;
state.bytesReceived = 0;
state.lastActivityTime = esp_timer_get_time() / 1000;
state.clientId = clientId;
state.hasError = false;
xSemaphoreTake(uploadsMutex_, portMAX_DELAY);
uploads_[transferId] = state;
xSemaphoreGive(uploadsMutex_);
response.success = true;
response.transfer_id = transferId;
@@ -582,16 +399,8 @@ socket_message_FSUploadStartResponse FileSystemHandler::handleUploadStart(const
void FileSystemHandler::handleUploadData(const socket_message_FSUploadData& req) {
uint32_t transferId = req.transfer_id;
// Auto-start writer task if not running
if (!writerTaskRunning_) {
startWriterTask();
}
xSemaphoreTake(uploadsMutex_, portMAX_DELAY);
auto it = uploads_.find(transferId);
if (it == uploads_.end()) {
xSemaphoreGive(uploadsMutex_);
ESP_LOGW(TAG, "Upload data for unknown transfer: %u", transferId);
return;
}
@@ -600,7 +409,6 @@ void FileSystemHandler::handleUploadData(const socket_message_FSUploadData& req)
state.lastActivityTime = esp_timer_get_time() / 1000;
if (state.hasError) {
xSemaphoreGive(uploadsMutex_);
return;
}
@@ -608,61 +416,32 @@ void FileSystemHandler::handleUploadData(const socket_message_FSUploadData& req)
ESP_LOGW(TAG, "Upload chunk out of order: expected %u, got %u", state.chunksReceived, req.chunk_index);
}
if (!req.data || req.data->size == 0) {
size_t bytesWritten = fwrite(req.data.bytes, 1, req.data.size, state.file);
if (bytesWritten != req.data.size) {
state.hasError = true;
state.errorMessage = "Empty or invalid data chunk";
xSemaphoreGive(uploadsMutex_);
state.errorMessage = "Failed to write chunk";
finalizeUpload(transferId, false, state.errorMessage);
return;
}
// Copy data for async write
WriteRequest writeReq;
writeReq.transferId = transferId;
writeReq.size = req.data->size;
writeReq.chunkIndex = req.chunk_index;
writeReq.data = static_cast<uint8_t*>(malloc(req.data->size));
if (!writeReq.data) {
state.hasError = true;
state.errorMessage = "Memory allocation failed";
xSemaphoreGive(uploadsMutex_);
finalizeUpload(transferId, false, state.errorMessage);
return;
}
memcpy(writeReq.data, req.data->bytes, req.data->size);
state.chunksReceived++;
state.bytesReceived += req.data->size;
writeReq.isLastChunk = (state.chunksReceived >= state.totalChunks);
state.bytesReceived += bytesWritten;
ESP_LOGD(TAG, "Queuing chunk %u/%u: %u bytes", state.chunksReceived, state.totalChunks, req.data->size);
xSemaphoreGive(uploadsMutex_);
// Check queue is valid
if (!writeQueue_) {
ESP_LOGE(TAG, "Write queue not initialized");
free(writeReq.data);
finalizeUpload(transferId, false, "Write queue not initialized");
return;
if (state.chunksReceived > 0 && state.chunksReceived % 64 == 0) {
ESP_LOGD(TAG, "Flushing file at chunk %u", state.chunksReceived);
fflush(state.file);
}
// Try to queue (non-blocking) - if full, do sync write to avoid blocking HTTP server
if (xQueueSend(writeQueue_, &writeReq, 0) != pdTRUE) {
ESP_LOGD(TAG, "Queue full, doing sync write for chunk %u", writeReq.chunkIndex);
processWriteRequest(writeReq);
free(writeReq.data);
ESP_LOGD(TAG, "Upload chunk %u/%u: %u bytes", state.chunksReceived, state.totalChunks, bytesWritten);
if (state.chunksReceived >= state.totalChunks) {
finalizeUpload(transferId, true);
}
}
void FileSystemHandler::finalizeUpload(uint32_t transferId, bool success, const std::string& error) {
xSemaphoreTake(uploadsMutex_, portMAX_DELAY);
auto it = uploads_.find(transferId);
if (it == uploads_.end()) {
xSemaphoreGive(uploadsMutex_);
return;
}
@@ -670,22 +449,13 @@ void FileSystemHandler::finalizeUpload(uint32_t transferId, bool success, const
if (state.file) {
fclose(state.file);
state.file = nullptr;
}
std::string path = state.path;
uint32_t bytesReceived = state.bytesReceived;
uint32_t chunksReceived = state.chunksReceived;
int clientId = state.clientId;
uploads_.erase(it);
xSemaphoreGive(uploadsMutex_);
if (!success) {
remove(path.c_str());
ESP_LOGW(TAG, "Upload failed, deleted partial file: %s", path.c_str());
remove(state.path.c_str());
ESP_LOGW(TAG, "Upload failed, deleted partial file: %s", state.path.c_str());
} else {
ESP_LOGI(TAG, "Upload completed: %s (%u bytes)", path.c_str(), bytesReceived);
ESP_LOGI(TAG, "Upload completed: %s (%u bytes)", state.path.c_str(), state.bytesReceived);
}
if (sendUploadCompleteCallback_) {
@@ -695,9 +465,11 @@ void FileSystemHandler::finalizeUpload(uint32_t transferId, bool success, const
if (!error.empty()) {
strncpy(complete.error, error.c_str(), sizeof(complete.error) - 1);
}
complete.chunks_received = chunksReceived;
sendUploadCompleteCallback_(complete, clientId);
complete.chunks_received = state.chunksReceived;
sendUploadCompleteCallback_(complete, state.clientId);
}
uploads_.erase(it);
}
socket_message_FSCancelTransferResponse FileSystemHandler::handleCancelTransfer(
@@ -717,22 +489,17 @@ socket_message_FSCancelTransferResponse FileSystemHandler::handleCancelTransfer(
return response;
}
xSemaphoreTake(uploadsMutex_, portMAX_DELAY);
auto ulIt = uploads_.find(transferId);
if (ulIt != uploads_.end()) {
if (ulIt->second.file) {
fclose(ulIt->second.file);
ulIt->second.file = nullptr;
}
std::string path = ulIt->second.path;
remove(ulIt->second.path.c_str());
uploads_.erase(ulIt);
xSemaphoreGive(uploadsMutex_);
remove(path.c_str());
response.success = true;
ESP_LOGI(TAG, "Upload cancelled: %u", transferId);
return response;
}
xSemaphoreGive(uploadsMutex_);
response.success = false;
return response;
+41 -50
View File
@@ -1,7 +1,6 @@
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <esp_log.h>
#include <esp_heap_caps.h>
#include <nvs_flash.h>
#include <wifi/wifi_idf.h>
#include <mdns.h>
@@ -21,6 +20,8 @@
#include <ap_service.h>
#include <mdns_service.h>
#include <system_service.h>
#include <consumers/event_storage_manager.h>
#include <event_bus/rest_endpoints.h>
#include <www_mount.hpp>
@@ -43,9 +44,7 @@ WiFiService wifiService;
APService apService;
void setupServer() {
ESP_LOGI("Main", "Free heap before server: %lu, largest block: %lu",
esp_get_free_heap_size(), heap_caps_get_largest_free_block(MALLOC_CAP_8BIT));
server.config(50 + WWW_ASSETS_COUNT, 16384);
server.config(50 + WWW_ASSETS_COUNT, 12288);
server.listen(80);
server.on("/api/system/reset", HTTP_POST,
@@ -59,21 +58,21 @@ void setupServer() {
server.on("/api/camera/stream", HTTP_GET,
[&](httpd_req_t *request) { return cameraService.cameraStream(request); });
server.on("/api/camera/settings", HTTP_GET,
[&](httpd_req_t *request) { return cameraService.protoEndpoint.getState(request); });
server.on("/api/camera/settings", HTTP_POST, [&](httpd_req_t *request, api_Request *protoReq) {
return cameraService.protoEndpoint.handleStateUpdate(request, protoReq);
[](httpd_req_t *request) { return CameraSettingsEndpoint::getSettings(request); });
server.on("/api/camera/settings", HTTP_POST, [](httpd_req_t *request, api_Request *protoReq) {
return CameraSettingsEndpoint::updateSettings(request, protoReq);
});
#endif
server.on("/api/servo/config", HTTP_GET,
[&](httpd_req_t *request) { return servoController.protoEndpoint.getState(request); });
server.on("/api/servo/config", HTTP_POST, [&](httpd_req_t *request, api_Request *protoReq) {
return servoController.protoEndpoint.handleStateUpdate(request, protoReq);
[](httpd_req_t *request) { return ServoSettingsEndpoint::getSettings(request); });
server.on("/api/servo/config", HTTP_POST, [](httpd_req_t *request, api_Request *protoReq) {
return ServoSettingsEndpoint::updateSettings(request, protoReq);
});
server.on("/api/wifi/sta/settings", HTTP_GET,
[&](httpd_req_t *request) { return wifiService.protoEndpoint.getState(request); });
server.on("/api/wifi/sta/settings", HTTP_POST, [&](httpd_req_t *request, api_Request *protoReq) {
return wifiService.protoEndpoint.handleStateUpdate(request, protoReq);
[](httpd_req_t *request) { return WiFiSettingsEndpoint::getSettings(request); });
server.on("/api/wifi/sta/settings", HTTP_POST, [](httpd_req_t *request, api_Request *protoReq) {
return WiFiSettingsEndpoint::updateSettings(request, protoReq);
});
server.on("/api/wifi/scan", HTTP_GET, [&](httpd_req_t *request) { return wifiService.handleScan(request); });
server.on("/api/wifi/networks", HTTP_GET, [&](httpd_req_t *request) { return wifiService.getNetworks(request); });
@@ -82,22 +81,22 @@ void setupServer() {
server.on("/api/ap/status", HTTP_GET, [&](httpd_req_t *request) { return apService.getStatusProto(request); });
server.on("/api/ap/settings", HTTP_GET,
[&](httpd_req_t *request) { return apService.protoEndpoint.getState(request); });
server.on("/api/ap/settings", HTTP_POST, [&](httpd_req_t *request, api_Request *protoReq) {
return apService.protoEndpoint.handleStateUpdate(request, protoReq);
[](httpd_req_t *request) { return APSettingsEndpoint::getSettings(request); });
server.on("/api/ap/settings", HTTP_POST, [](httpd_req_t *request, api_Request *protoReq) {
return APSettingsEndpoint::updateSettings(request, protoReq);
});
server.on("/api/peripherals/settings", HTTP_GET,
[&](httpd_req_t *request) { return peripherals.protoEndpoint.getState(request); });
server.on("/api/peripherals/settings", HTTP_POST, [&](httpd_req_t *request, api_Request *protoReq) {
return peripherals.protoEndpoint.handleStateUpdate(request, protoReq);
[](httpd_req_t *request) { return PeripheralSettingsEndpoint::getSettings(request); });
server.on("/api/peripherals/settings", HTTP_POST, [](httpd_req_t *request, api_Request *protoReq) {
return PeripheralSettingsEndpoint::updateSettings(request, protoReq);
});
#if FT_ENABLED(USE_MDNS)
server.on("/api/mdns/settings", HTTP_GET,
[&](httpd_req_t *request) { return mdnsService.protoEndpoint.getState(request); });
server.on("/api/mdns/settings", HTTP_POST, [&](httpd_req_t *request, api_Request *protoReq) {
return mdnsService.protoEndpoint.handleStateUpdate(request, protoReq);
[](httpd_req_t *request) { return MDNSSettingsEndpoint::getSettings(request); });
server.on("/api/mdns/settings", HTTP_POST, [](httpd_req_t *request, api_Request *protoReq) {
return MDNSSettingsEndpoint::updateSettings(request, protoReq);
});
server.on("/api/mdns/status", HTTP_GET, [&](httpd_req_t *request) { return mdnsService.getStatus(request); });
server.on("/api/mdns/query", HTTP_POST, [&](httpd_req_t *request, api_Request *protoReq) {
@@ -133,31 +132,17 @@ void setupEventSocket() {
[](const socket_message_FSDownloadComplete &complete, int clientId) { wsSocket.emit(complete, clientId); },
[](const socket_message_FSUploadComplete &complete, int clientId) { wsSocket.emit(complete, clientId); });
wsSocket.on<socket_message_ControllerData>(
[&](const socket_message_ControllerData &data, int clientId) { motionService.handleInput(data); });
wsSocket.on<socket_message_ModeData>([&](const socket_message_ModeData &data, int clientId) {
servoController.setMode(SERVO_CONTROL_STATE::ANGLE);
motionService.handleMode(data);
motionService.isActive() ? servoController.activate() : servoController.deactivate();
});
wsSocket.on<socket_message_WalkGaitData>(
[&](const socket_message_WalkGaitData &data, int clientId) { motionService.handleWalkGait(data); });
wsSocket.on<socket_message_AnglesData>(
[&](const socket_message_AnglesData &data, int clientId) { motionService.handleAngles(data); });
wsSocket.on<socket_message_ServoPWMData>([&](const socket_message_ServoPWMData &data, int clientId) {
servoController.setServoPWM(data.servo_id, data.servo_pwm);
});
wsSocket.on<socket_message_ServoStateData>([&](const socket_message_ServoStateData &data, int clientId) {
data.active ? servoController.activate() : servoController.deactivate();
});
wsSocket.onPublish<socket_message_ControllerData>();
wsSocket.onPublish<socket_message_ModeData>();
wsSocket.onPublish<socket_message_WalkGaitData>();
wsSocket.onPublish<socket_message_AnglesData>();
wsSocket.onPublish<socket_message_ServoPWMData>();
wsSocket.onPublish<socket_message_ServoStateData>();
wsSocket.on<socket_message_FSUploadData>(
[&](const socket_message_FSUploadData &data, int clientId) { TIME_IT(FileSystemWS::fsHandler.handleUploadData(data), handle_upload) });
[&](const socket_message_FSUploadData &data, int clientId) { FileSystemWS::fsHandler.handleUploadData(data); });
wsSocket.bridgeFromEventBus<socket_message_IMUData>(100);
using CorrelationHandler =
std::function<void(const socket_message_CorrelationRequest &, socket_message_CorrelationResponse &, int)>;
@@ -258,6 +243,10 @@ void IRAM_ATTR SpotControlLoopEntry(void *) {
peripherals.begin();
servoController.begin();
motionService.begin();
motionService.setModeChangeCallback([](bool active) {
servoController.setMode(SERVO_CONTROL_STATE::ANGLE);
active ? servoController.activate() : servoController.deactivate();
});
#if FT_ENABLED(USE_WS2812)
ledService.begin();
#endif
@@ -279,6 +268,10 @@ void IRAM_ATTR SpotControlLoopEntry(void *) {
void IRAM_ATTR serviceLoopEntry(void *) {
ESP_LOGI("main", "Service task starting");
static EventStorageManager storageManager;
storageManager.initialize();
ESP_LOGI("main", "Event storage initialized, settings loaded and published");
WiFi.init();
wifiService.begin();
mdns_init();
@@ -307,11 +300,9 @@ void IRAM_ATTR serviceLoopEntry(void *) {
});
EXECUTE_EVERY_N_MS(100, {
if (wsSocket.hasSubscribers(socket_message_Message_imu_tag)) {
socket_message_IMUData imu = socket_message_IMUData_init_zero;
peripherals.getIMUProto(imu);
wsSocket.emit(imu);
}
socket_message_IMUData imu = socket_message_IMUData_init_zero;
peripherals.getIMUProto(imu);
EventBus::publish(imu);
if (wsSocket.hasSubscribers(socket_message_Message_rssi_tag)) {
socket_message_RSSIData rssi = {.rssi = WiFi.RSSI()};
+63 -29
View File
@@ -2,16 +2,7 @@
#include <communication/webserver.h>
#include <esp_netif.h>
static const char *TAG = "MDNSService";
MDNSService::MDNSService()
: protoEndpoint(MDNSSettings_read, MDNSSettings_update, this,
API_REQUEST_EXTRACTOR(mdns_settings, api_MDNSSettings),
API_RESPONSE_ASSIGNER(mdns_settings, api_MDNSSettings)),
_persistence(MDNSSettings_read, MDNSSettings_update, this, MDNS_SETTINGS_FILE, api_MDNSSettings_fields,
api_MDNSSettings_size, MDNSSettings_defaults()) {
addUpdateHandler([&](const std::string &originId) { reconfigureMDNS(); }, false);
}
MDNSService::MDNSService() {}
MDNSService::~MDNSService() {
if (_started) {
@@ -20,10 +11,32 @@ MDNSService::~MDNSService() {
}
void MDNSService::begin() {
_persistence.readFromFS();
_settingsHandle = EventBus::subscribe<api_MDNSSettings>(
[this](const api_MDNSSettings &settings) { onSettingsChanged(settings); });
api_MDNSSettings initialSettings;
if (EventBus::peek(initialSettings)) {
onSettingsChanged(initialSettings);
}
startMDNS();
}
void MDNSService::onSettingsChanged(const api_MDNSSettings &newSettings) {
strncpy(_settings.hostname, newSettings.hostname, sizeof(_settings.hostname) - 1);
_settings.hostname[sizeof(_settings.hostname) - 1] = '\0';
strncpy(_settings.instance, newSettings.instance, sizeof(_settings.instance) - 1);
_settings.instance[sizeof(_settings.instance) - 1] = '\0';
_settings.services_count = newSettings.services_count;
for (size_t i = 0; i < newSettings.services_count; i++) {
_settings.services[i] = newSettings.services[i];
}
_settings.global_txt_records_count = newSettings.global_txt_records_count;
for (size_t i = 0; i < newSettings.global_txt_records_count; i++) {
_settings.global_txt_records[i] = newSettings.global_txt_records[i];
}
reconfigureMDNS();
}
void MDNSService::reconfigureMDNS() {
if (_started) {
stopMDNS();
@@ -32,7 +45,7 @@ void MDNSService::reconfigureMDNS() {
}
void MDNSService::startMDNS() {
ESP_LOGV(TAG, "Starting MDNS with hostname: %s", state().hostname);
ESP_LOGV(TAG, "Starting MDNS with hostname: %s", _settings.hostname);
esp_err_t err = mdns_init();
if (err != ESP_OK) {
@@ -41,7 +54,7 @@ void MDNSService::startMDNS() {
return;
}
err = mdns_hostname_set(state().hostname);
err = mdns_hostname_set(_settings.hostname);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to set MDNS hostname: %s", esp_err_to_name(err));
mdns_free();
@@ -49,7 +62,7 @@ void MDNSService::startMDNS() {
return;
}
err = mdns_instance_name_set(state().instance);
err = mdns_instance_name_set(_settings.instance);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Failed to set MDNS instance name: %s", esp_err_to_name(err));
}
@@ -57,7 +70,7 @@ void MDNSService::startMDNS() {
_started = true;
addServices();
ESP_LOGI(TAG, "MDNS started successfully with hostname: %s", state().hostname);
ESP_LOGI(TAG, "MDNS started successfully with hostname: %s", _settings.hostname);
}
void MDNSService::stopMDNS() {
@@ -67,8 +80,8 @@ void MDNSService::stopMDNS() {
}
void MDNSService::addServices() {
for (size_t i = 0; i < state().services_count; i++) {
const auto &service = state().services[i];
for (size_t i = 0; i < _settings.services_count; i++) {
const auto &service = _settings.services[i];
esp_err_t err = mdns_service_add(nullptr, service.service, service.protocol, service.port, nullptr, 0);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Failed to add service %s: %s", service.service, esp_err_to_name(err));
@@ -81,10 +94,10 @@ void MDNSService::addServices() {
}
}
for (size_t i = 0; i < state().global_txt_records_count; i++) {
const auto &txt = state().global_txt_records[i];
for (size_t j = 0; j < state().services_count; j++) {
const auto &service = state().services[j];
for (size_t i = 0; i < _settings.global_txt_records_count; i++) {
const auto &txt = _settings.global_txt_records[i];
for (size_t j = 0; j < _settings.services_count; j++) {
const auto &service = _settings.services[j];
mdns_service_txt_item_set(service.service, service.protocol, txt.key, txt.value);
}
}
@@ -96,17 +109,17 @@ esp_err_t MDNSService::getStatus(httpd_req_t *request) {
MDNSStatus &status = response.payload.mdns_status;
status.started = _started;
strncpy(status.hostname, state().hostname, sizeof(status.hostname) - 1);
strncpy(status.instance, state().instance, sizeof(status.instance) - 1);
strncpy(status.hostname, _settings.hostname, sizeof(status.hostname) - 1);
strncpy(status.instance, _settings.instance, sizeof(status.instance) - 1);
status.services_count = state().services_count;
for (size_t i = 0; i < state().services_count; i++) {
status.services[i] = state().services[i];
status.services_count = _settings.services_count;
for (size_t i = 0; i < _settings.services_count; i++) {
status.services[i] = _settings.services[i];
}
status.global_txt_records_count = state().global_txt_records_count;
for (size_t i = 0; i < state().global_txt_records_count; i++) {
status.global_txt_records[i] = state().global_txt_records[i];
status.global_txt_records_count = _settings.global_txt_records_count;
for (size_t i = 0; i < _settings.global_txt_records_count; i++) {
status.global_txt_records[i] = _settings.global_txt_records[i];
}
return WebServer::send(request, 200, response, api_Response_fields);
@@ -163,3 +176,24 @@ esp_err_t MDNSService::queryServices(httpd_req_t *request, api_Request *protoReq
return WebServer::send(request, 200, response, api_Response_fields);
}
esp_err_t MDNSService::getSettings(httpd_req_t *request) {
api_Response response = api_Response_init_zero;
response.status_code = 200;
response.which_payload = api_Response_mdns_settings_tag;
response.payload.mdns_settings = _settings;
return WebServer::send(request, 200, response, api_Response_fields);
}
esp_err_t MDNSService::updateSettings(httpd_req_t *request, api_Request *protoReq) {
if (protoReq->which_payload != api_Request_mdns_settings_tag) {
return ESP_FAIL;
}
EventBus::publish(protoReq->payload.mdns_settings, "HTTPEndpoint");
api_Response response = api_Response_init_zero;
response.status_code = 200;
response.which_payload = api_Response_empty_message_tag;
return WebServer::send(request, 200, response, api_Response_fields);
}
+19 -1
View File
@@ -1,6 +1,23 @@
#include <motion.h>
void MotionService::begin() { body_state.updateFeet(KinConfig::default_feet_positions); }
void MotionService::begin() {
body_state.updateFeet(KinConfig::default_feet_positions);
subscribeToEvents();
}
void MotionService::subscribeToEvents() {
controllerHandle_ = EventBus::subscribe<socket_message_ControllerData>(
[this](const socket_message_ControllerData& data) { handleInput(data); });
modeHandle_ =
EventBus::subscribe<socket_message_ModeData>([this](const socket_message_ModeData& data) { handleMode(data); });
anglesHandle_ = EventBus::subscribe<socket_message_AnglesData>(
[this](const socket_message_AnglesData& data) { handleAngles(data); });
walkGaitHandle_ = EventBus::subscribe<socket_message_WalkGaitData>(
[this](const socket_message_WalkGaitData& data) { handleWalkGait(data); });
}
void MotionService::handleAngles(const socket_message_AnglesData& data) {
for (int i = 0; i < 12 && i < data.angles_count; i++) {
@@ -41,6 +58,7 @@ void MotionService::handleMode(const socket_message_ModeData& data) {
case MOTION_STATE::DEACTIVATED: setState(nullptr); break;
default: setState(nullptr); break;
}
if (modeChangeCallback_) modeChangeCallback_(isActive());
}
void MotionService::handleGestures(const gesture_t ges) {
+60 -38
View File
@@ -4,8 +4,6 @@
namespace Camera {
static const char *const TAG = "CameraService";
static constexpr const char *_STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
static constexpr const char *_STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n";
static constexpr const char *_STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n";
@@ -31,18 +29,16 @@ sensor_t *safe_sensor_get() {
void safe_sensor_return() { xSemaphoreGiveRecursive(cameraMutex); }
CameraService::CameraService()
: protoEndpoint(CameraSettings_read, CameraSettings_update, this,
API_REQUEST_EXTRACTOR(camera_settings, api_CameraSettings),
API_RESPONSE_ASSIGNER(camera_settings, api_CameraSettings)),
_persistence(CameraSettings_read, CameraSettings_update, this,
CAMERA_SETTINGS_FILE, api_CameraSettings_fields, api_CameraSettings_size,
CameraSettings_defaults()) {
addUpdateHandler([&](const std::string &originId) { updateCamera(); }, false);
}
CameraService::CameraService() {}
esp_err_t CameraService::begin() {
_persistence.readFromFS();
_settingsHandle = EventBus::subscribe<api_CameraSettings>(
[this](const api_CameraSettings &settings) { onSettingsChanged(settings); });
api_CameraSettings initialSettings;
if (EventBus::peek(initialSettings)) {
onSettingsChanged(initialSettings);
}
camera_config_t camera_config;
camera_config.ledc_channel = LEDC_CHANNEL_0;
camera_config.ledc_timer = LEDC_TIMER_0;
@@ -146,39 +142,65 @@ esp_err_t CameraService::cameraStream(httpd_req_t *request) {
return ESP_OK;
}
void CameraService::onSettingsChanged(const api_CameraSettings &newSettings) {
_settings = newSettings;
updateCamera();
}
void CameraService::updateCamera() {
ESP_LOGI("CameraSettings", "Updating camera settings");
ESP_LOGI(TAG, "Updating camera settings");
sensor_t *s = safe_sensor_get();
if (!s) {
ESP_LOGE("CameraSettings", "Failed to update camera settings");
ESP_LOGE(TAG, "Failed to update camera settings");
safe_sensor_return();
return;
}
s->set_pixformat(s, static_cast<pixformat_t>(state().pixformat));
s->set_framesize(s, static_cast<framesize_t>(state().framesize));
s->set_brightness(s, state().brightness);
s->set_contrast(s, state().contrast);
s->set_saturation(s, state().saturation);
s->set_sharpness(s, state().sharpness);
s->set_denoise(s, state().denoise);
s->set_gainceiling(s, static_cast<gainceiling_t>(state().gainceiling));
s->set_quality(s, state().quality);
s->set_colorbar(s, state().colorbar);
s->set_awb_gain(s, state().awb_gain);
s->set_wb_mode(s, state().wb_mode);
s->set_aec2(s, state().aec2);
s->set_ae_level(s, state().ae_level);
s->set_aec_value(s, state().aec_value);
s->set_agc_gain(s, state().agc_gain);
s->set_bpc(s, state().bpc);
s->set_wpc(s, state().wpc);
s->set_special_effect(s, state().special_effect);
s->set_raw_gma(s, state().raw_gma);
s->set_lenc(s, state().lenc);
s->set_hmirror(s, state().hmirror);
s->set_vflip(s, state().vflip);
s->set_dcw(s, state().dcw);
s->set_pixformat(s, static_cast<pixformat_t>(_settings.pixformat));
s->set_framesize(s, static_cast<framesize_t>(_settings.framesize));
s->set_brightness(s, _settings.brightness);
s->set_contrast(s, _settings.contrast);
s->set_saturation(s, _settings.saturation);
s->set_sharpness(s, _settings.sharpness);
s->set_denoise(s, _settings.denoise);
s->set_gainceiling(s, static_cast<gainceiling_t>(_settings.gainceiling));
s->set_quality(s, _settings.quality);
s->set_colorbar(s, _settings.colorbar);
s->set_awb_gain(s, _settings.awb_gain);
s->set_wb_mode(s, _settings.wb_mode);
s->set_aec2(s, _settings.aec2);
s->set_ae_level(s, _settings.ae_level);
s->set_aec_value(s, _settings.aec_value);
s->set_agc_gain(s, _settings.agc_gain);
s->set_bpc(s, _settings.bpc);
s->set_wpc(s, _settings.wpc);
s->set_special_effect(s, _settings.special_effect);
s->set_raw_gma(s, _settings.raw_gma);
s->set_lenc(s, _settings.lenc);
s->set_hmirror(s, _settings.hmirror);
s->set_vflip(s, _settings.vflip);
s->set_dcw(s, _settings.dcw);
safe_sensor_return();
}
esp_err_t CameraService::getSettings(httpd_req_t *request) {
api_Response response = api_Response_init_zero;
response.status_code = 200;
response.which_payload = api_Response_camera_settings_tag;
response.payload.camera_settings = _settings;
return WebServer::send(request, 200, response, api_Response_fields);
}
esp_err_t CameraService::updateSettings(httpd_req_t *request, api_Request *protoReq) {
if (protoReq->which_payload != api_Request_camera_settings_tag) {
return ESP_FAIL;
}
EventBus::publish(protoReq->payload.camera_settings, "HTTPEndpoint");
api_Response response = api_Response_init_zero;
response.status_code = 200;
response.which_payload = api_Response_empty_message_tag;
return WebServer::send(request, 200, response, api_Response_fields);
}
} // namespace Camera
+43 -17
View File
@@ -1,18 +1,16 @@
#include <peripherals/peripherals.h>
#include <communication/webserver.h>
Peripherals::Peripherals()
: protoEndpoint(PeripheralsConfiguration_read, PeripheralsConfiguration_update, this,
API_REQUEST_EXTRACTOR(peripheral_settings, api_PeripheralSettings),
API_RESPONSE_ASSIGNER(peripheral_settings, api_PeripheralSettings)),
_persistence(PeripheralsConfiguration_read, PeripheralsConfiguration_update, this,
PERIPHERAL_SETTINGS_FILE, api_PeripheralSettings_fields, api_PeripheralSettings_size,
PeripheralsConfiguration_defaults()) {
_accessMutex = xSemaphoreCreateMutex();
addUpdateHandler([&](const std::string &originId) { updatePins(); }, false);
}
Peripherals::Peripherals() { _accessMutex = xSemaphoreCreateMutex(); }
void Peripherals::begin() {
_persistence.readFromFS();
_settingsHandle = EventBus::subscribe<api_PeripheralSettings>(
[this](const api_PeripheralSettings &settings) { onSettingsChanged(settings); });
api_PeripheralSettings initialSettings;
if (EventBus::peek(initialSettings)) {
onSettingsChanged(initialSettings);
}
updatePins();
@@ -42,14 +40,21 @@ void Peripherals::update() {
EXECUTE_EVERY_N_MS(500, { readSonar(); });
}
void Peripherals::onSettingsChanged(const api_PeripheralSettings &newSettings) {
_settings.sda = newSettings.sda;
_settings.scl = newSettings.scl;
_settings.frequency = newSettings.frequency;
updatePins();
}
void Peripherals::updatePins() {
if (i2c_active) {
I2CBus::instance().end();
}
if (state().sda != -1 && state().scl != -1) {
esp_err_t err = I2CBus::instance().begin(static_cast<gpio_num_t>(state().sda),
static_cast<gpio_num_t>(state().scl), state().frequency);
if (_settings.sda != -1 && _settings.scl != -1) {
esp_err_t err = I2CBus::instance().begin(static_cast<gpio_num_t>(_settings.sda),
static_cast<gpio_num_t>(_settings.scl), _settings.frequency);
i2c_active = (err == ESP_OK);
}
}
@@ -92,12 +97,33 @@ void Peripherals::getIMUProto(socket_message_IMUData &data) {
}
void Peripherals::getSettingsProto(socket_message_PeripheralSettingsData &data) {
data.sda = state().sda;
data.scl = state().scl;
data.frequency = state().frequency;
data.sda = _settings.sda;
data.scl = _settings.scl;
data.frequency = _settings.frequency;
data.pins_count = 0;
}
esp_err_t Peripherals::getSettings(httpd_req_t *request) {
api_Response response = api_Response_init_zero;
response.status_code = 200;
response.which_payload = api_Response_peripheral_settings_tag;
response.payload.peripheral_settings = _settings;
return WebServer::send(request, 200, response, api_Response_fields);
}
esp_err_t Peripherals::updateSettings(httpd_req_t *request, api_Request *protoReq) {
if (protoReq->which_payload != api_Request_peripheral_settings_tag) {
return ESP_FAIL;
}
EventBus::publish(protoReq->payload.peripheral_settings, "HTTPEndpoint");
api_Response response = api_Response_init_zero;
response.status_code = 200;
response.which_payload = api_Response_empty_message_tag;
return WebServer::send(request, 200, response, api_Response_fields);
}
/* IMU FUNCTIONS */
bool Peripherals::readImu() {
bool updated = false;
@@ -0,0 +1,23 @@
#include <peripherals/servo_controller.h>
#include <communication/webserver.h>
esp_err_t ServoController::getSettings(httpd_req_t *request) {
api_Response response = api_Response_init_zero;
response.status_code = 200;
response.which_payload = api_Response_servo_settings_tag;
response.payload.servo_settings = _settings;
return WebServer::send(request, 200, response, api_Response_fields);
}
esp_err_t ServoController::updateSettings(httpd_req_t *request, api_Request *protoReq) {
if (protoReq->which_payload != api_Request_servo_settings_tag) {
return ESP_FAIL;
}
EventBus::publish(protoReq->payload.servo_settings, "HTTPEndpoint");
api_Response response = api_Response_init_zero;
response.status_code = 200;
response.which_payload = api_Response_empty_message_tag;
return WebServer::send(request, 200, response, api_Response_fields);
}
+53 -35
View File
@@ -1,18 +1,7 @@
#include <wifi_service.h>
#include <communication/webserver.h>
static const char *TAG = "WiFiService";
WiFiService::WiFiService()
: protoEndpoint(WiFiSettings_read, WiFiSettings_update, this,
API_REQUEST_EXTRACTOR(wifi_settings, api_WifiSettings),
API_RESPONSE_ASSIGNER(wifi_settings, api_WifiSettings)),
_persistence(WiFiSettings_read, WiFiSettings_update, this, WIFI_SETTINGS_FILE, api_WifiSettings_fields,
api_WifiSettings_size, WiFiSettings_defaults()),
_lastConnectionAttempt(0),
_stopping(false) {
addUpdateHandler([&](const std::string &originId) { reconfigureWiFiConnection(); }, false);
}
WiFiService::WiFiService() : _initialized(false), _lastConnectionAttempt(0), _stopping(false) {}
WiFiService::~WiFiService() {}
@@ -25,15 +14,41 @@ void WiFiService::begin() {
WiFi.onEvent([this](int32_t event, void *data) { this->onStationModeStop(event, data); }, WIFI_EVENT_STA_STOP);
WiFi.onEvent(onStationModeGotIP, IP_EVENT_STA_GOT_IP_IDF);
_persistence.readFromFS();
_lastConnectionAttempt = 0;
_settingsHandle = EventBus::subscribe<api_WifiSettings>(
[this](const api_WifiSettings &settings) { onSettingsChanged(settings); });
if (state().wifi_networks_count >= 1) {
WiFi.mode(WIFI_MODE_STA);
vTaskDelay(100 / portTICK_PERIOD_MS);
uint32_t idx = state().selected_network;
if (idx >= state().wifi_networks_count) idx = 0;
configureNetwork(state().wifi_networks[idx]);
api_WifiSettings initialSettings;
if (EventBus::peek(initialSettings)) {
ESP_LOGI(TAG, "Applying initial WiFi settings from storage");
onSettingsChanged(initialSettings);
} else {
ESP_LOGW(TAG, "No WiFi settings found, using defaults");
}
}
void WiFiService::onSettingsChanged(const api_WifiSettings &newSettings) {
api_WifiSettings oldSettings = getSettings();
bool needsReconnect = _initialized && (strcmp(oldSettings.hostname, newSettings.hostname) != 0 ||
oldSettings.selected_network != newSettings.selected_network ||
oldSettings.wifi_networks_count != newSettings.wifi_networks_count);
if (!_initialized) {
_initialized = true;
_lastConnectionAttempt = 0;
ESP_LOGI(TAG, "Initializing WiFi with loaded settings");
if (newSettings.wifi_networks_count >= 1) {
WiFi.mode(WIFI_MODE_STA);
vTaskDelay(100 / portTICK_PERIOD_MS);
uint32_t idx = newSettings.selected_network;
if (idx >= newSettings.wifi_networks_count) idx = 0;
configureNetwork(newSettings.wifi_networks[idx]);
}
} else if (needsReconnect) {
ESP_LOGI(TAG, "Settings changed, reconnecting");
reconfigureWiFiConnection();
}
}
@@ -43,12 +58,12 @@ void WiFiService::reconfigureWiFiConnection() {
}
void WiFiService::selectNetwork(uint32_t index) {
if (index >= state().wifi_networks_count) return;
updateWithoutPropagation([&](WiFiSettings &settings) {
settings.selected_network = index;
return StateUpdateResult::CHANGED;
});
_persistence.writeToFS();
api_WifiSettings settings = getSettings();
if (index >= settings.wifi_networks_count) return;
settings.selected_network = index;
EventBus::publish(settings, "WiFiService");
reconfigureWiFiConnection();
}
@@ -99,7 +114,8 @@ esp_err_t WiFiService::getNetworks(httpd_req_t *request) {
void WiFiService::setupMDNS(const char *hostname) {
mdns_init();
mdns_hostname_set(state().hostname);
api_WifiSettings settings = getSettings();
mdns_hostname_set(settings.hostname);
mdns_instance_name_set(hostname);
mdns_service_add(nullptr, "_http", "_tcp", 80, nullptr, 0);
mdns_service_add(nullptr, "_ws", "_tcp", 80, nullptr, 0);
@@ -138,7 +154,8 @@ esp_err_t WiFiService::getNetworkStatus(httpd_req_t *request) {
}
void WiFiService::manageSTA() {
if (WiFi.isConnected() || state().wifi_networks_count == 0) return;
api_WifiSettings settings = getSettings();
if (WiFi.isConnected() || settings.wifi_networks_count == 0) return;
wifi_mode_t mode = WiFi.getMode();
if (mode == WIFI_MODE_NULL || mode == WIFI_MODE_AP) return;
@@ -153,23 +170,24 @@ void WiFiService::manageSTA() {
uint32_t now = esp_timer_get_time() / 1000;
if (now - startTime < 3000) return;
if (!attempted && state().wifi_networks_count > 0) {
if (!attempted && settings.wifi_networks_count > 0) {
attempted = true;
uint32_t idx = state().selected_network;
if (idx >= state().wifi_networks_count) idx = 0;
ESP_LOGI(TAG, "Connecting to: %s", state().wifi_networks[idx].ssid);
configureNetwork(state().wifi_networks[idx]);
uint32_t idx = settings.selected_network;
if (idx >= settings.wifi_networks_count) idx = 0;
ESP_LOGI(TAG, "Connecting to: %s", settings.wifi_networks[idx].ssid);
configureNetwork(settings.wifi_networks[idx]);
}
}
void WiFiService::configureNetwork(WiFiNetwork &network) {
void WiFiService::configureNetwork(const WiFiNetwork &network) {
if (network.static_ip_config) {
WiFi.config(IPAddress(network.local_ip), IPAddress(network.gateway_ip), IPAddress(network.subnet_mask),
IPAddress(network.dns_ip_1), IPAddress(network.dns_ip_2));
} else {
WiFi.config(IPAddress(0, 0, 0, 0), IPAddress(0, 0, 0, 0), IPAddress(0, 0, 0, 0));
}
WiFi.setHostname(state().hostname);
api_WifiSettings settings = getSettings();
WiFi.setHostname(settings.hostname);
WiFi.begin(network.ssid, network.password);
#if CONFIG_IDF_TARGET_ESP32C3
+2 -2
View File
@@ -16,11 +16,11 @@ socket_message.FSListResponse.directories max_count:20
# Streaming download messages
socket_message.FSDownloadRequest.path max_size:256
socket_message.FSDownloadMetadata.error max_size:128
socket_message.FSDownloadData.data type:FT_POINTER
socket_message.FSDownloadData.data max_size:16384
socket_message.FSDownloadComplete.error max_size:128
# Streaming upload messages
socket_message.FSUploadStart.path max_size:256
socket_message.FSUploadStartResponse.error max_size:128
socket_message.FSUploadData.data type:FT_POINTER
socket_message.FSUploadData.data max_size:16384
socket_message.FSUploadComplete.error max_size:128
+2 -10
View File
@@ -54,12 +54,6 @@ build_flags =
-D USS_RIGHT_PIN=14
-D SDA_PIN=47
-D SCL_PIN=21
-D SD_CMD_PIN=GPIO_NUM_38
-D SD_CLK_PIN=GPIO_NUM_39
-D SD_DATA_PIN=GPIO_NUM_40
[env:seeed-xiao-esp32s3]
platform = espressif32
@@ -88,8 +82,8 @@ platform = espressif32 @ 6.8.1
framework = espidf
monitor_speed = 115200
monitor_filters =
direct
esp32_exception_decoder
direct
esp32_exception_decoder
build_flags =
${factory_settings.build_flags}
${features.build_flags}
@@ -102,8 +96,6 @@ build_flags =
-fdata-sections
-Wl,--gc-sections
-I submodules/nanopb
-D PB_FIELD_32BIT=1
-D PB_ENABLE_MALLOC=1
-Wno-missing-braces
-Wno-format
-D CONFIG_HTTPD_WS_SUPPORT=1