diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9a305d3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +build/ +venv/ +.venv/ + +.vscode/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 934189c..85c4f34 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,8 @@ { - "idf.currentSetup": "/home/iosnaaente/esp/v5.5.1/esp-idf" + "idf.currentSetup": "/home/iosnaaente/esp/v5.5.1/esp-idf", + + "editor.rulers": [ 75, 85 ], + "workbench.colorCustomizations": { + "editorRuler.foreground": "#174117" + }, } \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index e69de29..49ace91 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -0,0 +1,35 @@ +idf_component_register( + SRCS + "src/core/serial_comm_dispatcher.cpp" + "src/core/serial_comm_protocol.cpp" + "src/core/serial_comm_watchdog.cpp" + "src/core/serial_comm_parser.cpp" + "src/core/serial_comm.cpp" + + "src/middleware/serial_comm_transaction_manager.cpp" + "src/middleware/serial_comm_manager_service.cpp" + "src/middleware/serial_comm_manager_action.cpp" + "src/middleware/serial_comm_manager_topic.cpp" + "src/middleware/serial_comm_manager.cpp" + + "src/transport/uart_serial_comm.cpp" + + INCLUDE_DIRS + "include" + "include/serial_comm" + "include/serial_comm/core" + "include/serial_comm/messages" + "include/serial_comm/middleware" + "include/serial_comm/middleware/service" + "include/serial_comm/middleware/action" + "include/serial_comm/middleware/topic" + "include/serial_comm/transport" + + REQUIRES + freertos + esp_timer + driver + log +) + +target_compile_features(${COMPONENT_LIB} PUBLIC cxx_std_17) \ No newline at end of file diff --git a/Kconfig b/Kconfig deleted file mode 100644 index 0189048..0000000 --- a/Kconfig +++ /dev/null @@ -1,98 +0,0 @@ -# -# Serial Communication Middleware Configuration -# - -menu "Serial Communication Middleware" - - config SERIAL_COMM_UART_PORT - int "UART Port" - range 0 2 - default 2 - help - UART peripheral number used by the middleware. - - config SERIAL_COMM_UART_BAUDRATE - int "UART Baudrate" - default 115200 - help - UART communication baudrate. - - config SERIAL_COMM_UART_TX_PIN - int "UART TX GPIO" - range -1 48 - default 17 - help - GPIO used for UART TX. - - config SERIAL_COMM_UART_RX_PIN - int "UART RX GPIO" - range -1 48 - default 16 - help - GPIO used for UART RX. - - config SERIAL_COMM_UART_RTS_PIN - int "UART RTS GPIO" - range -1 48 - default -1 - help - GPIO used for UART RTS. - - config SERIAL_COMM_UART_CTS_PIN - int "UART CTS GPIO" - range -1 48 - default -1 - help - GPIO used for UART CTS. - - config SERIAL_COMM_UART_DE_PIN - int "UART RS485 DE GPIO" - range -1 48 - default -1 - help - GPIO used for RS485 Driver Enable. - - config SERIAL_COMM_UART_RX_BUFFER_SIZE - int "UART RX Buffer Size" - default 1024 - help - UART RX driver buffer size. - - config SERIAL_COMM_UART_TX_BUFFER_SIZE - int "UART TX Buffer Size" - default 1024 - help - UART TX driver buffer size. - - config SERIAL_COMM_UART_EVENT_QUEUE_SIZE - int "UART Event Queue Size" - default 32 - help - UART event queue size. - - config SERIAL_COMM_UART_TASK_STACK_SIZE - int "UART Task Stack Size" - default 4096 - help - Stack size for UART event task. - - config SERIAL_COMM_UART_TASK_PRIORITY - int "UART Task Priority" - default 5 - help - FreeRTOS priority for UART task. - - config SERIAL_COMM_UART_TASK_CORE - int "UART Task Core" - range 0 1 - default 1 - help - CPU core affinity for UART task. - - config SERIAL_COMM_ENABLE_HALF_DUPLEX - bool "Enable Half Duplex Support" - default n - help - Enables RS485 half duplex mode. - -endmenu \ No newline at end of file diff --git a/Kconfig.projbuild b/Kconfig.projbuild new file mode 100644 index 0000000..5b3ab47 --- /dev/null +++ b/Kconfig.projbuild @@ -0,0 +1,219 @@ +menu "SerialComm Middleware" + + menu "Transport Configuration" + + config SERIAL_COMM_UART_PORT + int "UART Port" + range 0 2 + default 2 + + config SERIAL_COMM_UART_BAUDRATE + int "UART Baudrate" + range 9600 1000000 + default 921600 + + config SERIAL_COMM_UART_TX_PIN + int "UART TX GPIO" + range -1 48 + default 17 + + config SERIAL_COMM_UART_RX_PIN + int "UART RX GPIO" + range -1 48 + default 16 + + config SERIAL_COMM_UART_RTS_PIN + int "UART RTS GPIO" + range -1 48 + default -1 + + config SERIAL_COMM_UART_CTS_PIN + int "UART CTS GPIO" + range -1 48 + default -1 + + config SERIAL_COMM_UART_DE_PIN + int "UART RS485 DE GPIO" + range -1 48 + default -1 + + endmenu + + + menu "UART Driver" + + config SERIAL_COMM_UART_RX_BUFFER_SIZE + int "RX Buffer Size" + default 2048 + + config SERIAL_COMM_UART_TX_BUFFER_SIZE + int "TX Buffer Size" + default 2048 + + config SERIAL_COMM_UART_EVENT_QUEUE_SIZE + int "UART Event Queue Size" + default 32 + + endmenu + + + menu "Protocol" + + config SERIAL_COMM_MAX_PAYLOAD_SIZE + int "Maximum Payload Size" + default 512 + + config SERIAL_COMM_ENABLE_CRC + bool "Enable CRC16" + default y + + config SERIAL_COMM_ENABLE_SEQ_ID + bool "Enable Sequence ID" + default y + + endmenu + + + menu "Parser" + + config SERIAL_COMM_ENABLE_INTERBYTE_TIMEOUT + bool "Enable Inter-byte Timeout" + default y + + choice SERIAL_COMM_INTERBYTE_TIMEOUT_MODE + prompt "Inter-byte Timeout Mode" + default SERIAL_COMM_INTERBYTE_TIMEOUT_DYNAMIC + help + Select how the inter-byte timeout is calculated. + config SERIAL_COMM_INTERBYTE_TIMEOUT_FIXED + bool "Fixed Timeout" + config SERIAL_COMM_INTERBYTE_TIMEOUT_DYNAMIC + bool "Character Time Based" + + endchoice + + + config SERIAL_COMM_INTERBYTE_TIMEOUT_US + int "Fixed Inter-byte Timeout (us)" + default 1000 + depends on SERIAL_COMM_INTERBYTE_TIMEOUT_FIXED + help + Fixed timeout value in microseconds (us). + + + config SERIAL_COMM_INTERBYTE_TIMEOUT_CHARS + int "Character Times" + range 2 64 + default 5 + depends on SERIAL_COMM_INTERBYTE_TIMEOUT_DYNAMIC + help + Number of UART character times used to compute + the inter-byte timeout dynamically based on baudrate. + endmenu + + + menu "Dispatcher" + + config SERIAL_COMM_DISPATCHER_QUEUE_SIZE + int "Dispatcher Queue Size" + default 32 + + config SERIAL_COMM_DISPATCHER_TASK_STACK_SIZE + int "Dispatcher Task Stack Size" + range 2048 16384 + default 8192 + + config SERIAL_COMM_DISPATCHER_TASK_PRIORITY + int "Dispatcher Task Priority" + range 2 10 + default 5 + + endmenu + + + menu "Transactions" + + config SERIAL_COMM_MAX_PENDING_TRANSACTIONS + int "Maximum Pending Transactions" + range 1 32 + default 16 + + config SERIAL_COMM_TRANSACTION_TIMEOUT_MS + int "Default Transaction Timeout in ms" + default 1000 + + endmenu + + + menu "Middleware Features" + + config SERIAL_COMM_ENABLE_SERVICES + bool "Enable Services" + default y + + config SERIAL_COMM_ENABLE_TOPICS + bool "Enable Topics" + default y + + config SERIAL_COMM_ENABLE_ACTIONS + bool "Enable Actions" + default n + + if SERIAL_COMM_ENABLE_SERVICES + config SERIAL_COMM_MAX_SERVICES + int "Maximum Services" + range 1 64 + default 16 + help + Maximum number of registered service servers. + endif + + if SERIAL_COMM_ENABLE_TOPICS + config SERIAL_COMM_MAX_SUBSCRIPTIONS + int "Maximum Topic Subscriptions" + range 1 64 + default 16 + help + Maximum number of registered topic subscribers. + + config SERIAL_COMM_MAX_PUBLISHERS + int "Maximum Topic Publishers" + range 1 64 + default 16 + help + Maximum number of registered topic publishers. + endif + + if SERIAL_COMM_ENABLE_ACTIONS + config SERIAL_COMM_MAX_ACTIONS + int "Maximum Actions" + range 1 32 + default 8 + help + Maximum number of registered action servers. + endif + + endmenu + + + menu "Tasks" + + config SERIAL_COMM_UART_TASK_STACK_SIZE + int "UART Task Stack Size" + default 4096 + + config SERIAL_COMM_UART_TASK_PRIORITY + int "UART Task Priority" + range 1 10 + default 5 + + config SERIAL_COMM_UART_TASK_CORE + int "UART Task Core" + range -1 1 + default -1 + help + CPU core affinity for the UART task. Set to -1 for no affinity. + + endmenu + +endmenu \ No newline at end of file diff --git a/idf_component.yml b/idf_component.yml new file mode 100644 index 0000000..e18b03b --- /dev/null +++ b/idf_component.yml @@ -0,0 +1,247 @@ +version: "0.2.4-beta" + +description: > + Serial communication middleware for ESP-IDF with + services, topics, actions and transaction support. + + A lightweight, extensible middleware component for ESP-IDF that provides + a structured communication framework inspired by ROS2 and micro-ROS + semantics. + +url: "https://github.com/iOsnaaente/SerialComm" + +dependencies: + idf: ">=5.0" + + + +milestones: + - "Migration from callback-based transport to semantic middleware" + - "Protocol V2 introduction" + - "Middleware ROS-inspired abstraction" + - "Transport-independent architecture" + + + +planned: + + - version: "0.2.5" + description: > + Create an Abstration Layer where the user can define their own + middleware behavior based on a predefined ROS-like API structure. + It will allow users to define the structure of the packet of requests + and reply to automatically correlate the request and reply callbacks. + + features: + - Middleware abstraction layer + - ROS-like service/topic/action API + - Generic serialization API + + + - version: "0.3.x" + description: > + Alpha release with initial Wi-Fi transport support and middleware + abstraction layer. + + features: + - Wi-Fi transport support + - Bluetooth transport support + - ROS-like service/topic/action API + - Service discovery mechanism + - Middleware security features + - Enhanced logging and diagnostics + + breakpoints: + - "Added Wi-Fi transport implementation" + - "Introduced middleware abstraction layer" + - "Implemented ROS-like communication semantics" + - "Added generic serialization API" + + +roadmap: + + - version: "0.2.4" + description: > + Refactored middleware configuration system and integrated + advanced menuconfig support and reorganize the directory structure. + + features: + - Kconfig integration + - Config namespace migration + - Runtime feature toggles + - Dispatcher configuration + - Middleware feature flags + - Logging configuration + - Directory structure reorganization + - Add component documentation and roadmap + + breakpoints: + - "Migrated hardcoded values into menuconfig" + - "Separated public/internal configuration" + - "Added configurable middleware features" + - "Added dispatcher/task tunables" + - "Improved component modularity" + + + - version: "0.2.3" + description: > + Added transaction manager and synchronous request/reply service + support. + + features: + - Transaction manager + - Blocking service calls + - Reply synchronization + - Timeout handling + - Pending transaction tracking + + breakpoints: + - "Implemented pending transaction table" + - "Added service request blocking API" + - "Added timeout-based transaction cleanup" + - "Integrated transaction manager into middleware" + + + - version: "0.2.2" + description: > + Introduced middleware abstraction layer inspired by ROS2 and + micro-ROS communication semantics. + + features: + - SerialCommManager + - Service abstraction + - Topic abstraction + - Action abstraction + - Generic serializer API + - Middleware endpoint registration + + breakpoints: + - "Separated transport/core/middleware architecture" + - "Introduced ROS-like services/topics/actions" + - "Implemented typed callback abstraction" + - "Added middleware serialization layer" + - "Introduced semantic middleware API" + + + - version: "0.2.1" + description: > + Added Protocol V2 fields that support sequence identifiers and + semantic request/reply abstraction. + + design decisions: + - Added 16-bit SEQ_ID field to packet structure for transaction + correlation was demonstrate be an important feature for supportting + more complex communication patterns and to enable the implementation + of a transaction manager in the middleware layer, which is a key + component for supporting more complex communication patterns like + synchronous service calls and multi-packet transactions + - The Seq_id and the request/reply bit in the command field was + planned to be present in the protocol version 2, with the source + and destination fields. + + features: + - Some Protocol V2 fields of packet structure + - SEQ_ID support + - Request/reply command bit + - Transaction correlation + - Multiple pending transactions + - Reply packet helpers + + breakpoints: + - "Added 16-bit sequence identifier" + - "Implemented request/reply MSB semantic in command field" + - "Refactored protocol structures" + - "Updated parser for SEQ_ID support" + - "Added transaction matching architecture" + - "Updated Python client for V2 compatibility" + + troubleshooting: + - issue: "Protocol incompatibility with V1 clients" + solution: > + V2 packets now include SEQ_ID field and request/reply bit, that + forces a new implementation of the client to support the new + protocol version. + + + - version: "0.1.2" + description: > + Added dispatcher layer, asynchronous packet processing and + queue-based RX architecture. + + features: + - SerialCommDispatcher + - Async RX queue processing + - Dispatcher task abstraction + - Packet enqueue/dequeue + - Command callback registry + - Multi-packet parsing support + + breakpoints: + - "Separated parser from callback execution" + - "Migrated RX flow to dispatcher task" + - "Added queue-based packet routing" + - "Implemented support for multiple packets per RX buffer" + - "Improved middleware concurrency architecture" + + troubleshooting: + - issue: "Lost packets under high RX throughput" + solution: > + Increase dispatcher queue size and ensure callbacks do not + block dispatcher execution. + + - issue: "Parser only processing first packet" + solution: > + Refactor parse_buffer() to continue parsing after valid + packet extraction and create the parse_next_packet() helper + function that supports incremental parsing. + It solution also add a feature to support multiple packets per + RX buffer, which is a common scenario in a non stream-based + transport like Wi-Fi and other kinds of networks. + + + - version: "0.1.1" + description: > + Added inter-byte timeout watchdog support and parser stream + recovery mechanisms. + + features: + - Inter-byte timeout watchdog + - Parser reset recovery + - Dynamic timeout configuration + - UART timeout helpers + - Parser mutex protection + + breakpoints: + - "Integrated GPTimer-based watchdog abstraction" + - "Moved parser timeout logic from parser to middleware layer" + - "Added parser synchronization recovery" + - "Identified watchdog false-positive resets" + - "Adjusted timeout strategy for USB/UART scheduling jitter" + - "Changed the abstraction architecture for the whole protocol stack" + + troubleshooting: + - issue: "Parser timeout reset spam" + solution: > + Reset parser only when packet assembly is in progress. + + + - version: "0.1.0" + description: > + Initial release with UART transport layer, protocol framing, + CRC16 CCITT validation and incremental parser support. + + features: + - UART transport abstraction + - Protocol V1 implementation + - Incremental byte stream parser + - CRC16 CCITT packet validation + - Packet encoding/decoding + - RX callback registration + - Basic middleware lifecycle + + breakpoints: + - "Defined transport abstraction using ISerialCommTransport" + - "Implemented incremental parser state machine" + - "Added packet framing and CRC16 CCITT validation" + - "Added FreeRTOS-safe TX mutex protection" + - First public release \ No newline at end of file diff --git a/include/serial_comm_dispatcher.h b/include/serial_comm/core/serial_comm_dispatcher.h similarity index 93% rename from include/serial_comm_dispatcher.h rename to include/serial_comm/core/serial_comm_dispatcher.h index 5cec25b..2eb32f9 100644 --- a/include/serial_comm_dispatcher.h +++ b/include/serial_comm/core/serial_comm_dispatcher.h @@ -25,6 +25,7 @@ #pragma once +#include "serial_comm_config.h" #include "serial_comm_protocol.h" #include "serial_comm_utils.h" @@ -53,11 +54,14 @@ class SerialCommDispatcher { */ struct Config { /* RX packet queue size */ - uint32_t rx_queue_len = 16; + uint32_t rx_queue_len = + SerialCommConfig::DISPATCHER_QUEUE_SIZE; /* Dispatcher task stack size */ - uint32_t task_stack_size = 4096; + uint32_t task_stack_size = + SerialCommConfig::DISPATCHER_TASK_STACK_SIZE; /* Dispatcher task priority */ - UBaseType_t task_priority = 5; + UBaseType_t task_priority = + SerialCommConfig::DISPATCHER_TASK_PRIORITY; /* Dispatcher task name */ const char* task_name = "SerialCommDispatcher"; }; @@ -189,7 +193,7 @@ class SerialCommDispatcher { * @return Result code */ SerialCommResult_Codes::errCode register_callback( - SerialCommProtoCommand command, + SerialCommCommand command, packet_callback_t callback, void* ctx ); @@ -200,7 +204,7 @@ class SerialCommDispatcher { * @return Result code */ SerialCommResult_Codes::errCode unregister_callback( - SerialCommProtoCommand command + SerialCommCommand command ); public: diff --git a/include/serial_comm_parser.h b/include/serial_comm/core/serial_comm_parser.h similarity index 88% rename from include/serial_comm_parser.h rename to include/serial_comm/core/serial_comm_parser.h index 294a87c..93edeae 100644 --- a/include/serial_comm_parser.h +++ b/include/serial_comm/core/serial_comm_parser.h @@ -15,8 +15,8 @@ #pragma once -#include "serial_comm_protocol.h" -#include "serial_comm_utils.h" +#include "serial_comm/core/serial_comm_protocol.h" +#include "serial_comm/core/serial_comm_utils.h" #include #include @@ -34,6 +34,8 @@ class SerialCommParser { WAIT_HEADER_0 = 0, WAIT_HEADER_1, WAIT_HEADER_2, + GET_SEQ_ID_L, + GET_SEQ_ID_H, READ_VERSION, READ_COMMAND, READ_LENGTH_L, @@ -54,8 +56,6 @@ class SerialCommParser { uint16_t payload_index_; /** Temporary CRC low byte */ uint8_t crc_l_; - /** Parser synchronization state */ - bool synchronized_; public: /** @@ -64,9 +64,7 @@ class SerialCommParser { SerialCommParser() : state_( State::WAIT_HEADER_0 ), payload_index_(0), - crc_l_(0), - synchronized_(false) - { + crc_l_(0) { SerialCommProtocol::clear_packet( packet_ ); } @@ -92,13 +90,15 @@ class SerialCommParser { * @brief Parse a byte stream buffer * @param[in] data Stream buffer * @param[in] len Buffer length + * @param[out] consumed_bytes Number of bytes consumed from buffer * @param[out] out_packet Parsed packet * @return True Packet completed * @return False No valid packet found */ - bool parse_buffer( + bool parse_next_packet( const uint8_t* data, size_t len, + size_t &consumed_bytes, SerialCommProtoPacket& out_packet ); @@ -117,12 +117,6 @@ class SerialCommParser { */ State state() const; - /** - * @brief Check if parser is synchronized - * @return True Parser synchronized with stream - * @return False Parser waiting synchronization - */ - bool synchronized() const; /** * @brief Get current payload index diff --git a/include/serial_comm_protocol.h b/include/serial_comm/core/serial_comm_protocol.h similarity index 69% rename from include/serial_comm_protocol.h rename to include/serial_comm/core/serial_comm_protocol.h index fe7f237..85c190e 100644 --- a/include/serial_comm_protocol.h +++ b/include/serial_comm/core/serial_comm_protocol.h @@ -11,6 +11,8 @@ #pragma once +#include "serial_comm_config.h" +#include "serial_comm_messages.h" #include "serial_comm_utils.h" #include @@ -22,106 +24,101 @@ /** * @brief Protocol header bytes */ -#define SERIAL_COMM_HEADER_0 0xAA -#define SERIAL_COMM_HEADER_1 0x55 -#define SERIAL_COMM_HEADER_2 0xAA +constexpr uint8_t SERIAL_COMM_HEADER_0 = 0xAA; +constexpr uint8_t SERIAL_COMM_HEADER_1 = 0x55; +constexpr uint8_t SERIAL_COMM_HEADER_2 = 0xAA; + +/** + * @brief Protocol field sizes in bytes + */ +constexpr uint8_t SERIAL_COMM_HEADER_SIZE = 3; +constexpr uint8_t SERIAL_COMM_SEQ_SIZE = 2; +constexpr uint8_t SERIAL_COMM_PROTOCOL_VER_SIZE = 1; +constexpr uint8_t SERIAL_COMM_SRC_SIZE = 1; +constexpr uint8_t SERIAL_COMM_DST_SIZE = 1; +constexpr uint8_t SERIAL_COMM_FLAG_SIZE = 1; +constexpr uint8_t SERIAL_COMM_COMMAND_SIZE = 1; +constexpr uint8_t SERIAL_COMM_LENGTH_SIZE = 2; +constexpr uint8_t SERIAL_COMM_CRC_SIZE = 2; /** * @brief Current protocol version */ -#define SERIAL_COMM_PROTOCOL_VER1 0x01 -#define SERIAL_COMM_PROTOCOL_VER2 0x02 +constexpr uint8_t SERIAL_COMM_PROTOCOL_VER1 = 0x01; +constexpr uint8_t SERIAL_COMM_PROTOCOL_VER2 = 0x02; /** * @brief Maximum payload size */ -#define SERIAL_COMM_MAX_PAYLOAD_V1 512 -#define SERIAL_COMM_MAX_PAYLOAD_V2 1024 - -#define SERIAL_COMM_HEADER_SIZE 3 -#define SERIAL_COMM_PROTOCOL_VER_SIZE 1 -#define SERIAL_COMM_SRC_SIZE 1 -#define SERIAL_COMM_DST_SIZE 1 -#define SERIAL_COMM_SEQ_SIZE 2 -#define SERIAL_COMM_FLAG_SIZE 1 -#define SERIAL_COMM_COMMAND_SIZE 1 -#define SERIAL_COMM_LENGTH_SIZE 2 -#define SERIAL_COMM_CRC_SIZE 2 +constexpr size_t SERIAL_COMM_MAX_PAYLOAD = + SerialCommConfig::MAX_PAYLOAD_SIZE; /** * @brief Maximum packet size for V1 * HEADER(3) + * SEQ_ID(2) * VERSION(1) * COMMAND(1) * LENGTH(2) * PAYLOAD(1024) * CRC16(2) */ -#define SERIAL_COMM_MAX_PACKET_SIZE_V1 \ +constexpr size_t SERIAL_COMM_MAX_PACKET_SIZE_V1 = \ SERIAL_COMM_HEADER_SIZE + \ + SERIAL_COMM_SEQ_SIZE + \ SERIAL_COMM_PROTOCOL_VER_SIZE + \ SERIAL_COMM_COMMAND_SIZE + \ SERIAL_COMM_LENGTH_SIZE + \ - SERIAL_COMM_MAX_PAYLOAD_V1 + \ - SERIAL_COMM_CRC_SIZE + SERIAL_COMM_MAX_PAYLOAD + \ + SERIAL_COMM_CRC_SIZE; /** * @brief Maximum packet size for V2 * HEADER(3) + * SEQ_ID(2) * VERSION(1) * SRC(1) * DST(1) - * SEQ(2) * FLAG(1) * COMMAND(1) * LENGTH(2) * PAYLOAD(1024) * CRC16(2) */ -#define SERIAL_COMM_MAX_PACKET_SIZE_V2 \ +constexpr size_t SERIAL_COMM_MAX_PACKET_SIZE_V2 = \ SERIAL_COMM_HEADER_SIZE + \ + SERIAL_COMM_SEQ_SIZE + \ SERIAL_COMM_PROTOCOL_VER_SIZE + \ SERIAL_COMM_SRC_SIZE + \ SERIAL_COMM_DST_SIZE + \ - SERIAL_COMM_SEQ_SIZE + \ SERIAL_COMM_FLAG_SIZE + \ SERIAL_COMM_COMMAND_SIZE + \ SERIAL_COMM_LENGTH_SIZE + \ - SERIAL_COMM_MAX_PAYLOAD_V2 + \ - SERIAL_COMM_CRC_SIZE - -/** - * @brief Protocol command identifiers - */ -enum class SerialCommProtoCommand : uint8_t { - UNKNOWN = 0x00, - PING = 0x01, - READ = 0x02, - WRITE = 0x03, - REPLY = 0x04, - ERROR = 0x05, - SYNC_WRITE = 0x06, - SYNC_READ = 0x07, - EVENT = 0x08, - ACK = 0x09, - NACK = 0x0A -}; + SERIAL_COMM_MAX_PAYLOAD + \ + SERIAL_COMM_CRC_SIZE; /** * @brief Protocol packet structure * @param header Packet header containing metadata + * @param seq_id Packet sequence identifier: + * - For tracking and matching requests/replies * @param version Protocol version * @param command Command identifier * @param payload_len Packet payload length */ struct SerialCommProtoHeader { - uint8_t header[3]; - uint8_t version; - SerialCommProtoCommand command; - uint16_t payload_len; + uint8_t header[3] = { + SERIAL_COMM_HEADER_0, + SERIAL_COMM_HEADER_1, + SERIAL_COMM_HEADER_2 + }; + uint16_t seq_id = 0; + uint8_t version = SERIAL_COMM_PROTOCOL_VER1; + SerialCommCommand command = SerialCommCommand::UNDEFINED; + uint16_t payload_len = 0; }; @@ -134,7 +131,7 @@ struct SerialCommProtoHeader { */ struct SerialCommProtoPacket { SerialCommProtoHeader header; - uint8_t payload[SERIAL_COMM_MAX_PAYLOAD_V1 ]; + uint8_t payload[SERIAL_COMM_MAX_PAYLOAD ] = { 0 }; uint16_t crc = 0; }; @@ -221,14 +218,4 @@ class SerialCommProtocol { SerialCommProtoPacket& packet ); - - /** - * @brief Convert command enum to string - * @details Useful for logs/debugging. - * @param[in] cmd Command enum - * @return Command string - */ - static const char* command_to_str( - SerialCommProtoCommand cmd - ); -}; \ No newline at end of file +}; diff --git a/include/serial_comm_transport.h b/include/serial_comm/core/serial_comm_transport.h similarity index 97% rename from include/serial_comm_transport.h rename to include/serial_comm/core/serial_comm_transport.h index 7223c66..501c770 100644 --- a/include/serial_comm_transport.h +++ b/include/serial_comm/core/serial_comm_transport.h @@ -12,6 +12,7 @@ #pragma once +#include "serial_comm_config.h" #include "serial_comm_utils.h" #include @@ -47,9 +48,9 @@ class ISerialCommTransport{ /* TRANSPORT CONFIG */ struct Config{ - uint32_t baudrate = 115200; - size_t rx_buffer_size = 1024; - size_t tx_buffer_size = 1024; + uint32_t baudrate = SerialCommConfig::UART_BAUDRATE; + size_t rx_buffer_size = SerialCommConfig::UART_RX_BUFFER_SIZE; + size_t tx_buffer_size = SerialCommConfig::UART_TX_BUFFER_SIZE; bool auto_interbyte_timeout = true; float interbyte_chars = 3.5f; uint32_t timeout_ms = 10; diff --git a/include/serial_comm_utils.h b/include/serial_comm/core/serial_comm_utils.h similarity index 100% rename from include/serial_comm_utils.h rename to include/serial_comm/core/serial_comm_utils.h diff --git a/include/serial_comm/core/serial_comm_watchdog.h b/include/serial_comm/core/serial_comm_watchdog.h new file mode 100644 index 0000000..334bac2 --- /dev/null +++ b/include/serial_comm/core/serial_comm_watchdog.h @@ -0,0 +1,135 @@ +#pragma once + +#include "serial_comm_config.h" +#include "serial_comm_utils.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "freertos/task.h" + +#include "esp_timer.h" +#include "esp_log.h" +#include "esp_err.h" + +#include "driver/gptimer.h" + +#include +#include +#include +#include + + +/** + * @brief Serial communication middleware watchdog timer + * @details This class implements a watchdog timer using the ESP32's general + * purpose timer (GPTimer) and FreeRTOS tasks. It allows you to set + * a timeout duration and a callback function that will be called + * when the timer expires. The timer can be started, stopped, and + * kicked (reset) as needed. + */ +class SerialCommWatchdogTimer { + public: + /** + * @brief Callback function type for the watchdog + * @details The callback function is called when the timer expires + * and must be defined by the user to perform the desired + * action. + */ + using Callback = std::function; + + private: + gptimer_handle_t _timer = nullptr; + TaskHandle_t _taskHandle = nullptr; + Callback _callback; + + uint64_t _timeout_us = 0; + bool _running = false; + + const char *task_name; + uint32_t stack_size; + UBaseType_t priority; + + + /** + * @brief Timer Callback Function + * @details This function is called when the timer expires, and + * it sends a notification to the watchdog task to call + * the callback. + */ + static bool IRAM_ATTR timer_callback( + gptimer_handle_t timer, + const gptimer_alarm_event_data_t *data, + void *user_ctx + ); + + + /** + * @brief Watchdog task function + * @details This function is executed by the watchdog task and + * waits for timer notification events. When it receives + * a notification, it calls the user-defined callback. + */ + static void task_function(void *pvArg); + + + public: + /** + * @brief Constructor + * @details This constructor initializes the watchdog timer with the + * specified parameters. It creates a FreeRTOS task for the + * watchdog and configures the GPTimer with the specified + * timeout and callback. + * @param task_name Name of the FreeRTOS task for the watchdog + * @param timeout_us Timeout duration in microseconds + * @param callback User-defined callback function to call on timeout + * @param stack_size Stack size for the FreeRTOS task + * @param priority Priority for the FreeRTOS task + */ + SerialCommWatchdogTimer( + const char* task_name = "SerialCommWatchdog", + uint64_t timeout_us = uart_interbyte_timeout_us(SerialCommConfig::UART_BAUDRATE), + Callback callback = nullptr, + uint32_t stack_size = 1024*4, + UBaseType_t priority = 5 + ); + + + /** + * @brief Start the watchdog timer + * @details This function starts the watchdog timer. If the timer + * is already running, it does nothing. + * @return ESP_OK if the timer was started successfully + * @return ESP_ERR_INVALID_STATE if the timer is already running + * @return Other error codes from the underlying GPTimer functions + */ + esp_err_t start(); + + + /** + * @brief Stop the watchdog timer + * @details This function stops the watchdog timer. If the timer + * is not running, it does nothing. + * @return ESP_OK if the timer was stopped successfully + * @return ESP_ERR_INVALID_STATE if the timer is not running + * @return Other error codes from the underlying GPTimer functions + */ + esp_err_t stop(); + + + /** + * @brief Kick (reset) the watchdog timer + * @details This function resets the watchdog timer by stopping it, + * setting the timer count back to zero, and starting it + * again. + * @return ESP_OK if the timer was kicked successfully + * @return Other error codes from the underlying GPTimer functions + * @return ESP_ERR_INVALID_STATE if the timer is not running + */ + esp_err_t kick(); + + + /** + * @brief Destructor + */ + ~SerialCommWatchdogTimer(); +}; \ No newline at end of file diff --git a/include/serial_comm/messages/serial_comm_messages.h b/include/serial_comm/messages/serial_comm_messages.h new file mode 100644 index 0000000..b94ac00 --- /dev/null +++ b/include/serial_comm/messages/serial_comm_messages.h @@ -0,0 +1,79 @@ +/** + * @file serial_comm_messages.h + * @brief Serial communication protocol, structs, types and constants + */ + +#pragma once + +#include +#include + +/** + * @brief Serial communication protocol commands + * @note To add a new command, add it before the REPLY command and + * ensure the REPLY_MASK is set correctly + */ +enum class SerialCommCommand : uint8_t { + UNDEFINED = 0x00, + READ = 0x01, + WRITE = 0x02, + PING = 0x03, + STATUS_TOPIC = 0x06, + READ_UTILITY = 0x04, + WRITE_UTILITY = 0x05, +}; + + +/** + * @brief Serial communication protocol commands to string conversion + * @details Useful for logs/debugging. + * @param[in] cmd SerialCommCommand enum + * @return const char* SerialCommCommand string + */ +static inline const char* serial_command_to_str(SerialCommCommand cmd){ + switch (cmd) { + case SerialCommCommand::UNDEFINED: return "UNDEFINED"; + case SerialCommCommand::READ: return "READ"; + case SerialCommCommand::WRITE: return "WRITE"; + case SerialCommCommand::PING: return "PING"; + case SerialCommCommand::STATUS_TOPIC: return "STATUS_TOPIC"; + case SerialCommCommand::READ_UTILITY: return "READ_UTILITY"; + case SerialCommCommand::WRITE_UTILITY: return "WRITE_UTILITY"; + default: return "UNKNOWN_COMMAND"; + } +} + +using Command = SerialCommCommand; + +/** + * @brief Command flag bit mask + */ +#define SERIAL_COMM_CMD_REPLY_MASK 0x80 + + +/** + * @brief Commands Helpers + */ +static inline bool is_reply( SerialCommCommand cmd ) { + return ( static_cast(cmd) & SERIAL_COMM_CMD_REPLY_MASK ) != 0; +} + +static inline bool is_request( SerialCommCommand cmd ) { + return !is_reply(cmd); +} + + +static inline SerialCommCommand make_reply( SerialCommCommand cmd ) { + return static_cast( + static_cast(cmd) | + SERIAL_COMM_CMD_REPLY_MASK + ); +} + + +static inline SerialCommCommand make_request( SerialCommCommand cmd ) { + return static_cast( + static_cast(cmd) & + ~SERIAL_COMM_CMD_REPLY_MASK + ); +} \ No newline at end of file diff --git a/include/serial_comm/middleware/action/serial_comm_action.h b/include/serial_comm/middleware/action/serial_comm_action.h new file mode 100644 index 0000000..e4413b8 --- /dev/null +++ b/include/serial_comm/middleware/action/serial_comm_action.h @@ -0,0 +1,215 @@ +/** + * @file serial_comm_action.h + * @brief Generic Action abstraction for SerialComm middleware + * @details Provides a ROS-like action abstraction for long-running + * asynchronous tasks over the SerialComm protocol. + * + * Responsibilities: + * - Goal handling + * - Feedback handling + * - Result handling + * - Action execution abstraction + * - Cancel support (future) + * + * @note Initial implementation only defines the action abstraction. + * Full execution engine and state machine are future work. + * + * @author Bruno Gabriel Flores Sampaio + * @date Created on 26 of May, 2026 + */ + +#pragma once + +#include "serial_comm/messages/serial_comm_messages.h" + +#include "serial_comm/core/serial_comm_protocol.h" +#include "serial_comm/core/serial_comm_utils.h" + +#include "serial_comm/middleware/action/serial_comm_action_base.h" +#include "serial_comm/middleware/serial_comm_serializer.h" + +#include +#include + +/* To use the errCode and err_to_str easily */ +using namespace SerialCommResult_Codes; + + +/** + * @brief Generic Action abstraction + * @tparam Goal Action goal type + * @tparam Feedback Action feedback type + * @tparam Result Action result type + */ +template< typename Goal, typename Feedback, typename Result > +class SerialCommAction : public IActionBase { + public: + + /** + * @brief Goal callback type + * @param goal Incoming goal + * @param result Final result + * @return true Goal accepted/executed + * @return false Goal rejected/failed + */ + using goal_callback_t = bool (*)( const Goal& goal, Result& result ); + + + /** + * @brief Feedback callback type + * @param feedback Action feedback + */ + using feedback_callback_t = void (*)( const Feedback& feedback ); + + + private: + + /** + * @brief Action command ID + */ + Command command_ = static_cast(0); + + /** + * @brief Goal callback + */ + goal_callback_t goal_callback_ = nullptr; + + /** + * @brief Feedback callback + */ + feedback_callback_t feedback_callback_ = nullptr; + bool initialized_ = false; + + + public: + + /** + * @brief Constructor + */ + SerialCommAction() = default; + + + public: + + /** + * @brief Initialize action + * @param command Action command ID + * @param goal_cb Goal callback + * @param feedback_cb Feedback callback + * @return Result code + */ + errCode init( + Command command, + goal_callback_t goal_cb, + feedback_callback_t feedback_cb = nullptr + ) { + if ( goal_cb == nullptr ) { + return errCode::ERR_NULL_POINTER; + } + this->command_ = command; + this->goal_callback_ = goal_cb; + this->feedback_callback_ = feedback_cb; + this->initialized_ = true; + return errCode::OK; + } + + + private: + /** + * @brief Execute action goal + * @param goal Goal object + * @param result Result object + * @return true Goal executed successfully + * @return false Goal execution failed + */ + bool execute_goal( const Goal& goal, Result& result ) { + if ( !this->initialized_ ) { + return false; + } + if ( this->goal_callback_ == nullptr ) { + return false; + } + return this->goal_callback_( goal, result ); + } + + + public: + /** + * @brief Publish feedback + * @param feedback Feedback object + */ + void publish_feedback( const Feedback& feedback ) { + if ( !this->initialized_ ) { + return; + } + if ( this->feedback_callback_ == nullptr ) { + return; + } + this->feedback_callback_( feedback ); + } + + + /** + * @brief Get action command ID + */ + Command command() const override { + return this->command_; + } + + + /** + * @brief Handle incoming action packet + * @param packet Incoming action packet + * @return Result code + */ + errCode handle_packet( + const SerialCommProtoPacket& packet + ) override { + if ( !this->initialized_ ) { + return errCode::ERR_NOT_INITIALIZED; + } + Goal goal; + bool ret = Serializer::deserialize( + packet.payload, + packet.header.payload_len, + goal + ); + if ( !ret ) { + return errCode::ERR_PARSER; + } + + Result result; + ret = this->execute_goal( goal, result ); + if ( !ret ) { + return errCode::ERR_FAIL; + } + + return errCode::OK; + } + + + /** + * @brief Check if action is initialized + * @return true Action is initialized + * @return false Action is not initialized + */ + bool initialized() const { + return this->initialized_; + } + + /** + * @brief Check if action is valid + * @return true Action is valid + * @return false Action is invalid (e.g. missing callbacks) + */ + bool valid() const { + return ( this->goal_callback_ != nullptr ); + } + + + public: + /** + * @brief Destructor + */ + virtual ~SerialCommAction() = default; +}; \ No newline at end of file diff --git a/include/serial_comm/middleware/action/serial_comm_action_base.h b/include/serial_comm/middleware/action/serial_comm_action_base.h new file mode 100644 index 0000000..b4a20a9 --- /dev/null +++ b/include/serial_comm/middleware/action/serial_comm_action_base.h @@ -0,0 +1,39 @@ +/** + * @file serial_comm_action_base.h + * @brief Base polymorphic interface for middleware actions + */ + +#pragma once + +#include "serial_comm/core/serial_comm_protocol.h" +#include "serial_comm/core/serial_comm_utils.h" + +#include + + +/* To use the errCode and err_to_str easily */ +using namespace SerialCommResult_Codes; + +/** + * @brief Base action interface + */ +class IActionBase { + public: + virtual ~IActionBase() = default; + + public: + + /** + * @brief Get action command ID + */ + virtual Command command() const = 0; + + /** + * @brief Handle incoming action packet + * @param packet Incoming action packet + * @return Result code + */ + virtual errCode handle_packet( + const SerialCommProtoPacket& packet + ) = 0; +}; \ No newline at end of file diff --git a/include/serial_comm/middleware/serial_comm_manager.h b/include/serial_comm/middleware/serial_comm_manager.h new file mode 100644 index 0000000..1878223 --- /dev/null +++ b/include/serial_comm/middleware/serial_comm_manager.h @@ -0,0 +1,623 @@ +/** + * @file serial_comm_manager.h + * @brief High-level semantic middleware manager for SerialComm + * @details Responsible for: + * - Service orchestration + * - Topic orchestration + * - Action orchestration + * - Transaction management + * - Packet semantic routing + * - Request/Reply abstraction + * - Typed communication API + * + * This class is the main public middleware API and abstracts + * low-level packet manipulation from the application layer. + * + * @author Bruno Gabriel Flores Sampaio + * @date Created on 26 of May, 2026 + */ + +#pragma once + + +#include "serial_comm/messages/serial_comm_messages.h" +#include "serial_comm/serial_comm_config.h" +#include "serial_comm/core/serial_comm_protocol.h" +#include "serial_comm/core/serial_comm_utils.h" +#include "serial_comm/serial_comm.h" + +#include "serial_comm/middleware/serial_comm_transaction_manager.h" +#include "serial_comm/middleware/serial_comm_serializer.h" + +#include "serial_comm/middleware/service/serial_comm_service.h" +#include "serial_comm/middleware/action/serial_comm_action.h" +#include "serial_comm/middleware/topic/serial_comm_topic.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" + +#include +#include + + +/* To use the errCode and err_to_str easily */ +using namespace SerialCommResult_Codes; + +// Alias for command type used across the middleware +using Command = SerialCommCommand; + + +/** + * @brief High-level Serial Communication Middleware Manager + */ +class SerialCommManager { + private: + static constexpr size_t MAX_SERVICES = + (SerialCommConfig::MAX_SERVICES > 0) + ? SerialCommConfig::MAX_SERVICES + : 1; + static constexpr size_t MAX_TOPICS = + ((SerialCommConfig::MAX_SUBSCRIPTIONS + + SerialCommConfig::MAX_PUBLISHERS) > 0) + ? (SerialCommConfig::MAX_SUBSCRIPTIONS + + SerialCommConfig::MAX_PUBLISHERS) + : 1; + static constexpr size_t MAX_ACTIONS = + (SerialCommConfig::MAX_ACTIONS > 0) + ? SerialCommConfig::MAX_ACTIONS + : 1; + + // Service, Topic and Action registries + private: + struct ServiceEntry { + bool used = false; + Command command = static_cast(0); + IServiceBase* service = nullptr; + }; + struct TopicEntry { + bool used = false; + Command command = static_cast(0); + ITopicBase* topic = nullptr; + }; + struct ActionEntry { + bool used = false; + Command command = static_cast(0); + IActionBase* action = nullptr; + }; + + private: + ServiceEntry services_[ MAX_SERVICES ]; + TopicEntry topics_[ MAX_TOPICS ]; + ActionEntry actions_[ MAX_ACTIONS ]; + + + private: + SerialCommTransactionManager transactions_; + SemaphoreHandle_t registry_mutex_ = nullptr; + + + public: + + /** + * @brief Middleware runtime configuration + * @param enable_auto_reply If true, the manager will + * automatically send replies for handled requests + * @param enable_transactions If true, the manager will manage + * request-reply transactions and timeouts + * @param service_timeout_ms Default timeout for service calls + * in milliseconds + * @param enable_logs If true, the manager will output logs for + * operations and errors + */ + struct Config { + bool enable_auto_reply = true; + bool enable_transactions = true; + uint32_t service_timeout_ms = + SerialCommConfig::TRANSACTION_TIMEOUT_MS; + bool enable_logs = true; + }; + + private: + + /* Underlying SerialComm instance */ + SerialComm* serial_ = nullptr; + Config cfg_; + + bool initialized_ = false; + bool running_ = false; + + /* Sequence ID generator */ + uint16_t next_seq_id_ = 1; + /* Sequence ID protection mutex */ + SemaphoreHandle_t seq_mutex_ = nullptr; + + + public: + + /** + * @brief Constructor + * @param serial Underlying SerialComm instance + */ + explicit SerialCommManager( SerialComm* serial ) + : serial_( serial ) { } + + /* Delete copy constructor and assignment operator */ + SerialCommManager( const SerialCommManager& ) = delete; + SerialCommManager& operator=(const SerialCommManager& ) = delete; + /* Delete move constructor and assignment operator */ + SerialCommManager( SerialCommManager&& ) = delete; + SerialCommManager& operator=( SerialCommManager&& ) = delete; + + + public: + + /** + * @brief Initialize middleware manager + * @param cfg Runtime configuration + * @return Result code + */ + errCode init( const Config& cfg); + + /** + * @brief Start middleware manager + * @return Result code + */ + errCode start(); + + /** + * @brief Stop middleware manager + * @return Result code + */ + errCode stop(); + + /** + * @brief Deinitialize middleware manager + * @return Result code + */ + errCode deinit(); + + + // SERVICES + public: + + /** + * @brief Create service server + * @tparam Req Request type + * @tparam Res Response type + * @return Result code + */ + template< typename Req, typename Res > + errCode create_service( + SerialCommService* service + ) { + if ( service == nullptr ) { + return errCode::ERR_NULL_POINTER; + } + if ( !service->initialized() ) { + return errCode::ERR_NOT_INITIALIZED; + } + if ( xSemaphoreTake( this->registry_mutex_, portMAX_DELAY ) != pdTRUE ) { + return errCode::ERR_TIMEOUT; + } + for ( size_t i = 0; i < MAX_SERVICES; i++ ) { + if ( !this->services_[i].used ) { + this->services_[i].used = true; + this->services_[i].command = service->command(); + this->services_[i].service = service; + xSemaphoreGive( this->registry_mutex_ ); + return errCode::OK; + } + } + xSemaphoreGive( this->registry_mutex_ ); + return errCode::ERR_NO_MEMORY; + } + + + /** + * @brief Call remote service + * @tparam Req Request type + * @tparam Res Response type + * @param command Service command ID + * @param request Request object + * @param response Output response object + * @param timeout_ms Timeout in milliseconds + * @return Result code + */ + template< typename Req, typename Res> + errCode call_service( + Command command, + const Req& request, + Res& response, + uint32_t timeout_ms = portMAX_DELAY + ) { + uint8_t payload[ SERIAL_COMM_MAX_PAYLOAD ]; + size_t payload_size = 0; + + bool ok = Serializer::serialize( + request, + payload, + sizeof(payload), + payload_size + ); + if ( !ok ) { + return errCode::ERR_FAIL; + } + + uint16_t seq_id = this->allocate_seq_id(); + errCode err = this->transactions_.create_transaction( seq_id ); + if ( err != errCode::OK ) { + return err; + } + + SerialCommProtoPacket request_packet; + err = build_packet( + command, + seq_id, + payload, + payload_size, + request_packet + ); + if ( err != errCode::OK ) { + return err; + } + + err = this->serial_->send( request_packet ); + if ( err != errCode::OK ) { + return err; + } + + SerialCommProtoPacket reply_packet; + err = this->transactions_.wait_reply( + seq_id, + reply_packet, + timeout_ms + ); + if ( err != errCode::OK ) { + return err; + } + + ok = Serializer::deserialize( + reply_packet.payload, + reply_packet.header.payload_len, + response + ); + if ( !ok ) { + return errCode::ERR_FAIL; + } + return errCode::OK; + } + + + // TOPICS + public: + + /** + * @brief Create topic publisher + * @tparam Msg Topic message type + * @return Result code + */ + template< typename Msg > + errCode create_publisher( + SerialCommTopic* publisher + ) { + if ( publisher == nullptr ) { + return errCode::ERR_NULL_POINTER; + } + if ( !publisher->initialized() ) { + return errCode::ERR_NOT_INITIALIZED; + } + xSemaphoreTake( this->registry_mutex_, portMAX_DELAY ); + for ( size_t i = 0; i < MAX_TOPICS; i++) { + if ( !this->topics_[i].used ) { + this->topics_[i].used = true; + this->topics_[i].command = publisher->command(); + this->topics_[i].topic = publisher; + xSemaphoreGive( this->registry_mutex_ ); + return errCode::OK; + } + } + xSemaphoreGive( this->registry_mutex_ ); + return errCode::ERR_NO_MEMORY; + } + + + /** + * @brief Create topic subscription + * @tparam Msg Topic message type + * @return Result code + */ + template< typename Msg > + errCode create_subscription( + SerialCommTopic* subscription + ) { + if ( subscription == nullptr ) { + return errCode::ERR_NULL_POINTER; + } + if ( !subscription->initialized() ) { + return errCode::ERR_NOT_INITIALIZED; + } + xSemaphoreTake( this->registry_mutex_, portMAX_DELAY ); + for ( size_t i = 0; i < MAX_TOPICS; i++) { + if ( !this->topics_[i].used ) { + this->topics_[i].used = true; + this->topics_[i].command = subscription->command(); + this->topics_[i].topic = subscription; + xSemaphoreGive( this->registry_mutex_ ); + return errCode::OK; + } + } + xSemaphoreGive( this->registry_mutex_ ); + return errCode::ERR_NO_MEMORY; + } + + + /** + * @brief Publish topic message + * @tparam Msg Topic message type + * @param command Topic command ID + * @param msg Message object + * @return Result code + */ + template< typename Msg > + errCode publish( Command command, const Msg& msg ) { + uint8_t payload[ SERIAL_COMM_MAX_PAYLOAD ]; + size_t payload_size = 0; + bool ok = Serializer::serialize( + msg, + payload, + sizeof(payload), + payload_size + ); + if ( !ok ) { + return errCode::ERR_FAIL; + } + + SerialCommProtoPacket packet; + errCode err = build_packet( + command, + 0, + payload, + payload_size, + packet + ); + if ( err != errCode::OK ) { + return err; + } + return this->serial_->send( packet ); + } + + + // ACTIONS + public: + + /** + * @brief Create action server + * @tparam Goal Action goal type + * @tparam Feedback Action feedback type + * @tparam Result Action result type + * @param command Action command ID + * @param callback Action callback + * @return Result code + * @note Future implementation + */ + template< typename Goal, typename Feedback, typename Result > + errCode create_action( + SerialCommAction* action + ) { + if ( action == nullptr ) { + return errCode::ERR_NULL_POINTER; + } + if ( !action->initialized() ) { + return errCode::ERR_NOT_INITIALIZED; + } + xSemaphoreTake( this->registry_mutex_, portMAX_DELAY ); + for ( size_t i = 0; i < MAX_ACTIONS; i++ ) { + if ( !this->actions_[i].used ) { + this->actions_[i].used = true; + this->actions_[i].command = action->command(); + this->actions_[i].action = action; + xSemaphoreGive( this->registry_mutex_ ); + return errCode::OK; + } + } + xSemaphoreGive( this->registry_mutex_ ); + return errCode::ERR_NO_MEMORY; + } + + + // INTERNAL ROUTING + private: + + /** + * @brief Internal packet callback from SerialComm + * @param ctx User context (pointer to this manager instance) + * @param packet Received protocol packet + */ + static void serial_packet_callback( + void* ctx, + const SerialCommProtoPacket& packet + ); + + /** + * @brief Route received packet + * @param packet Incoming packet + */ + void route_packet( const SerialCommProtoPacket& packet ); + + /** + * @brief Handle request packet + * @param packet Incoming request packet + */ + void handle_request( const SerialCommProtoPacket& packet ); + + /** + * @brief Handle reply packet + * @param packet Incoming reply packet + */ + void handle_reply( const SerialCommProtoPacket& packet ); + + + // UTILITIES + private: + + /** + * @brief Generate next sequence ID + * @return Sequence ID + */ + uint16_t allocate_seq_id(); + + /** + * @brief Build protocol packet + * @param command Service command ID + * @param seq_id Sequence ID + * @param payload Payload data + * @param payload_len Payload length + * @param out_packet Output protocol packet + * @return Result code + */ + errCode build_packet( + Command command, + uint16_t seq_id, + const uint8_t* payload, + size_t payload_len, + SerialCommProtoPacket& out_packet + ); + + /** + * @brief Send reply packet + * @param command Service command ID + * @param seq_id Sequence ID to reply to + * @param payload Payload data + * @param payload_len Payload length + * @return Result code + */ + errCode send_reply( + Command command, + uint16_t seq_id, + const uint8_t* payload, + size_t payload_len + ); + + /** + * @brief Service send reply message helper + * @tparam Res Response type + * @param command Service command ID + * @param seq_id Sequence ID to reply to + * @param response Response object to serialize and send + * @return Result code + */ + template< typename Res > + errCode send_reply_message( + Command command, + uint16_t seq_id, + const Res& response + ); + + // HELPERS + private: + + /** + * @brief Find service entry by command ID + * @param command Service command ID + * @return Pointer to service entry or nullptr if not found + */ + ServiceEntry* find_service( Command command ); + + + /** + * @brief Find topic entry by command ID + * @param command Topic command ID + * @return Pointer to topic entry or nullptr if not found + */ + TopicEntry* find_topic( Command command ); + + + /** + * @brief Find action entry by command ID + * @param command Action command ID + * @return Pointer to action entry or nullptr if not found + */ + ActionEntry* find_action( Command command ); + + + /** + * @brief Check if command ID corresponds to a service + * @param command Command ID + * @return True if it's a service command + * @result False if it's not a service command + */ + bool is_service_command( Command command ) const; + + + /** + * @brief Check if command ID corresponds to a topic + * @param command Command ID + * @return True if it's a topic command + * @result False if it's not a topic command + */ + bool is_topic_command( Command command ) const; + + + /** + * @brief Check if command ID corresponds to an action + * @param command Command ID + * @return True if it's an action command + * @result False if it's not an action command + */ + bool is_action_command( Command command ) const; + + + /** + * @brief Clear all service, topic and action registries + */ + void clear_registries(); + + + // ROUTERS + private: + + /** + * @brief Route service request packet + * @param packet Incoming service request packet + */ + void handle_service_request( const SerialCommProtoPacket& packet ); + + /** + * @brief Route topic message packet + * @param packet Incoming topic message packet + */ + void handle_topic_message( const SerialCommProtoPacket& packet ); + + /** + * @brief Route action goal/feedback/result packet + * @param packet Incoming action packet + */ + void handle_action_packet( const SerialCommProtoPacket& packet ); + + + // GETTERS + public: + + /** + * @brief Check if manager is initialized + */ + bool initialized() const; + + /** + * @brief Check if manager is running + */ + bool running() const; + + /** + * @brief Get next sequence ID value + */ + uint16_t current_seq_id() const; + + + public: + + /** + * @brief Destructor + */ + virtual ~SerialCommManager(); +}; \ No newline at end of file diff --git a/include/serial_comm/middleware/serial_comm_serializer.h b/include/serial_comm/middleware/serial_comm_serializer.h new file mode 100644 index 0000000..238874c --- /dev/null +++ b/include/serial_comm/middleware/serial_comm_serializer.h @@ -0,0 +1,79 @@ +/** + * @file serial_comm_serializer.h + * @brief Generic serialization abstraction for SerialComm middleware + * @details Provides typed serialization/deserialization interfaces for + * middleware messages. + * + * Responsibilities: + * - Struct -> Payload serialization + * - Payload -> Struct deserialization + * - Middleware message ABI abstraction + * + * @note This is intentionally generic and must be specialized by the + * application layer for each message type. + * + * @author Bruno Gabriel Flores Sampaio + * @date Created on 26 of May, 2026 + */ + +#pragma once + +#include "core/serial_comm_utils.h" + +#include +#include +#include + + +/* To use the errCode and err_to_str easily */ +using namespace SerialCommResult_Codes; + +/** + * @brief Generic serializer interface + * @tparam T Message type + */ +template +struct Serializer { + + /** + * @brief Serialize message into payload buffer + * @param[in] msg Message object + * @param[out] buffer Output payload buffer + * @param[in] buffer_size Buffer capacity + * @param[out] serialized_size Bytes written + * @return true if serialization succeeded + * @return false otherwise + */ + static bool serialize( + const T& msg, + uint8_t* buffer, + size_t buffer_size, + size_t& serialized_size + ) { + (void)msg; + (void)buffer; + (void)buffer_size; + (void)serialized_size; + return false; + } + + + /** + * @brief Deserialize payload buffer into message object + * @param[in] buffer Input payload buffer + * @param[in] buffer_size Payload size + * @param[out] msg Output message object + * @return true if deserialization succeeded + * @return false otherwise + */ + static bool deserialize( + const uint8_t* buffer, + size_t buffer_size, + T& msg + ) { + (void)buffer; + (void)buffer_size; + (void)msg; + return false; + } +}; \ No newline at end of file diff --git a/include/serial_comm/middleware/serial_comm_transaction_manager.h b/include/serial_comm/middleware/serial_comm_transaction_manager.h new file mode 100644 index 0000000..7b7ed2e --- /dev/null +++ b/include/serial_comm/middleware/serial_comm_transaction_manager.h @@ -0,0 +1,162 @@ +/** + * @file serial_comm_transaction_manager.h + * @brief Transaction manager for SerialComm middleware + * @details Responsible for: + * - Pending request tracking + * - Reply matching using seq_id + * - Request timeout management + * - Synchronous service waiting + * - Transaction lifecycle management + * + * @author Bruno Gabriel Flores Sampaio + * @date Created on 26 of May, 2026 + */ + +#pragma once + +#include "serial_comm/messages/serial_comm_messages.h" +#include "serial_comm/serial_comm_config.h" + +#include "serial_comm/core/serial_comm_protocol.h" +#include "serial_comm/core/serial_comm_utils.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" + +#include +#include +#include + + +/* To use the errCode and err_to_str easily */ +using namespace SerialCommResult_Codes; + + +/** + * @brief Transaction manager + */ +class SerialCommTransactionManager { + private: + static constexpr size_t MAX_TRANSACTIONS = + (SerialCommConfig::MAX_PENDING_TRANSACTIONS > 0) + ? SerialCommConfig::MAX_PENDING_TRANSACTIONS + : 1; + + private: + /** + * @brief Pending transaction entry + * @param active Whether the transaction slot is active + * @param seq_id Sequence ID of the transaction + * @param completed Whether the transaction has been completed + * @param reply Reply packet for the transaction + * @param semaphore Semaphore for transaction synchronization + */ + struct Transaction { + bool active = false; + uint16_t seq_id = 0; + bool completed = false; + SerialCommProtoPacket reply; + SemaphoreHandle_t semaphore = nullptr; + }; + + + private: + + /** + * @brief Transaction table + * TODO: Fixed-size array of transactions for simplicity. + * In a real implementation, a more efficient data structure + * (e.g., hash map) could be used. + */ + Transaction transactions_[ MAX_TRANSACTIONS ]; + + /** + * @brief Internal protection mutex + */ + SemaphoreHandle_t mutex_ = nullptr; + bool initialized_ = false; + + + public: + + /** + * @brief Constructor + */ + SerialCommTransactionManager() = default; + + /* Delete copy constructor and assignment operator */ + SerialCommTransactionManager( const SerialCommTransactionManager& ) = delete; + SerialCommTransactionManager& operator=(const SerialCommTransactionManager& ) = delete; + + + public: + + /** + * @brief Initialize transaction manager + * @return Result code + */ + errCode init(); + + + /** + * @brief Deinitialize transaction manager + * @return Result code + */ + errCode deinit(); + + + public: + + /** + * @brief Allocate transaction slot + * @param seq_id Transaction sequence ID + * @return Result code + */ + errCode create_transaction( uint16_t seq_id ); + + + /** + * @brief Wait for transaction reply + * @param seq_id Transaction sequence ID + * @param out_reply Output reply packet + * @param timeout_ms Wait timeout + * @return Result code + */ + errCode wait_reply( + uint16_t seq_id, + SerialCommProtoPacket& out_reply, + uint32_t timeout_ms + ); + + + /** + * @brief Resolve transaction reply + * @param packet Reply packet + * @return Result code + */ + errCode resolve_transaction( + const SerialCommProtoPacket& packet + ); + + + private: + + /** + * @brief Find transaction by seq_id + */ + Transaction* find_transaction( uint16_t seq_id ); + + + /** + * @brief Destroy transaction + */ + void destroy_transaction( uint16_t seq_id ); + + + public: + /** + * @brief Destructor + */ + virtual ~SerialCommTransactionManager(); + +}; \ No newline at end of file diff --git a/include/serial_comm/middleware/service/serial_comm_service.h b/include/serial_comm/middleware/service/serial_comm_service.h new file mode 100644 index 0000000..e6f3e45 --- /dev/null +++ b/include/serial_comm/middleware/service/serial_comm_service.h @@ -0,0 +1,204 @@ +/** + * @file serial_comm_service.h + * @brief Generic Service abstraction for SerialComm middleware + * @details Provides a ROS-like request/reply abstraction over the + * SerialComm protocol. + * + * Responsibilities: + * - Typed request/reply callbacks + * - Automatic serialization abstraction + * - Automatic response packet generation + * - Service callback encapsulation + * + * @author Bruno Gabriel Flores Sampaio + * @date Created on 26 of May, 2026 + */ + +#pragma once + +#include "serial_comm/messages/serial_comm_messages.h" + +#include "serial_comm/core/serial_comm_protocol.h" +#include "serial_comm/core/serial_comm_utils.h" + +#include "serial_comm/middleware/service/serial_comm_service_base.h" +#include "serial_comm/middleware/serial_comm_serializer.h" + +#include +#include + +/* To use the errCode and err_to_str easily */ +using namespace SerialCommResult_Codes; + + +/** + * @brief Generic SerialComm Service abstraction + * @tparam Req Request type + * @tparam Res Response type + */ +template +class SerialCommService: public IServiceBase { + public: + + /** + * @brief Service callback type + * @param request Typed request object + * @param response Typed response object + * @return true Service executed successfully + * @return false Service execution failed + */ + using service_callback_t = + bool (*)( const Req& request, Res& response ); + + + public: + + /** + * @brief Constructor + */ + SerialCommService() = default; + + + private: + + /** + * @brief Registered command ID + */ + Command command_ = static_cast(0); + + /** + * @brief Service callback + */ + service_callback_t callback_ = nullptr; + bool initialized_ = false; + + + private: + /** + * @brief Execute service callback + * @param request Request object + * @param response Response object + * @return true Callback executed successfully + * @return false Callback execution failed + */ + bool execute( const Req& request, Res& response ) { + if ( !this->initialized_ ) { + return false; + } + if ( this->callback_ == nullptr ) { + return false; + } + return this->callback_( request, response ); + } + + + public: + + /** + * @brief Initialize service + * @param command Service command ID + * @param callback User callback + * @return Result code + */ + errCode init( Command command, service_callback_t callback ) { + if ( callback == nullptr ) { + return errCode::ERR_NULL_POINTER; + } + this->command_ = command; + this->callback_ = callback; + this->initialized_ = true; + return errCode::OK; + } + + public: + + /** + * @brief Get command ID + * @return Command ID + */ + Command command() const override { + return this->command_; + } + + + /** + * @brief Handle incoming service request packet, execute + * callback and generate response packet + * @param request Incoming request packet + * @param response Output response packet to fill + * @return Result code + */ + errCode handle_packet( + const SerialCommProtoPacket& request, + SerialCommProtoPacket& response + ) override { + if ( !this->initialized_ ) { + return errCode::ERR_NOT_INITIALIZED; + } + if ( this->callback_ == nullptr ) { + return errCode::ERR_NULL_POINTER; + } + + Req request_msg; + bool ret = Serializer::deserialize( + request.payload, + request.header.payload_len, + request_msg + ); + if ( !ret ) { + return errCode::ERR_PARSER; + } + + Res response_msg; + ret = this->execute( request_msg, response_msg ); + if ( !ret ) { + return errCode::ERR_FAIL; + } + + size_t serializer_size = 0; + ret = Serializer::serialize( + response_msg, + response.payload, + sizeof(response.payload), + serializer_size + ); + if ( !ret ) { + return errCode::ERR_PARSER; + } + + SerialCommProtocol::clear_packet( response ); + response.header.seq_id = request.header.seq_id; + response.header.version = SERIAL_COMM_PROTOCOL_VER1; + response.header.command = make_reply( request.header.command ); + response.header.payload_len = static_cast( serializer_size ); + return errCode::OK; + } + + + /** + * @brief Check if service is initialized + * @return true Service is initialized + * @return false Service is not initialized + */ + bool initialized() const { + return this->initialized_; + } + + + /** + * @brief Check if callback is valid + * @return true Callback is valid + * @return false Callback is not valid + */ + bool valid() const { + return ( this->callback_ != nullptr ); + } + + + public: + + /** + * @brief Destructor + */ + virtual ~SerialCommService() = default; +}; \ No newline at end of file diff --git a/include/serial_comm/middleware/service/serial_comm_service_base.h b/include/serial_comm/middleware/service/serial_comm_service_base.h new file mode 100644 index 0000000..2f084ed --- /dev/null +++ b/include/serial_comm/middleware/service/serial_comm_service_base.h @@ -0,0 +1,40 @@ +/** + * @file serial_comm_service_base.h + * @brief Base polymorphic interface for middleware services + */ + +#pragma once + +#include "serial_comm/core/serial_comm_protocol.h" +#include "serial_comm/core/serial_comm_utils.h" + +#include + + +/* To use the errCode and err_to_str easily */ +using namespace SerialCommResult_Codes; + +/** + * @brief Base service interface + */ +class IServiceBase { + public: + virtual ~IServiceBase() = default; + + public: + /** + * @brief Get service command ID + */ + virtual Command command() const = 0; + + /** + * @brief Handle incoming service packet + * @param request Incoming request packet + * @param response Output reply packet + * @return Result code + */ + virtual errCode handle_packet( + const SerialCommProtoPacket& request, + SerialCommProtoPacket& response + ) = 0; +}; \ No newline at end of file diff --git a/include/serial_comm/middleware/topic/serial_comm_topic.h b/include/serial_comm/middleware/topic/serial_comm_topic.h new file mode 100644 index 0000000..c128c58 --- /dev/null +++ b/include/serial_comm/middleware/topic/serial_comm_topic.h @@ -0,0 +1,164 @@ +/** + * @file serial_comm_topic.h + * @brief Generic Topic abstraction for SerialComm middleware + * @details Provides a ROS-like publish/subscribe abstraction over the + * SerialComm protocol. + * + * Responsibilities: + * - Typed publish/subscribe abstraction + * - Topic callback encapsulation + * - Lightweight async message delivery + * + * @author Bruno Gabriel Flores Sampaio + * @date Created on 26 of May, 2026 + */ + +#pragma once + +#include "serial_comm/messages/serial_comm_messages.h" + +#include "serial_comm/core/serial_comm_protocol.h" +#include "serial_comm/core/serial_comm_utils.h" + +#include "serial_comm/middleware/topic/serial_comm_topic_base.h" +#include "serial_comm/middleware/serial_comm_serializer.h" + +#include +#include + + +/* To use the errCode and err_to_str easily */ +using namespace SerialCommResult_Codes; + + +/** + * @brief Generic Topic abstraction + * @tparam Msg Topic message type + */ +template +class SerialCommTopic : public ITopicBase { + public: + + /** + * @brief Topic callback type + * @param msg Received message + */ + using topic_callback_t = void (*)( const Msg& msg ); + + + private: + + /** + * @brief Topic command ID + */ + Command command_ = static_cast(0); + + /** + * @brief Subscription callback + */ + topic_callback_t callback_ = nullptr; + bool initialized_ = false; + + + private: + /** + * @brief Execute topic callback + * @param msg Received message + */ + void execute( const Msg& msg ) { + if ( !this->initialized_ ) { + return; + } + if ( this->callback_ == nullptr ) { + return; + } + this->callback_( msg ); + } + + + public: + + /** + * @brief Constructor + */ + SerialCommTopic() = default; + + + public: + + /** + * @brief Initialize topic + * @param command Topic command ID + * @param callback Topic callback + * @return Result code + */ + errCode init( Command command, topic_callback_t callback ) { + if ( callback == nullptr ) { + return errCode::ERR_NULL_POINTER; + } + this->command_ = command; + this->callback_ = callback; + this->initialized_ = true; + return errCode::OK; + } + + + /** + * @brief Handle incoming topic packet + * @param packet Incoming packet + * @return Result code + */ + errCode handle_packet( const SerialCommProtoPacket& packet ) override { + if ( !this->initialized_ ) { + return errCode::ERR_NOT_INITIALIZED; + } + Msg msg; + bool ret = Serializer::deserialize( + packet.payload, + packet.header.payload_len, + msg + ); + if ( !ret ) { + return errCode::ERR_PARSER; + } + this->execute( msg ); + return errCode::OK; + } + + + /** + * @brief Get topic command ID + * @return Command ID + */ + Command command() const override { + return this->command_; + } + + + /** + * @brief Check if topic is initialized + * @return True if initialized + * @return False if not initialized + */ + bool initialized() const { + return this->initialized_; + } + + + /** + * @brief Check if topic callback is valid + * @return true Callback is valid + * @return false Callback is not valid + */ + bool valid() const { + return ( this->callback_ != nullptr ); + } + + + public: + + /** + * @brief Destructor + */ + virtual ~SerialCommTopic() = default; +}; \ No newline at end of file diff --git a/include/serial_comm/middleware/topic/serial_comm_topic_base.h b/include/serial_comm/middleware/topic/serial_comm_topic_base.h new file mode 100644 index 0000000..83f6652 --- /dev/null +++ b/include/serial_comm/middleware/topic/serial_comm_topic_base.h @@ -0,0 +1,39 @@ +/** + * @file serial_comm_topic_base.h + * @brief Base polymorphic interface for middleware topics + */ + +#pragma once + +#include "core/serial_comm_protocol.h" +#include "core/serial_comm_utils.h" + +#include + + +/* To use the errCode and err_to_str easily */ +using namespace SerialCommResult_Codes; + +/** + * @brief Base topic interface + */ +class ITopicBase { + public: + virtual ~ITopicBase() = default; + + public: + + /** + * @brief Get topic command ID + */ + virtual Command command() const = 0; + + /** + * @brief Handle incoming topic packet + * @param packet Incoming packet + * @return Result code + */ + virtual errCode handle_packet( + const SerialCommProtoPacket& packet + ) = 0; +}; \ No newline at end of file diff --git a/include/serial_comm.h b/include/serial_comm/serial_comm.h similarity index 91% rename from include/serial_comm.h rename to include/serial_comm/serial_comm.h index 6ba55ea..27ade92 100644 --- a/include/serial_comm.h +++ b/include/serial_comm/serial_comm.h @@ -24,6 +24,7 @@ #include "serial_comm_protocol.h" #include "serial_comm_parser.h" #include "serial_comm_utils.h" +#include "serial_comm_config.h" #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" @@ -62,9 +63,11 @@ class SerialComm { */ struct Config { /* Inter-byte timeout in microseconds (us) */ - uint32_t inter_byte_timeout_us = 1000; + uint32_t inter_byte_timeout_us = + SerialCommConfig::INTERBYTE_TIMEOUT_US; /* Enable timeout watchdog */ - bool enable_inter_byte_timeout = true; + bool enable_inter_byte_timeout = + SerialCommConfig::ENABLE_INTERBYTE_TIMEOUT; }; private: @@ -166,7 +169,7 @@ class SerialComm { * @return Middleware result */ errCode register_callback( - SerialCommProtoCommand command, + SerialCommCommand command, packet_callback_t callback, void* ctx ); @@ -177,12 +180,15 @@ class SerialComm { * @return Middleware result */ errCode unregister_callback( - SerialCommProtoCommand command + SerialCommCommand command ); private: /** * @brief Static RX callback from transport + * @param ctx User context (pointer to SerialComm instance) + * @param data Received data buffer + * @param len Length of received data */ static void transport_rx_callback( void* ctx, @@ -192,6 +198,8 @@ class SerialComm { /** * @brief Process incoming RX data + * @param data Received data buffer + * @param len Length of received data */ void process_rx_data( const uint8_t* data, size_t len ); diff --git a/include/serial_comm/serial_comm_config.h b/include/serial_comm/serial_comm_config.h new file mode 100644 index 0000000..e6087a3 --- /dev/null +++ b/include/serial_comm/serial_comm_config.h @@ -0,0 +1,196 @@ +/** + * @file serial_comm_config.hpp + * @brief Serial communication middleware configuration from menuconfig + * @note Please dont modify this file directly, as it is generated from + * the Kconfig system. + * @note If you want to change any configuration, please do it through + * the menuconfig interface. To do that, run: + * `idf.py menuconfig` + * and navigate to the Serial Communication + */ + +#pragma once + +#include "sdkconfig.h" + +#include +#include + + +/** + * @brief Namespace containing all configuration parameters for the + * serial communication middleware, populated from Kconfig + * menuconfig options. + * + * @details This namespace serves as a centralized location for all + * configuration parameters, making it easy to access and maintain. + * + * @example To use the configuration parameters, do: + * using namespace SerialCommConfig; + * int uart_port = UART_PORT; + * + * @example To use the configuration parameters without qualifying the + * namespace, do: + * SerialCommConfig::UART_PORT; + * + */ +namespace SerialCommConfig { + + // TRANSPORT + constexpr int UART_PORT = + CONFIG_SERIAL_COMM_UART_PORT; + + constexpr uint32_t UART_BAUDRATE = + CONFIG_SERIAL_COMM_UART_BAUDRATE; + + constexpr int UART_TX_PIN = + CONFIG_SERIAL_COMM_UART_TX_PIN; + + constexpr int UART_RX_PIN = + CONFIG_SERIAL_COMM_UART_RX_PIN; + + constexpr int UART_RTS_PIN = + CONFIG_SERIAL_COMM_UART_RTS_PIN; + + constexpr int UART_CTS_PIN = + CONFIG_SERIAL_COMM_UART_CTS_PIN; + + constexpr int UART_DE_PIN = + CONFIG_SERIAL_COMM_UART_DE_PIN; + + + // UART DRIVER + constexpr size_t UART_RX_BUFFER_SIZE = + CONFIG_SERIAL_COMM_UART_RX_BUFFER_SIZE; + + constexpr size_t UART_TX_BUFFER_SIZE = + CONFIG_SERIAL_COMM_UART_TX_BUFFER_SIZE; + + constexpr size_t UART_EVENT_QUEUE_SIZE = + CONFIG_SERIAL_COMM_UART_EVENT_QUEUE_SIZE; + + + // PROTOCOL + constexpr size_t MAX_PAYLOAD_SIZE = + CONFIG_SERIAL_COMM_MAX_PAYLOAD_SIZE; + + constexpr bool ENABLE_CRC = + CONFIG_SERIAL_COMM_ENABLE_CRC; + + constexpr bool ENABLE_SEQ_ID = + CONFIG_SERIAL_COMM_ENABLE_SEQ_ID; + + + // PARSER + constexpr bool ENABLE_INTERBYTE_TIMEOUT = + CONFIG_SERIAL_COMM_ENABLE_INTERBYTE_TIMEOUT; + + #if defined(CONFIG_SERIAL_COMM_INTERBYTE_TIMEOUT_DYNAMIC) + constexpr bool INTERBYTE_TIMEOUT_DYNAMIC = true; + #else + constexpr bool INTERBYTE_TIMEOUT_DYNAMIC = false; + #endif + + #if defined(CONFIG_SERIAL_COMM_INTERBYTE_TIMEOUT_CHARS) + constexpr uint32_t INTERBYTE_TIMEOUT_CHARS = + CONFIG_SERIAL_COMM_INTERBYTE_TIMEOUT_CHARS; + #else + constexpr uint32_t INTERBYTE_TIMEOUT_CHARS = 0; + #endif + + #if defined(CONFIG_SERIAL_COMM_INTERBYTE_TIMEOUT_US) + constexpr uint32_t INTERBYTE_TIMEOUT_US = + CONFIG_SERIAL_COMM_INTERBYTE_TIMEOUT_US; + #else + constexpr uint32_t INTERBYTE_TIMEOUT_US = + (INTERBYTE_TIMEOUT_DYNAMIC && UART_BAUDRATE > 0) + ? static_cast( + (1000000ULL * 10ULL * INTERBYTE_TIMEOUT_CHARS) / + static_cast(UART_BAUDRATE) + ) + : 0; + #endif + + + // DISPATCHER + constexpr size_t DISPATCHER_QUEUE_SIZE = + CONFIG_SERIAL_COMM_DISPATCHER_QUEUE_SIZE; + + constexpr size_t DISPATCHER_TASK_STACK_SIZE = + CONFIG_SERIAL_COMM_DISPATCHER_TASK_STACK_SIZE; + + constexpr uint32_t DISPATCHER_TASK_PRIORITY = + CONFIG_SERIAL_COMM_DISPATCHER_TASK_PRIORITY; + + + // TRANSACTIONS + constexpr size_t MAX_PENDING_TRANSACTIONS = + CONFIG_SERIAL_COMM_MAX_PENDING_TRANSACTIONS; + + constexpr uint32_t TRANSACTION_TIMEOUT_MS = + CONFIG_SERIAL_COMM_TRANSACTION_TIMEOUT_MS; + + + // MIDDLEWARE + constexpr bool ENABLE_SERVICES = + CONFIG_SERIAL_COMM_ENABLE_SERVICES; + + constexpr bool ENABLE_TOPICS = + CONFIG_SERIAL_COMM_ENABLE_TOPICS; + + #if defined(CONFIG_SERIAL_COMM_ENABLE_ACTIONS) + constexpr bool ENABLE_ACTIONS = + CONFIG_SERIAL_COMM_ENABLE_ACTIONS; + #else + constexpr bool ENABLE_ACTIONS = false; + #endif + + + constexpr size_t MAX_SERVICES = + #ifdef CONFIG_SERIAL_COMM_MAX_SERVICES + CONFIG_SERIAL_COMM_MAX_SERVICES; + #else + 0; + #endif + + constexpr size_t MAX_SUBSCRIPTIONS = + #ifdef CONFIG_SERIAL_COMM_MAX_SUBSCRIPTIONS + CONFIG_SERIAL_COMM_MAX_SUBSCRIPTIONS; + #else + 0; + #endif + + constexpr size_t MAX_PUBLISHERS = + #ifdef CONFIG_SERIAL_COMM_MAX_PUBLISHERS + CONFIG_SERIAL_COMM_MAX_PUBLISHERS; + #else + 0; + #endif + + constexpr size_t MAX_ACTIONS = + #ifdef CONFIG_SERIAL_COMM_MAX_ACTIONS + CONFIG_SERIAL_COMM_MAX_ACTIONS; + #else + 0; + #endif + + + // TASKS + constexpr size_t UART_TASK_STACK_SIZE = + CONFIG_SERIAL_COMM_UART_TASK_STACK_SIZE; + + constexpr uint32_t UART_TASK_PRIORITY = + CONFIG_SERIAL_COMM_UART_TASK_PRIORITY; + + // Note: If core affinity is enabled, the task will be pinned to the specified core. + constexpr bool UART_USE_TASK_CORE_AFINITY = + #if CONFIG_SERIAL_COMM_UART_TASK_CORE >= 0 + true; + #else + false; + #endif + + constexpr int UART_TASK_CORE = + CONFIG_SERIAL_COMM_UART_TASK_CORE; + +} diff --git a/include/uart_serial_comm.h b/include/serial_comm/transport/uart_serial_comm.h similarity index 82% rename from include/uart_serial_comm.h rename to include/serial_comm/transport/uart_serial_comm.h index 375ca0c..8bdf855 100644 --- a/include/uart_serial_comm.h +++ b/include/serial_comm/transport/uart_serial_comm.h @@ -10,9 +10,9 @@ #pragma once -#include "serial_comm_transport.h" -#include "serial_comm_config.h" -#include "serial_comm_utils.h" +#include "serial_comm/core/serial_comm_transport.h" +#include "serial_comm/serial_comm_config.h" +#include "serial_comm/core/serial_comm_utils.h" #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" @@ -33,12 +33,12 @@ class UARTTransport final : public ISerialCommTransport { * @brief ESP32 UART hardware configuration */ struct HardwareConfig { - int uart_port = SERIAL_COMM_UART_PORT; - int tx_pin = SERIAL_COMM_UART_TX_PIN; - int rx_pin = SERIAL_COMM_UART_RX_PIN; - int rts_pin = SERIAL_COMM_UART_RTS_PIN; - int cts_pin = SERIAL_COMM_UART_CTS_PIN; - int de_pin = SERIAL_COMM_UART_DE_PIN; + int uart_port = SerialCommConfig::UART_PORT; + int tx_pin = SerialCommConfig::UART_TX_PIN; + int rx_pin = SerialCommConfig::UART_RX_PIN; + int rts_pin = SerialCommConfig::UART_RTS_PIN; + int cts_pin = SerialCommConfig::UART_CTS_PIN; + int de_pin = SerialCommConfig::UART_DE_PIN; }; @@ -63,9 +63,7 @@ class UARTTransport final : public ISerialCommTransport { private: - static void uart_event_task( - void* args - ); + static void uart_event_task( void* args ); public: @@ -73,9 +71,7 @@ class UARTTransport final : public ISerialCommTransport { * @brief Constructor for UARTTransport * @param hw_cfg Hardware configuration for the UART transport */ - explicit UARTTransport( - const HardwareConfig& hw_cfg - ); + explicit UARTTransport( const HardwareConfig& hw_cfg ); /* Deleted copy and move constructors */ UARTTransport( UARTTransport&& ) = delete; diff --git a/include/serial_comm_config.h b/include/serial_comm_config.h deleted file mode 100644 index bf2504c..0000000 --- a/include/serial_comm_config.h +++ /dev/null @@ -1,60 +0,0 @@ -/** - * @file serial_comm_config.hpp - * @brief Serial communication middleware configuration from menuconfig - * - * @note Please dont modify this file directly, as it is generated from - * the Kconfig system. - * - * @note If you want to change any configuration, please do it through - * the menuconfig interface. To do that, run: - * `idf.py menuconfig` - * and navigate to the Serial Communication - */ - -#pragma once - -#include "sdkconfig.h" - -/* ============================================================================ - * UART HARDWARE CONFIGURATION - * ==========================================================================*/ - -#define SERIAL_COMM_UART_PORT CONFIG_SERIAL_COMM_UART_PORT - -#define SERIAL_COMM_UART_BAUDRATE CONFIG_SERIAL_COMM_UART_BAUDRATE - -#define SERIAL_COMM_UART_TX_PIN CONFIG_SERIAL_COMM_UART_TX_PIN - -#define SERIAL_COMM_UART_RX_PIN CONFIG_SERIAL_COMM_UART_RX_PIN - -#define SERIAL_COMM_UART_RTS_PIN CONFIG_SERIAL_COMM_UART_RTS_PIN - -#define SERIAL_COMM_UART_CTS_PIN CONFIG_SERIAL_COMM_UART_CTS_PIN - -#define SERIAL_COMM_UART_DE_PIN CONFIG_SERIAL_COMM_UART_DE_PIN - -/* ============================================================================ - * UART DRIVER CONFIGURATION - * ==========================================================================*/ - -#define SERIAL_COMM_UART_RX_BUFFER_SIZE CONFIG_SERIAL_COMM_UART_RX_BUFFER_SIZE - -#define SERIAL_COMM_UART_TX_BUFFER_SIZE CONFIG_SERIAL_COMM_UART_TX_BUFFER_SIZE - -#define SERIAL_COMM_UART_QUEUE_SIZE CONFIG_SERIAL_COMM_UART_EVENT_QUEUE_SIZE - -/* ============================================================================ - * TASK CONFIGURATION - * ==========================================================================*/ - -#define SERIAL_COMM_UART_TASK_STACK CONFIG_SERIAL_COMM_UART_TASK_STACK_SIZE - -#define SERIAL_COMM_UART_TASK_PRIORITY CONFIG_SERIAL_COMM_UART_TASK_PRIORITY - -#define SERIAL_COMM_UART_TASK_CORE CONFIG_SERIAL_COMM_UART_TASK_CORE - -/* ============================================================================ - * HALF DUPLEX - * ==========================================================================*/ - -#define SERIAL_COMM_HALF_DUPLEX_ENABLED CONFIG_SERIAL_COMM_ENABLE_HALF_DUPLEX \ No newline at end of file diff --git a/include/serial_comm_eventloop.h b/include/serial_comm_eventloop.h deleted file mode 100644 index 9e4c674..0000000 --- a/include/serial_comm_eventloop.h +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once -/** - * @brief This file defines the event loop for the serial communication component. - */ - -#include "serial_comm_types.h" - -#include "esp_event.h" -#include "esp_err.h" - -using packet_callback_t = - void (*)(const Packet& pkt); - -class EventLoop { -public: - - static esp_err_t init(); - - static esp_err_t register_callback( - int32_t event_id, - esp_event_handler_t callback - ); - - static esp_err_t post_event( - int32_t event_id, - const Packet& pkt - ); - -private: - - static esp_event_loop_handle_t loop_; -}; diff --git a/include/serial_comm_events.h b/include/serial_comm_events.h deleted file mode 100644 index b92503a..0000000 --- a/include/serial_comm_events.h +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @brief This file defines the events for the serial communication component. - */ - -#pragma once - -#include "esp_event.h" - - -ESP_EVENT_DECLARE_BASE(SERIAL_COMM_EVENTS); - -enum EventID{ - EVENT_PACKET_RECEIVED = 0, - EVENT_READ_JOINTS, - EVENT_WRITE_JOINTS, - EVENT_PING, - EVENT_READ_UTILITY, - EVENT_WRITE_UTILITY, - EVENT_SMOOTH_MOVE, - EVENT_PROTOCOL_ERROR -}; diff --git a/include/serial_comm_messages.h b/include/serial_comm_messages.h deleted file mode 100644 index 7fdd541..0000000 --- a/include/serial_comm_messages.h +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @brief Serial communication protocol, structs, types and constants - */ - -#pragma once - -#include -#include - -constexpr uint8_t HEADER_0 = 0xAA; -constexpr uint8_t HEADER_1 = 0x55; -constexpr uint8_t HEADER_2 = 0xAA; - -constexpr uint8_t PROTOCOL_VERSION = 0x01; -constexpr size_t MAX_PAYLOAD_SIZE = 256; - -enum class Command : uint8_t { - READ_JOINTS = 0x01, - WRITE_JOINTS = 0x02, - PING = 0x03, - READ_UTILITY = 0x04, - WRITE_UTILITY = 0x05, - SMOOTH_MOVE = 0x06 -}; - -struct Packet { - uint8_t version; - Command command; - uint16_t payload_len; - uint8_t payload[MAX_PAYLOAD_SIZE]; - uint16_t crc; -}; diff --git a/include/serial_comm_watchdog.h b/include/serial_comm_watchdog.h deleted file mode 100644 index 9b6b3c3..0000000 --- a/include/serial_comm_watchdog.h +++ /dev/null @@ -1,211 +0,0 @@ -#pragma once - -#include "serial_comm_config.h" -#include "serial_comm_utils.h" - -#include "freertos/FreeRTOS.h" -#include "freertos/semphr.h" -#include "freertos/task.h" - -#include "esp_timer.h" -#include "esp_log.h" -#include "esp_err.h" - -#include "driver/gptimer.h" - -#include -#include -#include -#include - - -using namespace SerialCommUtils; - -/** - * @brief Serial communication middleware watchdog timer - * @details This class implements a watchdog timer using the ESP32's general - * purpose timer (GPTimer) and FreeRTOS tasks. It allows you to set - * a timeout duration and a callback function that will be called - * when the timer expires. The timer can be started, stopped, and - * kicked (reset) as needed. - */ -class SerialCommWatchdogTimer { - public: - /** - * @brief Callback function type for the watchdog - * @details The callback function is called when the timer expires - * and must be defined by the user to perform the desired - * action. - */ - using Callback = std::function; - - private: - gptimer_handle_t _timer = nullptr; - TaskHandle_t _taskHandle = nullptr; - Callback _callback; - - uint64_t _timeout_us = 0; - bool _running = false; - - const char *task_name; - uint32_t stack_size; - UBaseType_t priority; - - - /** - * @brief Timer Callback Function - * @details This function is called when the timer expires, and - * it sends a notification to the watchdog task to call - * the callback. - */ - static bool IRAM_ATTR timer_callback( - gptimer_handle_t timer, - const gptimer_alarm_event_data_t *data, - void *user_ctx - ){ - (void)timer; - (void)data; - - // Gets the user context - SerialCommWatchdogTimer* usr_timer = - (SerialCommWatchdogTimer *)(user_ctx); - - // Send a notification to the watchdog task to call the callback - BaseType_t high_task_wakeup = pdFALSE; - vTaskNotifyGiveFromISR( - usr_timer->_taskHandle, - &high_task_wakeup - ); - return (high_task_wakeup == pdTRUE); - } - - /** - * @brief Watchdog task function - * @details This function is executed by the watchdog task and - * waits for timer notification events. When it receives - * a notification, it calls the user-defined callback. - */ - static void task_function(void *pvArg){ - SerialCommWatchdogTimer* timer = (SerialCommWatchdogTimer *)(pvArg); - while (true) { - ulTaskNotifyTake( pdTRUE, portMAX_DELAY ); - timer->stop(); - if ( timer->_callback ) { - timer->_callback(); - } - } - } - - public: - SerialCommWatchdogTimer( - const char* task_name = "SerialCommWatchdog", - uint64_t timeout_us = uart_interbyte_timeout_us(SERIAL_COMM_UART_BAUDRATE), - Callback callback = nullptr, - uint32_t stack_size = 1024*4, - UBaseType_t priority = 5 - ): - _callback(callback), - _timeout_us(timeout_us), - task_name(task_name), - stack_size(stack_size), - priority(priority) - { - xTaskCreate( - this->task_function, - this->task_name, - this->stack_size, - this, - this->priority, - &this->_taskHandle - ); - - gptimer_config_t timer_config = { - .clk_src = GPTIMER_CLK_SRC_DEFAULT, - .direction = GPTIMER_COUNT_UP, - .resolution_hz = 1000000, // 1 tick = 1 microsecond - }; - ESP_ERROR_CHECK( - gptimer_new_timer( - &timer_config, - &_timer - ) - ); - - gptimer_event_callbacks_t callbacks = { - .on_alarm = timer_callback, - }; - ESP_ERROR_CHECK( - gptimer_register_event_callbacks( - _timer, - &callbacks, - this - ) - ); - - gptimer_alarm_config_t alarm_config = { - .alarm_count = timeout_us, - .reload_count = 0, - .flags = { - .auto_reload_on_alarm = false, - } - }; - ESP_ERROR_CHECK( - gptimer_set_alarm_action( - _timer, - &alarm_config - ) - ); - ESP_ERROR_CHECK(gptimer_enable(_timer)); - } - - - esp_err_t start() { - if ( this->_running) { - return ESP_OK; - } - ESP_ERROR_CHECK( - gptimer_set_raw_count(this->_timer, 0) - ); - esp_err_t err = gptimer_start(this->_timer); - if (err == ESP_OK) { - this->_running = true; - } - return err; - } - - - esp_err_t stop(){ - if ( !this->_running ) { - return ESP_OK; - } - esp_err_t err = gptimer_stop( this->_timer); - if ( err == ESP_OK || err == ESP_ERR_INVALID_STATE ) { - this->_running = false; - return ESP_OK; - } - return err; - } - - - esp_err_t kick(){ - ESP_ERROR_CHECK(this->stop()); - ESP_ERROR_CHECK( - gptimer_set_raw_count(this->_timer, 0) - ); - return this->start(); - } - - - ~SerialCommWatchdogTimer(){ - this->stop(); - if (this->_timer != nullptr) { - gptimer_disable(this->_timer); - gptimer_del_timer(this->_timer); - this->_timer = nullptr; - } - if (this->_taskHandle != nullptr) { - vTaskDelete(this->_taskHandle); - this->_taskHandle = nullptr; - } - } -}; \ No newline at end of file diff --git a/src/core/README.md b/src/core/README.md new file mode 100644 index 0000000..3e143e7 --- /dev/null +++ b/src/core/README.md @@ -0,0 +1,33 @@ +# Protocol Layer + +## Protocol infrastructure layer + +Responsável por: + +- parser; +- framing; +- CRC; +- seq_id; +- dispatcher; +- watchdog; +- serialization raw; +- transport orchestration. + +Não conhece: + +- services; +- topics; +- actions; +- payloads da aplicação. +- middleware/ +- Semantic communication layer + +Responsável por: + +- services; +- topics; +- actions; +- transactions; +- typed callbacks; +- serializers; +- request/reply abstraction. diff --git a/src/serial_comm.cpp b/src/core/serial_comm.cpp similarity index 75% rename from src/serial_comm.cpp rename to src/core/serial_comm.cpp index 19f6757..709fed1 100644 --- a/src/serial_comm.cpp +++ b/src/core/serial_comm.cpp @@ -46,12 +46,23 @@ errCode SerialComm::init( const Config &cfg ) { // Dispatcher initialization SerialCommDispatcher::Config dispatcher_cfg = {}; dispatcher_cfg.task_name = "SerialCommDispatcher"; - dispatcher_cfg.rx_queue_len = 16; - dispatcher_cfg.task_stack_size = 4096; - dispatcher_cfg.task_priority = 5; + + dispatcher_cfg.rx_queue_len = + SerialCommConfig::DISPATCHER_QUEUE_SIZE; + + dispatcher_cfg.task_stack_size = + SerialCommConfig::DISPATCHER_TASK_STACK_SIZE; + + dispatcher_cfg.task_priority = + SerialCommConfig::DISPATCHER_TASK_PRIORITY; + errCode err = this->dispatcher_.init( dispatcher_cfg ); if ( err != errCode::OK ) { - ESP_LOGE( TAG, "Failed to initialize dispatcher: %s", err_to_str(err) ); + ESP_LOGE( + TAG, + "Failed to initialize dispatcher: %s", + err_to_str(err) + ); this->cleanup_resources(); return err; } @@ -101,7 +112,7 @@ errCode SerialComm::start() { return err; } // Start transport - errCode err = this->transport_->start(); + err = this->transport_->start(); if (err != errCode::OK) { ESP_LOGE( TAG, "Failed to start transport: %s", err_to_str(err) ); this->dispatcher_.stop(); @@ -159,7 +170,7 @@ errCode SerialComm::send( const SerialCommProtoPacket& packet ) { return errCode::ERR_TIMEOUT; } - uint8_t tx_buffer[ SERIAL_COMM_MAX_PACKET_SIZE_V1 ]; + uint8_t tx_buffer[ SERIAL_COMM_MAX_PACKET_SIZE_V2 ]; size_t packet_len = SerialCommProtocol::encode( packet, @@ -200,15 +211,21 @@ errCode SerialComm::setup_interbyte_watchdog( ){ void SerialComm::on_interbyte_timeout() { if ( xSemaphoreTake( this->parser_mutex_, portMAX_DELAY ) == pdTRUE ) { - this->parser_.reset(); + bool had_partial_packet = + this->parser_.state() != SerialCommParser::State::WAIT_HEADER_0; + if ( had_partial_packet ) { + this->parser_.reset(); + } xSemaphoreGive( this->parser_mutex_ ); - ESP_LOGW( TAG, "Parser timeout reset" ); + if ( had_partial_packet ) { + ESP_LOGW( TAG, "Parser timeout reset" ); + } } } errCode SerialComm::register_callback( - SerialCommProtoCommand command, + SerialCommCommand command, packet_callback_t callback, void* ctx ) { @@ -220,7 +237,7 @@ errCode SerialComm::register_callback( } -errCode SerialComm::unregister_callback( SerialCommProtoCommand command ) { +errCode SerialComm::unregister_callback( SerialCommCommand command ) { return this->dispatcher_.unregister_callback( command ); } @@ -230,7 +247,8 @@ void SerialComm::transport_rx_callback( const uint8_t* data, size_t len ) { - if (ctx == nullptr){ + // Verify context + if ( ctx == nullptr ){ return; } auto* self = static_cast(ctx); @@ -239,6 +257,7 @@ void SerialComm::transport_rx_callback( void SerialComm::process_rx_data( const uint8_t* data, size_t len ) { + // Verify data pointer if (data == nullptr){ ESP_LOGE( TAG, "Received null data pointer" ); return; @@ -247,17 +266,39 @@ void SerialComm::process_rx_data( const uint8_t* data, size_t len ) { if ( this->interbyte_watchdog_ != nullptr ) { this->interbyte_watchdog_->kick(); } - // Parse Stream - SerialCommProtoPacket packet; - xSemaphoreTake( this->parser_mutex_, portMAX_DELAY ); - bool valid_packet = this->parser_.parse_buffer( data, len, packet ); - xSemaphoreGive( this->parser_mutex_ ); - - // Dispatch packet if valid - if ( valid_packet ) { - errCode err = this->dispatcher_.enqueue( packet ); - if ( err != errCode::OK ) { - ESP_LOGE( TAG, "Failed to enqueue packet" ); + // Parse Stream with multiples packets support + size_t offset_packet = 0; + while ( offset_packet < len ){ + SerialCommProtoPacket packet; + size_t consumed_bytes = 0; + xSemaphoreTake( this->parser_mutex_, portMAX_DELAY ); + bool valid_packet = this->parser_.parse_next_packet( + &data[offset_packet], + len - offset_packet, + consumed_bytes, + packet + ); + xSemaphoreGive( this->parser_mutex_ ); + + // Safety check to avoid infinite loop on parser error + if ( consumed_bytes == 0 ) { + ESP_LOGE( TAG, "Parser failed to consume bytes" ); + break; + } + offset_packet += consumed_bytes; + + // Dispatch packet if valid + if ( valid_packet ) { + errCode err = this->dispatcher_.enqueue( packet ); + if ( err != errCode::OK ) { + ESP_LOGE( TAG, "Failed to enqueue packet" ); + } else { + ESP_LOGD( TAG, "Packet enqueued: Command=0x%02X, SeqID=%u, PayloadLen=%u", + packet.header.command, + packet.header.seq_id, + packet.header.payload_len + ); + } } } } diff --git a/src/serial_comm_dispatcher.cpp b/src/core/serial_comm_dispatcher.cpp similarity index 97% rename from src/serial_comm_dispatcher.cpp rename to src/core/serial_comm_dispatcher.cpp index 6f61907..005c8bc 100644 --- a/src/serial_comm_dispatcher.cpp +++ b/src/core/serial_comm_dispatcher.cpp @@ -137,7 +137,7 @@ errCode SerialCommDispatcher::enqueue( "RX queue full. Dropped command: 0x%02X", packet.header.command ); - return errCode::ERR_QUEUE_FULL; + return errCode::ERR_BUFFER_FULL; } // Update statistics @@ -151,7 +151,7 @@ errCode SerialCommDispatcher::enqueue( errCode SerialCommDispatcher::register_callback( - SerialCommProtoCommand command, + SerialCommCommand command, packet_callback_t callback, void* ctx ) { @@ -174,7 +174,7 @@ errCode SerialCommDispatcher::register_callback( errCode SerialCommDispatcher::unregister_callback( - SerialCommProtoCommand command + SerialCommCommand command ) { uint8_t cmd = static_cast( command ); xSemaphoreTake( this->callback_mutex_, portMAX_DELAY ); @@ -205,7 +205,7 @@ void SerialCommDispatcher::dispatcher_task_entry( void* args ) { void SerialCommDispatcher::dispatcher_task() { - SerialCommProtoPacket packet; + static SerialCommProtoPacket packet; while ( true ) { if ( xQueueReceive( this->rx_queue_, &packet, portMAX_DELAY ) == pdTRUE ) { dispatch_packet( packet ); diff --git a/src/serial_comm_parser.cpp b/src/core/serial_comm_parser.cpp similarity index 76% rename from src/serial_comm_parser.cpp rename to src/core/serial_comm_parser.cpp index 94ba011..f2a639f 100644 --- a/src/serial_comm_parser.cpp +++ b/src/core/serial_comm_parser.cpp @@ -10,7 +10,6 @@ /* To use the errCode and err_to_str easily */ using namespace SerialCommResult_Codes; - bool SerialCommParser::parse_byte( uint8_t byte, SerialCommProtoPacket& out_packet @@ -29,7 +28,7 @@ bool SerialCommParser::parse_byte( if (byte == SERIAL_COMM_HEADER_1) { this->state_ = State::WAIT_HEADER_2; } else { - reset(); + this->reset(); } break; } @@ -37,17 +36,32 @@ bool SerialCommParser::parse_byte( // Header 2 case State::WAIT_HEADER_2: { if (byte == SERIAL_COMM_HEADER_2) { - synchronized_ = true; - this->state_ = State::READ_VERSION; + this->state_ = State::GET_SEQ_ID_L; } else { - reset(); + this->reset(); } break; } + case State::GET_SEQ_ID_L: { + packet_.header.seq_id = byte; + this->state_ = State::GET_SEQ_ID_H; + break; + } + + case State::GET_SEQ_ID_H: { + packet_.header.seq_id |= (byte << 8); + this->state_ = State::READ_VERSION; + break; + } + // READ VERSION case State::READ_VERSION: { packet_.header.version = byte; + if ( packet_.header.version != SERIAL_COMM_PROTOCOL_VER1 ) { + this->reset(); + break; + } this->state_ = State::READ_COMMAND; break; } @@ -55,7 +69,7 @@ bool SerialCommParser::parse_byte( // READ COMMAND case State::READ_COMMAND: { packet_.header.command = - static_cast(byte); + static_cast(byte); this->state_ = State::READ_LENGTH_L; break; } @@ -70,15 +84,18 @@ bool SerialCommParser::parse_byte( // READ LENGTH HIGH BYTE case State::READ_LENGTH_H: { packet_.header.payload_len |= (byte << 8); + // VALIDATE PAYLOAD SIZE - if ( packet_.header.payload_len > SERIAL_COMM_MAX_PAYLOAD_V1 ) { - reset(); + if ( packet_.header.payload_len > SERIAL_COMM_MAX_PAYLOAD ) { + this->reset(); break; } + // NO PAYLOAD if ( packet_.header.payload_len == 0 ) { this->state_ = State::READ_CRC_L; + } else { payload_index_ = 0; this->state_ = State::READ_PAYLOAD; @@ -111,23 +128,21 @@ bool SerialCommParser::parse_byte( if ( SerialCommProtocol::validate_packet(packet_)) { out_packet = packet_; this->state_ = State::PACKET_READY; - reset(); + this->reset(); return true; } - reset(); + this->reset(); break; } // PACKET READY case State::PACKET_READY: { - reset(); + this->reset(); break; } - // ERROR - case State::ERROR: default: { - reset(); + this->reset(); break; } } @@ -135,15 +150,21 @@ bool SerialCommParser::parse_byte( } -bool SerialCommParser::parse_buffer( +bool SerialCommParser::parse_next_packet( const uint8_t* data, size_t len, + size_t &consumed_bytes, SerialCommProtoPacket& out_packet ) { + // Verify input parameters if (data == nullptr) return false; + + // Inicialize output parameters + consumed_bytes = 0; for (size_t i = 0; i < len; i++) { - if ( parse_byte( data[i], out_packet ) ) { + consumed_bytes++; + if ( this->parse_byte( data[i], out_packet ) ) { return true; } } @@ -152,11 +173,11 @@ bool SerialCommParser::parse_buffer( void SerialCommParser::reset() { + this->packet_.header.payload_len = 0; + this->payload_index_ = 0; + this->packet_.crc = 0; + this->crc_l_ = 0; this->state_ = State::WAIT_HEADER_0; - payload_index_ = 0; - crc_l_ = 0; - synchronized_ = false; - SerialCommProtocol::clear_packet( packet_ ); } @@ -165,18 +186,13 @@ SerialCommParser::State SerialCommParser::state() const { } -bool SerialCommParser::synchronized() const { - return synchronized_; -} - - size_t SerialCommParser::payload_index() const { - return payload_index_; + return this->payload_index_; } uint16_t SerialCommParser::expected_payload_size() const { - return packet_.header.payload_len; + return this->packet_.header.payload_len; } @@ -185,6 +201,8 @@ const char* SerialCommParser::state_to_str( State state ) { case State::WAIT_HEADER_0: return "WAIT_HEADER_0"; case State::WAIT_HEADER_1: return "WAIT_HEADER_1"; case State::WAIT_HEADER_2: return "WAIT_HEADER_2"; + case State::GET_SEQ_ID_L: return "GET_SEQ_ID_L"; + case State::GET_SEQ_ID_H: return "GET_SEQ_ID_H"; case State::READ_VERSION: return "READ_VERSION"; case State::READ_COMMAND: return "READ_COMMAND"; case State::READ_LENGTH_L: return "READ_LENGTH_L"; diff --git a/src/serial_comm_protocol.cpp b/src/core/serial_comm_protocol.cpp similarity index 78% rename from src/serial_comm_protocol.cpp rename to src/core/serial_comm_protocol.cpp index 4419065..4a61109 100644 --- a/src/serial_comm_protocol.cpp +++ b/src/core/serial_comm_protocol.cpp @@ -29,6 +29,10 @@ size_t SerialCommProtocol::encode( out_buffer[index++] = SERIAL_COMM_HEADER_1; out_buffer[index++] = SERIAL_COMM_HEADER_2; + // SEQ_ID + out_buffer[index++] = (packet.header.seq_id & 0xFF); + out_buffer[index++] = ((packet.header.seq_id >> 8) & 0xFF); + // VERSION out_buffer[index++] = packet.header.version; @@ -71,14 +75,15 @@ int32_t SerialCommProtocol::decode( return errCode::ERR_NULL_POINTER; } - // Check de maximum acceptable packet size - if ( raw_len > SERIAL_COMM_MAX_PACKET_SIZE_V1 ) { + // Check de maximum acceptable packet size (accept V2 max as upper bound) + if ( raw_len > SERIAL_COMM_MAX_PACKET_SIZE_V2 ) { return errCode::ERR_OVERFLOW; } // Check minimum size for a valid packet size_t min_packet_size = SERIAL_COMM_HEADER_SIZE + + SERIAL_COMM_SEQ_SIZE + SERIAL_COMM_PROTOCOL_VER_SIZE + SERIAL_COMM_COMMAND_SIZE + SERIAL_COMM_LENGTH_SIZE + @@ -98,12 +103,18 @@ int32_t SerialCommProtocol::decode( } size_t index = 3; + // SEQ_ID + out_packet.header.seq_id = + raw_data[index] | + (raw_data[index + 1] << 8); + index += 2; + // VERSION out_packet.header.version = raw_data[index++]; // COMMAND out_packet.header.command = - static_cast( + static_cast( raw_data[index++] ); @@ -114,11 +125,12 @@ int32_t SerialCommProtocol::decode( index += 2; // PAYLOAD SIZE VALIDATION - if ( out_packet.header.payload_len > SERIAL_COMM_MAX_PAYLOAD_V1 ) { + if ( out_packet.header.payload_len > SERIAL_COMM_MAX_PAYLOAD ) { return errCode::ERR_OVERFLOW; } size_t expected_size = SERIAL_COMM_HEADER_SIZE + + SERIAL_COMM_SEQ_SIZE + SERIAL_COMM_PROTOCOL_VER_SIZE + SERIAL_COMM_COMMAND_SIZE + SERIAL_COMM_LENGTH_SIZE + @@ -168,37 +180,30 @@ uint16_t SerialCommProtocol::compute_crc16( bool SerialCommProtocol::validate_crc( const SerialCommProtoPacket& packet ) { - uint8_t temp_buffer[ SERIAL_COMM_MAX_PACKET_SIZE_V1 ]; + uint8_t temp_buffer[ SERIAL_COMM_MAX_PACKET_SIZE_V2 ]; size_t index = 0; + // SEQ_ID (CRC excludes only the preamble header and the CRC field) + temp_buffer[index++] = (packet.header.seq_id & 0xFF); + temp_buffer[index++] = ((packet.header.seq_id >> 8) & 0xFF); + // VERSION temp_buffer[index++] = packet.header.version; // COMMAND - temp_buffer[index++] = - static_cast( - packet.header.command - ); + temp_buffer[index++] = static_cast(packet.header.command); // PAYLOAD LENGTH temp_buffer[index++] = (packet.header.payload_len & 0xFF); temp_buffer[index++] = ((packet.header.payload_len >> 8) & 0xFF); // PAYLOAD - memcpy( - &temp_buffer[index], - packet.payload, - packet.header.payload_len - ); + memcpy(&temp_buffer[index], packet.payload, packet.header.payload_len); index += packet.header.payload_len; // COMPUTE CRC16 TO COMPARE - uint16_t computed_crc = - compute_crc16( - temp_buffer, - index - ); - return ( computed_crc == packet.crc ); + uint16_t computed_crc = compute_crc16(temp_buffer, index); + return (computed_crc == packet.crc ); } @@ -210,7 +215,7 @@ bool SerialCommProtocol::validate_packet( return false; } // Validate payload length - if ( packet.header.payload_len > SERIAL_COMM_MAX_PAYLOAD_V1 ) { + if ( packet.header.payload_len > SERIAL_COMM_MAX_PAYLOAD ) { return false; } // Validate CRC @@ -226,6 +231,7 @@ size_t SerialCommProtocol::packet_size( ) { return SERIAL_COMM_HEADER_SIZE + + SERIAL_COMM_SEQ_SIZE + SERIAL_COMM_PROTOCOL_VER_SIZE + SERIAL_COMM_COMMAND_SIZE + SERIAL_COMM_LENGTH_SIZE + @@ -234,33 +240,7 @@ size_t SerialCommProtocol::packet_size( } void SerialCommProtocol::clear_packet( SerialCommProtoPacket& packet ) { - memset( &packet, 0, sizeof(SerialCommProtoPacket) ); + memset(&packet, 0, sizeof(SerialCommProtoPacket)); packet.header.version = SERIAL_COMM_PROTOCOL_VER1; - packet.header.command = SerialCommProtoCommand::UNKNOWN; + packet.header.command = SerialCommCommand::UNDEFINED; } - - -const char* SerialCommProtocol::command_to_str( - SerialCommProtoCommand cmd -) { - switch (cmd) { - case SerialCommProtoCommand::PING: - return "PING"; - case SerialCommProtoCommand::READ: - return "READ"; - case SerialCommProtoCommand::WRITE: - return "WRITE"; - case SerialCommProtoCommand::REPLY: - return "REPLY"; - case SerialCommProtoCommand::ERROR: - return "ERROR"; - case SerialCommProtoCommand::EVENT: - return "EVENT"; - case SerialCommProtoCommand::ACK: - return "ACK"; - case SerialCommProtoCommand::NACK: - return "NACK"; - default: - return "UNKNOWN"; - } -} \ No newline at end of file diff --git a/src/core/serial_comm_watchdog.cpp b/src/core/serial_comm_watchdog.cpp new file mode 100644 index 0000000..ab74a5a --- /dev/null +++ b/src/core/serial_comm_watchdog.cpp @@ -0,0 +1,159 @@ +/** + * @file serial_comm_watchdog.cpp + * @brief Serial communication watchdog timer implementation + * @author Bruno Gabriel Flores Sampaio + * @date Created on 26 of May, 2026 + */ + +#include "serial_comm_watchdog.h" + + +bool IRAM_ATTR SerialCommWatchdogTimer::timer_callback( + gptimer_handle_t timer, + const gptimer_alarm_event_data_t *data, + void *user_ctx +){ + (void)timer; + (void)data; + + // Gets the user context + SerialCommWatchdogTimer* usr_timer = + (SerialCommWatchdogTimer *)(user_ctx); + + // Send a notification to the watchdog task to call the callback + BaseType_t high_task_wakeup = pdFALSE; + vTaskNotifyGiveFromISR( + usr_timer->_taskHandle, + &high_task_wakeup + ); + return (high_task_wakeup == pdTRUE); +} + +/** + * @brief Watchdog task function + * @details This function is executed by the watchdog task and + * waits for timer notification events. When it receives + * a notification, it calls the user-defined callback. + */ +void SerialCommWatchdogTimer::task_function(void *pvArg){ + SerialCommWatchdogTimer* timer = (SerialCommWatchdogTimer *)(pvArg); + while (true) { + ulTaskNotifyTake( pdTRUE, portMAX_DELAY ); + timer->stop(); + if ( timer->_callback ) { + timer->_callback(); + } + } +} + +SerialCommWatchdogTimer::SerialCommWatchdogTimer( + const char* task_name, + uint64_t timeout_us, + Callback callback, + uint32_t stack_size, + UBaseType_t priority +): + _callback(callback), + _timeout_us(timeout_us), + task_name(task_name), + stack_size(stack_size), + priority(priority) +{ + xTaskCreate( + this->task_function, + this->task_name, + this->stack_size, + this, + this->priority, + &this->_taskHandle + ); + + gptimer_config_t timer_config = { + .clk_src = GPTIMER_CLK_SRC_DEFAULT, + .direction = GPTIMER_COUNT_UP, + .resolution_hz = 1000000, // 1 tick = 1 microsecond + }; + ESP_ERROR_CHECK( + gptimer_new_timer( + &timer_config, + &_timer + ) + ); + + gptimer_event_callbacks_t callbacks = { + .on_alarm = timer_callback, + }; + ESP_ERROR_CHECK( + gptimer_register_event_callbacks( + _timer, + &callbacks, + this + ) + ); + + gptimer_alarm_config_t alarm_config = { + .alarm_count = timeout_us, + .reload_count = 0, + .flags = { + .auto_reload_on_alarm = false, + } + }; + ESP_ERROR_CHECK( + gptimer_set_alarm_action( + _timer, + &alarm_config + ) + ); + ESP_ERROR_CHECK(gptimer_enable(_timer)); +} + + +esp_err_t SerialCommWatchdogTimer::start() { + if ( this->_running) { + return ESP_OK; + } + ESP_ERROR_CHECK( + gptimer_set_raw_count(this->_timer, 0) + ); + esp_err_t err = gptimer_start(this->_timer); + if (err == ESP_OK) { + this->_running = true; + } + return err; +} + + +esp_err_t SerialCommWatchdogTimer::stop(){ + if ( !this->_running ) { + return ESP_OK; + } + esp_err_t err = gptimer_stop( this->_timer); + if ( err == ESP_OK || err == ESP_ERR_INVALID_STATE ) { + this->_running = false; + return ESP_OK; + } + return err; +} + + +esp_err_t SerialCommWatchdogTimer::kick(){ + ESP_ERROR_CHECK(this->stop()); + ESP_ERROR_CHECK( + gptimer_set_raw_count(this->_timer, 0) + ); + return this->start(); +} + + +SerialCommWatchdogTimer::~SerialCommWatchdogTimer(){ + this->stop(); + if (this->_timer != nullptr) { + gptimer_disable(this->_timer); + gptimer_del_timer(this->_timer); + this->_timer = nullptr; + } + if (this->_taskHandle != nullptr) { + vTaskDelete(this->_taskHandle); + this->_taskHandle = nullptr; + } +} diff --git a/src/middleware/README.md b/src/middleware/README.md new file mode 100644 index 0000000..6eda539 --- /dev/null +++ b/src/middleware/README.md @@ -0,0 +1,17 @@ +# Middleware Layer + +## Orquestra abstrações semânticas + +Ele deve: + +- registrar services; +- registrar topics; +- registrar actions; +- integrar com SerialComm; +- integrar com TransactionManager; +- fazer roteamento semântico; +- encapsular packets; +- encapsular seq_id; +- encapsular replies. + +Ele abstrai o uso do SerialComm, Dispatcher e Parser do usuário. diff --git a/src/middleware/serial_comm_manager.cpp b/src/middleware/serial_comm_manager.cpp new file mode 100644 index 0000000..cb5ebfe --- /dev/null +++ b/src/middleware/serial_comm_manager.cpp @@ -0,0 +1,297 @@ +/** + * @file serial_comm_manager.cpp + * @brief High-level semantic middleware manager implementation + */ + +#include "serial_comm_manager.h" + + +/* To use the errCode and err_to_str easily */ +using namespace SerialCommResult_Codes; + + +static const char* TAG = "SERIAL_COMM_MANAGER"; + + +errCode SerialCommManager::init( const Config& cfg ) { + if ( this->serial_ == nullptr ) { + return errCode::ERR_NULL_POINTER; + } + if ( this->initialized_ ) { + return errCode::ERR_ALREADY_INITIALIZED; + } + + this->cfg_ = cfg; + + // Create mutexes + this->seq_mutex_ = xSemaphoreCreateMutex(); + if ( this->seq_mutex_ == nullptr ) { + return errCode::ERR_NO_MEMORY; + } + this->registry_mutex_ = xSemaphoreCreateMutex(); + if ( this->registry_mutex_ == nullptr ) { + vSemaphoreDelete( this->seq_mutex_ ); + this->seq_mutex_ = nullptr; + return errCode::ERR_NO_MEMORY; + } + + // Clear registries + this->clear_registries(); + + // Init transaction manager + errCode res = this->transactions_.init(); + if ( res != errCode::OK ) { + vSemaphoreDelete( this->seq_mutex_ ); + this->seq_mutex_ = nullptr; + vSemaphoreDelete( this->registry_mutex_ ); + this->registry_mutex_ = nullptr; + return res; + } + + // Register internal packet router for all commands + for ( uint16_t cmd = 0; cmd < 256; cmd++ ) { + this->serial_->register_callback( + static_cast(cmd), + serial_packet_callback, + this + ); + } + this->initialized_ = true; + ESP_LOGI( TAG, "SerialCommManager initialized" ); + return errCode::OK; +} + + +errCode SerialCommManager::start() { + if ( !this->initialized_ ) { + return errCode::ERR_NOT_INITIALIZED; + } + if ( this->running_ ) { + return errCode::ERR_INVALID_STATE; + } + this->running_ = true; + ESP_LOGI( TAG, "SerialCommManager started" ); + return errCode::OK; +} + + +errCode SerialCommManager::stop() { + if ( !this->running_ ) { + return errCode::ERR_INVALID_STATE; + } + this->running_ = false; + ESP_LOGI( TAG, "SerialCommManager stopped" ); + return errCode::OK; +} + + +errCode SerialCommManager::deinit() { + if ( this->running_ ) { + this->stop(); + } + this->transactions_.deinit(); + if ( this->seq_mutex_ != nullptr ) { + vSemaphoreDelete( this->seq_mutex_ ); + this->seq_mutex_ = nullptr; + } + if( this->registry_mutex_ != nullptr ) { + vSemaphoreDelete( this->registry_mutex_ ); + this->registry_mutex_ = nullptr; + } + this->clear_registries(); + this->initialized_ = false; + ESP_LOGI( TAG, "SerialCommManager deinitialized" ); + return errCode::OK; +} + + +void SerialCommManager::serial_packet_callback( + void* ctx, + const SerialCommProtoPacket& packet +) { + if ( ctx == nullptr ) { + return; + } + auto* self = static_cast( ctx ); + self->route_packet(packet); +} + + +void SerialCommManager::handle_reply( + const SerialCommProtoPacket& packet +) { + ESP_LOGD( + TAG, + "Reply packet received: cmd=0x%02X seq=%u len=%u", + static_cast( packet.header.command ), + packet.header.seq_id, + packet.header.payload_len + ); + // Try to resolve transaction + errCode res = this->transactions_.resolve_transaction( packet ); + if ( res != errCode::OK ) { + ESP_LOGW( + TAG, + "Failed to resolve transaction for reply: seq=%u err=%s", + packet.header.seq_id, + err_to_str( res ) + ); + } else { + ESP_LOGD( + TAG, + "Transaction resolved for reply: seq=%u", + packet.header.seq_id + ); + } +} + + +uint16_t SerialCommManager::allocate_seq_id() { + uint16_t seq = 0; + if ( xSemaphoreTake( this->seq_mutex_, portMAX_DELAY ) == pdTRUE ) { + seq = this->next_seq_id_++; + if ( this->next_seq_id_ == 0 ) { + this->next_seq_id_ = 1; + } + xSemaphoreGive( this->seq_mutex_ ); + } + return seq; +} + + +errCode SerialCommManager::build_packet( + Command command, + uint16_t seq_id, + const uint8_t* payload, + size_t payload_len, + SerialCommProtoPacket& out_packet +) { + if ( payload_len > SERIAL_COMM_MAX_PAYLOAD ) { + return errCode::ERR_INVALID_ARG; + } + SerialCommProtocol::clear_packet( out_packet ); + out_packet.header.seq_id = seq_id; + out_packet.header.version = SERIAL_COMM_PROTOCOL_VER1; + out_packet.header.command = command; + out_packet.header.payload_len = payload_len; + if ( payload != nullptr && payload_len > 0 ) { + memcpy( out_packet.payload, payload, payload_len ); + } + return errCode::OK; +} + + +bool SerialCommManager::initialized() const { + return this->initialized_; +} + + +bool SerialCommManager::running() const { + return this->running_; +} + + +uint16_t SerialCommManager::current_seq_id() const { + return this->next_seq_id_; +} + + +void SerialCommManager::clear_registries() { + memset( this->services_, 0, sizeof(this->services_) ); + memset( this->topics_, 0, sizeof(this->topics_) ); + memset( this->actions_, 0, sizeof(this->actions_) ); +} + + +errCode SerialCommManager::send_reply( + Command command, + uint16_t seq_id, + const uint8_t* payload, + size_t payload_len +) { + SerialCommProtoPacket packet; + errCode err = build_packet( + make_reply(command), seq_id, payload, payload_len, packet + ); + if ( err != errCode::OK ) { + return err; + } + return this->serial_->send( packet ); +} + + + +void SerialCommManager::route_packet( const SerialCommProtoPacket& packet ) { + if ( is_reply( packet.header.command ) ) { + this->handle_reply(packet); + return; + } + if ( this->is_service_command( packet.header.command ) ) { + this->handle_service_request(packet); + return; + } + if ( this->is_topic_command( packet.header.command ) ) { + this->handle_topic_message( packet ); + return; + } + if ( this->is_action_command( packet.header.command ) ) { + this->handle_action_packet( packet ); + return; + } + ESP_LOGW( + TAG, + "Unhandled packet command=0x%02X", + static_cast( packet.header.command ) + ); +} + + +void SerialCommManager::handle_topic_message( const SerialCommProtoPacket& packet ) { + ESP_LOGD( + TAG, + "Topic packet received: cmd=0x%02X seq=%u len=%u", + static_cast( packet.header.command ), + packet.header.seq_id, + packet.header.payload_len + ); + auto* entry = find_topic( packet.header.command ); + if ( entry == nullptr || entry->topic == nullptr ) { + ESP_LOGW(TAG, "No topic handler for command=0x%02X", static_cast( packet.header.command )); + return; + } + errCode res = entry->topic->handle_packet( packet ); + if ( res != errCode::OK ) { + ESP_LOGW(TAG, "Topic handler failed: cmd=0x%02X err=%s", static_cast(packet.header.command), err_to_str(res)); + } +} + + +void SerialCommManager::handle_action_packet( const SerialCommProtoPacket& packet ) { + ESP_LOGD( + TAG, + "Action packet received: cmd=0x%02X seq=%u len=%u", + static_cast( packet.header.command ), + packet.header.seq_id, + packet.header.payload_len + ); + auto* entry = find_action( packet.header.command ); + if ( entry == nullptr || entry->action == nullptr ) { + ESP_LOGW(TAG, "No action handler for command=0x%02X", static_cast( packet.header.command )); + return; + } + errCode res = entry->action->handle_packet( packet ); + if ( res != errCode::OK ) { + ESP_LOGW(TAG, "Action handler failed: cmd=0x%02X err=%s", static_cast(packet.header.command), err_to_str(res)); + } +} + + +void SerialCommManager::handle_request( const SerialCommProtoPacket& packet ) { + ESP_LOGD(TAG, "handle_request called for cmd=0x%02X (not implemented fully)", static_cast(packet.header.command)); +} + + +SerialCommManager::~SerialCommManager() { + this->deinit(); +} + diff --git a/src/middleware/serial_comm_manager_action.cpp b/src/middleware/serial_comm_manager_action.cpp new file mode 100644 index 0000000..63ddd98 --- /dev/null +++ b/src/middleware/serial_comm_manager_action.cpp @@ -0,0 +1,38 @@ +/** + * @file serial_comm_manager_action.cpp + * @brief SerialCommManager action handling implementation + * @author Bruno Gabriel Flores Sampaio + * @date Created on 26 of May, 2026 + */ + +#include "serial_comm_manager.h" + +bool SerialCommManager::is_action_command( Command command ) const { + for ( size_t i = 0; i < MAX_ACTIONS; i++ ) { + if ( + this->actions_[i].used && + this->actions_[i].command == command + ) { + return true; + } + } + return false; +} + + +SerialCommManager::ActionEntry* SerialCommManager::find_action( + Command command +) { + xSemaphoreTake( this->registry_mutex_, portMAX_DELAY ); + for ( size_t i = 0; i < MAX_ACTIONS; i++ ) { + if ( + this->actions_[i].used && + this->actions_[i].command == command + ) { + xSemaphoreGive( this->registry_mutex_ ); + return &this->actions_[i]; + } + } + xSemaphoreGive( this->registry_mutex_ ); + return nullptr; +} \ No newline at end of file diff --git a/src/middleware/serial_comm_manager_service.cpp b/src/middleware/serial_comm_manager_service.cpp new file mode 100644 index 0000000..5ad6f05 --- /dev/null +++ b/src/middleware/serial_comm_manager_service.cpp @@ -0,0 +1,89 @@ +/** + * @file serial_comm_manager_service.cpp + * @brief Serial communication middleware service handling + * implementation + * @author Bruno Gabriel Flores Sampaio + * @date Created on 26 of May, 2026 + */ + +#include "serial_comm_manager.h" + +static const char* TAG = "SERIAL_COMM_MANAGER"; + + +void SerialCommManager::handle_service_request( + const SerialCommProtoPacket& packet +) { + ESP_LOGD( + TAG, + "Request packet received: cmd=0x%02X seq=%u len=%u", + static_cast( packet.header.command ), + packet.header.seq_id, + packet.header.payload_len + ); + auto* entry = this->find_service( packet.header.command ); + if ( entry == nullptr || entry->service == nullptr ) { + ESP_LOGW( + TAG, + "No service handler for command=0x%02X", + static_cast( packet.header.command ) + ); + return; + } + + static SerialCommProtoPacket response; + SerialCommProtocol::clear_packet( response ); + errCode res = entry->service->handle_packet( packet, response ); + if ( res != errCode::OK ) { + ESP_LOGW( + TAG, + "Service handler failed: cmd=0x%02X err=%s", + static_cast( packet.header.command ), + err_to_str( res ) + ); + return; + } + + if ( this->cfg_.enable_auto_reply ) { + res = this->serial_->send( response ); + if ( res != errCode::OK ) { + ESP_LOGW( + TAG, + "Failed to send service reply: cmd=0x%02X err=%s", + static_cast( packet.header.command ), + err_to_str( res ) + ); + } + } +} + + +SerialCommManager::ServiceEntry* SerialCommManager::find_service( + Command command +) { + xSemaphoreTake( this->registry_mutex_, portMAX_DELAY ); + for ( size_t i = 0; i < MAX_SERVICES; i++ ) { + if ( + this->services_[i].used && + this->services_[i].command == command + ) { + xSemaphoreGive( this->registry_mutex_ ); + return &this->services_[i]; + } + } + xSemaphoreGive( this->registry_mutex_ ); + return nullptr; +} + + +bool SerialCommManager::is_service_command( Command command ) const { + for ( size_t i = 0; i < MAX_SERVICES; i++ ) { + if ( + this->services_[i].used && + this->services_[i].command == command + ) { + return true; + } + } + return false; +} \ No newline at end of file diff --git a/src/middleware/serial_comm_manager_topic.cpp b/src/middleware/serial_comm_manager_topic.cpp new file mode 100644 index 0000000..e3e581e --- /dev/null +++ b/src/middleware/serial_comm_manager_topic.cpp @@ -0,0 +1,39 @@ +/** + * @file serial_comm_manager_topic.cpp + * @brief SerialCommManager topic handling implementation + * @author Bruno Gabriel Flores Sampaio + * @date Created on 26 of May, 2026 + */ + +#include "serial_comm_manager.h" + + +SerialCommManager::TopicEntry* SerialCommManager::find_topic( + Command command +) { + xSemaphoreTake( this->registry_mutex_, portMAX_DELAY ); + for ( size_t i = 0; i < MAX_TOPICS; i++ ) { + if ( + this->topics_[i].used && + this->topics_[i].command == command + ) { + xSemaphoreGive( this->registry_mutex_ ); + return &this->topics_[i]; + } + } + xSemaphoreGive( this->registry_mutex_ ); + return nullptr; +} + + +bool SerialCommManager::is_topic_command( Command command ) const { + for ( size_t i = 0; i < MAX_TOPICS; i++ ) { + if ( + this->topics_[i].used && + this->topics_[i].command == command + ) { + return true; + } + } + return false; +} diff --git a/src/middleware/serial_comm_transaction_manager.cpp b/src/middleware/serial_comm_transaction_manager.cpp new file mode 100644 index 0000000..599e43c --- /dev/null +++ b/src/middleware/serial_comm_transaction_manager.cpp @@ -0,0 +1,152 @@ +/** + * @file serial_comm_transaction_manager.h + * @brief Transaction manager for SerialComm middleware + * @details Responsible for: + * - Pending request tracking + * - Reply matching using seq_id + * - Request timeout management + * - Synchronous service waiting + * - Transaction lifecycle management + * + * @author Bruno Gabriel Flores Sampaio + * @date Created on 26 of May, 2026 + */ + + +#include "serial_comm_transaction_manager.h" + + +/* To use the errCode and err_to_str easily */ +using namespace SerialCommResult_Codes; + + +errCode SerialCommTransactionManager::init() { + if ( this->initialized_ ) { + return errCode::ERR_ALREADY_INITIALIZED; + } + this->mutex_ = xSemaphoreCreateMutex(); + if ( this->mutex_ == nullptr ) { + return errCode::ERR_NO_MEMORY; + } + for ( size_t i = 0; i < MAX_TRANSACTIONS; i++ ) { + this->transactions_[i].semaphore = xSemaphoreCreateBinary(); + if ( this->transactions_[i].semaphore == nullptr ) { + return errCode::ERR_NO_MEMORY; + } + } + this->initialized_ = true; + return errCode::OK; +} + + +errCode SerialCommTransactionManager::deinit() { + for ( size_t i = 0; i < MAX_TRANSACTIONS; i++ ) { + if ( this->transactions_[i].semaphore != nullptr ) { + vSemaphoreDelete( this->transactions_[i].semaphore ); + this->transactions_[i].semaphore = nullptr; + } + } + if ( this->mutex_ != nullptr ) { + vSemaphoreDelete( this->mutex_ ); + this->mutex_ = nullptr; + } + this->initialized_ = false; + return errCode::OK; +} + + +errCode SerialCommTransactionManager::create_transaction( uint16_t seq_id ) { + if ( !this->initialized_ ) { + return errCode::ERR_NOT_INITIALIZED; + } + xSemaphoreTake( this->mutex_, portMAX_DELAY ); + for ( size_t i = 0; i < MAX_TRANSACTIONS; i++ ) { + if ( !this->transactions_[i].active ) { + this->transactions_[i].active = true; + this->transactions_[i].completed = false; + this->transactions_[i].seq_id = seq_id; + xSemaphoreGive( this->mutex_ ); + return errCode::OK; + } + } + xSemaphoreGive( this->mutex_ ); + return errCode::ERR_NO_MEMORY; +} + + +errCode SerialCommTransactionManager::wait_reply( + uint16_t seq_id, + SerialCommProtoPacket& out_reply, + uint32_t timeout_ms +) { + Transaction* transaction = this->find_transaction(seq_id); + if ( transaction == nullptr ) { + return errCode::ERR_EMPTY; + } + if ( xSemaphoreTake( + transaction->semaphore, + pdMS_TO_TICKS(timeout_ms) + ) != pdTRUE + ) { + this->destroy_transaction(seq_id); + return errCode::ERR_TIMEOUT; + } + out_reply = transaction->reply; + this->destroy_transaction(seq_id); + return errCode::OK; +} + + +errCode SerialCommTransactionManager::resolve_transaction( + const SerialCommProtoPacket& packet +) { + Transaction* transaction = + this->find_transaction( packet.header.seq_id ); + if ( transaction == nullptr ) { + return errCode::ERR_EMPTY; + } + transaction->reply = packet; + transaction->completed = true; + xSemaphoreGive( transaction->semaphore ); + return errCode::OK; +} + + +SerialCommTransactionManager::Transaction* SerialCommTransactionManager::find_transaction( uint16_t seq_id ) { + for ( size_t i = 0; i < MAX_TRANSACTIONS; i++ ) { + if ( + this->transactions_[i].active && + this->transactions_[i].seq_id == seq_id + ) { + return &this->transactions_[i]; + } + } + return nullptr; +} + + +void SerialCommTransactionManager::destroy_transaction( uint16_t seq_id ) { + xSemaphoreTake( this->mutex_, portMAX_DELAY ); + for ( size_t i = 0; i < MAX_TRANSACTIONS; i++ ) { + if ( + this->transactions_[i].active && + this->transactions_[i].seq_id == seq_id + ) { + this->transactions_[i].active = false; + this->transactions_[i].completed = false; + this->transactions_[i].seq_id = 0; + memset( + &this->transactions_[i].reply, + 0, + sizeof( SerialCommProtoPacket ) + ); + break; + } + } + xSemaphoreGive( this->mutex_ ); +} + + +SerialCommTransactionManager::~SerialCommTransactionManager() { + this->deinit(); +}; diff --git a/src/serial_comm_crc.cpp b/src/serial_comm_crc.cpp deleted file mode 100644 index e69de29..0000000 diff --git a/src/serial_comm_eventloop.cpp b/src/serial_comm_eventloop.cpp deleted file mode 100644 index e69de29..0000000 diff --git a/src/serial_comm_ringbuff.cpp b/src/serial_comm_ringbuff.cpp deleted file mode 100644 index e69de29..0000000 diff --git a/src/transport/README.md b/src/transport/README.md new file mode 100644 index 0000000..a47e92b --- /dev/null +++ b/src/transport/README.md @@ -0,0 +1,25 @@ +# Transport Layer + +## Hardware abstraction layer + +Responsável por: + +- UART; +- USB CDC; +- TCP; +- BLE; +- SPI; +- RS485. + +Não conhece: + +- packet; +- parser; +- services; +- topics. + +Trabalha apenas com bytes em formato de Stream. + +## TODO + +Implementar um transporte que seja em quadros e não stream como a serial é hoje. diff --git a/src/uart_serial_comm.cpp b/src/transport/uart_serial_comm.cpp similarity index 85% rename from src/uart_serial_comm.cpp rename to src/transport/uart_serial_comm.cpp index 1772cc6..65ce308 100644 --- a/src/uart_serial_comm.cpp +++ b/src/transport/uart_serial_comm.cpp @@ -13,17 +13,20 @@ using namespace SerialCommResult_Codes; static const char* TAG = "UART_TRANSPORT"; -UARTTransport::UARTTransport( const HardwareConfig& hw_cfg ) - : hw_cfg_(hw_cfg), - state_(State::UNINITIALIZED), - uart_queue_(nullptr), - uart_task_(nullptr), - rx_callback_(nullptr), - tx_done_callback_(nullptr), - event_callback_(nullptr), - rx_ctx_(nullptr), - tx_ctx_(nullptr), - event_ctx_(nullptr) +UARTTransport::UARTTransport( const HardwareConfig& hw_cfg ) : + hw_cfg_(hw_cfg), + uart_mutex_(nullptr), + state_mutex_(nullptr), + uart_queue_(nullptr), + uart_task_(nullptr), + tx_done_callback_(nullptr), + event_callback_(nullptr), + rx_callback_(nullptr), + event_ctx_(nullptr), + rx_ctx_(nullptr), + tx_ctx_(nullptr), + cfg_{}, + state_(State::UNINITIALIZED) { } @@ -46,7 +49,7 @@ errCode UARTTransport::init( const Config& cfg ) { (uart_port_t)hw_cfg_.uart_port, cfg.rx_buffer_size, cfg.tx_buffer_size, - 32, + SerialCommConfig::UART_EVENT_QUEUE_SIZE, &uart_queue_, 0 ); @@ -100,15 +103,32 @@ errCode UARTTransport::start() { return errCode::ERR_INVALID_STATE; } - xTaskCreatePinnedToCore( - uart_event_task, - "uart_event_task", - 4096, - this, - 5, - &uart_task_, - 1 - ); + // Check if the Task have Core afinity enabled + BaseType_t task_result; + if ( SerialCommConfig::UART_USE_TASK_CORE_AFINITY ){ + task_result = xTaskCreatePinnedToCore( + uart_event_task, + "uart_event_task", + SerialCommConfig::UART_TASK_STACK_SIZE, + this, + SerialCommConfig::UART_TASK_PRIORITY, + &uart_task_, + SerialCommConfig::UART_TASK_CORE + ); + } else { + task_result = xTaskCreate( + uart_event_task, + "uart_event_task", + SerialCommConfig::UART_TASK_STACK_SIZE, + this, + SerialCommConfig::UART_TASK_PRIORITY, + &uart_task_ + ); + } + if ( task_result != pdPASS ) { + ESP_LOGE( TAG, "Failed to create UART event task" ); + return errCode::ERR_FAIL; + } state_ = State::RUNNING; ESP_LOGI( TAG, "UART started" );