From 9be51af03485b03c01dfef80914932e7e6be0da9 Mon Sep 17 00:00:00 2001 From: superkor Date: Sun, 11 Jan 2026 20:19:00 -0500 Subject: [PATCH 01/11] init movement files --- components/movements/README.md | 3 +++ components/movements/include/movements.h | 4 ++++ components/movements/movements.cpp | 3 +++ 3 files changed, 10 insertions(+) create mode 100644 components/movements/README.md create mode 100644 components/movements/include/movements.h create mode 100644 components/movements/movements.cpp diff --git a/components/movements/README.md b/components/movements/README.md new file mode 100644 index 0000000..fbe5439 --- /dev/null +++ b/components/movements/README.md @@ -0,0 +1,3 @@ +# Preprogrammed Movement + +This component will handle the preprogrammed movements from the UI and then execute them (with or without a) connection to the PC/UI. \ No newline at end of file diff --git a/components/movements/include/movements.h b/components/movements/include/movements.h new file mode 100644 index 0000000..1630f8b --- /dev/null +++ b/components/movements/include/movements.h @@ -0,0 +1,4 @@ +#ifndef MOVEMENTS +#define MOVEMENTS + +#endif //MOVEMENTS \ No newline at end of file diff --git a/components/movements/movements.cpp b/components/movements/movements.cpp new file mode 100644 index 0000000..21e7a7f --- /dev/null +++ b/components/movements/movements.cpp @@ -0,0 +1,3 @@ +#ifdef MOVEMENTS + +#endif //MOVEMENTS \ No newline at end of file From b37e09e129a78d34aa1772e86981e5493f042b5e Mon Sep 17 00:00:00 2001 From: superkor Date: Tue, 13 Jan 2026 03:55:49 -0500 Subject: [PATCH 02/11] add some classes for movement set handling --- components/movements/CMakeLists.txt | 4 + components/movements/MovementManager.cpp | 25 ++++++ components/movements/MovementSetManager.cpp | 18 ++++ components/movements/README.md | 83 ++++++++++++++++++- components/movements/TopologyManager.cpp | 18 ++++ components/movements/include/MovementBlobs.h | 50 +++++++++++ .../movements/include/MovementManager.h | 23 +++++ components/movements/include/TopologyBlobs.h | 42 ++++++++++ components/movements/include/movements.h | 4 - components/movements/movements.cpp | 3 - main/CMakeLists.txt | 2 +- 11 files changed, 263 insertions(+), 9 deletions(-) create mode 100644 components/movements/CMakeLists.txt create mode 100644 components/movements/MovementManager.cpp create mode 100644 components/movements/MovementSetManager.cpp create mode 100644 components/movements/TopologyManager.cpp create mode 100644 components/movements/include/MovementBlobs.h create mode 100644 components/movements/include/MovementManager.h create mode 100644 components/movements/include/TopologyBlobs.h delete mode 100644 components/movements/include/movements.h delete mode 100644 components/movements/movements.cpp diff --git a/components/movements/CMakeLists.txt b/components/movements/CMakeLists.txt new file mode 100644 index 0000000..d922653 --- /dev/null +++ b/components/movements/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register(SRCS "MovementManager.cpp" "MovementManager.cpp" "TopologyManager.cpp" + PRIV_REQUIRES esp_event nvs_flash + # REQUIRES esp_timer + INCLUDE_DIRS "include") diff --git a/components/movements/MovementManager.cpp b/components/movements/MovementManager.cpp new file mode 100644 index 0000000..58349c4 --- /dev/null +++ b/components/movements/MovementManager.cpp @@ -0,0 +1,25 @@ +#include "MovementManager.h" +#include "nvs_flash.h" +#include "esp_log.h" +#ifdef MOVEMENTS + +MovementManager::MovementManager(){ + esp_err_t res = nvs_open(MOVEMENTS_NVS_NAMESPACE, NVS_READWRITE, &handle); + + if (res != ESP_OK){ + ESP_LOGE(MOVEMENTS_DEBUG_TAG, "Failed to open nvs for %s", MOVEMENTS_NVS_NAMESPACE); + return; + } + + status = ESP_OK; +}; + +esp_err_t MovementManager::get_movement_status(){ + return status; +} + +MovementManager::~MovementManager(){ + nvs_close(handle); +} + +#endif //MOVEMENTS \ No newline at end of file diff --git a/components/movements/MovementSetManager.cpp b/components/movements/MovementSetManager.cpp new file mode 100644 index 0000000..88138c7 --- /dev/null +++ b/components/movements/MovementSetManager.cpp @@ -0,0 +1,18 @@ +#include "MovementBlobs.h" +#include "esp_log.h" +#ifdef MOVEMENTS + +MovementSetManager::MovementSetManager(){ + esp_err_t res = nvs_open(MOVEMENTS_NVS_NAMESPACE, NVS_READWRITE, &handle); + + if (res != ESP_OK){ + ESP_LOGE(MOVEMENTS_DEBUG_TAG, "Failed to open nvs for %s", MOVEMENTS_NVS_NAMESPACE); + return; + } +}; + +MovementSetManager::~MovementSetManager(){ + nvs_close(handle); +} + +#endif //MOVEMENTS \ No newline at end of file diff --git a/components/movements/README.md b/components/movements/README.md index fbe5439..9f22b1a 100644 --- a/components/movements/README.md +++ b/components/movements/README.md @@ -1,3 +1,84 @@ +WIP + # Preprogrammed Movement -This component will handle the preprogrammed movements from the UI and then execute them (with or without a) connection to the PC/UI. \ No newline at end of file +This component will handle the preprogrammed movements from the UI and then execute them (with or without a) connection to the PC/UI. + +See [ESP32 NVS](https://docs.espressif.com/projects/esp-idf/en/stable/esp32s3/api-reference/storage/nvs_flash.html) for more information about the NVS. + +## Movement Entries Stored on NVS + +The movements will be stored on the NVS of the leader board on the network, under the namespace `MOVEMENTS_NVS_NAMESPACE`. + +The movements themselves will be a table under the key `MOVEMENTS_NVS_KEY` as a blob: + +``` +{ + num_movements: uint8_t, + movements: MovementEntry[] +} +``` + +where `MovementEntry` is: + +``` +{ + board_id: uint16_t, + moduleType: uint8_t, + value_action: uint16_t, + condition: ConditionBlob, + ack: uint8_t, + ack_ttl_ms: uint16_t, + post_delay_ms: uint16_t +} +``` + +and `ConditionBlob` is: + +``` +{ + value: uint16_t, + cond: uint8_t (0: <, 1: <=, 2: >, 3: >=, 4: ==, 5: !=), + moduleType: uint8_t (check value/sensor data if possible or set to UNUSED if not in use) +} +``` + +Each entry will have a size of 14B. With a max number of 255 movements stored, that will represent 3570 B that will be stored on the NVS. + +There will only be one set of movements stored on the board (meaning a new movement set would have to overwrite the existing movement set). + +Blobs are written in the NVS with the entry metadata written first, then the actual data written in subsequent entries (32B per entry). + +## Storing the Toplogy for the Movement Set + +To run the movement set stored on a particular board, the board itself has to be the leader on the network (to resolve conflicts with multiple movement sets stored on different boards). + +Since the boards on the network will most likely have different ids assigned, the leader will have manually assign ids based on the expected toplogy that will correspond to the saved movement set. This is done to ensure the correct board is being properly referenced in the movement set. However, the stored board id on the NVS will not be overwritten with the board id assigned by the movement set but rather a simple 1:1 translation map will be used (making the movement-assigned board id functionally in use but the underlying board id will be used purely for wired communications). + +In order to do this, the toplogy will need to be saved onto the NVS (along with the movement entries). This will be stored in the same namespace but under a different key, `MOVEMENTS_NVS_TOPOLOGY_KEY`. + +The structure of the topology data stored on the NVS will look like: + +``` +{ + boards: NeighbourBlob[] (index 0 is leader) +} +``` + +where `NeighbourBlob` is: + +``` +{ + curr_board_id: uint16_t, + neighbour_connections: ChannelBoardConn[] (max 4, min 1) +} +``` + +where `ChannelBoardConn` is: +``` +{ + channel: uint8_t, + board_id: uint16_t +} +``` + diff --git a/components/movements/TopologyManager.cpp b/components/movements/TopologyManager.cpp new file mode 100644 index 0000000..0e0c052 --- /dev/null +++ b/components/movements/TopologyManager.cpp @@ -0,0 +1,18 @@ +#include "TopologyBlobs.h" +#include "esp_log.h" +#ifdef MOVEMENTS + +TopologyManager::TopologyManager(){ + esp_err_t res = nvs_open(MOVEMENTS_NVS_NAMESPACE, NVS_READWRITE, &handle); + + if (res != ESP_OK){ + ESP_LOGE(MOVEMENTS_DEBUG_TAG, "Failed to open nvs for %s", MOVEMENTS_NVS_NAMESPACE); + return; + } +}; + +TopologyManager::~TopologyManager(){ + nvs_close(handle); +} + +#endif //MOVEMENTS \ No newline at end of file diff --git a/components/movements/include/MovementBlobs.h b/components/movements/include/MovementBlobs.h new file mode 100644 index 0000000..435bfca --- /dev/null +++ b/components/movements/include/MovementBlobs.h @@ -0,0 +1,50 @@ +#include "MovementManager.h" +#include "nvs_flash.h" +#include "TopologyBlobs.h" +#include +#include + +#ifdef MOVEMENTS +#pragma once + +typedef struct _condition_blob { + uint16_t value; + uint8_t cond; + uint8_t moduleType; + uint8_t in_use; +} ConditionBlob; + +typedef struct _movement_entry { + uint16_t board_id; + uint8_t moduleType; + uint16_t value_action; + ConditionBlob condition; + uint8_t ack; + uint16_t ack_ttl_ms; + uint16_t post_delay_ms; +} MovementEntry; + +typedef struct _movement { + uint8_t num_movements; + MovementEntry movements[]; +} MovementSet; + +class MovementSetManager { + public: + MovementSetManager(); + ~MovementSetManager(); + esp_err_t add_movement(MovementEntry& entry, uint8_t& index); + esp_err_t remove_movement(uint8_t index); + esp_err_t update_movement(MovementEntry& entry, uint8_t index); + esp_err_t get_curr_movement_set(MovementSet& set); + esp_err_t verify_movement_set(); + esp_err_t get_nvs_movement_set(MovementSet& set); + esp_err_t write_nvs_movement_set(); + + private: + std::unique_ptr topology_manager; + std::vector movements; + nvs_handle_t handle; +}; + +#endif //MOVEMENTS \ No newline at end of file diff --git a/components/movements/include/MovementManager.h b/components/movements/include/MovementManager.h new file mode 100644 index 0000000..f7ed809 --- /dev/null +++ b/components/movements/include/MovementManager.h @@ -0,0 +1,23 @@ +#ifndef MOVEMENTS +#define MOVEMENTS + +#include "nvs_flash.h" + +#define MOVEMENTS_NVS_NAMESPACE "movements" +#define MOVEMENTS_NVS_KEY "key" +#define MOVEMENTS_NVS_TOPOLOGY_KEY "topology" +#define MOVEMENTS_DEBUG_TAG "movements" + +class MovementManager { + public: + MovementManager(); + ~MovementManager(); + esp_err_t get_movement_status(); + + private: + nvs_handle_t handle = 0; + esp_err_t status = ESP_ERR_INVALID_STATE; + +}; + +#endif //MOVEMENTS \ No newline at end of file diff --git a/components/movements/include/TopologyBlobs.h b/components/movements/include/TopologyBlobs.h new file mode 100644 index 0000000..6f15e93 --- /dev/null +++ b/components/movements/include/TopologyBlobs.h @@ -0,0 +1,42 @@ +#include "MovementManager.h" +#include "nvs_flash.h" +#include + +#ifdef MOVEMENTS +#pragma once + +typedef struct _channel_board_conn { + uint8_t channel; + uint16_t board_id; +} ChannelBoardConn; + +typedef struct _neighbour_blob { + uint16_t curr_board_id; + uint8_t num_connections; + ChannelBoardConn neighbour_connections[]; +} NeighbourBlob; + +typedef struct _topology { + uint16_t num_boards; + NeighbourBlob boards[]; +} Topology; + +class TopologyManager { + public: + TopologyManager(); + ~TopologyManager(); + esp_err_t add_board_to_topology(NeighbourBlob blob); + esp_err_t remove_board_from_topology(uint16_t board_id); + esp_err_t update_board_in_topology(NeighbourBlob blob); + esp_err_t get_board_in_topology(NeighbourBlob& blob); + esp_err_t verify_topology(); + esp_err_t get_curr_topology(Topology& topology); + esp_err_t write_nvs_topology(); + esp_err_t get_nvs_topology(Topology& topology); + + private: + std::unordered_map topology; + nvs_handle_t handle; +}; + +#endif //MOVEMENTS \ No newline at end of file diff --git a/components/movements/include/movements.h b/components/movements/include/movements.h deleted file mode 100644 index 1630f8b..0000000 --- a/components/movements/include/movements.h +++ /dev/null @@ -1,4 +0,0 @@ -#ifndef MOVEMENTS -#define MOVEMENTS - -#endif //MOVEMENTS \ No newline at end of file diff --git a/components/movements/movements.cpp b/components/movements/movements.cpp deleted file mode 100644 index 21e7a7f..0000000 --- a/components/movements/movements.cpp +++ /dev/null @@ -1,3 +0,0 @@ -#ifdef MOVEMENTS - -#endif //MOVEMENTS \ No newline at end of file diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 260c604..a50cdc1 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -17,7 +17,7 @@ else() endif() idf_component_register(SRCS ${ALL_SRCS} - PRIV_REQUIRES esp_psram spi_flash nvs_flash esp_event rpc constants config rmt esp_driver_gptimer dataLink flatbuffers esp_driver_ledc + PRIV_REQUIRES esp_psram spi_flash nvs_flash esp_event rpc constants config rmt esp_driver_gptimer dataLink flatbuffers esp_driver_ledc movements INCLUDE_DIRS "include") if(DEFINED SRC_BOARD AND SRC_BOARD) From 66b0153d6192efd8aa333fdbe0b9376c1b6c0400 Mon Sep 17 00:00:00 2001 From: superkor Date: Wed, 14 Jan 2026 23:43:10 -0500 Subject: [PATCH 03/11] add flatbuffer definitions --- .../Movement_generated.h | 267 ++++++++++++++++++ .../Topology_generated.h | 229 +++++++++++++++ components/movements/CMakeLists.txt | 2 +- components/movements/MovementSetManager.cpp | 30 +- components/movements/TopologyManager.cpp | 83 +++++- components/movements/include/MovementBlobs.h | 50 ---- .../movements/include/MovementSetManager.h | 28 ++ components/movements/include/TopologyBlobs.h | 42 --- .../movements/include/TopologyManager.h | 31 ++ components/rmt/include/RMTManager.h | 1 - 10 files changed, 667 insertions(+), 96 deletions(-) create mode 100644 components/flatbuffers/include/flatbuffers_generated/Movement_generated.h create mode 100644 components/flatbuffers/include/flatbuffers_generated/Topology_generated.h delete mode 100644 components/movements/include/MovementBlobs.h create mode 100644 components/movements/include/MovementSetManager.h delete mode 100644 components/movements/include/TopologyBlobs.h create mode 100644 components/movements/include/TopologyManager.h diff --git a/components/flatbuffers/include/flatbuffers_generated/Movement_generated.h b/components/flatbuffers/include/flatbuffers_generated/Movement_generated.h new file mode 100644 index 0000000..783b6ea --- /dev/null +++ b/components/flatbuffers/include/flatbuffers_generated/Movement_generated.h @@ -0,0 +1,267 @@ +// automatically generated by the FlatBuffers compiler, do not modify + + +#ifndef FLATBUFFERS_GENERATED_MOVEMENT_MOVEMENT_H_ +#define FLATBUFFERS_GENERATED_MOVEMENT_MOVEMENT_H_ + +#include "flatbuffers/flatbuffers.h" + +// Ensure the included flatbuffers.h is the same version as when this file was +// generated, otherwise it may not be compatible. +static_assert(FLATBUFFERS_VERSION_MAJOR == 25 && + FLATBUFFERS_VERSION_MINOR == 12 && + FLATBUFFERS_VERSION_REVISION == 19, + "Non-compatible flatbuffers version included"); + +namespace movement { + +struct ConditionBlob; + +struct MovementEntry; +struct MovementEntryBuilder; + +struct MovementSet; +struct MovementSetBuilder; + +FLATBUFFERS_MANUALLY_ALIGNED_STRUCT(2) ConditionBlob FLATBUFFERS_FINAL_CLASS { + private: + uint16_t value_; + uint8_t cond_; + uint8_t module_type_; + uint8_t in_use_; + int8_t padding0__; + + public: + ConditionBlob() + : value_(0), + cond_(0), + module_type_(0), + in_use_(0), + padding0__(0) { + (void)padding0__; + } + ConditionBlob(uint16_t _value, uint8_t _cond, uint8_t _module_type, uint8_t _in_use) + : value_(::flatbuffers::EndianScalar(_value)), + cond_(::flatbuffers::EndianScalar(_cond)), + module_type_(::flatbuffers::EndianScalar(_module_type)), + in_use_(::flatbuffers::EndianScalar(_in_use)), + padding0__(0) { + (void)padding0__; + } + uint16_t value() const { + return ::flatbuffers::EndianScalar(value_); + } + uint8_t cond() const { + return ::flatbuffers::EndianScalar(cond_); + } + uint8_t module_type() const { + return ::flatbuffers::EndianScalar(module_type_); + } + uint8_t in_use() const { + return ::flatbuffers::EndianScalar(in_use_); + } +}; +FLATBUFFERS_STRUCT_END(ConditionBlob, 6); + +struct MovementEntry FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef MovementEntryBuilder Builder; + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_BOARD_ID = 4, + VT_MODULE_TYPE = 6, + VT_VALUE_ACTION = 8, + VT_CONDITION = 10, + VT_ACK = 12, + VT_ACK_TTL_MS = 14, + VT_POST_DELAY_MS = 16 + }; + uint16_t board_id() const { + return GetField(VT_BOARD_ID, 0); + } + uint8_t module_type() const { + return GetField(VT_MODULE_TYPE, 0); + } + uint16_t value_action() const { + return GetField(VT_VALUE_ACTION, 0); + } + const movement::ConditionBlob *condition() const { + return GetStruct(VT_CONDITION); + } + uint8_t ack() const { + return GetField(VT_ACK, 0); + } + uint16_t ack_ttl_ms() const { + return GetField(VT_ACK_TTL_MS, 0); + } + uint16_t post_delay_ms() const { + return GetField(VT_POST_DELAY_MS, 0); + } + template + bool Verify(::flatbuffers::VerifierTemplate &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_BOARD_ID, 2) && + VerifyField(verifier, VT_MODULE_TYPE, 1) && + VerifyField(verifier, VT_VALUE_ACTION, 2) && + VerifyField(verifier, VT_CONDITION, 2) && + VerifyField(verifier, VT_ACK, 1) && + VerifyField(verifier, VT_ACK_TTL_MS, 2) && + VerifyField(verifier, VT_POST_DELAY_MS, 2) && + verifier.EndTable(); + } +}; + +struct MovementEntryBuilder { + typedef MovementEntry Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_board_id(uint16_t board_id) { + fbb_.AddElement(MovementEntry::VT_BOARD_ID, board_id, 0); + } + void add_module_type(uint8_t module_type) { + fbb_.AddElement(MovementEntry::VT_MODULE_TYPE, module_type, 0); + } + void add_value_action(uint16_t value_action) { + fbb_.AddElement(MovementEntry::VT_VALUE_ACTION, value_action, 0); + } + void add_condition(const movement::ConditionBlob *condition) { + fbb_.AddStruct(MovementEntry::VT_CONDITION, condition); + } + void add_ack(uint8_t ack) { + fbb_.AddElement(MovementEntry::VT_ACK, ack, 0); + } + void add_ack_ttl_ms(uint16_t ack_ttl_ms) { + fbb_.AddElement(MovementEntry::VT_ACK_TTL_MS, ack_ttl_ms, 0); + } + void add_post_delay_ms(uint16_t post_delay_ms) { + fbb_.AddElement(MovementEntry::VT_POST_DELAY_MS, post_delay_ms, 0); + } + explicit MovementEntryBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateMovementEntry( + ::flatbuffers::FlatBufferBuilder &_fbb, + uint16_t board_id = 0, + uint8_t module_type = 0, + uint16_t value_action = 0, + const movement::ConditionBlob *condition = nullptr, + uint8_t ack = 0, + uint16_t ack_ttl_ms = 0, + uint16_t post_delay_ms = 0) { + MovementEntryBuilder builder_(_fbb); + builder_.add_condition(condition); + builder_.add_post_delay_ms(post_delay_ms); + builder_.add_ack_ttl_ms(ack_ttl_ms); + builder_.add_value_action(value_action); + builder_.add_board_id(board_id); + builder_.add_ack(ack); + builder_.add_module_type(module_type); + return builder_.Finish(); +} + +struct MovementSet FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef MovementSetBuilder Builder; + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_NUM_MOVEMENTS = 4, + VT_MOVEMENTS = 6 + }; + uint8_t num_movements() const { + return GetField(VT_NUM_MOVEMENTS, 0); + } + const ::flatbuffers::Vector<::flatbuffers::Offset> *movements() const { + return GetPointer> *>(VT_MOVEMENTS); + } + template + bool Verify(::flatbuffers::VerifierTemplate &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_NUM_MOVEMENTS, 1) && + VerifyOffset(verifier, VT_MOVEMENTS) && + verifier.VerifyVector(movements()) && + verifier.VerifyVectorOfTables(movements()) && + verifier.EndTable(); + } +}; + +struct MovementSetBuilder { + typedef MovementSet Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_num_movements(uint8_t num_movements) { + fbb_.AddElement(MovementSet::VT_NUM_MOVEMENTS, num_movements, 0); + } + void add_movements(::flatbuffers::Offset<::flatbuffers::Vector<::flatbuffers::Offset>> movements) { + fbb_.AddOffset(MovementSet::VT_MOVEMENTS, movements); + } + explicit MovementSetBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateMovementSet( + ::flatbuffers::FlatBufferBuilder &_fbb, + uint8_t num_movements = 0, + ::flatbuffers::Offset<::flatbuffers::Vector<::flatbuffers::Offset>> movements = 0) { + MovementSetBuilder builder_(_fbb); + builder_.add_movements(movements); + builder_.add_num_movements(num_movements); + return builder_.Finish(); +} + +inline ::flatbuffers::Offset CreateMovementSetDirect( + ::flatbuffers::FlatBufferBuilder &_fbb, + uint8_t num_movements = 0, + const std::vector<::flatbuffers::Offset> *movements = nullptr) { + auto movements__ = movements ? _fbb.CreateVector<::flatbuffers::Offset>(*movements) : 0; + return movement::CreateMovementSet( + _fbb, + num_movements, + movements__); +} + +inline const movement::MovementSet *GetMovementSet(const void *buf) { + return ::flatbuffers::GetRoot(buf); +} + +inline const movement::MovementSet *GetSizePrefixedMovementSet(const void *buf) { + return ::flatbuffers::GetSizePrefixedRoot(buf); +} + +template +inline bool VerifyMovementSetBuffer( + ::flatbuffers::VerifierTemplate &verifier) { + return verifier.template VerifyBuffer(nullptr); +} + +template +inline bool VerifySizePrefixedMovementSetBuffer( + ::flatbuffers::VerifierTemplate &verifier) { + return verifier.template VerifySizePrefixedBuffer(nullptr); +} + +inline void FinishMovementSetBuffer( + ::flatbuffers::FlatBufferBuilder &fbb, + ::flatbuffers::Offset root) { + fbb.Finish(root); +} + +inline void FinishSizePrefixedMovementSetBuffer( + ::flatbuffers::FlatBufferBuilder &fbb, + ::flatbuffers::Offset root) { + fbb.FinishSizePrefixed(root); +} + +} // namespace movement + +#endif // FLATBUFFERS_GENERATED_MOVEMENT_MOVEMENT_H_ diff --git a/components/flatbuffers/include/flatbuffers_generated/Topology_generated.h b/components/flatbuffers/include/flatbuffers_generated/Topology_generated.h new file mode 100644 index 0000000..3bb0d37 --- /dev/null +++ b/components/flatbuffers/include/flatbuffers_generated/Topology_generated.h @@ -0,0 +1,229 @@ +// automatically generated by the FlatBuffers compiler, do not modify + + +#ifndef FLATBUFFERS_GENERATED_TOPOLOGY_TOPOLOGY_H_ +#define FLATBUFFERS_GENERATED_TOPOLOGY_TOPOLOGY_H_ + +#include "flatbuffers/flatbuffers.h" + +// Ensure the included flatbuffers.h is the same version as when this file was +// generated, otherwise it may not be compatible. +static_assert(FLATBUFFERS_VERSION_MAJOR == 25 && + FLATBUFFERS_VERSION_MINOR == 12 && + FLATBUFFERS_VERSION_REVISION == 19, + "Non-compatible flatbuffers version included"); + +namespace topology { + +struct ChannelBoardConn; + +struct NeighbourBlob; +struct NeighbourBlobBuilder; + +struct Topology; +struct TopologyBuilder; + +FLATBUFFERS_MANUALLY_ALIGNED_STRUCT(2) ChannelBoardConn FLATBUFFERS_FINAL_CLASS { + private: + uint8_t channel_; + int8_t padding0__; + uint16_t board_id_; + + public: + ChannelBoardConn() + : channel_(0), + padding0__(0), + board_id_(0) { + (void)padding0__; + } + ChannelBoardConn(uint8_t _channel, uint16_t _board_id) + : channel_(::flatbuffers::EndianScalar(_channel)), + padding0__(0), + board_id_(::flatbuffers::EndianScalar(_board_id)) { + (void)padding0__; + } + uint8_t channel() const { + return ::flatbuffers::EndianScalar(channel_); + } + uint16_t board_id() const { + return ::flatbuffers::EndianScalar(board_id_); + } +}; +FLATBUFFERS_STRUCT_END(ChannelBoardConn, 4); + +struct NeighbourBlob FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef NeighbourBlobBuilder Builder; + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_CURR_BOARD_ID = 4, + VT_NUM_CONNECTIONS = 6, + VT_NEIGHBOUR_CONNECTIONS = 8 + }; + uint16_t curr_board_id() const { + return GetField(VT_CURR_BOARD_ID, 0); + } + uint8_t num_connections() const { + return GetField(VT_NUM_CONNECTIONS, 0); + } + const ::flatbuffers::Vector *neighbour_connections() const { + return GetPointer *>(VT_NEIGHBOUR_CONNECTIONS); + } + template + bool Verify(::flatbuffers::VerifierTemplate &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_CURR_BOARD_ID, 2) && + VerifyField(verifier, VT_NUM_CONNECTIONS, 1) && + VerifyOffset(verifier, VT_NEIGHBOUR_CONNECTIONS) && + verifier.VerifyVector(neighbour_connections()) && + verifier.EndTable(); + } +}; + +struct NeighbourBlobBuilder { + typedef NeighbourBlob Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_curr_board_id(uint16_t curr_board_id) { + fbb_.AddElement(NeighbourBlob::VT_CURR_BOARD_ID, curr_board_id, 0); + } + void add_num_connections(uint8_t num_connections) { + fbb_.AddElement(NeighbourBlob::VT_NUM_CONNECTIONS, num_connections, 0); + } + void add_neighbour_connections(::flatbuffers::Offset<::flatbuffers::Vector> neighbour_connections) { + fbb_.AddOffset(NeighbourBlob::VT_NEIGHBOUR_CONNECTIONS, neighbour_connections); + } + explicit NeighbourBlobBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateNeighbourBlob( + ::flatbuffers::FlatBufferBuilder &_fbb, + uint16_t curr_board_id = 0, + uint8_t num_connections = 0, + ::flatbuffers::Offset<::flatbuffers::Vector> neighbour_connections = 0) { + NeighbourBlobBuilder builder_(_fbb); + builder_.add_neighbour_connections(neighbour_connections); + builder_.add_curr_board_id(curr_board_id); + builder_.add_num_connections(num_connections); + return builder_.Finish(); +} + +inline ::flatbuffers::Offset CreateNeighbourBlobDirect( + ::flatbuffers::FlatBufferBuilder &_fbb, + uint16_t curr_board_id = 0, + uint8_t num_connections = 0, + const std::vector *neighbour_connections = nullptr) { + auto neighbour_connections__ = neighbour_connections ? _fbb.CreateVectorOfStructs(*neighbour_connections) : 0; + return topology::CreateNeighbourBlob( + _fbb, + curr_board_id, + num_connections, + neighbour_connections__); +} + +struct Topology FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef TopologyBuilder Builder; + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_NUM_BOARDS = 4, + VT_BOARDS = 6 + }; + uint16_t num_boards() const { + return GetField(VT_NUM_BOARDS, 0); + } + const ::flatbuffers::Vector<::flatbuffers::Offset> *boards() const { + return GetPointer> *>(VT_BOARDS); + } + template + bool Verify(::flatbuffers::VerifierTemplate &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_NUM_BOARDS, 2) && + VerifyOffset(verifier, VT_BOARDS) && + verifier.VerifyVector(boards()) && + verifier.VerifyVectorOfTables(boards()) && + verifier.EndTable(); + } +}; + +struct TopologyBuilder { + typedef Topology Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_num_boards(uint16_t num_boards) { + fbb_.AddElement(Topology::VT_NUM_BOARDS, num_boards, 0); + } + void add_boards(::flatbuffers::Offset<::flatbuffers::Vector<::flatbuffers::Offset>> boards) { + fbb_.AddOffset(Topology::VT_BOARDS, boards); + } + explicit TopologyBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateTopology( + ::flatbuffers::FlatBufferBuilder &_fbb, + uint16_t num_boards = 0, + ::flatbuffers::Offset<::flatbuffers::Vector<::flatbuffers::Offset>> boards = 0) { + TopologyBuilder builder_(_fbb); + builder_.add_boards(boards); + builder_.add_num_boards(num_boards); + return builder_.Finish(); +} + +inline ::flatbuffers::Offset CreateTopologyDirect( + ::flatbuffers::FlatBufferBuilder &_fbb, + uint16_t num_boards = 0, + const std::vector<::flatbuffers::Offset> *boards = nullptr) { + auto boards__ = boards ? _fbb.CreateVector<::flatbuffers::Offset>(*boards) : 0; + return topology::CreateTopology( + _fbb, + num_boards, + boards__); +} + +inline const topology::Topology *GetTopology(const void *buf) { + return ::flatbuffers::GetRoot(buf); +} + +inline const topology::Topology *GetSizePrefixedTopology(const void *buf) { + return ::flatbuffers::GetSizePrefixedRoot(buf); +} + +template +inline bool VerifyTopologyBuffer( + ::flatbuffers::VerifierTemplate &verifier) { + return verifier.template VerifyBuffer(nullptr); +} + +template +inline bool VerifySizePrefixedTopologyBuffer( + ::flatbuffers::VerifierTemplate &verifier) { + return verifier.template VerifySizePrefixedBuffer(nullptr); +} + +inline void FinishTopologyBuffer( + ::flatbuffers::FlatBufferBuilder &fbb, + ::flatbuffers::Offset root) { + fbb.Finish(root); +} + +inline void FinishSizePrefixedTopologyBuffer( + ::flatbuffers::FlatBufferBuilder &fbb, + ::flatbuffers::Offset root) { + fbb.FinishSizePrefixed(root); +} + +} // namespace topology + +#endif // FLATBUFFERS_GENERATED_TOPOLOGY_TOPOLOGY_H_ diff --git a/components/movements/CMakeLists.txt b/components/movements/CMakeLists.txt index d922653..75904a6 100644 --- a/components/movements/CMakeLists.txt +++ b/components/movements/CMakeLists.txt @@ -1,4 +1,4 @@ idf_component_register(SRCS "MovementManager.cpp" "MovementManager.cpp" "TopologyManager.cpp" - PRIV_REQUIRES esp_event nvs_flash + PRIV_REQUIRES esp_event nvs_flash rmt dataLink flatbuffers # REQUIRES esp_timer INCLUDE_DIRS "include") diff --git a/components/movements/MovementSetManager.cpp b/components/movements/MovementSetManager.cpp index 88138c7..53d288e 100644 --- a/components/movements/MovementSetManager.cpp +++ b/components/movements/MovementSetManager.cpp @@ -1,4 +1,4 @@ -#include "MovementBlobs.h" +#include "MovementSetManager.h" #include "esp_log.h" #ifdef MOVEMENTS @@ -15,4 +15,32 @@ MovementSetManager::~MovementSetManager(){ nvs_close(handle); } +// esp_err_t MovementSetManager::add_movement(struct MovementEntry& entry, uint8_t& index){ +// return ESP_OK; +// } + +// esp_err_t MovementSetManager::remove_movement(uint8_t index){ +// return ESP_OK; +// } + +// esp_err_t MovementSetManager::update_movement(struct MovementEntry& entry, uint8_t index){ +// return ESP_OK; +// } + +// esp_err_t MovementSetManager::get_curr_movement_set(struct MovementSet& set){ +// return ESP_OK; +// } + +// esp_err_t MovementSetManager::verify_movement_set(){ +// return ESP_OK; +// } + +// esp_err_t MovementSetManager::get_nvs_movement_set(struct MovementSet& set){ +// return ESP_OK; +// } + +// esp_err_t MovementSetManager::write_nvs_movement_set(){ +// return ESP_OK; +// } + #endif //MOVEMENTS \ No newline at end of file diff --git a/components/movements/TopologyManager.cpp b/components/movements/TopologyManager.cpp index 0e0c052..edce60a 100644 --- a/components/movements/TopologyManager.cpp +++ b/components/movements/TopologyManager.cpp @@ -1,4 +1,4 @@ -#include "TopologyBlobs.h" +#include "TopologyManager.h" #include "esp_log.h" #ifdef MOVEMENTS @@ -9,10 +9,91 @@ TopologyManager::TopologyManager(){ ESP_LOGE(MOVEMENTS_DEBUG_TAG, "Failed to open nvs for %s", MOVEMENTS_NVS_NAMESPACE); return; } + + ready = true; }; TopologyManager::~TopologyManager(){ nvs_close(handle); } +// esp_err_t TopologyManager::add_board_to_topology(struct NeighbourBlob& blob){ +// if (!ready){ +// return ESP_ERR_INVALID_STATE; +// } + +// if (blob.num_connections > MAX_CHANNELS || blob.curr_board_id == PC_ADDR || blob.curr_board_id == BROADCAST_ADDR){ +// return ESP_ERR_INVALID_ARG; +// } + +// for (uint8_t i = 0; i < blob.num_connections; i++){ +// ChannelBoardConn connection = blob.neighbour_connections[i]; +// if (connection.channel > MAX_CHANNELS || connection.board_id == PC_ADDR +// || connection == BROADCAST_ADDR || connection.board_id == blob.curr_board_id){ +// return ESP_ERR_INVALID_ARG; +// } +// } + +// topology[blob.curr_board_id] = blob; + +// return ESP_OK; +// } + +// esp_err_t TopologyManager::remove_board_from_topology(uint16_t board_id){ +// if (!ready){ +// return ESP_ERR_INVALID_STATE; +// } + +// if (topology.find(board_id) == topology.end()){ +// return ESP_ERR_NOT_FOUND; +// } + +// topology.erase(board_id); + +// return ESP_OK; +// } + +// esp_err_t TopologyManager::get_board_in_topology(struct NeighbourBlob& blob){ +// if (!ready){ +// return ESP_ERR_INVALID_STATE; +// } + +// if (topology.find(board_id) == topology.end()){ +// return ESP_ERR_NOT_FOUND; +// } + +// blob = topology.find(board_id); + +// return ESP_OK; +// } + +// esp_err_t TopologyManager::verify_topology(){ +// if (!ready){ +// return ESP_ERR_INVALID_STATE; +// } +// return ESP_OK; +// } + +// esp_err_t TopologyManager::get_curr_topology(struct Topology& topology){ +// if (!ready){ +// return ESP_ERR_INVALID_STATE; +// } + +// return ESP_OK; +// } + +// esp_err_t TopologyManager::write_nvs_topology(){ +// if (!ready){ +// return ESP_ERR_INVALID_STATE; +// } +// return ESP_OK; +// } + +// esp_err_t TopologyManager::get_nvs_topology(struct Topology& topology){ +// if (!ready){ +// return ESP_ERR_INVALID_STATE; +// } +// return ESP_OK; +// } + #endif //MOVEMENTS \ No newline at end of file diff --git a/components/movements/include/MovementBlobs.h b/components/movements/include/MovementBlobs.h deleted file mode 100644 index 435bfca..0000000 --- a/components/movements/include/MovementBlobs.h +++ /dev/null @@ -1,50 +0,0 @@ -#include "MovementManager.h" -#include "nvs_flash.h" -#include "TopologyBlobs.h" -#include -#include - -#ifdef MOVEMENTS -#pragma once - -typedef struct _condition_blob { - uint16_t value; - uint8_t cond; - uint8_t moduleType; - uint8_t in_use; -} ConditionBlob; - -typedef struct _movement_entry { - uint16_t board_id; - uint8_t moduleType; - uint16_t value_action; - ConditionBlob condition; - uint8_t ack; - uint16_t ack_ttl_ms; - uint16_t post_delay_ms; -} MovementEntry; - -typedef struct _movement { - uint8_t num_movements; - MovementEntry movements[]; -} MovementSet; - -class MovementSetManager { - public: - MovementSetManager(); - ~MovementSetManager(); - esp_err_t add_movement(MovementEntry& entry, uint8_t& index); - esp_err_t remove_movement(uint8_t index); - esp_err_t update_movement(MovementEntry& entry, uint8_t index); - esp_err_t get_curr_movement_set(MovementSet& set); - esp_err_t verify_movement_set(); - esp_err_t get_nvs_movement_set(MovementSet& set); - esp_err_t write_nvs_movement_set(); - - private: - std::unique_ptr topology_manager; - std::vector movements; - nvs_handle_t handle; -}; - -#endif //MOVEMENTS \ No newline at end of file diff --git a/components/movements/include/MovementSetManager.h b/components/movements/include/MovementSetManager.h new file mode 100644 index 0000000..20981f9 --- /dev/null +++ b/components/movements/include/MovementSetManager.h @@ -0,0 +1,28 @@ +#include "MovementManager.h" +#include "nvs_flash.h" +#include "TopologyManager.h" +#include +#include + +#ifdef MOVEMENTS +#pragma once + +class MovementSetManager { + public: + MovementSetManager(); + ~MovementSetManager(); + // esp_err_t add_movement(struct MovementEntry& entry, uint8_t& index); + // esp_err_t remove_movement(uint8_t index); + // esp_err_t update_movement(struct MovementEntry& entry, uint8_t index); + // esp_err_t get_curr_movement_set(struct MovementSet& set); + // esp_err_t verify_movement_set(); + // esp_err_t get_nvs_movement_set(struct MovementSet& set); + // esp_err_t write_nvs_movement_set(); + + private: + std::unique_ptr topology_manager; + std::vector movements; + nvs_handle_t handle; +}; + +#endif //MOVEMENTS \ No newline at end of file diff --git a/components/movements/include/TopologyBlobs.h b/components/movements/include/TopologyBlobs.h deleted file mode 100644 index 6f15e93..0000000 --- a/components/movements/include/TopologyBlobs.h +++ /dev/null @@ -1,42 +0,0 @@ -#include "MovementManager.h" -#include "nvs_flash.h" -#include - -#ifdef MOVEMENTS -#pragma once - -typedef struct _channel_board_conn { - uint8_t channel; - uint16_t board_id; -} ChannelBoardConn; - -typedef struct _neighbour_blob { - uint16_t curr_board_id; - uint8_t num_connections; - ChannelBoardConn neighbour_connections[]; -} NeighbourBlob; - -typedef struct _topology { - uint16_t num_boards; - NeighbourBlob boards[]; -} Topology; - -class TopologyManager { - public: - TopologyManager(); - ~TopologyManager(); - esp_err_t add_board_to_topology(NeighbourBlob blob); - esp_err_t remove_board_from_topology(uint16_t board_id); - esp_err_t update_board_in_topology(NeighbourBlob blob); - esp_err_t get_board_in_topology(NeighbourBlob& blob); - esp_err_t verify_topology(); - esp_err_t get_curr_topology(Topology& topology); - esp_err_t write_nvs_topology(); - esp_err_t get_nvs_topology(Topology& topology); - - private: - std::unordered_map topology; - nvs_handle_t handle; -}; - -#endif //MOVEMENTS \ No newline at end of file diff --git a/components/movements/include/TopologyManager.h b/components/movements/include/TopologyManager.h new file mode 100644 index 0000000..f036f79 --- /dev/null +++ b/components/movements/include/TopologyManager.h @@ -0,0 +1,31 @@ +#include "MovementManager.h" +#include "nvs_flash.h" +#include + +// move these to constants header file? +#include "RMTManager.h" +#include "DataLinkManager.h" +#include "Frames.h" + +#ifdef MOVEMENTS +#pragma once + +class TopologyManager { + public: + TopologyManager(); + ~TopologyManager(); + // esp_err_t add_board_to_topology(struct NeighbourBlob& blob); + // esp_err_t remove_board_from_topology(uint16_t board_id); + // esp_err_t get_board_in_topology(struct NeighbourBlob& blob); + // esp_err_t verify_topology(); + // esp_err_t get_curr_topology(struct Topology& topology); + // esp_err_t write_nvs_topology(); + // esp_err_t get_nvs_topology(struct Topology& topology); + + private: + // std::unordered_map topology; + nvs_handle_t handle; + bool ready = false; +}; + +#endif //MOVEMENTS \ No newline at end of file diff --git a/components/rmt/include/RMTManager.h b/components/rmt/include/RMTManager.h index 172c1d9..f1e6109 100644 --- a/components/rmt/include/RMTManager.h +++ b/components/rmt/include/RMTManager.h @@ -1,6 +1,5 @@ #ifndef RMT_COMMUNICATIONS #define RMT_COMMUNICATIONS - #include #include "esp_event.h" From 4b9eaefb28c9d01adafb7b9fe6c394503b56fbe8 Mon Sep 17 00:00:00 2001 From: superkor Date: Thu, 15 Jan 2026 02:14:48 -0500 Subject: [PATCH 04/11] flatbuffer wrapper for movement sets --- components/flatbuffers/MovementSetBuilder.cpp | 45 +++++++++++++++++ .../flatbuffers/include/MovementSetBuilder.h | 29 +++++++++++ .../Movement_generated.h | 44 ++++++++--------- .../Topology_generated.h | 48 +++++++++---------- .../movements/include/MovementSetManager.h | 2 +- 5 files changed, 121 insertions(+), 47 deletions(-) create mode 100644 components/flatbuffers/MovementSetBuilder.cpp create mode 100644 components/flatbuffers/include/MovementSetBuilder.h diff --git a/components/flatbuffers/MovementSetBuilder.cpp b/components/flatbuffers/MovementSetBuilder.cpp new file mode 100644 index 0000000..02d5515 --- /dev/null +++ b/components/flatbuffers/MovementSetBuilder.cpp @@ -0,0 +1,45 @@ +// Justin Chow created file >:) + +#include "MovementSetBuilder.h" +#include "SerializedMessage.h" + +#ifdef MOVEMENTSETBUILDER + +namespace Flatbuffers { + SerializedMessage MovementSetBuilder::build_movement_set(const std::vector>& set){ + + builder_.Clear(); + + const auto set_vector = builder_.CreateVector(set); + + const auto message = Movement::CreateMovementSet( + builder_, + static_cast(set.size()), + set_vector + ); + + builder_.Finish(message); + + return {builder_.GetBufferPointer(), builder_.GetSize()}; + } + + flatbuffers::Offset MovementSetBuilder::build_movement_entry(uint16_t board_id, uint8_t module_type, uint16_t value_action, + const Movement::ConditionBlob& condition, uint8_t ack, uint16_t ack_ttl_ms, uint16_t post_delay_ms){ + return Movement::CreateMovementEntry( + builder_, + board_id, + module_type, + value_action, + &condition, + ack, + ack_ttl_ms, + post_delay_ms + ); + } + + Movement::ConditionBlob MovementSetBuilder::build_condition_blob(uint16_t value, uint8_t cond, uint8_t module_type, uint8_t in_use){ + return Movement::ConditionBlob(value, cond, module_type, in_use); + } +} + +#endif //MOVEMENTSETBUILDER diff --git a/components/flatbuffers/include/MovementSetBuilder.h b/components/flatbuffers/include/MovementSetBuilder.h new file mode 100644 index 0000000..cbf5af6 --- /dev/null +++ b/components/flatbuffers/include/MovementSetBuilder.h @@ -0,0 +1,29 @@ +// Justin Chow created file >:) + +#ifndef MOVEMENTSETBUILDER +#define MOVEMENTSETBUILDER + +#define MAX_MOVEMENTS_IN_SET 255 //max number movements in `MovementEntry.num_movements` can hold + +#include + +#include "SerializedMessage.h" +#include "flatbuffers_generated/Movement_generated.h" +#include "flatbuffers/flatbuffers.h" + +namespace Flatbuffers { + class MovementSetBuilder { + public: + MovementSetBuilder() : builder_(MAX_MOVEMENTS_IN_SET*sizeof(Movement::MovementEntry)) {} + + SerializedMessage build_movement_set(const std::vector>& set); + flatbuffers::Offset build_movement_entry(uint16_t board_id, uint8_t module_type, uint16_t value_action, + const Movement::ConditionBlob& condition, uint8_t ack, uint16_t ack_ttl_ms, uint16_t post_delay_ms); + Movement::ConditionBlob build_condition_blob(uint16_t value, uint8_t cond, uint8_t module_type, uint8_t in_use); + + private: + flatbuffers::FlatBufferBuilder builder_; + }; +} + +#endif //MOVEMENTSETBUILDER diff --git a/components/flatbuffers/include/flatbuffers_generated/Movement_generated.h b/components/flatbuffers/include/flatbuffers_generated/Movement_generated.h index 783b6ea..a8acf9a 100644 --- a/components/flatbuffers/include/flatbuffers_generated/Movement_generated.h +++ b/components/flatbuffers/include/flatbuffers_generated/Movement_generated.h @@ -13,7 +13,7 @@ static_assert(FLATBUFFERS_VERSION_MAJOR == 25 && FLATBUFFERS_VERSION_REVISION == 19, "Non-compatible flatbuffers version included"); -namespace movement { +namespace Movement { struct ConditionBlob; @@ -83,8 +83,8 @@ struct MovementEntry FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { uint16_t value_action() const { return GetField(VT_VALUE_ACTION, 0); } - const movement::ConditionBlob *condition() const { - return GetStruct(VT_CONDITION); + const Movement::ConditionBlob *condition() const { + return GetStruct(VT_CONDITION); } uint8_t ack() const { return GetField(VT_ACK, 0); @@ -101,7 +101,7 @@ struct MovementEntry FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { VerifyField(verifier, VT_BOARD_ID, 2) && VerifyField(verifier, VT_MODULE_TYPE, 1) && VerifyField(verifier, VT_VALUE_ACTION, 2) && - VerifyField(verifier, VT_CONDITION, 2) && + VerifyField(verifier, VT_CONDITION, 2) && VerifyField(verifier, VT_ACK, 1) && VerifyField(verifier, VT_ACK_TTL_MS, 2) && VerifyField(verifier, VT_POST_DELAY_MS, 2) && @@ -122,7 +122,7 @@ struct MovementEntryBuilder { void add_value_action(uint16_t value_action) { fbb_.AddElement(MovementEntry::VT_VALUE_ACTION, value_action, 0); } - void add_condition(const movement::ConditionBlob *condition) { + void add_condition(const Movement::ConditionBlob *condition) { fbb_.AddStruct(MovementEntry::VT_CONDITION, condition); } void add_ack(uint8_t ack) { @@ -150,7 +150,7 @@ inline ::flatbuffers::Offset CreateMovementEntry( uint16_t board_id = 0, uint8_t module_type = 0, uint16_t value_action = 0, - const movement::ConditionBlob *condition = nullptr, + const Movement::ConditionBlob *condition = nullptr, uint8_t ack = 0, uint16_t ack_ttl_ms = 0, uint16_t post_delay_ms = 0) { @@ -174,8 +174,8 @@ struct MovementSet FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { uint8_t num_movements() const { return GetField(VT_NUM_MOVEMENTS, 0); } - const ::flatbuffers::Vector<::flatbuffers::Offset> *movements() const { - return GetPointer> *>(VT_MOVEMENTS); + const ::flatbuffers::Vector<::flatbuffers::Offset> *movements() const { + return GetPointer> *>(VT_MOVEMENTS); } template bool Verify(::flatbuffers::VerifierTemplate &verifier) const { @@ -195,7 +195,7 @@ struct MovementSetBuilder { void add_num_movements(uint8_t num_movements) { fbb_.AddElement(MovementSet::VT_NUM_MOVEMENTS, num_movements, 0); } - void add_movements(::flatbuffers::Offset<::flatbuffers::Vector<::flatbuffers::Offset>> movements) { + void add_movements(::flatbuffers::Offset<::flatbuffers::Vector<::flatbuffers::Offset>> movements) { fbb_.AddOffset(MovementSet::VT_MOVEMENTS, movements); } explicit MovementSetBuilder(::flatbuffers::FlatBufferBuilder &_fbb) @@ -212,7 +212,7 @@ struct MovementSetBuilder { inline ::flatbuffers::Offset CreateMovementSet( ::flatbuffers::FlatBufferBuilder &_fbb, uint8_t num_movements = 0, - ::flatbuffers::Offset<::flatbuffers::Vector<::flatbuffers::Offset>> movements = 0) { + ::flatbuffers::Offset<::flatbuffers::Vector<::flatbuffers::Offset>> movements = 0) { MovementSetBuilder builder_(_fbb); builder_.add_movements(movements); builder_.add_num_movements(num_movements); @@ -222,46 +222,46 @@ inline ::flatbuffers::Offset CreateMovementSet( inline ::flatbuffers::Offset CreateMovementSetDirect( ::flatbuffers::FlatBufferBuilder &_fbb, uint8_t num_movements = 0, - const std::vector<::flatbuffers::Offset> *movements = nullptr) { - auto movements__ = movements ? _fbb.CreateVector<::flatbuffers::Offset>(*movements) : 0; - return movement::CreateMovementSet( + const std::vector<::flatbuffers::Offset> *movements = nullptr) { + auto movements__ = movements ? _fbb.CreateVector<::flatbuffers::Offset>(*movements) : 0; + return Movement::CreateMovementSet( _fbb, num_movements, movements__); } -inline const movement::MovementSet *GetMovementSet(const void *buf) { - return ::flatbuffers::GetRoot(buf); +inline const Movement::MovementSet *GetMovementSet(const void *buf) { + return ::flatbuffers::GetRoot(buf); } -inline const movement::MovementSet *GetSizePrefixedMovementSet(const void *buf) { - return ::flatbuffers::GetSizePrefixedRoot(buf); +inline const Movement::MovementSet *GetSizePrefixedMovementSet(const void *buf) { + return ::flatbuffers::GetSizePrefixedRoot(buf); } template inline bool VerifyMovementSetBuffer( ::flatbuffers::VerifierTemplate &verifier) { - return verifier.template VerifyBuffer(nullptr); + return verifier.template VerifyBuffer(nullptr); } template inline bool VerifySizePrefixedMovementSetBuffer( ::flatbuffers::VerifierTemplate &verifier) { - return verifier.template VerifySizePrefixedBuffer(nullptr); + return verifier.template VerifySizePrefixedBuffer(nullptr); } inline void FinishMovementSetBuffer( ::flatbuffers::FlatBufferBuilder &fbb, - ::flatbuffers::Offset root) { + ::flatbuffers::Offset root) { fbb.Finish(root); } inline void FinishSizePrefixedMovementSetBuffer( ::flatbuffers::FlatBufferBuilder &fbb, - ::flatbuffers::Offset root) { + ::flatbuffers::Offset root) { fbb.FinishSizePrefixed(root); } -} // namespace movement +} // namespace Movement #endif // FLATBUFFERS_GENERATED_MOVEMENT_MOVEMENT_H_ diff --git a/components/flatbuffers/include/flatbuffers_generated/Topology_generated.h b/components/flatbuffers/include/flatbuffers_generated/Topology_generated.h index 3bb0d37..aa30ca0 100644 --- a/components/flatbuffers/include/flatbuffers_generated/Topology_generated.h +++ b/components/flatbuffers/include/flatbuffers_generated/Topology_generated.h @@ -13,7 +13,7 @@ static_assert(FLATBUFFERS_VERSION_MAJOR == 25 && FLATBUFFERS_VERSION_REVISION == 19, "Non-compatible flatbuffers version included"); -namespace topology { +namespace Topology { struct ChannelBoardConn; @@ -64,8 +64,8 @@ struct NeighbourBlob FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { uint8_t num_connections() const { return GetField(VT_NUM_CONNECTIONS, 0); } - const ::flatbuffers::Vector *neighbour_connections() const { - return GetPointer *>(VT_NEIGHBOUR_CONNECTIONS); + const ::flatbuffers::Vector *neighbour_connections() const { + return GetPointer *>(VT_NEIGHBOUR_CONNECTIONS); } template bool Verify(::flatbuffers::VerifierTemplate &verifier) const { @@ -88,7 +88,7 @@ struct NeighbourBlobBuilder { void add_num_connections(uint8_t num_connections) { fbb_.AddElement(NeighbourBlob::VT_NUM_CONNECTIONS, num_connections, 0); } - void add_neighbour_connections(::flatbuffers::Offset<::flatbuffers::Vector> neighbour_connections) { + void add_neighbour_connections(::flatbuffers::Offset<::flatbuffers::Vector> neighbour_connections) { fbb_.AddOffset(NeighbourBlob::VT_NEIGHBOUR_CONNECTIONS, neighbour_connections); } explicit NeighbourBlobBuilder(::flatbuffers::FlatBufferBuilder &_fbb) @@ -106,7 +106,7 @@ inline ::flatbuffers::Offset CreateNeighbourBlob( ::flatbuffers::FlatBufferBuilder &_fbb, uint16_t curr_board_id = 0, uint8_t num_connections = 0, - ::flatbuffers::Offset<::flatbuffers::Vector> neighbour_connections = 0) { + ::flatbuffers::Offset<::flatbuffers::Vector> neighbour_connections = 0) { NeighbourBlobBuilder builder_(_fbb); builder_.add_neighbour_connections(neighbour_connections); builder_.add_curr_board_id(curr_board_id); @@ -118,9 +118,9 @@ inline ::flatbuffers::Offset CreateNeighbourBlobDirect( ::flatbuffers::FlatBufferBuilder &_fbb, uint16_t curr_board_id = 0, uint8_t num_connections = 0, - const std::vector *neighbour_connections = nullptr) { - auto neighbour_connections__ = neighbour_connections ? _fbb.CreateVectorOfStructs(*neighbour_connections) : 0; - return topology::CreateNeighbourBlob( + const std::vector *neighbour_connections = nullptr) { + auto neighbour_connections__ = neighbour_connections ? _fbb.CreateVectorOfStructs(*neighbour_connections) : 0; + return Topology::CreateNeighbourBlob( _fbb, curr_board_id, num_connections, @@ -136,8 +136,8 @@ struct Topology FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { uint16_t num_boards() const { return GetField(VT_NUM_BOARDS, 0); } - const ::flatbuffers::Vector<::flatbuffers::Offset> *boards() const { - return GetPointer> *>(VT_BOARDS); + const ::flatbuffers::Vector<::flatbuffers::Offset> *boards() const { + return GetPointer> *>(VT_BOARDS); } template bool Verify(::flatbuffers::VerifierTemplate &verifier) const { @@ -157,7 +157,7 @@ struct TopologyBuilder { void add_num_boards(uint16_t num_boards) { fbb_.AddElement(Topology::VT_NUM_BOARDS, num_boards, 0); } - void add_boards(::flatbuffers::Offset<::flatbuffers::Vector<::flatbuffers::Offset>> boards) { + void add_boards(::flatbuffers::Offset<::flatbuffers::Vector<::flatbuffers::Offset>> boards) { fbb_.AddOffset(Topology::VT_BOARDS, boards); } explicit TopologyBuilder(::flatbuffers::FlatBufferBuilder &_fbb) @@ -174,7 +174,7 @@ struct TopologyBuilder { inline ::flatbuffers::Offset CreateTopology( ::flatbuffers::FlatBufferBuilder &_fbb, uint16_t num_boards = 0, - ::flatbuffers::Offset<::flatbuffers::Vector<::flatbuffers::Offset>> boards = 0) { + ::flatbuffers::Offset<::flatbuffers::Vector<::flatbuffers::Offset>> boards = 0) { TopologyBuilder builder_(_fbb); builder_.add_boards(boards); builder_.add_num_boards(num_boards); @@ -184,46 +184,46 @@ inline ::flatbuffers::Offset CreateTopology( inline ::flatbuffers::Offset CreateTopologyDirect( ::flatbuffers::FlatBufferBuilder &_fbb, uint16_t num_boards = 0, - const std::vector<::flatbuffers::Offset> *boards = nullptr) { - auto boards__ = boards ? _fbb.CreateVector<::flatbuffers::Offset>(*boards) : 0; - return topology::CreateTopology( + const std::vector<::flatbuffers::Offset> *boards = nullptr) { + auto boards__ = boards ? _fbb.CreateVector<::flatbuffers::Offset>(*boards) : 0; + return Topology::CreateTopology( _fbb, num_boards, boards__); } -inline const topology::Topology *GetTopology(const void *buf) { - return ::flatbuffers::GetRoot(buf); +inline const Topology::Topology *GetTopology(const void *buf) { + return ::flatbuffers::GetRoot(buf); } -inline const topology::Topology *GetSizePrefixedTopology(const void *buf) { - return ::flatbuffers::GetSizePrefixedRoot(buf); +inline const Topology::Topology *GetSizePrefixedTopology(const void *buf) { + return ::flatbuffers::GetSizePrefixedRoot(buf); } template inline bool VerifyTopologyBuffer( ::flatbuffers::VerifierTemplate &verifier) { - return verifier.template VerifyBuffer(nullptr); + return verifier.template VerifyBuffer(nullptr); } template inline bool VerifySizePrefixedTopologyBuffer( ::flatbuffers::VerifierTemplate &verifier) { - return verifier.template VerifySizePrefixedBuffer(nullptr); + return verifier.template VerifySizePrefixedBuffer(nullptr); } inline void FinishTopologyBuffer( ::flatbuffers::FlatBufferBuilder &fbb, - ::flatbuffers::Offset root) { + ::flatbuffers::Offset root) { fbb.Finish(root); } inline void FinishSizePrefixedTopologyBuffer( ::flatbuffers::FlatBufferBuilder &fbb, - ::flatbuffers::Offset root) { + ::flatbuffers::Offset root) { fbb.FinishSizePrefixed(root); } -} // namespace topology +} // namespace Topology #endif // FLATBUFFERS_GENERATED_TOPOLOGY_TOPOLOGY_H_ diff --git a/components/movements/include/MovementSetManager.h b/components/movements/include/MovementSetManager.h index 20981f9..db2b8f7 100644 --- a/components/movements/include/MovementSetManager.h +++ b/components/movements/include/MovementSetManager.h @@ -21,7 +21,7 @@ class MovementSetManager { private: std::unique_ptr topology_manager; - std::vector movements; + // std::vector movements; nvs_handle_t handle; }; From 996d5f3f46878371bce945c8f0591fa8426ff8a5 Mon Sep 17 00:00:00 2001 From: superkor Date: Thu, 15 Jan 2026 19:21:18 -0500 Subject: [PATCH 05/11] move data link layer constants to constants component --- components/constants/CMakeLists.txt | 2 +- .../constants/include/constants/datalink.h | 76 +++++++++++++++++++ components/dataLink/CMakeLists.txt | 2 +- components/dataLink/include/Frames.h | 34 +-------- components/dataLink/include/Scheduler.h | 13 +--- components/dataLink/include/Tables.h | 14 +--- 6 files changed, 81 insertions(+), 60 deletions(-) create mode 100644 components/constants/include/constants/datalink.h diff --git a/components/constants/CMakeLists.txt b/components/constants/CMakeLists.txt index a23ad82..9862a8e 100644 --- a/components/constants/CMakeLists.txt +++ b/components/constants/CMakeLists.txt @@ -1 +1 @@ -idf_component_register(INCLUDE_DIRS "include" PRIV_REQUIRES "flatbuffers") +idf_component_register(INCLUDE_DIRS "include" REQUIRES "flatbuffers") diff --git a/components/constants/include/constants/datalink.h b/components/constants/include/constants/datalink.h new file mode 100644 index 0000000..1882632 --- /dev/null +++ b/components/constants/include/constants/datalink.h @@ -0,0 +1,76 @@ +#ifndef DATALINK_H +#define DATALINK_H +#include "module.h" + +/** + * @brief Frame Constants + */ + +#define BROADCAST_ADDR 0xFF //used for discovery (finding the board's neighbours). this will mean the board ids will have 2^8-2 = 254 unique IDs that could be assigned + +#define START_OF_FRAME 0xAB //0b1010_1011 - denotes the start of frame + +#define MAX_FRAME_SIZE 121 //Max 121B (due to rmt) - note this includes the overhead of the frame. the actual payload max depends on the frame type (eg. 121 - 9 B is the max control data length) + +#define MAX_GENERIC_NUM_FRAG (1 << 16) // Max 2**16 Fragments can be made with a generic frame (total 2**16 *MAX_GENERIC_DATA_LEN B of data can be sent ~ 6.7 MiB) + +#define MAX_FRAME_QUEUE_SIZE 15 //Size of the queue for the frame scheduler (per channel) + +//Flags +#define FLAG_FRAG 0x8 //0b1000 //this fragmented frame is part of a larger frame +#define FLAG_DISCOVERY 0x4 //0b0100 +#define FLAG_NEIGH_TABLE 0x2 //0b0010 - used to denote the frame contains the neighbour tables (used for finding the configuration/topology of the network); similar to an ARP or MAC table +#define FLAG_ACK 0x1 //0b0001_0000 - used for confirming receipt of different types of frames from the neighbours + +#define GET_TYPE(x) ((x) & 0xF0) +#define GET_FLAG(x) ((x) & 0x0F) +#define MAKE_TYPE_FLAG(type, flag) ((uint8_t)((type & 0xF0) | (flag & 0xF))) +#define IS_CONTROL_FRAME(x) (((x) & 0x80) != 0) + +#define CONTROL_FRAME_OVERHEAD 9 +#define GENERIC_FRAME_OVERHEAD 14 + +#define MAX_GENERIC_DATA_LEN (MAX_FRAME_SIZE - GENERIC_FRAME_OVERHEAD) +#define MAX_CONTROL_DATA_LEN (MAX_FRAME_SIZE - CONTROL_FRAME_OVERHEAD) + +//Generic Frame Fragment ACK +#define GENERIC_FRAG_ACK_DATA_SIZE 7 +#define GENERIC_FRAG_ACK_PREAMBLE 0x69 + +#define CONTROL_FRAME_TYPE 0x80 //if the frame type MSB is set to 1, use the control frame + +/** + * @brief Scheduler Constants + */ + +#define SCHEDULER_MUTEX_WAIT 10 //max time duration to wait +#define SCHEDULER_PERIOD_MS 140 +#define RECEIVE_TASK_PERIOD_MS 2 + +#define GENERIC_FRAME_SLIDING_WINDOW_SIZE 5 //defines the maximum size of the sliding window before resending previously un-ack'd fragments +#define SLIDING_WINDOW_MUTEX_TIMEOUT_MS 5 +#define GENERIC_FRAME_MOD_TIMEOUT 10 //be scheduled at most 9 + GENERIC_FRAME_MIN_TIMEOUT times before sending another fragment +#define GENERIC_FRAME_MIN_TIMEOUT 10 + +#define SEND_ACK_PERIOD_MS 50 +#define SEND_ACK_MUTEX_WAIT 10 + +/** + * @brief RIP Table Constants + */ + +#define RIP_MAX_HOPS 15 //16 or more is infinite +#define RIP_MAX_ROUTES 10 //for the demo we will use up to 10 boards in total (9 other boards will be connected = 9 rows) +#define RIP_INVALID_ROW 0 +#define RIP_VALID_ROW 1 +#define RIP_NEW_ROW 2 +#define RIP_BROADCAST_INTERVAL 30000 //broadcast every 30 seconds (30000ms) +// #define RIP_BROADCAST_INTERVAL 3000 //temp broadcast every 3 seconds (3000ms) +#define RIP_TTL_START 180 //seconds +#define RIP_MS_TO_SEC 1000 //1000 ms to 1 sec +#define RIP_MAX_SEM_WAIT_MS 30 +#define RIP_FLUSH_COUNT 8 //flush after 8*30 seconds = 240 seconds + +#define RIP_DISCOVERY_MESSAGE_SIZE 1 + +#endif //DATALINK_H \ No newline at end of file diff --git a/components/dataLink/CMakeLists.txt b/components/dataLink/CMakeLists.txt index 60a931a..adbd3fa 100644 --- a/components/dataLink/CMakeLists.txt +++ b/components/dataLink/CMakeLists.txt @@ -1,4 +1,4 @@ idf_component_register(SRCS "DataLinkManager.cpp" "DataLinkRIP.cpp" "DataLinkScheduler.cpp" "DataLinkFrames.cpp" PRIV_REQUIRES driver esp_event nvs_flash esp_netif rmt - REQUIRES esp_timer + REQUIRES esp_timer constants INCLUDE_DIRS "include") diff --git a/components/dataLink/include/Frames.h b/components/dataLink/include/Frames.h index d39e0b5..e7c8371 100644 --- a/components/dataLink/include/Frames.h +++ b/components/dataLink/include/Frames.h @@ -4,40 +4,8 @@ #include #include #include +#include "constants/datalink.h" -#define BROADCAST_ADDR 0xFF //used for discovery (finding the board's neighbours). this will mean the board ids will have 2^8-2 = 254 unique IDs that could be assigned -#define PC_ADDR 0x0 //setting 0 to be the PC - -#define START_OF_FRAME 0xAB //0b1010_1011 - denotes the start of frame - -#define MAX_FRAME_SIZE 121 //Max 121B (due to rmt) - note this includes the overhead of the frame. the actual payload max depends on the frame type (eg. 121 - 9 B is the max control data length) - -#define MAX_GENERIC_NUM_FRAG (1 << 16) // Max 2**16 Fragments can be made with a generic frame (total 2**16 *MAX_GENERIC_DATA_LEN B of data can be sent ~ 6.7 MiB) - -#define MAX_FRAME_QUEUE_SIZE 15 //Size of the queue for the frame scheduler (per channel) - -//Flags -#define FLAG_FRAG 0x8 //0b1000 //this fragmented frame is part of a larger frame -#define FLAG_DISCOVERY 0x4 //0b0100 -#define FLAG_NEIGH_TABLE 0x2 //0b0010 - used to denote the frame contains the neighbour tables (used for finding the configuration/topology of the network); similar to an ARP or MAC table -#define FLAG_ACK 0x1 //0b0001_0000 - used for confirming receipt of different types of frames from the neighbours - -#define GET_TYPE(x) ((x) & 0xF0) -#define GET_FLAG(x) ((x) & 0x0F) -#define MAKE_TYPE_FLAG(type, flag) ((uint8_t)((type & 0xF0) | (flag & 0xF))) -#define IS_CONTROL_FRAME(x) (((x) & 0x80) != 0) - -#define CONTROL_FRAME_OVERHEAD 9 -#define GENERIC_FRAME_OVERHEAD 14 - -#define MAX_GENERIC_DATA_LEN (MAX_FRAME_SIZE - GENERIC_FRAME_OVERHEAD) -#define MAX_CONTROL_DATA_LEN (MAX_FRAME_SIZE - CONTROL_FRAME_OVERHEAD) - -//Generic Frame Fragment ACK -#define GENERIC_FRAG_ACK_DATA_SIZE 7 -#define GENERIC_FRAG_ACK_PREAMBLE 0x69 - -#define CONTROL_FRAME_TYPE 0x80 //if the frame type MSB is set to 1, use the control frame //Types (total 2^4 = 16 different types) enum class FrameType : uint8_t { //Control Frames diff --git a/components/dataLink/include/Scheduler.h b/components/dataLink/include/Scheduler.h index 756e6e7..8e6d37d 100644 --- a/components/dataLink/include/Scheduler.h +++ b/components/dataLink/include/Scheduler.h @@ -2,18 +2,7 @@ #include "Frames.h" #include "esp_timer.h" #include - -#define SCHEDULER_MUTEX_WAIT 10 //max time duration to wait -#define SCHEDULER_PERIOD_MS 140 -#define RECEIVE_TASK_PERIOD_MS 2 - -#define GENERIC_FRAME_SLIDING_WINDOW_SIZE 5 //defines the maximum size of the sliding window before resending previously un-ack'd fragments -#define SLIDING_WINDOW_MUTEX_TIMEOUT_MS 5 -#define GENERIC_FRAME_MOD_TIMEOUT 10 //be scheduled at most 9 + GENERIC_FRAME_MIN_TIMEOUT times before sending another fragment -#define GENERIC_FRAME_MIN_TIMEOUT 10 - -#define SEND_ACK_PERIOD_MS 50 -#define SEND_ACK_MUTEX_WAIT 10 +#include "constants/datalink.h" //Metadata representing the frame to be sent but is currently scheduled typedef struct _frame_scheduler_metadata { diff --git a/components/dataLink/include/Tables.h b/components/dataLink/include/Tables.h index 8545af1..49e97a4 100644 --- a/components/dataLink/include/Tables.h +++ b/components/dataLink/include/Tables.h @@ -1,20 +1,8 @@ #pragma once #ifdef DATA_LINK #include "freertos/FreeRTOS.h" +#include "constants/datalink.h" -#define RIP_MAX_HOPS 15 //16 or more is infinite -#define RIP_MAX_ROUTES 10 //for the demo we will use up to 10 boards in total (9 other boards will be connected = 9 rows) -#define RIP_INVALID_ROW 0 -#define RIP_VALID_ROW 1 -#define RIP_NEW_ROW 2 -#define RIP_BROADCAST_INTERVAL 30000 //broadcast every 30 seconds (30000ms) -// #define RIP_BROADCAST_INTERVAL 3000 //temp broadcast every 3 seconds (3000ms) -#define RIP_TTL_START 180 //seconds -#define RIP_MS_TO_SEC 1000 //1000 ms to 1 sec -#define RIP_MAX_SEM_WAIT_MS 30 -#define RIP_FLUSH_COUNT 8 //flush after 8*30 seconds = 240 seconds - -#define RIP_DISCOVERY_MESSAGE_SIZE 1 /** * @brief Routing data to a board * This struct will be sent to other boards From 6b0d7332297da897d00c8e7c57cc2cd05fe8d5b5 Mon Sep 17 00:00:00 2001 From: superkor Date: Thu, 15 Jan 2026 19:31:52 -0500 Subject: [PATCH 06/11] move rmt constants to constants comp --- components/constants/include/constants/rmt.h | 32 ++++++++++++++++++++ components/flatbuffers/CMakeLists.txt | 2 +- components/rmt/CMakeLists.txt | 2 +- components/rmt/include/RMTManager.h | 15 +-------- components/rmt/include/RMTSymbols.h | 6 +--- 5 files changed, 36 insertions(+), 21 deletions(-) create mode 100644 components/constants/include/constants/rmt.h diff --git a/components/constants/include/constants/rmt.h b/components/constants/include/constants/rmt.h new file mode 100644 index 0000000..048d5c0 --- /dev/null +++ b/components/constants/include/constants/rmt.h @@ -0,0 +1,32 @@ +#ifndef RMT_H +#define RMT_H + +/** + * @brief RMT Symbol Constants + */ + +#define RMT_RESOLUTION_HZ 4 * 1000 * 1000 // 4 MHz resolution +#define RMT_DURATION_SYMBOL 2 //1 us + +#define RMT_DURATION_MAX (2 * RMT_DURATION_SYMBOL) + +/** + * @brief RMT Constants + */ + +#define MAX_CHANNELS 4 +#define RMT_SYMBOL_BLOCK_SIZE 48 + +#define RECEIVE_BUFFER_SIZE 1024 //this is some value (we should probably set it to some packet size that we predetermine in some custom protocol:tm:) +#define DEBUG_TAG "RMTManager" + +#define CHANNEL_LISTENING (0x2) //channel waiting to receive +#define CHANNEL_READY_STATUS (0x1) //channel able to send and ready to start receive async job +#define CHANNEL_NOT_READY_STATUS (0x0) //channel is not ready (cannot send and/or receive) + +#define QUEUE_SIZE 10 + +#define MUTEX_MAX_WAIT_TICKS 100 + + +#endif //RMT_H \ No newline at end of file diff --git a/components/flatbuffers/CMakeLists.txt b/components/flatbuffers/CMakeLists.txt index a7b031f..85f614f 100644 --- a/components/flatbuffers/CMakeLists.txt +++ b/components/flatbuffers/CMakeLists.txt @@ -1,2 +1,2 @@ -idf_component_register(SRCS "MPIMessageBuilder.cpp" "AngleControlMessageBuilder.cpp" "TopologyMessageBuilder.cpp" "SensorMessageBuilder.cpp" +idf_component_register(SRCS "MPIMessageBuilder.cpp" "AngleControlMessageBuilder.cpp" "TopologyMessageBuilder.cpp" "SensorMessageBuilder.cpp" REQUIRES "constants" INCLUDE_DIRS "include") diff --git a/components/rmt/CMakeLists.txt b/components/rmt/CMakeLists.txt index d9d523d..9aefed6 100644 --- a/components/rmt/CMakeLists.txt +++ b/components/rmt/CMakeLists.txt @@ -1,4 +1,4 @@ idf_component_register(SRCS "RMTManager.cpp" PRIV_REQUIRES driver esp_event nvs_flash esp_netif - REQUIRES esp_driver_rmt + REQUIRES esp_driver_rmt constants INCLUDE_DIRS "include") diff --git a/components/rmt/include/RMTManager.h b/components/rmt/include/RMTManager.h index f1e6109..036c591 100644 --- a/components/rmt/include/RMTManager.h +++ b/components/rmt/include/RMTManager.h @@ -8,20 +8,7 @@ #include "driver/rmt_tx.h" #include "driver/rmt_rx.h" #include "soc/gpio_num.h" - -#define MAX_CHANNELS 4 -#define RMT_SYMBOL_BLOCK_SIZE 48 - -#define RECEIVE_BUFFER_SIZE 1024 //this is some value (we should probably set it to some packet size that we predetermine in some custom protocol:tm:) -#define DEBUG_TAG "RMTManager" - -#define CHANNEL_LISTENING (0x2) //channel waiting to receive -#define CHANNEL_READY_STATUS (0x1) //channel able to send and ready to start receive async job -#define CHANNEL_NOT_READY_STATUS (0x0) //channel is not ready (cannot send and/or receive) - -#define QUEUE_SIZE 10 - -#define MUTEX_MAX_WAIT_TICKS 100 +#include "constants/rmt.h" /** * @brief This struct keeps track of the current byte and bit index of the user data being transmmitted via RMT diff --git a/components/rmt/include/RMTSymbols.h b/components/rmt/include/RMTSymbols.h index 023141d..0ab7853 100644 --- a/components/rmt/include/RMTSymbols.h +++ b/components/rmt/include/RMTSymbols.h @@ -1,11 +1,7 @@ #ifdef RMT_COMMUNICATIONS #include "driver/rmt_tx.h" - -#define RMT_RESOLUTION_HZ 4 * 1000 * 1000 // 4 MHz resolution -#define RMT_DURATION_SYMBOL 2 //1 us - -#define RMT_DURATION_MAX (2 * RMT_DURATION_SYMBOL) +#include "constants/rmt.h" //MANCHESTER ENCODING (ETHERNET STANDARD) From 54128ef410576aa67cb500abf30593fd4a9d4240 Mon Sep 17 00:00:00 2001 From: superkor Date: Thu, 15 Jan 2026 20:02:28 -0500 Subject: [PATCH 07/11] Add topology builder --- components/flatbuffers/TopologyBuilder.cpp | 29 ++++++++++ .../flatbuffers/include/TopologyBuilder.h | 26 +++++++++ .../Topology_generated.h | 54 +++++++++---------- 3 files changed, 82 insertions(+), 27 deletions(-) create mode 100644 components/flatbuffers/TopologyBuilder.cpp create mode 100644 components/flatbuffers/include/TopologyBuilder.h diff --git a/components/flatbuffers/TopologyBuilder.cpp b/components/flatbuffers/TopologyBuilder.cpp new file mode 100644 index 0000000..d87daba --- /dev/null +++ b/components/flatbuffers/TopologyBuilder.cpp @@ -0,0 +1,29 @@ +#include "TopologyBuilder.h" +#ifdef TOPOLOGYBUILDER +namespace Flatbuffers{ + SerializedMessage TopologyBuilder::build_topology(const std::vector>& topology){ + builder_.Clear(); + + auto neighbours_vec = builder_.CreateVector(topology); + auto topology_offset = Topology::CreateTopologyInfo(builder_, static_cast(topology.size()), neighbours_vec); + + builder_.Finish(topology_offset); + + return { + builder_.GetBufferPointer(), + builder_.GetSize() + }; + } + + flatbuffers::Offset TopologyBuilder::build_neighbour_info(uint16_t board_id, const std::vector& connections){ + auto conn = builder_.CreateVectorOfStructs(connections); + return Topology::CreateNeighbourBlob(builder_, board_id, static_cast(connections.size()), conn); + } + + Topology::ChannelBoardConn TopologyBuilder::build_connections(uint8_t channel, uint16_t board_id){ + return Topology::ChannelBoardConn(channel, board_id); + } + +} + +#endif //TOPOLOGYBUILDER \ No newline at end of file diff --git a/components/flatbuffers/include/TopologyBuilder.h b/components/flatbuffers/include/TopologyBuilder.h new file mode 100644 index 0000000..14c98e6 --- /dev/null +++ b/components/flatbuffers/include/TopologyBuilder.h @@ -0,0 +1,26 @@ +#ifndef TOPOLOGYBUILDER +#define TOPOLOGYBUILDER + +#include "SerializedMessage.h" +#include +#include "constants/datalink.h" +#include "constants/rmt.h" + +#include "flatbuffers_generated/Topology_generated.h" +#include "flatbuffers/flatbuffers.h" + +namespace Flatbuffers { + class TopologyBuilder { + public: + TopologyBuilder() : builder_(RIP_MAX_ROUTES*(sizeof(Topology::NeighbourBlob) + sizeof(Topology::ChannelBoardConn)*MAX_CHANNELS)) {} + + SerializedMessage build_topology(const std::vector>& topology); + flatbuffers::Offset build_neighbour_info(uint16_t board_id, const std::vector& connections); + Topology::ChannelBoardConn build_connections(uint8_t channel, uint16_t board_id); + + private: + flatbuffers::FlatBufferBuilder builder_; + }; +} + +#endif //TOPOLOGYBUILDER \ No newline at end of file diff --git a/components/flatbuffers/include/flatbuffers_generated/Topology_generated.h b/components/flatbuffers/include/flatbuffers_generated/Topology_generated.h index aa30ca0..6dad170 100644 --- a/components/flatbuffers/include/flatbuffers_generated/Topology_generated.h +++ b/components/flatbuffers/include/flatbuffers_generated/Topology_generated.h @@ -20,8 +20,8 @@ struct ChannelBoardConn; struct NeighbourBlob; struct NeighbourBlobBuilder; -struct Topology; -struct TopologyBuilder; +struct TopologyInfo; +struct TopologyInfoBuilder; FLATBUFFERS_MANUALLY_ALIGNED_STRUCT(2) ChannelBoardConn FLATBUFFERS_FINAL_CLASS { private: @@ -127,8 +127,8 @@ inline ::flatbuffers::Offset CreateNeighbourBlobDirect( neighbour_connections__); } -struct Topology FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { - typedef TopologyBuilder Builder; +struct TopologyInfo FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef TopologyInfoBuilder Builder; enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { VT_NUM_BOARDS = 4, VT_BOARDS = 6 @@ -150,77 +150,77 @@ struct Topology FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { } }; -struct TopologyBuilder { - typedef Topology Table; +struct TopologyInfoBuilder { + typedef TopologyInfo Table; ::flatbuffers::FlatBufferBuilder &fbb_; ::flatbuffers::uoffset_t start_; void add_num_boards(uint16_t num_boards) { - fbb_.AddElement(Topology::VT_NUM_BOARDS, num_boards, 0); + fbb_.AddElement(TopologyInfo::VT_NUM_BOARDS, num_boards, 0); } void add_boards(::flatbuffers::Offset<::flatbuffers::Vector<::flatbuffers::Offset>> boards) { - fbb_.AddOffset(Topology::VT_BOARDS, boards); + fbb_.AddOffset(TopologyInfo::VT_BOARDS, boards); } - explicit TopologyBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + explicit TopologyInfoBuilder(::flatbuffers::FlatBufferBuilder &_fbb) : fbb_(_fbb) { start_ = fbb_.StartTable(); } - ::flatbuffers::Offset Finish() { + ::flatbuffers::Offset Finish() { const auto end = fbb_.EndTable(start_); - auto o = ::flatbuffers::Offset(end); + auto o = ::flatbuffers::Offset(end); return o; } }; -inline ::flatbuffers::Offset CreateTopology( +inline ::flatbuffers::Offset CreateTopologyInfo( ::flatbuffers::FlatBufferBuilder &_fbb, uint16_t num_boards = 0, ::flatbuffers::Offset<::flatbuffers::Vector<::flatbuffers::Offset>> boards = 0) { - TopologyBuilder builder_(_fbb); + TopologyInfoBuilder builder_(_fbb); builder_.add_boards(boards); builder_.add_num_boards(num_boards); return builder_.Finish(); } -inline ::flatbuffers::Offset CreateTopologyDirect( +inline ::flatbuffers::Offset CreateTopologyInfoDirect( ::flatbuffers::FlatBufferBuilder &_fbb, uint16_t num_boards = 0, const std::vector<::flatbuffers::Offset> *boards = nullptr) { auto boards__ = boards ? _fbb.CreateVector<::flatbuffers::Offset>(*boards) : 0; - return Topology::CreateTopology( + return Topology::CreateTopologyInfo( _fbb, num_boards, boards__); } -inline const Topology::Topology *GetTopology(const void *buf) { - return ::flatbuffers::GetRoot(buf); +inline const Topology::TopologyInfo *GetTopologyInfo(const void *buf) { + return ::flatbuffers::GetRoot(buf); } -inline const Topology::Topology *GetSizePrefixedTopology(const void *buf) { - return ::flatbuffers::GetSizePrefixedRoot(buf); +inline const Topology::TopologyInfo *GetSizePrefixedTopologyInfo(const void *buf) { + return ::flatbuffers::GetSizePrefixedRoot(buf); } template -inline bool VerifyTopologyBuffer( +inline bool VerifyTopologyInfoBuffer( ::flatbuffers::VerifierTemplate &verifier) { - return verifier.template VerifyBuffer(nullptr); + return verifier.template VerifyBuffer(nullptr); } template -inline bool VerifySizePrefixedTopologyBuffer( +inline bool VerifySizePrefixedTopologyInfoBuffer( ::flatbuffers::VerifierTemplate &verifier) { - return verifier.template VerifySizePrefixedBuffer(nullptr); + return verifier.template VerifySizePrefixedBuffer(nullptr); } -inline void FinishTopologyBuffer( +inline void FinishTopologyInfoBuffer( ::flatbuffers::FlatBufferBuilder &fbb, - ::flatbuffers::Offset root) { + ::flatbuffers::Offset root) { fbb.Finish(root); } -inline void FinishSizePrefixedTopologyBuffer( +inline void FinishSizePrefixedTopologyInfoBuffer( ::flatbuffers::FlatBufferBuilder &fbb, - ::flatbuffers::Offset root) { + ::flatbuffers::Offset root) { fbb.FinishSizePrefixed(root); } From 335a8dcd5570ab5ffcf8f71039db25ee424db1b3 Mon Sep 17 00:00:00 2001 From: superkor Date: Fri, 23 Jan 2026 18:51:06 -0500 Subject: [PATCH 08/11] updates to topology with flatbuffers --- .../Movement_generated.h | 8 +- .../Topology_generated.h | 8 +- components/movements/TopologyManager.cpp | 210 ++++++++++++++---- .../movements/include/TopologyManager.h | 27 ++- main/README.md | 10 + main/main.cpp | 2 +- main/main_preprogrammed_test.cpp | 48 ++++ main/main_rmt_test.cpp | 2 +- 8 files changed, 245 insertions(+), 70 deletions(-) create mode 100644 main/main_preprogrammed_test.cpp diff --git a/components/flatbuffers/include/flatbuffers_generated/Movement_generated.h b/components/flatbuffers/include/flatbuffers_generated/Movement_generated.h index a8acf9a..b6e8010 100644 --- a/components/flatbuffers/include/flatbuffers_generated/Movement_generated.h +++ b/components/flatbuffers/include/flatbuffers_generated/Movement_generated.h @@ -8,10 +8,10 @@ // Ensure the included flatbuffers.h is the same version as when this file was // generated, otherwise it may not be compatible. -static_assert(FLATBUFFERS_VERSION_MAJOR == 25 && - FLATBUFFERS_VERSION_MINOR == 12 && - FLATBUFFERS_VERSION_REVISION == 19, - "Non-compatible flatbuffers version included"); +// static_assert(FLATBUFFERS_VERSION_MAJOR == 25 && +// FLATBUFFERS_VERSION_MINOR == 12 && +// FLATBUFFERS_VERSION_REVISION == 19, +// "Non-compatible flatbuffers version included"); namespace Movement { diff --git a/components/flatbuffers/include/flatbuffers_generated/Topology_generated.h b/components/flatbuffers/include/flatbuffers_generated/Topology_generated.h index 6dad170..5a3bced 100644 --- a/components/flatbuffers/include/flatbuffers_generated/Topology_generated.h +++ b/components/flatbuffers/include/flatbuffers_generated/Topology_generated.h @@ -8,10 +8,10 @@ // Ensure the included flatbuffers.h is the same version as when this file was // generated, otherwise it may not be compatible. -static_assert(FLATBUFFERS_VERSION_MAJOR == 25 && - FLATBUFFERS_VERSION_MINOR == 12 && - FLATBUFFERS_VERSION_REVISION == 19, - "Non-compatible flatbuffers version included"); +// static_assert(FLATBUFFERS_VERSION_MAJOR == 25 && +// FLATBUFFERS_VERSION_MINOR == 12 && +// FLATBUFFERS_VERSION_REVISION == 19, +// "Non-compatible flatbuffers version included"); namespace Topology { diff --git a/components/movements/TopologyManager.cpp b/components/movements/TopologyManager.cpp index edce60a..c010f9e 100644 --- a/components/movements/TopologyManager.cpp +++ b/components/movements/TopologyManager.cpp @@ -17,70 +17,182 @@ TopologyManager::~TopologyManager(){ nvs_close(handle); } -// esp_err_t TopologyManager::add_board_to_topology(struct NeighbourBlob& blob){ -// if (!ready){ -// return ESP_ERR_INVALID_STATE; -// } +/** + * @brief Creates a `Topology::NeighbourBlob` based on the input parameters and adds the blob to the internal topology map + * + * @param connections This is a vector of pairs (uint8_t: `channel number`, uint16_t: `board_id`) representing the connection from the `curr_board_id` to `board_id` via `channel number` + * @param curr_board_id This is the board id, from its perspective, has the connections to the other boards in the vector `connections` + * @return esp_err_t + * + * @note + * Connections to fail: + * + * 1. Any of the `board_id` in the vector is `PC_ADDR` or `BROADCAST_ADDR` - Reserved addresses + * + * 2. Any of the `board_id` in the vector is `curr_board_id` - No loopback connections are allowed + * + * 3. Any of the `channel` in the vector is greater than or equal to `MAX_CHANNELS` - Channels are 0-indexed and there are at most 4 channels + * + * 4. All `board_id` is not unique in the vector - Should be a 1:1 connection between the boards + */ +esp_err_t TopologyManager::add_board_to_topology(std::vector>& connections, uint16_t curr_board_id){ + if (!ready){ + return ESP_ERR_INVALID_STATE; + } -// if (blob.num_connections > MAX_CHANNELS || blob.curr_board_id == PC_ADDR || blob.curr_board_id == BROADCAST_ADDR){ -// return ESP_ERR_INVALID_ARG; -// } + if (connections.size() == 0 || connections.size() > MAX_CHANNELS){ + return ESP_ERR_INVALID_ARG; + } -// for (uint8_t i = 0; i < blob.num_connections; i++){ -// ChannelBoardConn connection = blob.neighbour_connections[i]; -// if (connection.channel > MAX_CHANNELS || connection.board_id == PC_ADDR -// || connection == BROADCAST_ADDR || connection.board_id == blob.curr_board_id){ -// return ESP_ERR_INVALID_ARG; -// } -// } + if (curr_board_id == PC_ADDR || curr_board_id == BROADCAST_ADDR){ + return ESP_ERR_INVALID_ARG; + } -// topology[blob.curr_board_id] = blob; + //verify the connections in the vector + std::unordered_set check_set; + + std::vector conns; + + for (const std::pair& pair : connections){ + if (pair.second >= MAX_CHANNELS || pair.first == curr_board_id || pair.first == PC_ADDR + || pair.first == BROADCAST_ADDR || check_set.find(pair.first) != check_set.end()){ + return ESP_ERR_INVALID_ARG; + } + + check_set.insert(pair.first); + + conns.push_back(builder.build_connections(pair.first, pair.second)); + } + + topology[curr_board_id] = conns; -// return ESP_OK; -// } + return ESP_OK; +} -// esp_err_t TopologyManager::remove_board_from_topology(uint16_t board_id){ -// if (!ready){ -// return ESP_ERR_INVALID_STATE; -// } +esp_err_t TopologyManager::remove_board_from_topology(uint16_t board_id){ + if (!ready){ + return ESP_ERR_INVALID_STATE; + } -// if (topology.find(board_id) == topology.end()){ -// return ESP_ERR_NOT_FOUND; -// } + if (topology.find(board_id) == topology.end()){ + return ESP_ERR_NOT_FOUND; + } -// topology.erase(board_id); + topology.erase(board_id); -// return ESP_OK; -// } + return ESP_OK; +} -// esp_err_t TopologyManager::get_board_in_topology(struct NeighbourBlob& blob){ -// if (!ready){ -// return ESP_ERR_INVALID_STATE; -// } +esp_err_t TopologyManager::get_board_in_topology(std::vector>& connections, uint16_t curr_board_id){ + if (!ready){ + return ESP_ERR_INVALID_STATE; + } -// if (topology.find(board_id) == topology.end()){ -// return ESP_ERR_NOT_FOUND; -// } + if (topology.find(curr_board_id) == topology.end()){ + return ESP_ERR_NOT_FOUND; + } -// blob = topology.find(board_id); + std::vector conn = topology[curr_board_id]; -// return ESP_OK; -// } + connections.clear(); -// esp_err_t TopologyManager::verify_topology(){ -// if (!ready){ -// return ESP_ERR_INVALID_STATE; -// } -// return ESP_OK; -// } + for (const Topology::ChannelBoardConn c : conn){ + connections.push_back(std::pair(c.channel(), c.board_id())); + } -// esp_err_t TopologyManager::get_curr_topology(struct Topology& topology){ -// if (!ready){ -// return ESP_ERR_INVALID_STATE; -// } + return ESP_OK; +} -// return ESP_OK; -// } +esp_err_t TopologyManager::verify_topology(){ + if (!ready){ + return ESP_ERR_INVALID_STATE; + } + + if (topology.size() == 0){ + return ESP_ERR_INVALID_STATE; + } + + /** + * Conditions to fail: + * 1. Any board that is referenced in the topology does not have a reciprocal connection (eg. A -> B and B -> A) + * 2. The topology contains 2 separate graphs (a board in the topology should be in the same graph with some sort of path to all other boards) + */ + + //condition 1 + + for (const auto& pair : topology){ + for (const Topology::ChannelBoardConn& conn : pair.second){ + if (topology.find(conn.board_id()) == topology.end()){ + return ESP_ERR_INVALID_STATE; //could not find reciprocal board + } + + std::vector topology_board_conn = topology[conn.board_id()]; + bool found = false; + for (const Topology::ChannelBoardConn& c : topology_board_conn){ + if (c.board_id() == pair.first){ + found = true; + break; + } + } + if (!found){ + return ESP_ERR_INVALID_STATE; //reciprocal board does not have a connection back to `pair.first` + } + + } + } + + //condition 2 - dfs + std::unordered_set visited; + std::stack backtrack; + uint32_t count = 0; + + backtrack.push(topology.begin()->first); + + while (!backtrack.empty()){ + uint16_t curr_node = backtrack.top(); + backtrack.pop(); + + if (visited.find(curr_node) != visited.end()) { + continue; + } + + visited.insert(curr_node); + count++; + + const std::vector& conns = topology[curr_node]; + + for (const Topology::ChannelBoardConn& conn : conns) { + uint16_t next = conn.board_id(); + if (visited.find(next) != visited.end()) { + backtrack.push(next); + } + } + } + + if (count != topology.size()){ + return ESP_ERR_INVALID_STATE; + } + + + return ESP_OK; +} + +esp_err_t TopologyManager::get_curr_topology(std::unordered_map>>& topology){ + if (!ready){ + return ESP_ERR_INVALID_STATE; + } + + topology.clear(); + + for (const auto& [key, value] : this->topology){ + topology[key] = std::vector>(); + for (const Topology::ChannelBoardConn c : value){ + topology[key].push_back(std::pair(c.channel(), c.board_id())); + } + } + + return ESP_OK; +} // esp_err_t TopologyManager::write_nvs_topology(){ // if (!ready){ diff --git a/components/movements/include/TopologyManager.h b/components/movements/include/TopologyManager.h index f036f79..9bcab08 100644 --- a/components/movements/include/TopologyManager.h +++ b/components/movements/include/TopologyManager.h @@ -1,31 +1,36 @@ #include "MovementManager.h" + +#ifdef MOVEMENTS #include "nvs_flash.h" #include +#include // move these to constants header file? #include "RMTManager.h" #include "DataLinkManager.h" #include "Frames.h" - -#ifdef MOVEMENTS -#pragma once +#include "TopologyBuilder.h" +#include +#include +#include class TopologyManager { public: TopologyManager(); ~TopologyManager(); - // esp_err_t add_board_to_topology(struct NeighbourBlob& blob); - // esp_err_t remove_board_from_topology(uint16_t board_id); - // esp_err_t get_board_in_topology(struct NeighbourBlob& blob); - // esp_err_t verify_topology(); - // esp_err_t get_curr_topology(struct Topology& topology); - // esp_err_t write_nvs_topology(); - // esp_err_t get_nvs_topology(struct Topology& topology); + esp_err_t add_board_to_topology(std::vector>& connections, uint16_t curr_board_id); + esp_err_t remove_board_from_topology(uint16_t board_id); + esp_err_t get_board_in_topology(std::vector>& connections, uint16_t curr_board_id); + esp_err_t verify_topology(); + esp_err_t get_curr_topology(std::unordered_map>>& topology); + esp_err_t write_nvs_topology(); + esp_err_t get_nvs_topology(std::unordered_map>& topology); private: - // std::unordered_map topology; + std::unordered_map> topology; nvs_handle_t handle; bool ready = false; + Flatbuffers::TopologyBuilder builder; }; #endif //MOVEMENTS \ No newline at end of file diff --git a/main/README.md b/main/README.md index 44aa224..215a013 100644 --- a/main/README.md +++ b/main/README.md @@ -12,6 +12,8 @@ To run the sample code, add the compiler variable `RMT_TEST=1` when compiling: ` With `main_rmt_test.cpp`, flash one or more boards, ensuring that `BOARD_ID` are unique and `RECEIVER_BOARD_ID` is a board that exists and is connected on the same network. +Please note that `SRC_BOARD` and `RECEIVER_BOARD` should be an 8 bit value (that is not 0x0 or 0xFF). + ### Example Usage: Board A: `idf.py -B ./build_A build -D SRC_BOARD=1 -D RECEIVER_BOARD=69 -D RMT_TEST=1 flash monitor -p COM3` @@ -32,6 +34,14 @@ When monitoring both boards, you should see `Received message '%.*s' on channel The specific message being sent can vary depending on the setup (determined at compile time). Simply changing the `snprintf` on the `send_buf` and/or `FrameType` argument of `send()` will change the message/frames being sent (from the perspective of the user). +# Preprogrammed Movements Sample Code + +See `main_preprogrammed_test.cpp`. + +To run the sample code, add the compiler variable `PP_MOVE=1` when compiling. + +You can use the `SRC_BOARD` compiler variable to set a custom board id. + # cat ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⡷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ diff --git a/main/main.cpp b/main/main.cpp index 8118ebb..04521cc 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -1,4 +1,4 @@ -#if !defined(RMT_TEST) || (defined(RMT_TEST) && RMT_TEST == 0) +#if (!defined(RMT_TEST) || (defined(RMT_TEST) && RMT_TEST == 0)) && (!defined(PP_MOVE) || (defined(PP_MOVE) && PP_MOVE == 0)) // #include // #include diff --git a/main/main_preprogrammed_test.cpp b/main/main_preprogrammed_test.cpp new file mode 100644 index 0000000..392e1e6 --- /dev/null +++ b/main/main_preprogrammed_test.cpp @@ -0,0 +1,48 @@ +#if (defined(RMT_TEST) && RMT_TEST == 0) && (!defined(PP_MOVE) || (defined(PP_MOVE) && PP_MOVE == 1)) +#include +#include + +#include "sdkconfig.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_flash.h" +#include "nvs_flash.h" + +#include "constants/datalink.h" +#include "TopologyManager.h" + +#include +#include +#include +#include "driver/gptimer.h" +#include "esp_log.h" + +#if !defined(SRC_BOARD) +const uint8_t BOARD_ID = 1; +#else +const uint8_t BOARD_ID = SRC_BOARD +#endif + +extern "C" [[noreturn]] void app_main(void) { + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + esp_netif_init(); + esp_event_loop_create_default(); + + printf("finished esp init\n"); + + printf("Hello world!\n"); + + TopologyManager obj = TopologyManager(); + + while(true){ + //do nothing + vTaskDelay(1000 / portTICK_PERIOD_MS); + } +} +#endif \ No newline at end of file diff --git a/main/main_rmt_test.cpp b/main/main_rmt_test.cpp index 7bc05da..47cf2b7 100644 --- a/main/main_rmt_test.cpp +++ b/main/main_rmt_test.cpp @@ -1,4 +1,4 @@ -#if defined(RMT_TEST) && RMT_TEST == 1 +#if (defined(RMT_TEST) && RMT_TEST == 1) && (!defined(PP_MOVE) || (defined(PP_MOVE) && PP_MOVE == 0)) //Used for link layer testing (change name to main.cpp to use) #include #include From b83e5c08032e89ca645f4e8c095df0641a19d12c Mon Sep 17 00:00:00 2001 From: superkor Date: Sat, 24 Jan 2026 02:05:56 -0500 Subject: [PATCH 09/11] able to add, verify, remove, and get current topology --- components/flatbuffers/CMakeLists.txt | 10 +- components/movements/TopologyManager.cpp | 20 ++- .../movements/include/TopologyManager.h | 4 +- main/CMakeLists.txt | 9 + main/main_preprogrammed_test.cpp | 156 +++++++++++++++++- 5 files changed, 189 insertions(+), 10 deletions(-) diff --git a/components/flatbuffers/CMakeLists.txt b/components/flatbuffers/CMakeLists.txt index 85f614f..cee0c94 100644 --- a/components/flatbuffers/CMakeLists.txt +++ b/components/flatbuffers/CMakeLists.txt @@ -1,2 +1,8 @@ -idf_component_register(SRCS "MPIMessageBuilder.cpp" "AngleControlMessageBuilder.cpp" "TopologyMessageBuilder.cpp" "SensorMessageBuilder.cpp" REQUIRES "constants" - INCLUDE_DIRS "include") +idf_component_register(SRCS "MPIMessageBuilder.cpp" + "AngleControlMessageBuilder.cpp" + "TopologyMessageBuilder.cpp" + "SensorMessageBuilder.cpp" + "TopologyBuilder.cpp" + REQUIRES "constants" + INCLUDE_DIRS "include" +) diff --git a/components/movements/TopologyManager.cpp b/components/movements/TopologyManager.cpp index c010f9e..7199263 100644 --- a/components/movements/TopologyManager.cpp +++ b/components/movements/TopologyManager.cpp @@ -35,16 +35,18 @@ TopologyManager::~TopologyManager(){ * * 4. All `board_id` is not unique in the vector - Should be a 1:1 connection between the boards */ -esp_err_t TopologyManager::add_board_to_topology(std::vector>& connections, uint16_t curr_board_id){ +esp_err_t TopologyManager::add_board_to_topology(const std::vector>& connections, uint16_t curr_board_id){ if (!ready){ return ESP_ERR_INVALID_STATE; } if (connections.size() == 0 || connections.size() > MAX_CHANNELS){ + ESP_LOGE(TOPOLOGY_DEBUG_TAG, "vector size is invalid. got size %d", connections.size()); return ESP_ERR_INVALID_ARG; } if (curr_board_id == PC_ADDR || curr_board_id == BROADCAST_ADDR){ + ESP_LOGE(TOPOLOGY_DEBUG_TAG, "curr_board_id is invalid. got size %d", curr_board_id); return ESP_ERR_INVALID_ARG; } @@ -54,8 +56,9 @@ esp_err_t TopologyManager::add_board_to_topology(std::vector conns; for (const std::pair& pair : connections){ - if (pair.second >= MAX_CHANNELS || pair.first == curr_board_id || pair.first == PC_ADDR - || pair.first == BROADCAST_ADDR || check_set.find(pair.first) != check_set.end()){ + if (pair.first >= MAX_CHANNELS || pair.second == curr_board_id || pair.second == PC_ADDR + || pair.second == BROADCAST_ADDR || check_set.find(pair.first) != check_set.end()){ + ESP_LOGE(TOPOLOGY_DEBUG_TAG, "Invalid pair detected. Got: %d -> %d on Ch. %d", curr_board_id, pair.second, pair.first); return ESP_ERR_INVALID_ARG; } @@ -123,6 +126,7 @@ esp_err_t TopologyManager::verify_topology(){ for (const auto& pair : topology){ for (const Topology::ChannelBoardConn& conn : pair.second){ if (topology.find(conn.board_id()) == topology.end()){ + ESP_LOGE(TOPOLOGY_DEBUG_TAG, "Could not find board %d in topology", conn.board_id()); return ESP_ERR_INVALID_STATE; //could not find reciprocal board } @@ -135,6 +139,7 @@ esp_err_t TopologyManager::verify_topology(){ } } if (!found){ + ESP_LOGE(TOPOLOGY_DEBUG_TAG, "Board %d does not have a connection to board %d", conn.board_id(), pair.first); return ESP_ERR_INVALID_STATE; //reciprocal board does not have a connection back to `pair.first` } @@ -152,6 +157,8 @@ esp_err_t TopologyManager::verify_topology(){ uint16_t curr_node = backtrack.top(); backtrack.pop(); + // printf("On board %d\n", curr_node); + if (visited.find(curr_node) != visited.end()) { continue; } @@ -161,19 +168,22 @@ esp_err_t TopologyManager::verify_topology(){ const std::vector& conns = topology[curr_node]; + // printf("Board %d has %d connections\n", curr_node, conns.size()); + for (const Topology::ChannelBoardConn& conn : conns) { uint16_t next = conn.board_id(); - if (visited.find(next) != visited.end()) { + if (visited.find(next) == visited.end()) { + // printf("Found new board %d\n", next); backtrack.push(next); } } } if (count != topology.size()){ + ESP_LOGE(TOPOLOGY_DEBUG_TAG, "Not all boards could be traversed to all other boards. Got %d out of total %d boards", count, topology.size()); return ESP_ERR_INVALID_STATE; } - return ESP_OK; } diff --git a/components/movements/include/TopologyManager.h b/components/movements/include/TopologyManager.h index 9bcab08..a87c1bb 100644 --- a/components/movements/include/TopologyManager.h +++ b/components/movements/include/TopologyManager.h @@ -14,11 +14,13 @@ #include #include +#define TOPOLOGY_DEBUG_TAG "topology" + class TopologyManager { public: TopologyManager(); ~TopologyManager(); - esp_err_t add_board_to_topology(std::vector>& connections, uint16_t curr_board_id); + esp_err_t add_board_to_topology(const std::vector>& connections, uint16_t curr_board_id); esp_err_t remove_board_from_topology(uint16_t board_id); esp_err_t get_board_in_topology(std::vector>& connections, uint16_t curr_board_id); esp_err_t verify_topology(); diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index a50cdc1..be031a9 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -8,11 +8,20 @@ if(DEFINED RMT_TEST AND RMT_TEST EQUAL 1) message(STATUS "Building for testing RMT/Link Layer Functionality with main_rmt_test.cpp") list(REMOVE_ITEM ALL_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/main.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/main_preprogrammed_test.cpp" ) idf_build_set_property(COMPILE_DEFINITIONS RMT_TEST APPEND) +elseif(DEFINED PP_MOVE AND PP_MOVE EQUAL 1) + message(STATUS "Building for testing preprogrammed movements") + list(REMOVE_ITEM ALL_SRCS + "${CMAKE_CURRENT_SOURCE_DIR}/main.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/main_rmt_test.cpp" + ) + idf_build_set_property(COMPILE_DEFINITIONS PP_MOVE APPEND) else() list(REMOVE_ITEM ALL_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/main_rmt_test.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/main_preprogrammed_test.cpp" ) endif() diff --git a/main/main_preprogrammed_test.cpp b/main/main_preprogrammed_test.cpp index 392e1e6..2b36d7c 100644 --- a/main/main_preprogrammed_test.cpp +++ b/main/main_preprogrammed_test.cpp @@ -1,4 +1,4 @@ -#if (defined(RMT_TEST) && RMT_TEST == 0) && (!defined(PP_MOVE) || (defined(PP_MOVE) && PP_MOVE == 1)) +#if (defined(PP_MOVE) && PP_MOVE == 1) && (!defined(RMT_TEST) || (defined(RMT_TEST) && RMT_TEST == 0)) #include #include @@ -16,12 +16,25 @@ #include #include "driver/gptimer.h" #include "esp_log.h" +#include #if !defined(SRC_BOARD) const uint8_t BOARD_ID = 1; #else -const uint8_t BOARD_ID = SRC_BOARD +const uint8_t BOARD_ID = SRC_BOARD; #endif +#define MOVEMENT_DEBUG_TAG "movement" + +[[noreturn]] void restart(){ + for (int i = 5; i >= 0; i--) { + printf("Restarting in %d seconds...\n", i); + vTaskDelay(1000 / portTICK_PERIOD_MS); + } + printf("Restarting now.\n"); + fflush(stdout); + esp_restart(); +} + extern "C" [[noreturn]] void app_main(void) { esp_err_t ret = nvs_flash_init(); @@ -39,7 +52,146 @@ extern "C" [[noreturn]] void app_main(void) { printf("Hello world!\n"); TopologyManager obj = TopologyManager(); + + /** + * @brief test topology + * + * Board | Channel | Connected to + * 1 | 0 | 2 + * 1 | 1 | 100 + * 2 | 0 | 1 + * 2 | 1 | 4 + * 2 | 2 | 7 + * 2 | 3 | 69 + * 4 | 2 | 2 + * 4 | 3 | 100 + * 7 | 0 | 69 + * 7 | 1 | 2 + * 69 | 0 | 2 + * 69 | 1 | 7 + * 100 | 0 | 4 + * 100 | 1 | 1 + */ + + uint8_t succ_count = 0; + + std::vector>, uint16_t>> test_connections = { + { { {0, 2}, {1, 100} }, 1 }, + { { {0, 1}, {1, 4}, {2, 7}, {3, 69} }, 2 }, + { { {2, 2}, {3, 100} }, 4 }, + { { {0, 69}, {1, 2} }, 7 }, + { { {0, 2}, {1, 7} }, 69 }, + { { {0, 4}, {1, 1} }, 100 } + }; + + for (const auto& connections : test_connections){ + ESP_LOGI(MOVEMENT_DEBUG_TAG, "Adding board %d...", connections.second); + ESP_LOGI(MOVEMENT_DEBUG_TAG, "Connections:"); + + for (const auto& c : connections.first){ + ESP_LOGI(MOVEMENT_DEBUG_TAG, "curr board %d -> board %d on channel %d", connections.second, c.second, c.first); + } + + if (obj.add_board_to_topology(connections.first, connections.second) == ESP_OK) succ_count++; + else{ + ESP_LOGE(MOVEMENT_DEBUG_TAG, "Failed to add board %d", connections.second); + } + } + ESP_LOGI(MOVEMENT_DEBUG_TAG, "%d board connections successfully created out of %d", succ_count, test_connections.size()); + + if (succ_count != test_connections.size()){ + restart(); + } + + succ_count = 0; + + esp_err_t res; + + for (const auto& connections : test_connections){ + std::vector> check; + res = obj.get_board_in_topology(check, connections.second); + + if (res != ESP_OK){ + continue; + } + + if (check.size() != connections.first.size()){ + continue; + } + + bool broke = false; + for (uint8_t i = 0; i < check.size(); i++){ + if (check.at(i) != connections.first.at(i)){ + broke = true; + ESP_LOGE(MOVEMENT_DEBUG_TAG, "Board %d in topology is not correct! Failed at connection %d", connections.second, i); + break; + } + } + + if (!broke){ + succ_count++; + } + } + + ESP_LOGI(MOVEMENT_DEBUG_TAG, "%d board connections successfully verified out of %d", succ_count, test_connections.size()); + + res = obj.verify_topology(); + + if (res != ESP_OK){ + ESP_LOGE(MOVEMENT_DEBUG_TAG, "Topology verification incorrectly failed"); + } else { + ESP_LOGI(MOVEMENT_DEBUG_TAG, "Topology verification passed"); + } + + //test removing a board from topology + + uint16_t board_id_to_remove = 2; + + if (obj.remove_board_from_topology(board_id_to_remove) != ESP_OK){ + ESP_LOGE(MOVEMENT_DEBUG_TAG, "Failed to remove board %d", board_id_to_remove); + restart(); + } + + ESP_LOGI(MOVEMENT_DEBUG_TAG, "Successfully removed board %d", board_id_to_remove); + + res = obj.verify_topology(); + + if (res == ESP_OK){ + ESP_LOGE(MOVEMENT_DEBUG_TAG, "Topology verification incorrectly passed"); + } else { + ESP_LOGI(MOVEMENT_DEBUG_TAG, "Topology verification correctly failed"); + } + + std::unordered_map>> test_topology; + + if (obj.get_curr_topology(test_topology) != ESP_OK){ + ESP_LOGE(MOVEMENT_DEBUG_TAG, "Getting current topology failed"); + restart(); + } + + for (const auto& connections : test_connections){ + if (connections.second == board_id_to_remove){ + continue; + } + + const auto& test = connections.first; + + if (test_topology.find(connections.second) == test_topology.end()){ + ESP_LOGE(MOVEMENT_DEBUG_TAG, "Failed to find board %d in topology", connections.second); + restart(); + } + + if (test.size() != test_topology[connections.second].size()){ + ESP_LOGE(MOVEMENT_DEBUG_TAG, "Board %d connections are different in topology", connections.second); + restart(); + } + } + + ESP_LOGI(MOVEMENT_DEBUG_TAG, "Current topology is correct"); + + restart(); + while(true){ //do nothing vTaskDelay(1000 / portTICK_PERIOD_MS); From 60b94b16508e08d3329a5041afd939a707404f49 Mon Sep 17 00:00:00 2001 From: superkor Date: Sat, 24 Jan 2026 22:17:27 -0500 Subject: [PATCH 10/11] able to write and read from nvs; completed topology test main --- components/flatbuffers/TopologyBuilder.cpp | 47 +++++++-- .../flatbuffers/include/TopologyBuilder.h | 6 +- components/movements/TopologyManager.cpp | 96 +++++++++++++++--- .../movements/include/MovementManager.h | 1 + .../movements/include/TopologyManager.h | 8 +- main/README.md | 4 + main/images/sample_topology.png | Bin 0 -> 66709 bytes main/main_preprogrammed_test.cpp | 50 ++++++++- 8 files changed, 181 insertions(+), 31 deletions(-) create mode 100644 main/images/sample_topology.png diff --git a/components/flatbuffers/TopologyBuilder.cpp b/components/flatbuffers/TopologyBuilder.cpp index d87daba..8ea5319 100644 --- a/components/flatbuffers/TopologyBuilder.cpp +++ b/components/flatbuffers/TopologyBuilder.cpp @@ -1,23 +1,48 @@ #include "TopologyBuilder.h" #ifdef TOPOLOGYBUILDER namespace Flatbuffers{ - SerializedMessage TopologyBuilder::build_topology(const std::vector>& topology){ + SerializedMessage TopologyBuilder::build_topology(const std::unordered_map>& topology_map) { builder_.Clear(); - - auto neighbours_vec = builder_.CreateVector(topology); - auto topology_offset = Topology::CreateTopologyInfo(builder_, static_cast(topology.size()), neighbours_vec); + + std::vector> neighbours; + neighbours.reserve(topology_map.size()); + + for (const auto& [board_id, connections] : topology_map) { + auto conn_vec = builder_.CreateVectorOfStructs(connections); + auto blob = Topology::CreateNeighbourBlob( + builder_, + board_id, + static_cast(connections.size()), + conn_vec + ); + + neighbours.push_back(blob); + } + + auto neighbours_vec = builder_.CreateVector(neighbours); + auto topology_offset = Topology::CreateTopologyInfo( + builder_, + static_cast(neighbours.size()), + neighbours_vec + ); builder_.Finish(topology_offset); - return { - builder_.GetBufferPointer(), - builder_.GetSize() - }; + return { builder_.GetBufferPointer(), builder_.GetSize() }; } - flatbuffers::Offset TopologyBuilder::build_neighbour_info(uint16_t board_id, const std::vector& connections){ - auto conn = builder_.CreateVectorOfStructs(connections); - return Topology::CreateNeighbourBlob(builder_, board_id, static_cast(connections.size()), conn); + flatbuffers::Offset TopologyBuilder::build_neighbour_info( + uint16_t board_id, + const std::vector& connections + ) { + auto conn_vec = builder_.CreateVectorOfStructs(connections); + + return Topology::CreateNeighbourBlob( + builder_, + board_id, + static_cast(connections.size()), + conn_vec + ); } Topology::ChannelBoardConn TopologyBuilder::build_connections(uint8_t channel, uint16_t board_id){ diff --git a/components/flatbuffers/include/TopologyBuilder.h b/components/flatbuffers/include/TopologyBuilder.h index 14c98e6..f996818 100644 --- a/components/flatbuffers/include/TopologyBuilder.h +++ b/components/flatbuffers/include/TopologyBuilder.h @@ -14,11 +14,11 @@ namespace Flatbuffers { public: TopologyBuilder() : builder_(RIP_MAX_ROUTES*(sizeof(Topology::NeighbourBlob) + sizeof(Topology::ChannelBoardConn)*MAX_CHANNELS)) {} - SerializedMessage build_topology(const std::vector>& topology); - flatbuffers::Offset build_neighbour_info(uint16_t board_id, const std::vector& connections); + SerializedMessage build_topology(const std::unordered_map>& topology_map); Topology::ChannelBoardConn build_connections(uint8_t channel, uint16_t board_id); - + private: + flatbuffers::Offset build_neighbour_info(uint16_t board_id, const std::vector& connections); flatbuffers::FlatBufferBuilder builder_; }; } diff --git a/components/movements/TopologyManager.cpp b/components/movements/TopologyManager.cpp index 7199263..3031258 100644 --- a/components/movements/TopologyManager.cpp +++ b/components/movements/TopologyManager.cpp @@ -204,18 +204,90 @@ esp_err_t TopologyManager::get_curr_topology(std::unordered_map(m.size)); + + res = nvs_commit(handle); + if (res != ESP_OK) { + ESP_LOGE(TOPOLOGY_DEBUG_TAG, "Failed to commit data size"); + return res; + } + + res = nvs_set_blob(handle, MOVEMENTS_NVS_TOPOLOGY_KEY, m.data, m.size); + + if (res != ESP_OK){ + ESP_LOGE(TOPOLOGY_DEBUG_TAG, "Failed to write to nvs"); + return res; + } + + res = nvs_commit(handle); + if (res != ESP_OK) { + ESP_LOGE(TOPOLOGY_DEBUG_TAG, "Failed to commit topology"); + return res; + } + + return ESP_OK; +} + +esp_err_t TopologyManager::get_nvs_topology(std::unordered_map>>& topology){ + if (!ready) { + return ESP_ERR_INVALID_STATE; + } + + size_t size = 0; + esp_err_t res = nvs_get_u32(handle,MOVEMENTS_NVS_TOPOLOGY_DATA_SIZE_KEY, reinterpret_cast(&size)); + + if (res != ESP_OK) { + ESP_LOGE(TOPOLOGY_DEBUG_TAG, "Failed to read data size"); + return res; + } + + std::vector buffer(size); + + res = nvs_get_blob(handle, MOVEMENTS_NVS_TOPOLOGY_KEY, buffer.data(), &size); + + if (res != ESP_OK) { + ESP_LOGE(TOPOLOGY_DEBUG_TAG, "Failed to read blob"); + return res; + } + + // Verify the FlatBuffer + flatbuffers::Verifier verifier(buffer.data(), buffer.size()); + if (!Topology::VerifyTopologyInfoBuffer(verifier)) { + ESP_LOGE(TOPOLOGY_DEBUG_TAG, "FlatBuffer verification failed"); + return ESP_ERR_INVALID_ARG; + } + + // Parse root + const Topology::TopologyInfo* topo = Topology::GetTopologyInfo(buffer.data()); + + topology.clear(); + + // Replace neighbours() with your actual accessor name + auto neighbours = topo->boards(); + + for (const auto* nb : *neighbours) { + uint16_t board_id = nb->curr_board_id(); + + std::vector> connections; + connections.reserve(nb->neighbour_connections()->size()); + + for (const auto* conn : *nb->neighbour_connections()) { + connections.emplace_back(conn->channel(), conn->board_id()); + } + + topology.emplace(board_id, std::move(connections)); + } + + return ESP_OK; +} #endif //MOVEMENTS \ No newline at end of file diff --git a/components/movements/include/MovementManager.h b/components/movements/include/MovementManager.h index f7ed809..dbc071c 100644 --- a/components/movements/include/MovementManager.h +++ b/components/movements/include/MovementManager.h @@ -6,6 +6,7 @@ #define MOVEMENTS_NVS_NAMESPACE "movements" #define MOVEMENTS_NVS_KEY "key" #define MOVEMENTS_NVS_TOPOLOGY_KEY "topology" +#define MOVEMENTS_NVS_TOPOLOGY_DATA_SIZE_KEY "data_size" #define MOVEMENTS_DEBUG_TAG "movements" class MovementManager { diff --git a/components/movements/include/TopologyManager.h b/components/movements/include/TopologyManager.h index a87c1bb..94ebf6d 100644 --- a/components/movements/include/TopologyManager.h +++ b/components/movements/include/TopologyManager.h @@ -5,9 +5,9 @@ #include #include -// move these to constants header file? -#include "RMTManager.h" -#include "DataLinkManager.h" +#include "constants/rmt.h" +#include "constants/datalink.h" + #include "Frames.h" #include "TopologyBuilder.h" #include @@ -26,7 +26,7 @@ class TopologyManager { esp_err_t verify_topology(); esp_err_t get_curr_topology(std::unordered_map>>& topology); esp_err_t write_nvs_topology(); - esp_err_t get_nvs_topology(std::unordered_map>& topology); + esp_err_t get_nvs_topology(std::unordered_map>>& topology); private: std::unordered_map> topology; diff --git a/main/README.md b/main/README.md index 215a013..0e1298e 100644 --- a/main/README.md +++ b/main/README.md @@ -42,6 +42,10 @@ To run the sample code, add the compiler variable `PP_MOVE=1` when compiling. You can use the `SRC_BOARD` compiler variable to set a custom board id. +The sample topology used to test: + +![Sample Topology Used for Testing](images/sample_topology.png) + # cat ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⡷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ diff --git a/main/images/sample_topology.png b/main/images/sample_topology.png new file mode 100644 index 0000000000000000000000000000000000000000..54504a6073df253fbb7e4ad23ff2cded686bf57d GIT binary patch literal 66709 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 literal 0 HcmV?d00001 diff --git a/main/main_preprogrammed_test.cpp b/main/main_preprogrammed_test.cpp index 2b36d7c..0d73426 100644 --- a/main/main_preprogrammed_test.cpp +++ b/main/main_preprogrammed_test.cpp @@ -44,7 +44,6 @@ extern "C" [[noreturn]] void app_main(void) { } ESP_ERROR_CHECK(ret); - esp_netif_init(); esp_event_loop_create_default(); printf("finished esp init\n"); @@ -140,10 +139,59 @@ extern "C" [[noreturn]] void app_main(void) { if (res != ESP_OK){ ESP_LOGE(MOVEMENT_DEBUG_TAG, "Topology verification incorrectly failed"); + restart(); } else { ESP_LOGI(MOVEMENT_DEBUG_TAG, "Topology verification passed"); } + res = obj.write_nvs_topology(); + + if (res == ESP_OK){ + ESP_LOGI(MOVEMENT_DEBUG_TAG, "Topology NVS write success"); + } else { + ESP_LOGE(MOVEMENT_DEBUG_TAG, "Topology NVS write failed"); + restart(); + } + + std::unordered_map>> nvs_topology; + + res = obj.get_nvs_topology(nvs_topology); + if (res == ESP_OK){ + ESP_LOGI(MOVEMENT_DEBUG_TAG, "Topology NVS read success"); + } else { + ESP_LOGE(MOVEMENT_DEBUG_TAG, "Topology NVS read failed"); + restart(); + } + + if (nvs_topology.size() != test_connections.size()){ + ESP_LOGE(MOVEMENT_DEBUG_TAG, "Topology NVS size is different"); + restart(); + } + + for (const auto& connections : test_connections){ + const auto& test = connections.first; + + if (nvs_topology.find(connections.second) == nvs_topology.end()){ + ESP_LOGE(MOVEMENT_DEBUG_TAG, "Failed to find board %d in topology", connections.second); + restart(); + } + + if (test.size() != nvs_topology[connections.second].size()){ + ESP_LOGE(MOVEMENT_DEBUG_TAG, "Board %d connections are different in topology", connections.second); + restart(); + } + } + + ESP_LOGI(MOVEMENT_DEBUG_TAG, "Topology NVS R/W success! Got back the same topology"); + + ESP_LOGI(MOVEMENT_DEBUG_TAG, "Topology NVS got:"); + + for (const auto& [key, connections] : nvs_topology){ + for (const auto& c : connections){ + ESP_LOGI(MOVEMENT_DEBUG_TAG, "Board %d -> board %d on channel %d", key, c.second, c.first); + } + } + //test removing a board from topology uint16_t board_id_to_remove = 2; From 44af33921bf6588a170a6ee7e6461c5197bdb878 Mon Sep 17 00:00:00 2001 From: superkor Date: Tue, 27 Jan 2026 22:17:44 -0500 Subject: [PATCH 11/11] update movement set --- components/flatbuffers/MovementSetBuilder.cpp | 76 +++++++++---- components/flatbuffers/TopologyBuilder.cpp | 16 +-- .../flatbuffers/include/MovementSetBuilder.h | 14 ++- .../flatbuffers/include/TopologyBuilder.h | 1 - .../Movement_generated.h | 102 ++++++++++++++---- components/movements/MovementSetManager.cpp | 89 +++++++++++---- components/movements/TopologyManager.cpp | 49 ++++++++- .../movements/include/MovementSetManager.h | 22 ++-- .../movements/include/TopologyManager.h | 1 + 9 files changed, 278 insertions(+), 92 deletions(-) diff --git a/components/flatbuffers/MovementSetBuilder.cpp b/components/flatbuffers/MovementSetBuilder.cpp index 02d5515..8b9c571 100644 --- a/components/flatbuffers/MovementSetBuilder.cpp +++ b/components/flatbuffers/MovementSetBuilder.cpp @@ -6,40 +6,72 @@ #ifdef MOVEMENTSETBUILDER namespace Flatbuffers { - SerializedMessage MovementSetBuilder::build_movement_set(const std::vector>& set){ - + SerializedMessage MovementSetBuilder::build_movement_set(const std::unordered_map>& input){ builder_.Clear(); - const auto set_vector = builder_.CreateVector(set); + std::vector> map_entries; + map_entries.reserve(input.size()); - const auto message = Movement::CreateMovementSet( - builder_, - static_cast(set.size()), - set_vector + // 1. Collect and sort keys (DO NOT sort offsets) + std::vector keys; + keys.reserve(input.size()); + + for (const auto& [key, _] : input) { + keys.push_back(key); + } + + std::sort(keys.begin(), keys.end()); + + // 2. Build MovementVector entries in key order + for (uint8_t key : keys) { + const auto& entries = input.at(key); + + std::vector> fb_entries; + fb_entries.reserve(entries.size()); + + for (const auto& e : entries) { + fb_entries.push_back( + Movement::CreateMovementEntry( + builder_, + e.board_id, + e.module_type, + e.value_action, + &e.condition, + e.ack, + e.ack_ttl_ms, + e.post_delay_ms + ) + ); + } + + auto movements_vector = builder_.CreateVector(fb_entries); + + map_entries.push_back( + Movement::CreateMovementVector( + builder_, + key, + movements_vector + ) + ); + } + + // 3. Build the map vector + auto map_vector = builder_.CreateVector(map_entries); + + auto message = Movement::CreateMovementSet( + builder_, + map_vector ); builder_.Finish(message); - return {builder_.GetBufferPointer(), builder_.GetSize()}; - } - - flatbuffers::Offset MovementSetBuilder::build_movement_entry(uint16_t board_id, uint8_t module_type, uint16_t value_action, - const Movement::ConditionBlob& condition, uint8_t ack, uint16_t ack_ttl_ms, uint16_t post_delay_ms){ - return Movement::CreateMovementEntry( - builder_, - board_id, - module_type, - value_action, - &condition, - ack, - ack_ttl_ms, - post_delay_ms - ); + return { builder_.GetBufferPointer(), builder_.GetSize() }; } Movement::ConditionBlob MovementSetBuilder::build_condition_blob(uint16_t value, uint8_t cond, uint8_t module_type, uint8_t in_use){ return Movement::ConditionBlob(value, cond, module_type, in_use); } + } #endif //MOVEMENTSETBUILDER diff --git a/components/flatbuffers/TopologyBuilder.cpp b/components/flatbuffers/TopologyBuilder.cpp index 8ea5319..5d07a55 100644 --- a/components/flatbuffers/TopologyBuilder.cpp +++ b/components/flatbuffers/TopologyBuilder.cpp @@ -30,21 +30,7 @@ namespace Flatbuffers{ return { builder_.GetBufferPointer(), builder_.GetSize() }; } - - flatbuffers::Offset TopologyBuilder::build_neighbour_info( - uint16_t board_id, - const std::vector& connections - ) { - auto conn_vec = builder_.CreateVectorOfStructs(connections); - - return Topology::CreateNeighbourBlob( - builder_, - board_id, - static_cast(connections.size()), - conn_vec - ); - } - + Topology::ChannelBoardConn TopologyBuilder::build_connections(uint8_t channel, uint16_t board_id){ return Topology::ChannelBoardConn(channel, board_id); } diff --git a/components/flatbuffers/include/MovementSetBuilder.h b/components/flatbuffers/include/MovementSetBuilder.h index cbf5af6..eb63a2c 100644 --- a/components/flatbuffers/include/MovementSetBuilder.h +++ b/components/flatbuffers/include/MovementSetBuilder.h @@ -11,14 +11,22 @@ #include "flatbuffers_generated/Movement_generated.h" #include "flatbuffers/flatbuffers.h" +struct MovementEntryInput { + uint16_t board_id; + uint8_t module_type; + uint16_t value_action; + Movement::ConditionBlob condition; + uint8_t ack; + uint16_t ack_ttl_ms; + uint16_t post_delay_ms; +}; + namespace Flatbuffers { class MovementSetBuilder { public: MovementSetBuilder() : builder_(MAX_MOVEMENTS_IN_SET*sizeof(Movement::MovementEntry)) {} - SerializedMessage build_movement_set(const std::vector>& set); - flatbuffers::Offset build_movement_entry(uint16_t board_id, uint8_t module_type, uint16_t value_action, - const Movement::ConditionBlob& condition, uint8_t ack, uint16_t ack_ttl_ms, uint16_t post_delay_ms); + SerializedMessage build_movement_set(const std::unordered_map>& input); Movement::ConditionBlob build_condition_blob(uint16_t value, uint8_t cond, uint8_t module_type, uint8_t in_use); private: diff --git a/components/flatbuffers/include/TopologyBuilder.h b/components/flatbuffers/include/TopologyBuilder.h index f996818..773d8ef 100644 --- a/components/flatbuffers/include/TopologyBuilder.h +++ b/components/flatbuffers/include/TopologyBuilder.h @@ -18,7 +18,6 @@ namespace Flatbuffers { Topology::ChannelBoardConn build_connections(uint8_t channel, uint16_t board_id); private: - flatbuffers::Offset build_neighbour_info(uint16_t board_id, const std::vector& connections); flatbuffers::FlatBufferBuilder builder_; }; } diff --git a/components/flatbuffers/include/flatbuffers_generated/Movement_generated.h b/components/flatbuffers/include/flatbuffers_generated/Movement_generated.h index b6e8010..58c2499 100644 --- a/components/flatbuffers/include/flatbuffers_generated/Movement_generated.h +++ b/components/flatbuffers/include/flatbuffers_generated/Movement_generated.h @@ -20,6 +20,9 @@ struct ConditionBlob; struct MovementEntry; struct MovementEntryBuilder; +struct MovementVector; +struct MovementVectorBuilder; + struct MovementSet; struct MovementSetBuilder; @@ -165,14 +168,20 @@ inline ::flatbuffers::Offset CreateMovementEntry( return builder_.Finish(); } -struct MovementSet FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { - typedef MovementSetBuilder Builder; +struct MovementVector FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef MovementVectorBuilder Builder; enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { - VT_NUM_MOVEMENTS = 4, + VT_KEY = 4, VT_MOVEMENTS = 6 }; - uint8_t num_movements() const { - return GetField(VT_NUM_MOVEMENTS, 0); + uint8_t key() const { + return GetField(VT_KEY, 0); + } + bool KeyCompareLessThan(const MovementVector * const o) const { + return key() < o->key(); + } + int KeyCompareWithValue(uint8_t _key) const { + return static_cast(key() > _key) - static_cast(key() < _key); } const ::flatbuffers::Vector<::flatbuffers::Offset> *movements() const { return GetPointer> *>(VT_MOVEMENTS); @@ -180,7 +189,7 @@ struct MovementSet FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { template bool Verify(::flatbuffers::VerifierTemplate &verifier) const { return VerifyTableStart(verifier) && - VerifyField(verifier, VT_NUM_MOVEMENTS, 1) && + VerifyField(verifier, VT_KEY, 1) && VerifyOffset(verifier, VT_MOVEMENTS) && verifier.VerifyVector(movements()) && verifier.VerifyVectorOfTables(movements()) && @@ -188,15 +197,72 @@ struct MovementSet FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { } }; +struct MovementVectorBuilder { + typedef MovementVector Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_key(uint8_t key) { + fbb_.AddElement(MovementVector::VT_KEY, key, 0); + } + void add_movements(::flatbuffers::Offset<::flatbuffers::Vector<::flatbuffers::Offset>> movements) { + fbb_.AddOffset(MovementVector::VT_MOVEMENTS, movements); + } + explicit MovementVectorBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateMovementVector( + ::flatbuffers::FlatBufferBuilder &_fbb, + uint8_t key = 0, + ::flatbuffers::Offset<::flatbuffers::Vector<::flatbuffers::Offset>> movements = 0) { + MovementVectorBuilder builder_(_fbb); + builder_.add_movements(movements); + builder_.add_key(key); + return builder_.Finish(); +} + +inline ::flatbuffers::Offset CreateMovementVectorDirect( + ::flatbuffers::FlatBufferBuilder &_fbb, + uint8_t key = 0, + const std::vector<::flatbuffers::Offset> *movements = nullptr) { + auto movements__ = movements ? _fbb.CreateVector<::flatbuffers::Offset>(*movements) : 0; + return Movement::CreateMovementVector( + _fbb, + key, + movements__); +} + +struct MovementSet FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef MovementSetBuilder Builder; + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_MOVEMENT_MAP = 4 + }; + const ::flatbuffers::Vector<::flatbuffers::Offset> *movement_map() const { + return GetPointer> *>(VT_MOVEMENT_MAP); + } + template + bool Verify(::flatbuffers::VerifierTemplate &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_MOVEMENT_MAP) && + verifier.VerifyVector(movement_map()) && + verifier.VerifyVectorOfTables(movement_map()) && + verifier.EndTable(); + } +}; + struct MovementSetBuilder { typedef MovementSet Table; ::flatbuffers::FlatBufferBuilder &fbb_; ::flatbuffers::uoffset_t start_; - void add_num_movements(uint8_t num_movements) { - fbb_.AddElement(MovementSet::VT_NUM_MOVEMENTS, num_movements, 0); - } - void add_movements(::flatbuffers::Offset<::flatbuffers::Vector<::flatbuffers::Offset>> movements) { - fbb_.AddOffset(MovementSet::VT_MOVEMENTS, movements); + void add_movement_map(::flatbuffers::Offset<::flatbuffers::Vector<::flatbuffers::Offset>> movement_map) { + fbb_.AddOffset(MovementSet::VT_MOVEMENT_MAP, movement_map); } explicit MovementSetBuilder(::flatbuffers::FlatBufferBuilder &_fbb) : fbb_(_fbb) { @@ -211,23 +277,19 @@ struct MovementSetBuilder { inline ::flatbuffers::Offset CreateMovementSet( ::flatbuffers::FlatBufferBuilder &_fbb, - uint8_t num_movements = 0, - ::flatbuffers::Offset<::flatbuffers::Vector<::flatbuffers::Offset>> movements = 0) { + ::flatbuffers::Offset<::flatbuffers::Vector<::flatbuffers::Offset>> movement_map = 0) { MovementSetBuilder builder_(_fbb); - builder_.add_movements(movements); - builder_.add_num_movements(num_movements); + builder_.add_movement_map(movement_map); return builder_.Finish(); } inline ::flatbuffers::Offset CreateMovementSetDirect( ::flatbuffers::FlatBufferBuilder &_fbb, - uint8_t num_movements = 0, - const std::vector<::flatbuffers::Offset> *movements = nullptr) { - auto movements__ = movements ? _fbb.CreateVector<::flatbuffers::Offset>(*movements) : 0; + std::vector<::flatbuffers::Offset> *movement_map = nullptr) { + auto movement_map__ = movement_map ? _fbb.CreateVectorOfSortedTables(movement_map) : 0; return Movement::CreateMovementSet( _fbb, - num_movements, - movements__); + movement_map__); } inline const Movement::MovementSet *GetMovementSet(const void *buf) { diff --git a/components/movements/MovementSetManager.cpp b/components/movements/MovementSetManager.cpp index 53d288e..6ab6fa0 100644 --- a/components/movements/MovementSetManager.cpp +++ b/components/movements/MovementSetManager.cpp @@ -9,38 +9,85 @@ MovementSetManager::MovementSetManager(){ ESP_LOGE(MOVEMENTS_DEBUG_TAG, "Failed to open nvs for %s", MOVEMENTS_NVS_NAMESPACE); return; } + + if (!topology_manager->is_ready()){ + ESP_LOGE(MOVEMENTS_DEBUG_TAG, "Topology Manager is not ready!"); + return; + } + + ready = true; }; MovementSetManager::~MovementSetManager(){ nvs_close(handle); } -// esp_err_t MovementSetManager::add_movement(struct MovementEntry& entry, uint8_t& index){ -// return ESP_OK; -// } +esp_err_t MovementSetManager::add_movement(Movement::MovementEntry& entry, uint8_t& index){ + if (!ready){ + return ESP_ERR_INVALID_STATE; + } -// esp_err_t MovementSetManager::remove_movement(uint8_t index){ -// return ESP_OK; -// } + if (entry.board_id() == PC_ADDR || entry.board_id() == BROADCAST_ADDR){ + return ESP_ERR_INVALID_ARG; + } -// esp_err_t MovementSetManager::update_movement(struct MovementEntry& entry, uint8_t index){ -// return ESP_OK; -// } + if (movements.find(index) != movements.end()){ + for (Movement::MovementEntry& e : movements[index]){ + if (e.board_id() == entry.board_id()){ + return ESP_ERR_INVALID_ARG; //only unique board ids per index + } + } + } + + movements[index].push_back(entry); -// esp_err_t MovementSetManager::get_curr_movement_set(struct MovementSet& set){ -// return ESP_OK; -// } + return ESP_OK; +} -// esp_err_t MovementSetManager::verify_movement_set(){ -// return ESP_OK; -// } +esp_err_t MovementSetManager::remove_movement(uint8_t index, uint8_t board_id){ + if (board_id == PC_ADDR || board_id == BROADCAST_ADDR){ + return ESP_ERR_INVALID_ARG; + } -// esp_err_t MovementSetManager::get_nvs_movement_set(struct MovementSet& set){ -// return ESP_OK; -// } + if (movements.find(index) == movements.end()){ + return ESP_ERR_INVALID_ARG; + } -// esp_err_t MovementSetManager::write_nvs_movement_set(){ -// return ESP_OK; -// } + bool found = false; + + for (auto it = movements[index].begin(); it != movements[index].end(); ++it) { + if (it->board_id() == board_id){ + movements[index].erase(it); + found = true; + break; + } + } + + if (!found){ + return ESP_ERR_NOT_FOUND; + } + + return ESP_OK; +} + +esp_err_t MovementSetManager::update_movement(Movement::MovementEntry& entry, uint8_t index){ + return ESP_OK; +} + +esp_err_t MovementSetManager::get_curr_movement_set(Movement::MovementSet& set){ + return ESP_OK; +} + +esp_err_t MovementSetManager::verify_movement_set(){ + return ESP_OK; +} + +esp_err_t MovementSetManager::get_nvs_movement_set(Movement::MovementSet& set){ + return ESP_OK; +} + +esp_err_t MovementSetManager::write_nvs_movement_set(){ + return ESP_OK; +} #endif //MOVEMENTS \ No newline at end of file diff --git a/components/movements/TopologyManager.cpp b/components/movements/TopologyManager.cpp index 3031258..96431a4 100644 --- a/components/movements/TopologyManager.cpp +++ b/components/movements/TopologyManager.cpp @@ -17,6 +17,10 @@ TopologyManager::~TopologyManager(){ nvs_close(handle); } +bool TopologyManager::is_ready(){ + return ready; +} + /** * @brief Creates a `Topology::NeighbourBlob` based on the input parameters and adds the blob to the internal topology map * @@ -72,6 +76,14 @@ esp_err_t TopologyManager::add_board_to_topology(const std::vector>& connections, uint16_t curr_board_id){ if (!ready){ return ESP_ERR_INVALID_STATE; @@ -106,12 +125,23 @@ esp_err_t TopologyManager::get_board_in_topology(std::vector B and B -> A) + * + * 2. The topology contains 2 separate graphs (a board in the topology should be in the same graph with some sort of path to all other boards) + * + * @return esp_err_t + */ esp_err_t TopologyManager::verify_topology(){ if (!ready){ return ESP_ERR_INVALID_STATE; } - if (topology.size() == 0){ + if (topology.size() == 0 || topology.size() >= RIP_MAX_ROUTES){ return ESP_ERR_INVALID_STATE; } @@ -187,6 +217,12 @@ esp_err_t TopologyManager::verify_topology(){ return ESP_OK; } +/** + * @brief Gets the current topology stored in the manager + * + * @param topology + * @return esp_err_t + */ esp_err_t TopologyManager::get_curr_topology(std::unordered_map>>& topology){ if (!ready){ return ESP_ERR_INVALID_STATE; @@ -204,6 +240,11 @@ esp_err_t TopologyManager::get_curr_topology(std::unordered_map>>& topology){ if (!ready) { return ESP_ERR_INVALID_STATE; diff --git a/components/movements/include/MovementSetManager.h b/components/movements/include/MovementSetManager.h index db2b8f7..c286d1a 100644 --- a/components/movements/include/MovementSetManager.h +++ b/components/movements/include/MovementSetManager.h @@ -3,6 +3,8 @@ #include "TopologyManager.h" #include #include +#include "MovementSetBuilder.h" +#include "unordered_map" #ifdef MOVEMENTS #pragma once @@ -11,18 +13,20 @@ class MovementSetManager { public: MovementSetManager(); ~MovementSetManager(); - // esp_err_t add_movement(struct MovementEntry& entry, uint8_t& index); - // esp_err_t remove_movement(uint8_t index); - // esp_err_t update_movement(struct MovementEntry& entry, uint8_t index); - // esp_err_t get_curr_movement_set(struct MovementSet& set); - // esp_err_t verify_movement_set(); - // esp_err_t get_nvs_movement_set(struct MovementSet& set); - // esp_err_t write_nvs_movement_set(); + esp_err_t add_movement(Movement::MovementEntry& entry, uint8_t& index); + esp_err_t remove_movement(uint8_t index, uint8_t board_id); + esp_err_t update_movement(Movement::MovementEntry& entry, uint8_t index); + esp_err_t get_curr_movement_set(Movement::MovementSet& set); + esp_err_t verify_movement_set(); + esp_err_t get_nvs_movement_set(Movement::MovementSet& set); + esp_err_t write_nvs_movement_set(); private: - std::unique_ptr topology_manager; - // std::vector movements; + std::unique_ptr topology_manager = std::make_unique(); + std::unordered_map> movements; nvs_handle_t handle; + bool ready = false; + Flatbuffers::MovementSetBuilder builder; }; #endif //MOVEMENTS \ No newline at end of file diff --git a/components/movements/include/TopologyManager.h b/components/movements/include/TopologyManager.h index 94ebf6d..f4abedc 100644 --- a/components/movements/include/TopologyManager.h +++ b/components/movements/include/TopologyManager.h @@ -20,6 +20,7 @@ class TopologyManager { public: TopologyManager(); ~TopologyManager(); + bool is_ready(); esp_err_t add_board_to_topology(const std::vector>& connections, uint16_t curr_board_id); esp_err_t remove_board_from_topology(uint16_t board_id); esp_err_t get_board_in_topology(std::vector>& connections, uint16_t curr_board_id);