Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 18ec2e618e | |||
| 0c0ef0ac40 | |||
| 57e0aac2aa | |||
| 780d178e87 |
Vendored
+1
-1
@@ -13,7 +13,7 @@
|
||||
},
|
||||
"editor.tabSize": 4,
|
||||
"editor.detectIndentation": false,
|
||||
"cmake.sourceDirectory": "C:/data/repos/Hardware/Spot_Micro_Leika",
|
||||
"cmake.sourceDirectory": "C:/data/repos/Hardware/Spot Micro - Leika/.pio/libdeps/esp32cam/esp32-camera",
|
||||
"cSpell.words": [
|
||||
"Adafruit",
|
||||
"IRAM",
|
||||
|
||||
@@ -262,9 +262,8 @@
|
||||
const rotatedXm = settings.xm * cosTotal - settings.zm * sinTotal
|
||||
const rotatedZm = settings.xm * sinTotal + settings.zm * cosTotal
|
||||
|
||||
const mpuHeadingRad = degToRad($mpu.heading)
|
||||
const cosHead = Math.cos(mpuHeadingRad)
|
||||
const sinHead = Math.sin(mpuHeadingRad)
|
||||
const cosHead = Math.cos(headingYaw)
|
||||
const sinHead = Math.sin(headingYaw)
|
||||
const rotatedCumX = body_state.cumulative_x * cosHead - body_state.cumulative_z * sinHead
|
||||
const rotatedCumZ = body_state.cumulative_x * sinHead + body_state.cumulative_z * cosHead
|
||||
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"build": {
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DBOARD_HAS_PSRAM"
|
||||
],
|
||||
"f_cpu": "360000000L",
|
||||
"f_flash": "80000000L",
|
||||
"f_psram": "200000000L",
|
||||
"flash_mode": "qio",
|
||||
"mcu": "esp32p4",
|
||||
"variant": "esp32p4"
|
||||
},
|
||||
"connectivity": [
|
||||
"wifi"
|
||||
],
|
||||
"debug": {
|
||||
"openocd_target": "esp32p4.cfg"
|
||||
},
|
||||
"frameworks": [
|
||||
"espidf"
|
||||
],
|
||||
"name": "ESP32-P4 Dev Board (32MB PSRAM + 32MB Flash, C6 coprocessor)",
|
||||
"upload": {
|
||||
"flash_size": "32MB",
|
||||
"maximum_ram_size": 786432,
|
||||
"maximum_size": 33554432,
|
||||
"require_upload_port": true,
|
||||
"speed": 1500000
|
||||
},
|
||||
"url": "https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32p4/",
|
||||
"vendor": "Espressif"
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <template/stateful_service.h>
|
||||
#include <template/stateful_proto_endpoint.h>
|
||||
#include <template/stateful_persistence.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;
|
||||
|
||||
@@ -6,8 +6,11 @@
|
||||
#include <functional>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <type_traits>
|
||||
#include <communication/proto_helpers.h>
|
||||
#include <event_bus/event_bus.h>
|
||||
|
||||
class CommAdapterBase {
|
||||
public:
|
||||
@@ -35,38 +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), 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:
|
||||
@@ -116,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_);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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)
|
||||
@@ -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);
|
||||
@@ -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>;
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
enum class SystemEventType : uint8_t {
|
||||
STORAGE_HYDRATION_COMPLETE,
|
||||
SYSTEM_BOOT_COMPLETE,
|
||||
};
|
||||
|
||||
struct SystemEvent {
|
||||
SystemEventType type;
|
||||
};
|
||||
@@ -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; }
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <sdkconfig.h>
|
||||
#include <wifi/wifi_idf.h>
|
||||
#include <esp_http_server.h>
|
||||
#include "platform_shared/message.pb.h"
|
||||
|
||||
+1
-24
@@ -23,39 +23,16 @@
|
||||
#ifndef ESP_PLATFORM_NAME
|
||||
#define ESP_PLATFORM_NAME "ESP32-S3"
|
||||
#endif
|
||||
#elif CONFIG_IDF_TARGET_ESP32C6
|
||||
#include "esp32c6/rom/rtc.h"
|
||||
#ifndef ESP_PLATFORM_NAME
|
||||
#define ESP_PLATFORM_NAME "ESP32-C6"
|
||||
#endif
|
||||
#elif CONFIG_IDF_TARGET_ESP32P4
|
||||
#include "esp32p4/rom/rtc.h"
|
||||
#ifndef ESP_PLATFORM_NAME
|
||||
#define ESP_PLATFORM_NAME "ESP32-P4"
|
||||
#endif
|
||||
#define ESP32P4_USES_C6_COPROCESSOR 1
|
||||
#else
|
||||
#error Target CONFIG_IDF_TARGET is not supported
|
||||
#endif
|
||||
|
||||
/*
|
||||
* I2C software connection
|
||||
*/
|
||||
#if CONFIG_IDF_TARGET_ESP32P4
|
||||
#ifndef SDA_PIN
|
||||
#define SDA_PIN 7
|
||||
#endif
|
||||
#ifndef SCL_PIN
|
||||
#define SCL_PIN 8
|
||||
#endif
|
||||
#else
|
||||
#ifndef SDA_PIN
|
||||
#define SDA_PIN 21
|
||||
#endif
|
||||
#ifndef SCL_PIN
|
||||
#define SCL_PIN 22
|
||||
#endif
|
||||
#endif
|
||||
#ifndef I2C_FREQUENCY
|
||||
#define I2C_FREQUENCY 1000000UL
|
||||
#define I2C_FREQUENCY 100000UL
|
||||
#endif
|
||||
|
||||
@@ -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.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,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
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include <motion_states/state.h>
|
||||
#include <utils/math_utils.h>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <functional>
|
||||
|
||||
|
||||
@@ -3,33 +3,25 @@
|
||||
#include <esp_http_server.h>
|
||||
|
||||
#include <features.h>
|
||||
#include <template/stateful_service.h>
|
||||
#include <template/stateful_proto_endpoint.h>
|
||||
#include <template/stateful_persistence.h>
|
||||
#include <event_bus/event_bus.h>
|
||||
|
||||
#include <settings/camera_settings.h>
|
||||
|
||||
namespace Camera {
|
||||
|
||||
#define USE_DVP_CAMERA (USE_CAMERA && !CONFIG_IDF_TARGET_ESP32P4)
|
||||
#define USE_CSI_CAMERA (USE_CAMERA && CONFIG_IDF_TARGET_ESP32P4)
|
||||
|
||||
#if USE_DVP_CAMERA
|
||||
#include <esp_camera.h>
|
||||
#include <peripherals/camera_pins.h>
|
||||
|
||||
camera_fb_t *safe_camera_fb_get();
|
||||
sensor_t *safe_sensor_get();
|
||||
void safe_sensor_return();
|
||||
#if USE_CAMERA
|
||||
#include <peripherals/camera_pins.h>
|
||||
#endif
|
||||
|
||||
#define PART_BOUNDARY "frame"
|
||||
|
||||
class CameraService
|
||||
#if USE_DVP_CAMERA
|
||||
: public StatefulService<CameraSettings>
|
||||
#endif
|
||||
{
|
||||
camera_fb_t *safe_camera_fb_get();
|
||||
sensor_t *safe_sensor_get();
|
||||
void safe_sensor_return();
|
||||
|
||||
class CameraService {
|
||||
public:
|
||||
CameraService();
|
||||
|
||||
@@ -38,12 +30,16 @@ class CameraService
|
||||
esp_err_t cameraStill(httpd_req_t *request);
|
||||
esp_err_t cameraStream(httpd_req_t *request);
|
||||
|
||||
#if USE_DVP_CAMERA
|
||||
StatefulProtoEndpoint<CameraSettings, api_CameraSettings> protoEndpoint;
|
||||
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();
|
||||
#endif
|
||||
|
||||
CameraSettings _settings = CameraSettings_defaults();
|
||||
EventBus::Handle<api_CameraSettings> _settingsHandle;
|
||||
};
|
||||
} // namespace Camera
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <driver/i2c_master.h>
|
||||
#include <driver/i2c.h>
|
||||
#include <esp_log.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
#include <cstring>
|
||||
|
||||
class I2CBus {
|
||||
public:
|
||||
@@ -25,21 +24,30 @@ class I2CBus {
|
||||
_scl = scl;
|
||||
_freq = freq;
|
||||
|
||||
i2c_master_bus_config_t bus_cfg = {};
|
||||
bus_cfg.i2c_port = port;
|
||||
bus_cfg.sda_io_num = sda;
|
||||
bus_cfg.scl_io_num = scl;
|
||||
bus_cfg.clk_source = I2C_CLK_SRC_DEFAULT;
|
||||
bus_cfg.glitch_ignore_cnt = 7;
|
||||
#if CONFIG_IDF_TARGET_ESP32P4
|
||||
bus_cfg.flags.enable_internal_pullup = false;
|
||||
#else
|
||||
bus_cfg.flags.enable_internal_pullup = true;
|
||||
#endif
|
||||
i2c_config_t conf = {};
|
||||
conf.mode = I2C_MODE_MASTER;
|
||||
conf.sda_io_num = sda;
|
||||
conf.scl_io_num = scl;
|
||||
conf.sda_pullup_en = GPIO_PULLUP_ENABLE;
|
||||
conf.scl_pullup_en = GPIO_PULLUP_ENABLE;
|
||||
conf.master.clk_speed = freq;
|
||||
|
||||
esp_err_t err = i2c_new_master_bus(&bus_cfg, &_bus);
|
||||
esp_err_t err = i2c_param_config(_port, &conf);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "i2c_new_master_bus failed: %s", esp_err_to_name(err));
|
||||
ESP_LOGE(TAG, "i2c_param_config failed: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
err = i2c_driver_install(_port, I2C_MODE_MASTER, 0, 0, 0);
|
||||
if (err == ESP_ERR_INVALID_STATE) {
|
||||
ESP_LOGW(TAG, "I2C driver already installed for port %d, reconfiguring", _port);
|
||||
i2c_driver_delete(_port);
|
||||
err = i2c_param_config(_port, &conf);
|
||||
if (err != ESP_OK) return err;
|
||||
err = i2c_driver_install(_port, I2C_MODE_MASTER, 0, 0, 0);
|
||||
}
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "i2c_driver_install failed: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
@@ -49,51 +57,73 @@ class I2CBus {
|
||||
|
||||
void end() {
|
||||
if (_initialized) {
|
||||
if (_dev) {
|
||||
i2c_master_bus_rm_device(_dev);
|
||||
_dev = NULL;
|
||||
_dev_addr = 0xFF;
|
||||
}
|
||||
i2c_del_master_bus(_bus);
|
||||
_bus = NULL;
|
||||
i2c_driver_delete(_port);
|
||||
_initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool isInitialized() const { return _initialized; }
|
||||
|
||||
i2c_master_bus_handle_t busHandle() const { return _bus; }
|
||||
|
||||
esp_err_t writeBytes(uint8_t addr, const uint8_t* data, size_t len) {
|
||||
if (!_initialized) return ESP_ERR_INVALID_STATE;
|
||||
esp_err_t err = ensureDevice(addr);
|
||||
if (err != ESP_OK) return err;
|
||||
return i2c_master_transmit(_dev, data, len, pdMS_TO_TICKS(200));
|
||||
|
||||
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
|
||||
i2c_master_start(cmd);
|
||||
i2c_master_write_byte(cmd, (addr << 1) | I2C_MASTER_WRITE, true);
|
||||
if (len > 0 && data != nullptr) {
|
||||
i2c_master_write(cmd, data, len, true);
|
||||
}
|
||||
i2c_master_stop(cmd);
|
||||
esp_err_t ret = i2c_master_cmd_begin(_port, cmd, pdMS_TO_TICKS(100));
|
||||
i2c_cmd_link_delete(cmd);
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t writeReg(uint8_t addr, uint8_t reg, const uint8_t* data, size_t len) {
|
||||
if (!_initialized) return ESP_ERR_INVALID_STATE;
|
||||
esp_err_t err = ensureDevice(addr);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
uint8_t buf[len + 1];
|
||||
buf[0] = reg;
|
||||
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
|
||||
i2c_master_start(cmd);
|
||||
i2c_master_write_byte(cmd, (addr << 1) | I2C_MASTER_WRITE, true);
|
||||
i2c_master_write_byte(cmd, reg, true);
|
||||
if (len > 0 && data != nullptr) {
|
||||
memcpy(buf + 1, data, len);
|
||||
i2c_master_write(cmd, data, len, true);
|
||||
}
|
||||
return i2c_master_transmit(_dev, buf, len + 1, pdMS_TO_TICKS(200));
|
||||
i2c_master_stop(cmd);
|
||||
esp_err_t ret = i2c_master_cmd_begin(_port, cmd, pdMS_TO_TICKS(100));
|
||||
i2c_cmd_link_delete(cmd);
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t readReg(uint8_t addr, uint8_t reg, uint8_t* data, size_t len) {
|
||||
if (!_initialized) return ESP_ERR_INVALID_STATE;
|
||||
esp_err_t err = ensureDevice(addr);
|
||||
if (err != ESP_OK) return err;
|
||||
return i2c_master_transmit_receive(_dev, ®, 1, data, len, pdMS_TO_TICKS(200));
|
||||
|
||||
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
|
||||
i2c_master_start(cmd);
|
||||
i2c_master_write_byte(cmd, (addr << 1) | I2C_MASTER_WRITE, true);
|
||||
i2c_master_write_byte(cmd, reg, true);
|
||||
i2c_master_start(cmd);
|
||||
i2c_master_write_byte(cmd, (addr << 1) | I2C_MASTER_READ, true);
|
||||
if (len > 1) {
|
||||
i2c_master_read(cmd, data, len - 1, I2C_MASTER_ACK);
|
||||
}
|
||||
i2c_master_read_byte(cmd, data + len - 1, I2C_MASTER_NACK);
|
||||
i2c_master_stop(cmd);
|
||||
esp_err_t ret = i2c_master_cmd_begin(_port, cmd, pdMS_TO_TICKS(100));
|
||||
i2c_cmd_link_delete(cmd);
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool probe(uint8_t addr) {
|
||||
if (!_initialized) return false;
|
||||
return i2c_master_probe(_bus, addr, pdMS_TO_TICKS(200)) == ESP_OK;
|
||||
|
||||
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
|
||||
i2c_master_start(cmd);
|
||||
i2c_master_write_byte(cmd, (addr << 1) | I2C_MASTER_WRITE, true);
|
||||
i2c_master_stop(cmd);
|
||||
esp_err_t ret = i2c_master_cmd_begin(_port, cmd, pdMS_TO_TICKS(50));
|
||||
i2c_cmd_link_delete(cmd);
|
||||
return ret == ESP_OK;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> scan(uint8_t lower = 1, uint8_t upper = 127) {
|
||||
@@ -127,23 +157,4 @@ class I2CBus {
|
||||
gpio_num_t _scl = GPIO_NUM_NC;
|
||||
uint32_t _freq = 100000;
|
||||
bool _initialized = false;
|
||||
|
||||
i2c_master_bus_handle_t _bus = NULL;
|
||||
i2c_master_dev_handle_t _dev = NULL;
|
||||
uint8_t _dev_addr = 0xFF;
|
||||
|
||||
esp_err_t ensureDevice(uint8_t addr) {
|
||||
if (_dev && _dev_addr == addr) return ESP_OK;
|
||||
if (_dev) {
|
||||
i2c_master_bus_rm_device(_dev);
|
||||
_dev = NULL;
|
||||
}
|
||||
i2c_device_config_t dev_cfg = {};
|
||||
dev_cfg.dev_addr_length = I2C_ADDR_BIT_LEN_7;
|
||||
dev_cfg.device_address = addr;
|
||||
dev_cfg.scl_speed_hz = _freq;
|
||||
esp_err_t err = i2c_master_bus_add_device(_bus, &dev_cfg, &_dev);
|
||||
if (err == ESP_OK) _dev_addr = addr;
|
||||
return err;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#ifndef LEDService_h
|
||||
#define LEDService_h
|
||||
|
||||
#include <sdkconfig.h>
|
||||
#include <driver/rmt_tx.h>
|
||||
#include <led_strip.h>
|
||||
#include <led_strip_rmt.h>
|
||||
@@ -10,12 +9,8 @@
|
||||
#include <esp_log.h>
|
||||
|
||||
#ifndef WS2812_PIN
|
||||
#if CONFIG_IDF_TARGET_ESP32P4
|
||||
#define WS2812_PIN 27
|
||||
#else
|
||||
#define WS2812_PIN 12
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef WS2812_NUM_LEDS
|
||||
#define WS2812_NUM_LEDS 13
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <template/stateful_persistence.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); }
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
#define ServoController_h
|
||||
|
||||
#include <peripherals/drivers/pca9685.h>
|
||||
#include <template/stateful_persistence.h>
|
||||
#include <template/stateful_proto_endpoint.h>
|
||||
#include <template/stateful_service.h>
|
||||
#include <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
|
||||
@@ -34,27 +34,30 @@ inline ServoSettings ServoSettings_defaults() {
|
||||
return settings;
|
||||
}
|
||||
|
||||
inline void ServoSettings_read(const ServoSettings &settings, ServoSettings &proto) { proto = settings; }
|
||||
|
||||
inline StateUpdateResult ServoSettings_update(const ServoSettings &proto, ServoSettings &settings) {
|
||||
settings = proto;
|
||||
return StateUpdateResult::CHANGED;
|
||||
}
|
||||
|
||||
class ServoController : public StatefulService<ServoSettings> {
|
||||
class ServoController {
|
||||
public:
|
||||
ServoController()
|
||||
: protoEndpoint(ServoSettings_read, ServoSettings_update, this,
|
||||
API_REQUEST_EXTRACTOR(servo_settings, ServoSettings),
|
||||
API_RESPONSE_ASSIGNER(servo_settings, ServoSettings)),
|
||||
_persistence(ServoSettings_read, ServoSettings_update, this, SERVO_SETTINGS_FILE, api_ServoSettings_fields,
|
||||
api_ServoSettings_size, ServoSettings_defaults()) {}
|
||||
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);
|
||||
@@ -103,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);
|
||||
@@ -115,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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <sdkconfig.h>
|
||||
#include <platform_shared/api.pb.h>
|
||||
|
||||
#if !CONFIG_IDF_TARGET_ESP32P4
|
||||
#include <esp_camera.h>
|
||||
#else
|
||||
#define PIXFORMAT_JPEG 0
|
||||
#define FRAMESIZE_VGA 0
|
||||
#define GAINCEILING_2X 0
|
||||
#endif
|
||||
|
||||
namespace Camera {
|
||||
|
||||
// Use proto type directly as settings type
|
||||
using CameraSettings = api_CameraSettings;
|
||||
|
||||
// Default factory settings
|
||||
inline CameraSettings CameraSettings_defaults() {
|
||||
CameraSettings settings = api_CameraSettings_init_zero;
|
||||
settings.pixformat = PIXFORMAT_JPEG;
|
||||
@@ -50,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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,9 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <template/state_result.h>
|
||||
#include <sdkconfig.h>
|
||||
#include <platform_shared/api.pb.h>
|
||||
#include <global.h>
|
||||
|
||||
/*
|
||||
* I2C software connection
|
||||
*/
|
||||
#ifndef SDA_PIN
|
||||
#define SDA_PIN 21
|
||||
#endif
|
||||
#ifndef SCL_PIN
|
||||
#define SCL_PIN 22
|
||||
#endif
|
||||
#ifndef I2C_FREQUENCY
|
||||
#define I2C_FREQUENCY 1000000UL
|
||||
#endif
|
||||
|
||||
// Use proto types directly
|
||||
using PinConfig = api_PinConfig;
|
||||
@@ -18,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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
enum class StateUpdateResult {
|
||||
CHANGED = 0, // The update changed the state and propagation should take place if required
|
||||
UNCHANGED, // The state was unchanged, propagation should not take place
|
||||
ERROR // There was a problem updating the state, propagation should not take place
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <esp_http_server.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <template/stateful_service.h>
|
||||
#include <communication/webserver.h>
|
||||
|
||||
#include <functional>
|
||||
|
||||
#define HTTP_ENDPOINT_ORIGIN_ID "http"
|
||||
#define HTTPS_ENDPOINT_ORIGIN_ID "https"
|
||||
|
||||
template <class T>
|
||||
class StatefulHttpEndpoint {
|
||||
protected:
|
||||
JsonStateReader<T> _stateReader;
|
||||
JsonStateUpdater<T> _stateUpdater;
|
||||
StatefulService<T> *_statefulService;
|
||||
|
||||
public:
|
||||
StatefulHttpEndpoint(JsonStateReader<T> stateReader, JsonStateUpdater<T> stateUpdater,
|
||||
StatefulService<T> *statefulService)
|
||||
: _stateReader(stateReader), _stateUpdater(stateUpdater), _statefulService(statefulService) {}
|
||||
|
||||
esp_err_t handleStateUpdate(httpd_req_t *request, JsonVariant &json) {
|
||||
JsonVariant jsonObject = json.as<JsonVariant>();
|
||||
StateUpdateResult outcome = _statefulService->updateWithoutPropagation(jsonObject, _stateUpdater);
|
||||
|
||||
if (outcome == StateUpdateResult::ERROR) {
|
||||
return WebServer::sendError(request, 400, "Invalid state");
|
||||
} else if ((outcome == StateUpdateResult::CHANGED)) {
|
||||
_statefulService->callUpdateHandlers(HTTP_ENDPOINT_ORIGIN_ID);
|
||||
}
|
||||
|
||||
JsonDocument doc;
|
||||
JsonVariant root = doc.to<JsonVariant>();
|
||||
_statefulService->read(root, _stateReader);
|
||||
return WebServer::sendJson(request, 200, doc);
|
||||
}
|
||||
|
||||
esp_err_t getState(httpd_req_t *request) {
|
||||
JsonDocument doc;
|
||||
JsonVariant root = doc.to<JsonVariant>();
|
||||
_statefulService->read(root, _stateReader);
|
||||
return WebServer::sendJson(request, 200, doc);
|
||||
}
|
||||
};
|
||||
@@ -1,141 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <template/stateful_service.h>
|
||||
#include <template/state_result.h>
|
||||
#include <filesystem.h>
|
||||
#include <pb_encode.h>
|
||||
#include <pb_decode.h>
|
||||
#include <cstdio>
|
||||
#include <sys/stat.h>
|
||||
#include <esp_log.h>
|
||||
|
||||
static const char *TAG_PERSISTENCE = "FSPersistencePB";
|
||||
|
||||
template <class T>
|
||||
class FSPersistencePB {
|
||||
public:
|
||||
using ProtoStateReader = std::function<void(const T &, T &)>;
|
||||
using ProtoStateUpdater = std::function<StateUpdateResult(const T &, T &)>;
|
||||
|
||||
FSPersistencePB(ProtoStateReader stateReader, ProtoStateUpdater stateUpdater, StatefulService<T> *statefulService,
|
||||
const char *filePath, const pb_msgdesc_t *msgDescriptor, size_t maxSize, const T &defaultState)
|
||||
: _stateReader(stateReader),
|
||||
_stateUpdater(stateUpdater),
|
||||
_statefulService(statefulService),
|
||||
_filePath(filePath),
|
||||
_msgDescriptor(msgDescriptor),
|
||||
_maxSize(maxSize),
|
||||
_defaultState(defaultState),
|
||||
_updateHandlerId(0) {
|
||||
enableUpdateHandler();
|
||||
}
|
||||
|
||||
void readFromFS() {
|
||||
FILE *file = fopen(_filePath, "rb");
|
||||
|
||||
if (file) {
|
||||
fseek(file, 0, SEEK_END);
|
||||
size_t fileSize = ftell(file);
|
||||
fseek(file, 0, SEEK_SET);
|
||||
|
||||
if (fileSize > 0 && fileSize <= _maxSize) {
|
||||
uint8_t *buffer = new uint8_t[fileSize];
|
||||
size_t bytesRead = fread(buffer, 1, fileSize, file);
|
||||
fclose(file);
|
||||
|
||||
if (bytesRead == fileSize) {
|
||||
T *protoMsg = new T();
|
||||
*protoMsg = {};
|
||||
pb_istream_t stream = pb_istream_from_buffer(buffer, bytesRead);
|
||||
|
||||
if (pb_decode(&stream, _msgDescriptor, protoMsg)) {
|
||||
_statefulService->updateWithoutPropagation(
|
||||
[this, protoMsg](T &state) { return _stateUpdater(*protoMsg, state); });
|
||||
delete protoMsg;
|
||||
delete[] buffer;
|
||||
return;
|
||||
}
|
||||
delete protoMsg;
|
||||
}
|
||||
delete[] buffer;
|
||||
} else {
|
||||
fclose(file);
|
||||
}
|
||||
}
|
||||
|
||||
applyDefaults();
|
||||
writeToFS();
|
||||
}
|
||||
|
||||
bool writeToFS() {
|
||||
uint8_t *buffer = new uint8_t[_maxSize];
|
||||
pb_ostream_t stream = pb_ostream_from_buffer(buffer, _maxSize);
|
||||
|
||||
T *protoMsg = new T();
|
||||
*protoMsg = {};
|
||||
_statefulService->read([this, protoMsg](const T &state) { _stateReader(state, *protoMsg); });
|
||||
|
||||
bool encodeSuccess = pb_encode(&stream, _msgDescriptor, protoMsg);
|
||||
delete protoMsg;
|
||||
|
||||
if (!encodeSuccess) {
|
||||
delete[] buffer;
|
||||
return false;
|
||||
}
|
||||
|
||||
mkdirs();
|
||||
|
||||
FILE *file = fopen(_filePath, "wb");
|
||||
if (!file) {
|
||||
ESP_LOGE(TAG_PERSISTENCE, "Failed to open file for writing: %s", _filePath);
|
||||
delete[] buffer;
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t written = fwrite(buffer, 1, stream.bytes_written, file);
|
||||
fclose(file);
|
||||
delete[] buffer;
|
||||
|
||||
return written == stream.bytes_written;
|
||||
}
|
||||
|
||||
void disableUpdateHandler() {
|
||||
if (_updateHandlerId) {
|
||||
_statefulService->removeUpdateHandler(_updateHandlerId);
|
||||
_updateHandlerId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void enableUpdateHandler() {
|
||||
if (!_updateHandlerId) {
|
||||
_updateHandlerId = _statefulService->addUpdateHandler([&](const std::string &originId) { writeToFS(); });
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
ProtoStateReader _stateReader;
|
||||
ProtoStateUpdater _stateUpdater;
|
||||
StatefulService<T> *_statefulService;
|
||||
const char *_filePath;
|
||||
const pb_msgdesc_t *_msgDescriptor;
|
||||
size_t _maxSize;
|
||||
T _defaultState;
|
||||
HandlerId _updateHandlerId;
|
||||
|
||||
void mkdirs() {
|
||||
std::string path(_filePath);
|
||||
size_t index = 0;
|
||||
while ((index = path.find('/', index + 1)) != std::string::npos) {
|
||||
std::string segment = path.substr(0, index);
|
||||
struct stat st;
|
||||
if (stat(segment.c_str(), &st) != 0) {
|
||||
FileSystem::mkdirRecursive(segment.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
void applyDefaults() {
|
||||
_statefulService->updateWithoutPropagation([this](T &state) { return _stateUpdater(_defaultState, state); });
|
||||
}
|
||||
};
|
||||
@@ -1,135 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <esp_http_server.h>
|
||||
#include <template/stateful_service.h>
|
||||
#include <communication/webserver.h>
|
||||
#include <platform_shared/api.pb.h>
|
||||
#include <pb_encode.h>
|
||||
#include <pb_decode.h>
|
||||
|
||||
#include <functional>
|
||||
|
||||
#define PROTO_ENDPOINT_ORIGIN_ID "proto"
|
||||
|
||||
/**
|
||||
* A stateful HTTP endpoint that uses protobuf encoding with api::Request/Response wrappers.
|
||||
*
|
||||
* @tparam T The internal state type (e.g., APSettings C++ class)
|
||||
* @tparam ProtoT The protobuf message type within the oneof (e.g., api_APSettings)
|
||||
*
|
||||
* The endpoint receives api::Request, extracts the specific payload from the oneof,
|
||||
* and returns api::Response with the updated state.
|
||||
*/
|
||||
template <class T, class ProtoT>
|
||||
class StatefulProtoEndpoint {
|
||||
public:
|
||||
/** Converts internal state to protobuf message for responses */
|
||||
// Formats are passed as referenced const (local variable) we want to read from, and a reference (proto) we write to
|
||||
using ProtoStateReader = std::function<void(const T&, ProtoT&)>;
|
||||
/** Converts incoming protobuf message to internal state */
|
||||
// Formats are passed as referenced const (new object) we read from, and a reference to the local variable we write to
|
||||
using ProtoStateUpdater = std::function<StateUpdateResult(const ProtoT&, T&)>;
|
||||
/** Extracts the specific proto type from Request oneof */
|
||||
using RequestExtractor = std::function<bool(const api_Request&, ProtoT&)>;
|
||||
/** Assigns the specific proto type to Response oneof */
|
||||
using ResponseAssigner = std::function<void(api_Response&, const ProtoT&)>;
|
||||
|
||||
protected:
|
||||
ProtoStateReader _stateReader;
|
||||
ProtoStateUpdater _stateUpdater;
|
||||
StatefulService<T>* _statefulService;
|
||||
RequestExtractor _requestExtractor;
|
||||
ResponseAssigner _responseAssigner;
|
||||
|
||||
public:
|
||||
/**
|
||||
* Constructor for wrapped proto endpoint
|
||||
* @param stateReader Converts internal state to proto
|
||||
* @param stateUpdater Converts proto to internal state
|
||||
* @param statefulService The stateful service to manage
|
||||
* @param requestExtractor Extracts specific type from Request oneof
|
||||
* @param responseAssigner Assigns specific type to Response oneof
|
||||
*/
|
||||
StatefulProtoEndpoint(ProtoStateReader stateReader, ProtoStateUpdater stateUpdater,
|
||||
StatefulService<T>* statefulService,
|
||||
RequestExtractor requestExtractor, ResponseAssigner responseAssigner)
|
||||
: _stateReader(stateReader),
|
||||
_stateUpdater(stateUpdater),
|
||||
_statefulService(statefulService),
|
||||
_requestExtractor(requestExtractor),
|
||||
_responseAssigner(responseAssigner) {}
|
||||
|
||||
/**
|
||||
* Handles POST requests: extracts payload from pre-decoded Request, updates state, returns Response
|
||||
*/
|
||||
esp_err_t handleStateUpdate(httpd_req_t* httpReq, api_Request* protoReq) {
|
||||
ProtoT protoMsg = {};
|
||||
if (!_requestExtractor(*protoReq, protoMsg)) {
|
||||
return sendErrorResponse(httpReq, 400, "Invalid request type");
|
||||
}
|
||||
|
||||
StateUpdateResult outcome = _statefulService->update(
|
||||
[this, &protoMsg](T& settings) { return _stateUpdater(protoMsg, settings); }, PROTO_ENDPOINT_ORIGIN_ID);
|
||||
|
||||
if (outcome == StateUpdateResult::ERROR) {
|
||||
return sendErrorResponse(httpReq, 400, "Invalid state");
|
||||
}
|
||||
|
||||
return sendStateResponse(httpReq, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles GET requests: reads current state and returns it as Response
|
||||
*/
|
||||
esp_err_t getState(httpd_req_t* request) {
|
||||
return sendStateResponse(request, 200);
|
||||
}
|
||||
|
||||
private:
|
||||
/** Sends current state wrapped in Response */
|
||||
esp_err_t sendStateResponse(httpd_req_t* request, uint32_t statusCode) {
|
||||
api_Response res = api_Response_init_zero;
|
||||
res.status_code = statusCode;
|
||||
|
||||
ProtoT protoState = {};
|
||||
_statefulService->read([this, &protoState](const T& settings) { _stateReader(settings, protoState); });
|
||||
_responseAssigner(res, protoState);
|
||||
|
||||
return WebServer::send(request, 200, res, api_Response_fields);
|
||||
}
|
||||
|
||||
/** Sends error wrapped in Response */
|
||||
esp_err_t sendErrorResponse(httpd_req_t* request, uint32_t statusCode, const char* message) {
|
||||
api_Response res = api_Response_init_zero;
|
||||
res.status_code = statusCode;
|
||||
res.error_message = (char*)message;
|
||||
return WebServer::send(request, statusCode == 200 ? 200 : 400, res, api_Response_fields);
|
||||
}
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Helper macros for defining request extractors and response assigners
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Creates a request extractor lambda for a specific payload type
|
||||
* Usage: API_REQUEST_EXTRACTOR(ap_settings, api_APSettings)
|
||||
*/
|
||||
#define API_REQUEST_EXTRACTOR(field_name, proto_type) \
|
||||
[](const api_Request& req, proto_type& out) -> bool { \
|
||||
if (req.which_payload == api_Request_##field_name##_tag) { \
|
||||
out = req.payload.field_name; \
|
||||
return true; \
|
||||
} \
|
||||
return false; \
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a response assigner lambda for a specific payload type
|
||||
* Usage: API_RESPONSE_ASSIGNER(ap_settings, api_APSettings)
|
||||
*/
|
||||
#define API_RESPONSE_ASSIGNER(field_name, proto_type) \
|
||||
[](api_Response& res, const proto_type& data) { \
|
||||
res.which_payload = api_Response_##field_name##_tag; \
|
||||
res.payload.field_name = data; \
|
||||
}
|
||||
@@ -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_;
|
||||
};
|
||||
@@ -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.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,8 +0,0 @@
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
# 32MB Flash partition table for ESP32-P4
|
||||
nvs, data, nvs, 0x9000, 0x5000,
|
||||
otadata, data, ota, 0xe000, 0x2000,
|
||||
app0, app, ota_0, 0x10000, 0xC80000,
|
||||
app1, app, ota_1, 0xC90000, 0xC80000,
|
||||
spiffs, data, spiffs, 0x1910000,0x6E0000,
|
||||
coredump, data, coredump,0x1FF0000,0x10000,
|
||||
|
+14
-23
@@ -1,25 +1,3 @@
|
||||
set(COMPONENT_REQUIRES
|
||||
driver
|
||||
esp_driver_i2c
|
||||
esp_http_server
|
||||
nvs_flash
|
||||
esp_wifi
|
||||
esp_event
|
||||
esp_netif
|
||||
mdns
|
||||
esp_timer
|
||||
esp_psram
|
||||
spi_flash
|
||||
littlefs
|
||||
esp-dsp
|
||||
)
|
||||
|
||||
if(IDF_TARGET STREQUAL "esp32p4")
|
||||
list(APPEND COMPONENT_REQUIRES esp_wifi_remote esp_hosted esp_driver_cam esp_driver_isp esp_driver_jpeg espressif__esp_sccb_intf espressif__esp_cam_sensor)
|
||||
else()
|
||||
list(APPEND COMPONENT_REQUIRES esp32-camera)
|
||||
endif()
|
||||
|
||||
idf_component_register(
|
||||
SRC_DIRS
|
||||
"."
|
||||
@@ -27,10 +5,23 @@ idf_component_register(
|
||||
"peripherals"
|
||||
"wifi"
|
||||
"platform_shared"
|
||||
"event_bus"
|
||||
"../../submodules/nanopb"
|
||||
INCLUDE_DIRS
|
||||
"../include"
|
||||
"../../submodules/nanopb"
|
||||
REQUIRES
|
||||
${COMPONENT_REQUIRES}
|
||||
driver
|
||||
esp_http_server
|
||||
nvs_flash
|
||||
esp_wifi
|
||||
esp_event
|
||||
esp_netif
|
||||
mdns
|
||||
esp_timer
|
||||
esp_psram
|
||||
spi_flash
|
||||
littlefs
|
||||
esp32-camera
|
||||
esp-dsp
|
||||
)
|
||||
|
||||
+52
-20
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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*);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -16,21 +16,3 @@ dependencies:
|
||||
|
||||
espressif/esp32-camera:
|
||||
version: "^2.0.0"
|
||||
rules:
|
||||
- if: "idf_version >=5.0.0"
|
||||
- if: "target not in [esp32p4]"
|
||||
|
||||
espressif/esp_wifi_remote:
|
||||
version: ">=0.3.0"
|
||||
rules:
|
||||
- if: "target in [esp32p4]"
|
||||
|
||||
espressif/esp_hosted:
|
||||
version: ">=0.0.6"
|
||||
rules:
|
||||
- if: "target in [esp32p4]"
|
||||
|
||||
espressif/esp_cam_sensor:
|
||||
version: ">=0.5.0"
|
||||
rules:
|
||||
- if: "target in [esp32p4]"
|
||||
+40
-68
@@ -20,10 +20,8 @@
|
||||
#include <ap_service.h>
|
||||
#include <mdns_service.h>
|
||||
#include <system_service.h>
|
||||
|
||||
#if CONFIG_IDF_TARGET_ESP32P4
|
||||
#include <esp_hosted.h>
|
||||
#endif
|
||||
#include <consumers/event_storage_manager.h>
|
||||
#include <event_bus/rest_endpoints.h>
|
||||
|
||||
#include <www_mount.hpp>
|
||||
|
||||
@@ -46,7 +44,7 @@ WiFiService wifiService;
|
||||
APService apService;
|
||||
|
||||
void setupServer() {
|
||||
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,24 +57,22 @@ void setupServer() {
|
||||
server.on("/api/camera/still", HTTP_GET, [&](httpd_req_t *request) { return cameraService.cameraStill(request); });
|
||||
server.on("/api/camera/stream", HTTP_GET,
|
||||
[&](httpd_req_t *request) { return cameraService.cameraStream(request); });
|
||||
#if USE_DVP_CAMERA
|
||||
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
|
||||
#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); });
|
||||
@@ -85,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) {
|
||||
@@ -136,32 +132,18 @@ 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) { FileSystemWS::fsHandler.handleUploadData(data); });
|
||||
|
||||
wsSocket.bridgeFromEventBus<socket_message_IMUData>(100);
|
||||
|
||||
using CorrelationHandler =
|
||||
std::function<void(const socket_message_CorrelationRequest &, socket_message_CorrelationResponse &, int)>;
|
||||
static std::map<pb_size_t, CorrelationHandler> correlationHandlers = {
|
||||
@@ -261,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
|
||||
@@ -281,21 +267,10 @@ void IRAM_ATTR SpotControlLoopEntry(void *) {
|
||||
|
||||
void IRAM_ATTR serviceLoopEntry(void *) {
|
||||
ESP_LOGI("main", "Service task starting");
|
||||
#if CONFIG_IDF_TARGET_ESP32P4
|
||||
ESP_LOGI("main", "Initializing ESP-Hosted for C6 coprocessor WiFi...");
|
||||
int ret = esp_hosted_init();
|
||||
if (ret != 0) {
|
||||
ESP_LOGE("main", "ESP-Hosted init failed: %d", ret);
|
||||
} else {
|
||||
ESP_LOGI("main", "ESP-Hosted initialized, connecting to C6...");
|
||||
ret = esp_hosted_connect_to_slave();
|
||||
if (ret != 0) {
|
||||
ESP_LOGW("main", "ESP-Hosted connect failed: %d - WiFi may not work", ret);
|
||||
} else {
|
||||
ESP_LOGI("main", "ESP-Hosted link established with C6");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
static EventStorageManager storageManager;
|
||||
storageManager.initialize();
|
||||
ESP_LOGI("main", "Event storage initialized, settings loaded and published");
|
||||
|
||||
WiFi.init();
|
||||
wifiService.begin();
|
||||
@@ -312,7 +287,6 @@ void IRAM_ATTR serviceLoopEntry(void *) {
|
||||
setupEventSocket();
|
||||
|
||||
ESP_LOGI("main", "Service task started");
|
||||
|
||||
for (;;) {
|
||||
wifiService.loop();
|
||||
apService.loop();
|
||||
@@ -326,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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -4,15 +4,9 @@
|
||||
|
||||
namespace Camera {
|
||||
|
||||
static const char *const TAG = "CameraService";
|
||||
|
||||
#if USE_DVP_CAMERA || USE_CSI_CAMERA
|
||||
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";
|
||||
#endif
|
||||
|
||||
#if USE_DVP_CAMERA
|
||||
|
||||
SemaphoreHandle_t cameraMutex = xSemaphoreCreateMutex();
|
||||
|
||||
@@ -35,20 +29,20 @@ 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;
|
||||
#if FT_ENABLED(USE_CAMERA)
|
||||
camera_config.pin_d0 = Y2_GPIO_NUM;
|
||||
camera_config.pin_d1 = Y3_GPIO_NUM;
|
||||
camera_config.pin_d2 = Y4_GPIO_NUM;
|
||||
@@ -65,6 +59,7 @@ esp_err_t CameraService::begin() {
|
||||
camera_config.pin_sccb_scl = SIOC_GPIO_NUM;
|
||||
camera_config.pin_pwdn = PWDN_GPIO_NUM;
|
||||
camera_config.pin_reset = RESET_GPIO_NUM;
|
||||
#endif
|
||||
camera_config.xclk_freq_hz = 20000000;
|
||||
camera_config.pixel_format = PIXFORMAT_JPEG;
|
||||
|
||||
@@ -147,498 +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();
|
||||
}
|
||||
|
||||
#elif USE_CSI_CAMERA
|
||||
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);
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
#include "esp_cam_ctlr.h"
|
||||
#include "esp_cam_ctlr_csi.h"
|
||||
#include "esp_cam_ctlr_types.h"
|
||||
#include "driver/isp.h"
|
||||
#include "driver/jpeg_encode.h"
|
||||
#include "esp_sccb_intf.h"
|
||||
#include "esp_sccb_i2c.h"
|
||||
#include "esp_cam_sensor.h"
|
||||
#include "ov5647.h"
|
||||
#include "esp_ldo_regulator.h"
|
||||
#include "driver/isp_demosaic.h"
|
||||
#include "driver/isp_bf.h"
|
||||
#include "driver/isp_sharpen.h"
|
||||
}
|
||||
|
||||
#include <peripherals/i2c_bus.h>
|
||||
|
||||
namespace Camera {
|
||||
|
||||
#ifndef MIPI_CSI_HRES
|
||||
#define MIPI_CSI_HRES 640
|
||||
#endif
|
||||
#ifndef MIPI_CSI_VRES
|
||||
#define MIPI_CSI_VRES 480
|
||||
#endif
|
||||
#ifndef MIPI_CSI_LANE_BITRATE_MBPS
|
||||
#define MIPI_CSI_LANE_BITRATE_MBPS 200
|
||||
#endif
|
||||
#ifndef MIPI_CSI_DATA_LANES
|
||||
#define MIPI_CSI_DATA_LANES 2
|
||||
#endif
|
||||
#ifndef CAM_SCCB_FREQ_HZ
|
||||
#define CAM_SCCB_FREQ_HZ 100000
|
||||
#endif
|
||||
#ifndef CAM_SENSOR_ADDR
|
||||
#define CAM_SENSOR_ADDR 0x36
|
||||
#endif
|
||||
#ifndef CAM_XCLK_PIN
|
||||
#define CAM_XCLK_PIN -1
|
||||
#endif
|
||||
#ifndef CAM_XCLK_FREQ_HZ
|
||||
#define CAM_XCLK_FREQ_HZ 25000000
|
||||
#endif
|
||||
#ifndef CAM_RESET_PIN
|
||||
#define CAM_RESET_PIN -1
|
||||
#endif
|
||||
#ifndef CAM_PWDN_PIN
|
||||
#define CAM_PWDN_PIN -1
|
||||
#endif
|
||||
#ifndef CSI_JPEG_QUALITY
|
||||
#define CSI_JPEG_QUALITY 80
|
||||
#endif
|
||||
|
||||
#define NUM_FRAME_BUFS 2
|
||||
static constexpr size_t CACHE_LINE_SIZE = 64;
|
||||
#define ALIGN_UP(n, a) (((n) + ((a) - 1)) & ~((a) - 1))
|
||||
|
||||
static esp_cam_ctlr_handle_t s_cam_handle = NULL;
|
||||
static isp_proc_handle_t s_isp_proc = NULL;
|
||||
static jpeg_encoder_handle_t s_jpeg_enc = NULL;
|
||||
|
||||
static uint8_t *s_frame_bufs[NUM_FRAME_BUFS] = {};
|
||||
static size_t s_frame_buf_size = 0;
|
||||
static uint8_t *s_jpeg_bufs[NUM_FRAME_BUFS] = {};
|
||||
static size_t s_jpeg_buf_alloc = 0;
|
||||
|
||||
static bool s_cam_initialized = false;
|
||||
static uint16_t s_frame_hres = MIPI_CSI_HRES;
|
||||
static uint16_t s_frame_vres = MIPI_CSI_VRES;
|
||||
|
||||
static SemaphoreHandle_t s_frame_done = NULL;
|
||||
static SemaphoreHandle_t s_jpeg_lock = NULL;
|
||||
static SemaphoreHandle_t s_jpeg_ready = NULL;
|
||||
static TaskHandle_t s_capture_task = NULL;
|
||||
static volatile bool s_capture_running = false;
|
||||
|
||||
static int s_write_idx = 0;
|
||||
static int s_ready_idx = -1;
|
||||
static size_t s_ready_jpeg_len = 0;
|
||||
|
||||
static uint8_t *s_send_buf = NULL;
|
||||
static size_t s_send_buf_size = 0;
|
||||
|
||||
static bool on_trans_finished(esp_cam_ctlr_handle_t handle, esp_cam_ctlr_trans_t *trans, void *user_data) {
|
||||
BaseType_t woken = pdFALSE;
|
||||
xSemaphoreGiveFromISR(s_frame_done, &woken);
|
||||
return (woken == pdTRUE);
|
||||
}
|
||||
|
||||
static void capture_task_fn(void *arg) {
|
||||
while (s_capture_running) {
|
||||
int idx = s_write_idx;
|
||||
|
||||
esp_cam_ctlr_trans_t trans = {};
|
||||
trans.buffer = s_frame_bufs[idx];
|
||||
trans.buflen = s_frame_buf_size;
|
||||
|
||||
if (esp_cam_ctlr_receive(s_cam_handle, &trans, 2000) != ESP_OK) {
|
||||
vTaskDelay(pdMS_TO_TICKS(5));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (xSemaphoreTake(s_frame_done, pdMS_TO_TICKS(2000)) != pdTRUE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
jpeg_encode_cfg_t enc_cfg = {};
|
||||
enc_cfg.src_type = JPEG_ENCODE_IN_FORMAT_RGB565;
|
||||
enc_cfg.sub_sample = JPEG_DOWN_SAMPLING_YUV420;
|
||||
enc_cfg.image_quality = CSI_JPEG_QUALITY;
|
||||
enc_cfg.width = s_frame_hres;
|
||||
enc_cfg.height = s_frame_vres;
|
||||
|
||||
uint32_t out_size = 0;
|
||||
esp_err_t err = jpeg_encoder_process(s_jpeg_enc, &enc_cfg, s_frame_bufs[idx], trans.received_size,
|
||||
s_jpeg_bufs[idx], s_jpeg_buf_alloc, &out_size);
|
||||
if (err != ESP_OK) {
|
||||
continue;
|
||||
}
|
||||
|
||||
xSemaphoreTake(s_jpeg_lock, portMAX_DELAY);
|
||||
s_ready_idx = idx;
|
||||
s_ready_jpeg_len = out_size;
|
||||
xSemaphoreGive(s_jpeg_lock);
|
||||
|
||||
s_write_idx = (idx + 1) % NUM_FRAME_BUFS;
|
||||
|
||||
xSemaphoreGive(s_jpeg_ready);
|
||||
}
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
CameraService::CameraService() {
|
||||
s_frame_done = xSemaphoreCreateBinary();
|
||||
s_jpeg_lock = xSemaphoreCreateMutex();
|
||||
s_jpeg_ready = xSemaphoreCreateBinary();
|
||||
}
|
||||
|
||||
esp_err_t CameraService::begin() {
|
||||
ESP_LOGI(TAG, "Initializing MIPI-CSI camera for ESP32-P4");
|
||||
|
||||
esp_ldo_channel_handle_t ldo_mipi_phy = NULL;
|
||||
esp_ldo_channel_config_t ldo_cfg = {};
|
||||
ldo_cfg.chan_id = 3;
|
||||
ldo_cfg.voltage_mv = 2500;
|
||||
esp_err_t ldo_err = esp_ldo_acquire_channel(&ldo_cfg, &ldo_mipi_phy);
|
||||
if (ldo_err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to acquire MIPI PHY LDO: %s", esp_err_to_name(ldo_err));
|
||||
return ldo_err;
|
||||
}
|
||||
|
||||
i2c_master_bus_handle_t i2c_bus = I2CBus::instance().busHandle();
|
||||
if (!i2c_bus) {
|
||||
ESP_LOGE(TAG, "I2C bus not initialized, cannot init camera SCCB");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
esp_sccb_io_handle_t sccb_io = NULL;
|
||||
sccb_i2c_config_t sccb_cfg = {};
|
||||
sccb_cfg.scl_speed_hz = CAM_SCCB_FREQ_HZ;
|
||||
sccb_cfg.device_address = CAM_SENSOR_ADDR;
|
||||
sccb_cfg.dev_addr_length = I2C_ADDR_BIT_LEN_7;
|
||||
|
||||
esp_err_t err = sccb_new_i2c_io(i2c_bus, &sccb_cfg, &sccb_io);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to create SCCB I/O handle: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_cam_sensor_config_t cam_sensor_cfg = {};
|
||||
cam_sensor_cfg.sccb_handle = sccb_io;
|
||||
cam_sensor_cfg.reset_pin = static_cast<gpio_num_t>(CAM_RESET_PIN);
|
||||
cam_sensor_cfg.pwdn_pin = static_cast<gpio_num_t>(CAM_PWDN_PIN);
|
||||
cam_sensor_cfg.xclk_pin = static_cast<gpio_num_t>(CAM_XCLK_PIN);
|
||||
cam_sensor_cfg.xclk_freq_hz = CAM_XCLK_FREQ_HZ;
|
||||
cam_sensor_cfg.sensor_port = ESP_CAM_SENSOR_MIPI_CSI;
|
||||
|
||||
esp_cam_sensor_device_t *cam_sensor = ov5647_detect(&cam_sensor_cfg);
|
||||
if (!cam_sensor) {
|
||||
ESP_LOGE(TAG, "OV5647 detection failed");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
ESP_LOGI(TAG, "OV5647 camera sensor detected");
|
||||
|
||||
esp_cam_sensor_format_array_t fmt_array = {};
|
||||
err = esp_cam_sensor_query_format(cam_sensor, &fmt_array);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to query sensor formats: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
const esp_cam_sensor_format_t *selected_format = NULL;
|
||||
uint32_t best_area = 0;
|
||||
for (uint32_t i = 0; i < fmt_array.count; i++) {
|
||||
const auto &f = fmt_array.format_array[i];
|
||||
ESP_LOGI(TAG, "Sensor format[%u]: %dx%d mipi_clk=%uHz lanes=%d", (unsigned)i, f.width, f.height,
|
||||
(unsigned)f.mipi_info.mipi_clk, f.mipi_info.lane_num);
|
||||
}
|
||||
for (uint32_t i = 0; i < fmt_array.count; i++) {
|
||||
const uint16_t w = fmt_array.format_array[i].width;
|
||||
const uint16_t h = fmt_array.format_array[i].height;
|
||||
if (w <= MIPI_CSI_HRES && h <= MIPI_CSI_VRES) {
|
||||
const uint32_t area = (uint32_t)w * (uint32_t)h;
|
||||
if (!selected_format || area > best_area) {
|
||||
selected_format = &fmt_array.format_array[i];
|
||||
best_area = area;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!selected_format && fmt_array.count > 0) {
|
||||
uint32_t min_area = UINT32_MAX;
|
||||
for (uint32_t i = 0; i < fmt_array.count; i++) {
|
||||
const uint32_t area =
|
||||
(uint32_t)fmt_array.format_array[i].width * (uint32_t)fmt_array.format_array[i].height;
|
||||
if (area < min_area) {
|
||||
selected_format = &fmt_array.format_array[i];
|
||||
min_area = area;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!selected_format) {
|
||||
ESP_LOGE(TAG, "No sensor formats available");
|
||||
esp_err_t CameraService::updateSettings(httpd_req_t *request, api_Request *protoReq) {
|
||||
if (protoReq->which_payload != api_Request_camera_settings_tag) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
err = esp_cam_sensor_set_format(cam_sensor, selected_format);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to set sensor format: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
EventBus::publish(protoReq->payload.camera_settings, "HTTPEndpoint");
|
||||
|
||||
s_frame_hres = selected_format->width;
|
||||
s_frame_vres = selected_format->height;
|
||||
ESP_LOGI(TAG, "Sensor format set: %dx%d", s_frame_hres, s_frame_vres);
|
||||
|
||||
esp_cam_sensor_format_t cur_fmt = {};
|
||||
if (esp_cam_sensor_get_format(cam_sensor, &cur_fmt) == ESP_OK) {
|
||||
ESP_LOGI(TAG, "Active format: %dx%d, mipi_clk=%uHz, lanes=%d", cur_fmt.width, cur_fmt.height,
|
||||
(unsigned)cur_fmt.mipi_info.mipi_clk, cur_fmt.mipi_info.lane_num);
|
||||
}
|
||||
|
||||
int stream_on = 1;
|
||||
err = esp_cam_sensor_ioctl(cam_sensor, ESP_CAM_SENSOR_IOC_S_STREAM, &stream_on);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to start sensor stream: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_isp_processor_cfg_t isp_cfg = {};
|
||||
isp_cfg.clk_src = static_cast<isp_clk_src_t>(0);
|
||||
isp_cfg.clk_hz = 80 * 1000 * 1000;
|
||||
isp_cfg.input_data_source = ISP_INPUT_DATA_SOURCE_CSI;
|
||||
isp_cfg.input_data_color_type = ISP_COLOR_RAW8;
|
||||
isp_cfg.output_data_color_type = ISP_COLOR_RGB565;
|
||||
isp_cfg.has_line_start_packet = false;
|
||||
isp_cfg.has_line_end_packet = false;
|
||||
isp_cfg.h_res = s_frame_hres;
|
||||
isp_cfg.v_res = s_frame_vres;
|
||||
isp_cfg.bayer_order = COLOR_RAW_ELEMENT_ORDER_GBRG;
|
||||
|
||||
err = esp_isp_new_processor(&isp_cfg, &s_isp_proc);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "ISP processor init failed: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
err = esp_isp_enable(s_isp_proc);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "ISP enable failed: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_isp_demosaic_config_t demosaic_cfg = {};
|
||||
demosaic_cfg.grad_ratio.val = 16;
|
||||
demosaic_cfg.padding_mode = ISP_DEMOSAIC_EDGE_PADDING_MODE_SRND_DATA;
|
||||
esp_isp_demosaic_configure(s_isp_proc, &demosaic_cfg);
|
||||
esp_isp_demosaic_enable(s_isp_proc);
|
||||
|
||||
esp_isp_bf_config_t bf_cfg = {};
|
||||
bf_cfg.denoising_level = 10;
|
||||
bf_cfg.padding_mode = ISP_BF_EDGE_PADDING_MODE_SRND_DATA;
|
||||
uint8_t bf_tpl[ISP_BF_TEMPLATE_X_NUMS][ISP_BF_TEMPLATE_Y_NUMS] = {{1, 2, 1}, {2, 4, 2}, {1, 2, 1}};
|
||||
memcpy(bf_cfg.bf_template, bf_tpl, sizeof(bf_tpl));
|
||||
esp_isp_bf_configure(s_isp_proc, &bf_cfg);
|
||||
esp_isp_bf_enable(s_isp_proc);
|
||||
|
||||
esp_isp_sharpen_config_t sharp_cfg = {};
|
||||
sharp_cfg.h_thresh = 255;
|
||||
sharp_cfg.l_thresh = 20;
|
||||
sharp_cfg.padding_mode = ISP_SHARPEN_EDGE_PADDING_MODE_SRND_DATA;
|
||||
uint8_t sharp_m[ISP_SHARPEN_TEMPLATE_X_NUMS][ISP_SHARPEN_TEMPLATE_Y_NUMS] = {{1, 2, 1}, {2, 4, 2}, {1, 2, 1}};
|
||||
memcpy(sharp_cfg.sharpen_template, sharp_m, sizeof(sharp_m));
|
||||
sharp_cfg.h_freq_coeff.integer = 1;
|
||||
sharp_cfg.h_freq_coeff.decimal = 0;
|
||||
sharp_cfg.m_freq_coeff.integer = 1;
|
||||
sharp_cfg.m_freq_coeff.decimal = 0;
|
||||
esp_isp_sharpen_configure(s_isp_proc, &sharp_cfg);
|
||||
esp_isp_sharpen_enable(s_isp_proc);
|
||||
|
||||
esp_cam_ctlr_csi_config_t csi_cfg = {};
|
||||
csi_cfg.ctlr_id = 0;
|
||||
csi_cfg.h_res = s_frame_hres;
|
||||
csi_cfg.v_res = s_frame_vres;
|
||||
csi_cfg.lane_bit_rate_mbps = MIPI_CSI_LANE_BITRATE_MBPS;
|
||||
csi_cfg.input_data_color_type = CAM_CTLR_COLOR_RAW8;
|
||||
csi_cfg.output_data_color_type = CAM_CTLR_COLOR_RGB565;
|
||||
csi_cfg.data_lane_num = MIPI_CSI_DATA_LANES;
|
||||
csi_cfg.byte_swap_en = false;
|
||||
csi_cfg.queue_items = NUM_FRAME_BUFS;
|
||||
|
||||
err = esp_cam_new_csi_ctlr(&csi_cfg, &s_cam_handle);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "CSI controller init failed: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_cam_ctlr_evt_cbs_t cbs = {};
|
||||
cbs.on_trans_finished = on_trans_finished;
|
||||
err = esp_cam_ctlr_register_event_callbacks(s_cam_handle, &cbs, NULL);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "CSI register callbacks failed: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
s_frame_buf_size = ALIGN_UP((size_t)s_frame_hres * s_frame_vres * 2, CACHE_LINE_SIZE);
|
||||
for (int i = 0; i < NUM_FRAME_BUFS; i++) {
|
||||
s_frame_bufs[i] = (uint8_t *)heap_caps_aligned_alloc(CACHE_LINE_SIZE, s_frame_buf_size, MALLOC_CAP_SPIRAM);
|
||||
if (!s_frame_bufs[i]) {
|
||||
ESP_LOGE(TAG, "Failed to allocate frame buffer %d (%d bytes)", i, (int)s_frame_buf_size);
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
}
|
||||
|
||||
jpeg_encode_memory_alloc_cfg_t jpeg_mem_cfg = {};
|
||||
jpeg_mem_cfg.buffer_direction = JPEG_ENC_ALLOC_OUTPUT_BUFFER;
|
||||
for (int i = 0; i < NUM_FRAME_BUFS; i++) {
|
||||
size_t alloc_sz = 0;
|
||||
s_jpeg_bufs[i] = (uint8_t *)jpeg_alloc_encoder_mem(s_frame_hres * s_frame_vres, &jpeg_mem_cfg, &alloc_sz);
|
||||
if (!s_jpeg_bufs[i]) {
|
||||
ESP_LOGE(TAG, "Failed to allocate JPEG buffer %d", i);
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
if (i == 0) s_jpeg_buf_alloc = alloc_sz;
|
||||
}
|
||||
|
||||
s_send_buf_size = s_jpeg_buf_alloc;
|
||||
s_send_buf = (uint8_t *)heap_caps_aligned_alloc(CACHE_LINE_SIZE, s_send_buf_size, MALLOC_CAP_SPIRAM);
|
||||
if (!s_send_buf) {
|
||||
ESP_LOGE(TAG, "Failed to allocate send buffer");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
jpeg_encode_engine_cfg_t enc_eng_cfg = {};
|
||||
enc_eng_cfg.timeout_ms = 500;
|
||||
err = jpeg_new_encoder_engine(&enc_eng_cfg, &s_jpeg_enc);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "JPEG encoder init failed: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
err = esp_cam_ctlr_enable(s_cam_handle);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "CSI controller enable failed: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
err = esp_cam_ctlr_start(s_cam_handle);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "CSI controller start failed: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
s_cam_initialized = true;
|
||||
|
||||
s_capture_running = true;
|
||||
xTaskCreatePinnedToCore(capture_task_fn, "csi_cap", 4096, NULL, 6, &s_capture_task, 1);
|
||||
|
||||
ESP_LOGI(TAG, "MIPI-CSI camera initialized (%dx%d, %d-lane, %d Mbps)", s_frame_hres, s_frame_vres,
|
||||
MIPI_CSI_DATA_LANES, MIPI_CSI_LANE_BITRATE_MBPS);
|
||||
return ESP_OK;
|
||||
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);
|
||||
}
|
||||
|
||||
esp_err_t CameraService::cameraStill(httpd_req_t *request) {
|
||||
if (!s_cam_initialized) {
|
||||
return WebServer::sendError(request, 503, "Camera not initialized");
|
||||
}
|
||||
|
||||
if (xSemaphoreTake(s_jpeg_ready, pdMS_TO_TICKS(3000)) != pdTRUE) {
|
||||
return WebServer::sendError(request, 500, "Camera capture timed out");
|
||||
}
|
||||
|
||||
xSemaphoreTake(s_jpeg_lock, portMAX_DELAY);
|
||||
size_t len = s_ready_jpeg_len;
|
||||
if (s_ready_idx >= 0 && len > 0) {
|
||||
memcpy(s_send_buf, s_jpeg_bufs[s_ready_idx], len);
|
||||
}
|
||||
xSemaphoreGive(s_jpeg_lock);
|
||||
|
||||
if (len == 0) {
|
||||
return WebServer::sendError(request, 500, "No frame available");
|
||||
}
|
||||
|
||||
httpd_resp_set_type(request, "image/jpeg");
|
||||
httpd_resp_set_hdr(request, "Content-Disposition", "inline; filename=capture.jpg");
|
||||
return httpd_resp_send(request, (const char *)s_send_buf, len);
|
||||
}
|
||||
|
||||
esp_err_t CameraService::cameraStream(httpd_req_t *request) {
|
||||
if (!s_cam_initialized) {
|
||||
return WebServer::sendError(request, 503, "Camera not initialized");
|
||||
}
|
||||
|
||||
httpd_resp_set_type(request, _STREAM_CONTENT_TYPE);
|
||||
|
||||
char part_buf[64];
|
||||
esp_err_t res = ESP_OK;
|
||||
|
||||
while (res == ESP_OK) {
|
||||
if (xSemaphoreTake(s_jpeg_ready, pdMS_TO_TICKS(3000)) != pdTRUE) {
|
||||
break;
|
||||
}
|
||||
|
||||
xSemaphoreTake(s_jpeg_lock, portMAX_DELAY);
|
||||
size_t jpeg_len = s_ready_jpeg_len;
|
||||
if (s_ready_idx >= 0 && jpeg_len > 0) {
|
||||
memcpy(s_send_buf, s_jpeg_bufs[s_ready_idx], jpeg_len);
|
||||
}
|
||||
xSemaphoreGive(s_jpeg_lock);
|
||||
|
||||
if (jpeg_len == 0) continue;
|
||||
|
||||
size_t hlen = snprintf(part_buf, 64, _STREAM_PART, (unsigned int)jpeg_len);
|
||||
res = httpd_resp_send_chunk(request, part_buf, hlen);
|
||||
if (res == ESP_OK) res = httpd_resp_send_chunk(request, (const char *)s_send_buf, jpeg_len);
|
||||
if (res == ESP_OK) res = httpd_resp_send_chunk(request, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY));
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Stream ended");
|
||||
httpd_resp_send_chunk(request, NULL, 0);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
CameraService::CameraService() {}
|
||||
esp_err_t CameraService::begin() { return ESP_ERR_NOT_SUPPORTED; }
|
||||
esp_err_t CameraService::cameraStill(httpd_req_t *request) {
|
||||
return WebServer::sendError(request, 501, "Camera not supported on this platform");
|
||||
}
|
||||
esp_err_t CameraService::cameraStream(httpd_req_t *request) {
|
||||
return WebServer::sendError(request, 501, "Camera not supported on this platform");
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace Camera
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -8,8 +8,7 @@
|
||||
#include <esp_sleep.h>
|
||||
#include <soc/soc.h>
|
||||
|
||||
#if CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || \
|
||||
CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32P4
|
||||
#if CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32C6
|
||||
#include <driver/temperature_sensor.h>
|
||||
|
||||
static float temperatureRead() {
|
||||
@@ -101,7 +100,7 @@ void sleep() {
|
||||
|
||||
uint64_t bitmask = (uint64_t)1 << (WAKEUP_PIN_NUMBER);
|
||||
|
||||
#if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32P4
|
||||
#ifdef CONFIG_IDF_TARGET_ESP32C3
|
||||
esp_deep_sleep_enable_gpio_wakeup(bitmask, (esp_deepsleep_gpio_wake_up_mode_t)WAKEUP_SIGNAL);
|
||||
#else
|
||||
esp_sleep_enable_ext1_wakeup(bitmask, (esp_sleep_ext1_wakeup_mode_t)WAKEUP_SIGNAL);
|
||||
@@ -125,7 +124,6 @@ static const char *getChipModel() {
|
||||
case CHIP_ESP32C2: return "ESP32-C2";
|
||||
case CHIP_ESP32C6: return "ESP32-C6";
|
||||
case CHIP_ESP32H2: return "ESP32-H2";
|
||||
case CHIP_ESP32P4: return "ESP32-P4";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
+53
-35
@@ -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
|
||||
|
||||
@@ -15,7 +15,6 @@ src_dir = esp32/src
|
||||
include_dir = esp32/include
|
||||
lib_dir = esp32/lib
|
||||
test_dir = esp32/test
|
||||
boards_dir = boards
|
||||
extra_configs =
|
||||
esp32/factory_settings.ini
|
||||
esp32/features.ini
|
||||
@@ -74,45 +73,6 @@ board_build.partitions = esp32/partition_table/min_spiffs.csv
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
|
||||
[env:esp32-p4]
|
||||
platform = https://github.com/pioarduino/platform-espressif32.git
|
||||
framework = espidf
|
||||
board = esp32p4_dev
|
||||
board_build.mcu = esp32p4
|
||||
board_build.f_cpu = 360000000L
|
||||
board_build.f_flash = 80000000L
|
||||
board_build.f_psram = 200000000L
|
||||
board_build.flash_mode = qio
|
||||
board_upload.flash_size = 32MB
|
||||
board_build.partitions = esp32/partition_table/default_32MB.csv
|
||||
board_build.filesystem = littlefs
|
||||
board_build.sdkconfig_defaults =
|
||||
esp32/sdkconfig.defaults
|
||||
esp32/sdkconfig.defaults.esp32p4
|
||||
upload_speed = 921600
|
||||
monitor_speed = 115200
|
||||
monitor_filters =
|
||||
direct
|
||||
esp32_exception_decoder
|
||||
extra_scripts =
|
||||
pre:esp32/scripts/pre_build.py
|
||||
pre:esp32/scripts/build_app.py
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D USE_CAMERA=1
|
||||
-D CAM_XCLK_PIN=-1
|
||||
-D CAM_RESET_PIN=-1
|
||||
-D CAM_PWDN_PIN=-1
|
||||
-D MIPI_CSI_HRES=800
|
||||
-D MIPI_CSI_VRES=640
|
||||
-D MIPI_CSI_LANE_BITRATE_MBPS=400
|
||||
-D MIPI_CSI_DATA_LANES=2
|
||||
-D CSI_JPEG_QUALITY=65
|
||||
-D BOARD_HAS_PSRAM
|
||||
-D SDA_PIN=7
|
||||
-D SCL_PIN=8
|
||||
-D WS2812_PIN=27
|
||||
|
||||
|
||||
; ================================================================
|
||||
; General environment section
|
||||
|
||||
@@ -143,8 +143,8 @@ A PyBullet-based physics simulation is available for algorithm development and r
|
||||
|
||||
```bash
|
||||
cd simulation
|
||||
uv sync
|
||||
uv run play.py
|
||||
pip install -r requirements.txt
|
||||
python play.py
|
||||
```
|
||||
|
||||
Features:
|
||||
@@ -245,7 +245,7 @@ Complete build instructions are available in the documentation:
|
||||
**Build and flash:**
|
||||
|
||||
```bash
|
||||
git clone --recurse-submodules https://github.com/runeharlyk/SpotMicroESP32-Leika
|
||||
git clone https://github.com/runeharlyk/SpotMicroESP32-Leika
|
||||
cd SpotMicroESP32-Leika
|
||||
|
||||
cd app
|
||||
@@ -264,8 +264,8 @@ To experiment with the simulation environments without hardware:
|
||||
|
||||
```bash
|
||||
cd simulation
|
||||
uv sync
|
||||
uv run play.py
|
||||
pip install -r requirements.txt
|
||||
python play.py
|
||||
```
|
||||
|
||||
For development workflows and contribution guidelines, see [docs/6_developing.md](docs/6_developing.md) and [docs/7_contributing.md](docs/7_contributing.md).
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
3.13
|
||||
@@ -1,21 +0,0 @@
|
||||
[project]
|
||||
name = "simulation-leika"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"gymnasium>=1.2.3",
|
||||
"matplotlib>=3.10.9",
|
||||
"msgpack>=1.1.2",
|
||||
"numpy>=2.4.4",
|
||||
"onnx>=1.21.0",
|
||||
"onnxruntime>=1.26.0",
|
||||
"pybullet>=3.2.7",
|
||||
"stable-baselines3[extra]>=2.0.0",
|
||||
"tensorboard>=2.20.0",
|
||||
"tensorflow>=2.21.0",
|
||||
"torch>=2.11.0",
|
||||
"tqdm>=4.67.3",
|
||||
"websockets>=16.0",
|
||||
]
|
||||
@@ -0,0 +1,14 @@
|
||||
torch
|
||||
onnx
|
||||
onnxruntime
|
||||
tensorflow
|
||||
numpy
|
||||
matplotlib
|
||||
pybullet
|
||||
websockets
|
||||
msgpack
|
||||
asyncio
|
||||
gymnasium
|
||||
stable-baselines3[extra]>=2.0.0
|
||||
tensorboard
|
||||
tqdm
|
||||
@@ -0,0 +1,24 @@
|
||||
from src.envs.quadruped_env import QuadrupedEnv
|
||||
from training.model import SimpleNN
|
||||
|
||||
import resources as resources
|
||||
|
||||
|
||||
def main():
|
||||
env = QuadrupedEnv(resources.getDataPath() + "/spot.urdf")
|
||||
env.reset()
|
||||
|
||||
input_size = env.robot.get_observation().shape[0]
|
||||
output_size = env.robot.get_observation().shape[0]
|
||||
agent = SimpleNN(input_size, output_size)
|
||||
|
||||
done = False
|
||||
observation = []
|
||||
|
||||
while not done:
|
||||
action = agent.select_action(observation)
|
||||
observation, reward, done = env.step(action)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Generated
-1724
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user