xnecas 1 týždeň pred
commit
4ad085e82e

+ 37 - 0
examples/InterSerialBridge.cpp

@@ -0,0 +1,37 @@
+#include "Arduino.h"
+
+#include "EspNowSerial/EspNowSerial.h"
+#include "InterSerialBridge/InterSerialBridge.h"
+
+
+EspMacAddress peer_addr = {0x9c, 0x9e, 0x6e, 0xe0, 0x87, 0xf4};
+CcmpMasterKey pmk = {0x2a, 0xc9, 0xe6, 0x4e, 0x36, 0x4c, 0xa7, 0xf4, 0xca, 0xd0, 0x4a, 0xdd, 0x34, 0xc2, 0x28, 0x25};
+CcmpMasterKey lmk = {0x8c, 0xd3, 0xa5, 0x7a, 0x25, 0xdf, 0x92, 0x45, 0x2a, 0xf3, 0xd5, 0x0d, 0x56, 0xc8, 0x3c, 0x34};
+
+EspNowSerial EnSerial(WIFI_MODE_STA);
+InterSerialBridge<1024> SerialBridge(Serial, EnSerial);
+
+void setup() 
+{  
+  // Initialize HW Serial
+  Serial.begin(921600);
+
+  // Initialize ESP-NOW as serial
+  uint8_t status = EnSerial.begin(11, false, pmk);
+  status += EnSerial.add_peer_encrypted(peer_addr, lmk);
+
+  // Init ESP-NOW
+  if (status != ESP_OK) 
+  {
+    while (1)
+    { 
+      delay(1000);
+    }
+  }
+}
+ 
+void loop() 
+{
+  // process bridge stack
+  SerialBridge.loop_callback();
+}

+ 156 - 0
src/BleSerial/Client/BleSerialClient.cpp

@@ -0,0 +1,156 @@
+#include "BleSerialClient.h"
+#include "Arduino.h"
+
+BleSerialClient::BleSerialClient(const char* server_name) : _xAdvertisedCallbacks{server_name}
+{
+
+}
+
+bool BleSerialClient::begin(uint32_t duration)
+{
+    if (BLEDevice::getInitialized() == false)
+        BLEDevice::init("");
+    
+    // Init rx buffer
+    _rxBuffer.init();   
+
+    _pClient = BLEDevice::createClient();
+    _scan_servers(duration);
+    _bInitialized = connect();
+    
+    return _bInitialized;
+}
+
+int BleSerialClient::available()
+{
+    return _rxBuffer.size();
+}
+
+int BleSerialClient::read()
+{
+    return _rxBuffer.read_byte();
+}
+
+size_t BleSerialClient::readBytes(uint8_t *buffer, size_t length)
+{
+    return _rxBuffer.read_data(buffer, length);
+}
+
+int BleSerialClient::peek()
+{
+    return _rxBuffer.peek();
+}
+
+size_t BleSerialClient::write(uint8_t value)
+{
+    if (_xClientCallbacks.isConnected() == false || _bInitialized == false)
+        return 0;
+
+    uint8_t tx_buffer[1] = {value};
+    _pTxRemoteCharacteristic->writeValue(tx_buffer, 1, false);
+    return 1;
+}
+
+size_t BleSerialClient::write(const uint8_t *buffer, size_t size)
+{
+    if (_xClientCallbacks.isConnected() == false || _bInitialized == false)
+        return 0;
+
+    _pTxRemoteCharacteristic->writeValue((uint8_t*)buffer, size, false);
+    return size;
+}
+
+void BleSerialClient::_scan_servers(uint32_t duration)
+{
+    _xAdvertisedCallbacks.resetFoundStatus();
+    
+    BLEScan* pBLEScan = BLEDevice::getScan();
+    pBLEScan->setAdvertisedDeviceCallbacks(&_xAdvertisedCallbacks);
+    pBLEScan->setActiveScan(true);
+    pBLEScan->start(duration);
+}
+
+bool BleSerialClient::connect()
+{
+    if (_xAdvertisedCallbacks.getFoundStatus() == false)
+        return false;
+
+    _pClient->setClientCallbacks(&_xClientCallbacks);
+    _pClient->connect(_xAdvertisedCallbacks.getAdvertisedDevice());
+    _pClient->setMTU(ESP_GATT_MAX_MTU_SIZE);
+
+    // Get reference to remote service
+    _pNusRemoteService = _pClient->getService(BLE_SERIAL_NUS_SERVICE_UUID);
+
+    if (_pNusRemoteService == nullptr) 
+        return false;
+
+    // Get references to remote characteristics
+    _pRxRemoteCharacteristic = _pNusRemoteService->getCharacteristic(_xRxCharClientUUID);
+    _pTxRemoteCharacteristic = _pNusRemoteService->getCharacteristic(_xTxCharClientUUID);
+
+    if (_pRxRemoteCharacteristic == nullptr || _pTxRemoteCharacteristic == nullptr) 
+        return false;
+
+    // Assign callback functions for the Characteristics
+    _pRxRemoteCharacteristic->registerForNotify(
+        [this](BLERemoteCharacteristic* pBLERemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify) {
+            this->_rxBuffer.write_data(pData, length);
+    });
+    
+    return true;
+}
+
+bool BleSerialClient::is_connected()
+{
+    return _xClientCallbacks.isConnected();
+}
+
+bool BleSerialClient::BleSerialClientCallbacks::isConnected()
+{
+    return _bConnected;
+}
+
+void BleSerialClient::BleSerialClientCallbacks::onConnect(BLEClient* pclient) 
+{
+    _bConnected = true;
+   // digitalWrite(8, LOW);
+}
+
+void BleSerialClient::BleSerialClientCallbacks::onDisconnect(BLEClient* pclient) 
+{
+    _bConnected = false;
+   // digitalWrite(8, HIGH);
+}
+
+BleSerialClient::BleSerialAdvertisedCallbacks::BleSerialAdvertisedCallbacks(const char* server_name) 
+: _sServerName{server_name} 
+{
+
+}
+
+BLEAdvertisedDevice* BleSerialClient::BleSerialAdvertisedCallbacks::getAdvertisedDevice()
+{
+    return &_xAdvertisedDevice;
+}
+
+bool BleSerialClient::BleSerialAdvertisedCallbacks::getFoundStatus()
+{
+    return _bFound;
+}
+
+void BleSerialClient::BleSerialAdvertisedCallbacks::resetFoundStatus()
+{
+    _bFound = false;
+}
+
+void BleSerialClient::BleSerialAdvertisedCallbacks::onResult(BLEAdvertisedDevice advertisedDevice)
+{
+    //Check if the name of the advertiser matches
+    if (advertisedDevice.getName() == _sServerName && advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(_xNusServiceUUID))
+    { 
+        advertisedDevice.getScan()->stop(); // Scan can be stopped, we found what we are looking for
+        _xAdvertisedDevice = advertisedDevice;
+        _bFound = true;
+    }
+}

+ 105 - 0
src/BleSerial/Client/BleSerialClient.h

@@ -0,0 +1,105 @@
+#ifndef _BLE_SERIAL_CLIENT_H_
+#define _BLE_SERIAL_CLIENT_H_
+
+#include <BLEDevice.h>
+#include <Stream.h>
+#include "IoQueue/IoQueue.h"
+#include "BleSerial/defines.h"
+
+
+class BleSerialClient: public Stream
+{
+public:
+
+  BleSerialClient(const char* server_name);
+
+  bool begin(uint32_t duration);
+  bool connect();
+  bool is_connected();
+
+  /** Get number of received bytes.
+   * @return number of received bytes
+   */
+  int available() override;
+  
+  /** Read byte from stream.
+   * @return 8-bit number or -1 if not available
+   */
+  int read() override;
+  
+  /** Read multiple bytes from stream.
+   * @param buffer: destination buffer
+   * @param length: requested length 
+   * @return actual length
+   */
+  size_t readBytes(uint8_t *buffer, size_t length) override;
+
+  /** Get first byte from stream without removing it. 
+   * @return first byte or -1 if not available
+  */
+  int peek() override;
+
+  /** Write one byte to the stream (not recommended).
+   * @param value: value to write.
+   * @return number of sent bytes: 1 if success, 0 if not
+   */
+  size_t write(uint8_t value) override;
+  
+  /** Write multiple bytes to the stream.
+   * @param buffer: buffer to write
+   * @param size: size of buffer
+   * @return number of written bytes
+   */
+  size_t write(const uint8_t *buffer, size_t size) override;
+
+private:
+
+    void _scan_servers(uint32_t duration);
+
+    class BleSerialClientCallbacks : public BLEClientCallbacks 
+    {
+    public:
+
+    bool isConnected();
+
+    private:
+
+    void onConnect(BLEClient* pclient) override;
+    void onDisconnect(BLEClient* pclient) override;
+    bool _bConnected{false};
+    };
+
+    class BleSerialAdvertisedCallbacks: public BLEAdvertisedDeviceCallbacks 
+    {
+    public:
+
+        BleSerialAdvertisedCallbacks(const char* server_name);
+        BLEAdvertisedDevice* getAdvertisedDevice();
+        bool getFoundStatus();
+        void resetFoundStatus();
+
+    private:
+
+        void onResult(BLEAdvertisedDevice advertisedDevice);  //Callback function that gets called, when another device's advertisement has been received
+
+        BLEUUID                           _xNusServiceUUID{BLE_SERIAL_NUS_SERVICE_UUID};
+        const char*                       _sServerName;
+        bool                              _bFound{false};
+        BLEAdvertisedDevice               _xAdvertisedDevice;
+    };
+
+    bool                                  _bInitialized{false};
+    IoQueue<4*BLE_SERIAL_RX_BUFFER_SIZE> _rxBuffer;
+
+    BLEUUID                               _xRxCharClientUUID{BLE_SERIAL_CHARACTERISTIC_TX_UUID};
+    BLEUUID                               _xTxCharClientUUID{BLE_SERIAL_CHARACTERISTIC_RX_UUID};
+    BleSerialClientCallbacks              _xClientCallbacks;
+    BleSerialAdvertisedCallbacks          _xAdvertisedCallbacks;
+
+    BLEClient*                            _pClient{nullptr};
+    BLERemoteService*                     _pNusRemoteService{nullptr};
+    BLERemoteCharacteristic*              _pRxRemoteCharacteristic{nullptr};
+    BLERemoteCharacteristic*              _pTxRemoteCharacteristic{nullptr};
+};
+
+#endif // _BLE_SERIAL_CLIENT_H_

+ 119 - 0
src/BleSerial/Server/BleSerialServer.cpp

@@ -0,0 +1,119 @@
+#include "BleSerialServer.h"
+
+bool BleSerialServer::begin(const char *server_name)
+{
+    // Create the BLE Device
+    if (_bInitialized)
+        return false;
+    
+    // Init rx buffer
+    _rxBuffer.init();                                                
+
+    BLEDevice::init(server_name);
+    BLEDevice::setMTU(BLE_SERIAL_RX_BUFFER_SIZE);
+
+    // Create the BLE Server
+    _pServer = BLEDevice::createServer();
+
+    if (_pServer == nullptr)
+        return false;
+
+    _pServer->setCallbacks(&_xServerCallbacks);
+
+    // Create the BLE Service
+    _pNusService = _pServer->createService(BLE_SERIAL_NUS_SERVICE_UUID);
+
+    if (_pNusService == nullptr)
+        return false;
+
+    // Create Tx Characteristic
+    _pTxCharacteristic = _pNusService->createCharacteristic(
+        BLE_SERIAL_CHARACTERISTIC_TX_UUID,
+        BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY
+	);
+
+    // Create Rx Characteristic
+    _pRxCharacteristic = _pNusService->createCharacteristic(
+        BLE_SERIAL_CHARACTERISTIC_RX_UUID,
+        BLECharacteristic::PROPERTY_WRITE_NR
+    );
+
+    if (_pRxCharacteristic == nullptr || _pTxCharacteristic == nullptr)
+        return false;
+
+    // Customize characteristics
+    _pTxCharacteristic->addDescriptor(&_xTxDescriptor);
+
+    _pRxCharacteristic->addDescriptor(&_xRxDescriptor);
+    _pRxCharacteristic->setCallbacks(&_xRxCharCallbacks);
+
+    // Start the NUS service
+    _pNusService->start();
+
+    // Start advertising
+    BLEAdvertising* pAdvertising = _pServer->getAdvertising();
+
+    if (pAdvertising == nullptr)
+        return false;
+
+    pAdvertising->addServiceUUID(BLE_SERIAL_NUS_SERVICE_UUID);   
+    pAdvertising->setScanResponse(true);          
+    pAdvertising->start();
+
+    _bInitialized = true;
+    return true;
+}
+
+int BleSerialServer::available()
+{
+    return _rxBuffer.size();
+}
+
+int BleSerialServer::read()
+{
+    return _rxBuffer.read_byte();
+}
+
+size_t BleSerialServer::readBytes(uint8_t *buffer, size_t length)
+{
+    return _rxBuffer.read_data(buffer, length);
+}
+
+int BleSerialServer::peek()
+{
+    return _rxBuffer.peek();
+}
+
+size_t BleSerialServer::write(uint8_t value)
+{
+    uint8_t buffer[1] = {value};
+    return write(buffer, 1);
+}
+
+size_t BleSerialServer::write(const uint8_t *buffer, size_t size)
+{
+    _pTxCharacteristic->setValue(const_cast<uint8_t*>(buffer), size);
+    _pTxCharacteristic->notify(true);
+
+    return size;
+}
+
+void BleSerialServer::BleSerialServerCallbacks::onDisconnect(BLEServer* pServer) 
+{  
+    BLEAdvertising* pAdvertising = pServer->getAdvertising();
+
+    if (pAdvertising != nullptr) 
+        pAdvertising->start();
+}
+
+BleSerialServer::BleSerialRxCharCallbacks::BleSerialRxCharCallbacks(BleSerialServer *pServerInstance)
+: _pServerInstance{pServerInstance} 
+{
+    
+}
+
+void BleSerialServer::BleSerialRxCharCallbacks::onWrite(BLECharacteristic *pCharacteristic) 
+{
+    if (pCharacteristic == _pServerInstance->_pRxCharacteristic)
+        _pServerInstance->_rxBuffer.write_data(pCharacteristic->getData(), pCharacteristic->getLength());
+}

+ 87 - 0
src/BleSerial/Server/BleSerialServer.h

@@ -0,0 +1,87 @@
+#ifndef _BLE_SERIAL_SERVER_H_
+#define _BLE_SERIAL_SERVER_H_
+
+#include <BLEDevice.h>
+#include <BLE2902.h>
+#include <Stream.h>
+#include "IoQueue/IoQueue.h"
+#include "BleSerial/defines.h"
+
+class BleSerialServer: public Stream
+{
+public:
+    BleSerialServer() = default;
+
+    bool begin(const char *server_name);
+
+    /** Get number of received bytes.
+     * @return number of received bytes
+     */
+    int available() override;
+
+    /** Read byte from stream.
+     * @return 8-bit number or -1 if not available
+     */
+    int read() override;
+
+    /** Read multiple bytes from stream.
+     * @param buffer: destination buffer
+     * @param length: requested length 
+     * @return actual length
+     */
+    size_t readBytes(uint8_t *buffer, size_t length) override;
+
+    /** Get first byte from stream without removing it. 
+     * @return first byte or -1 if not available
+     */
+    int peek() override;
+
+    /** Write one byte to the stream (not recommended).
+     * @param value: value to write.
+     * @return number of sent bytes: 1 if success, 0 if not
+     */
+    size_t write(uint8_t value) override;
+
+    /** Write multiple bytes to the stream.
+     * @param buffer: buffer to write
+     * @param size: size of buffer
+     * @return number of written bytes
+     */
+    size_t write(const uint8_t *buffer, size_t size) override;
+
+private:
+
+    class BleSerialServerCallbacks: public BLEServerCallbacks 
+    {
+    public:
+        void onDisconnect(BLEServer* pServer);
+    };
+
+    class BleSerialRxCharCallbacks: public BLECharacteristicCallbacks 
+    {
+        public:
+            BleSerialRxCharCallbacks(BleSerialServer *pServerInstance);
+
+        private:
+            void onWrite(BLECharacteristic *pCharacteristic) override;
+            BleSerialServer *_pServerInstance;
+    };
+
+    bool                                  _bInitialized{false};
+    IoQueue<4*BLE_SERIAL_RX_BUFFER_SIZE> _rxBuffer;
+
+    BLEUUID                               _xRxCharServerUUID{BLE_SERIAL_CHARACTERISTIC_RX_UUID};
+    BLEUUID                               _xTxCharServerUUID{BLE_SERIAL_CHARACTERISTIC_TX_UUID};
+    BleSerialServerCallbacks              _xServerCallbacks;
+    BleSerialRxCharCallbacks              _xRxCharCallbacks{this};
+    BLE2902                               _xTxDescriptor;
+    BLEDescriptor                         _xRxDescriptor{BLEUUID{static_cast<uint16_t>(0x2901)}};
+
+    BLEServer*                            _pServer = nullptr;
+    BLEService*                           _pNusService = nullptr;
+    BLECharacteristic*                    _pRxCharacteristic = nullptr;
+    BLECharacteristic*                    _pTxCharacteristic = nullptr;
+    BLEServerCallbacks*                   _pServerCallbacks = nullptr;
+};
+
+#endif // _BLE_SERIAL_SERVER_H_

+ 11 - 0
src/BleSerial/defines.h

@@ -0,0 +1,11 @@
+#ifndef _BLE_SERIAL_DEFINES_H_
+#define _BLE_SERIAL_DEFINES_H_
+
+#include <esp_gatt_common_api.h>    // for ESP_GATT_MAX_MTU_SIZE
+
+#define BLE_SERIAL_RX_BUFFER_SIZE           ESP_GATT_MAX_MTU_SIZE
+#define BLE_SERIAL_NUS_SERVICE_UUID         "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
+#define BLE_SERIAL_CHARACTERISTIC_RX_UUID   "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
+#define BLE_SERIAL_CHARACTERISTIC_TX_UUID   "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
+
+#endif // _BLE_SERIAL_DEFINES_H_

+ 148 - 0
src/EspNowSerial/EspNowSerial.cpp

@@ -0,0 +1,148 @@
+#include "EspNowSerial.h"
+
+EspNowSerial* EspNowSerial::_instance = nullptr;
+
+EspNowSerial::EspNowSerial(wifi_mode_t mode)
+: _wifi_mode{mode}, _wifi_if{mode == WIFI_MODE_AP ? WIFI_IF_AP : WIFI_IF_STA} 
+{
+
+}
+
+esp_err_t EspNowSerial::add_peer(EspMacAddress peer_address)
+{
+    if (_broadcast == true)
+        return -1;
+
+    esp_now_peer_info_t peer_info;
+
+    memcpy(peer_info.peer_addr, peer_address, ESP_NOW_ETH_ALEN);
+    peer_info.channel = 0;      // current channel  
+    peer_info.encrypt = false;
+    peer_info.ifidx = _wifi_if;
+    memset(peer_info.lmk, 0, ESP_NOW_KEY_LEN);
+    peer_info.priv = nullptr;
+
+    return esp_now_add_peer(&peer_info);
+}
+
+esp_err_t EspNowSerial::add_peer_encrypted(EspMacAddress peer_address, CcmpMasterKey lmk)
+{
+    if (_broadcast == true || _has_pmk == false)
+        return -1;
+
+    esp_now_peer_info_t peer_info;
+
+    memcpy(peer_info.peer_addr, peer_address, ESP_NOW_ETH_ALEN);
+    peer_info.channel = 0;      // current channel
+    peer_info.encrypt = true;
+    peer_info.ifidx = _wifi_if;
+    memcpy(peer_info.lmk, lmk, ESP_NOW_KEY_LEN);
+    peer_info.priv = nullptr;
+
+    return esp_now_add_peer(&peer_info);
+}
+
+esp_err_t EspNowSerial::begin(uint8_t channel, bool broadcast, CcmpMasterKey pmk)
+{
+    _broadcast = broadcast;                                             // set broadcast flag
+    EspNowSerial::_instance = this;                                     // attach global instance as this object
+    _rx_queue.init();                                                    // init rx buffer
+    _data_sent = 1;                                                     // reset data sent flag
+
+    WiFi.mode(_wifi_mode);                                              // set device as a Wi-Fi Station
+    esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE);               // force WiFi channel
+                                            
+    esp_err_t status = esp_now_init();                                  // init esp-now
+
+    if (status == ESP_OK)
+    {      
+        esp_now_register_send_cb(_on_data_sent);                        // register callbacks
+        esp_now_register_recv_cb(_on_data_recv);
+        
+        if (_broadcast == false && pmk != nullptr)                      // set pmk if encrypted (can't encrypted broadcast)
+        {            
+            _has_pmk = true;  
+            esp_now_set_pmk(pmk);
+        }
+    
+        if (_broadcast == broadcast)                                    // set broadcast peer if needed
+        {
+            esp_now_peer_info_t broadcast_peer;
+
+            memset(broadcast_peer.peer_addr, 0xFF, ESP_NOW_ETH_ALEN);
+            broadcast_peer.channel = 0;  
+            broadcast_peer.encrypt = false;
+            broadcast_peer.ifidx = _wifi_if;
+            memset(broadcast_peer.lmk, 0, ESP_NOW_KEY_LEN);
+            broadcast_peer.priv = nullptr;
+
+            status = esp_now_add_peer(&broadcast_peer);
+        }
+    }
+
+    return status;
+}
+
+void EspNowSerial::end()
+{
+    esp_now_deinit();       
+    WiFi.mode(WIFI_OFF); 
+}
+
+int EspNowSerial::available()
+{
+    return _rx_queue.size();
+}
+
+int EspNowSerial::read()
+{
+    return _rx_queue.read_byte();
+}
+
+size_t EspNowSerial::readBytes(uint8_t *buffer, size_t length)
+{
+    return _rx_queue.read_data(buffer, length);
+}
+
+int EspNowSerial::peek()
+{
+    return _rx_queue.peek();
+}
+
+size_t EspNowSerial::write(uint8_t value)
+{ 
+    if (!_data_sent)
+        return 0;
+   
+   uint8_t buffer[1] = {value};
+   esp_err_t result = esp_now_send(nullptr, buffer, 1);
+
+    if (result != ESP_OK)
+        return 0;
+    
+    return 1;
+};
+
+size_t EspNowSerial::write(const uint8_t *buffer, size_t size)
+{
+    if (!_data_sent)
+        return 0;
+    
+    _data_sent = 0;     // reset data sent flag
+    esp_err_t result = esp_now_send(nullptr, buffer, size);
+
+    if (result != ESP_OK)
+        return 0;
+    
+    return size;
+}
+
+void EspNowSerial::_on_data_sent(const uint8_t *mac_addr, esp_now_send_status_t status) 
+{
+    EspNowSerial::_instance->_data_sent = 1;
+}
+
+void EspNowSerial::_on_data_recv(const uint8_t *mac_addr, const uint8_t *data, int len) 
+{
+    EspNowSerial::_instance->_rx_queue.write_data((uint8_t*)data, len);
+}

+ 168 - 0
src/EspNowSerial/EspNowSerial.h

@@ -0,0 +1,168 @@
+#ifndef _ESPNOW_SERIAL_H_
+#define _ESPNOW_SERIAL_H_
+
+#include <Stream.h>
+#include <WiFi.h>
+#include <esp_wifi.h>
+#include <esp_now.h>
+#include "defines.h"
+#include "IoQueue/IoQueue.h"
+
+
+/**
+ * @brief MAC address type used by ESP-NOW.
+ */
+typedef uint8_t EspMacAddress[ESP_NOW_ETH_ALEN];
+
+/**
+ * @brief CCMP key type (used for PMK or LMK).
+ */
+typedef uint8_t CcmpMasterKey[ESP_NOW_KEY_LEN];
+
+/**
+ * @brief Stream-like interface over ESP-NOW communication.
+ *
+ * This class wraps ESP-NOW into an Arduino-compatible Stream interface,
+ * allowing ESP-NOW packets to be read and written similarly to a serial port.
+ */
+class EspNowSerial : public Stream
+{
+public:
+
+    /**
+     * @brief Construct a new EspNowSerial object.
+     *
+     * @param mode WiFi mode to operate in (default: WIFI_MODE_STA)
+     */
+    explicit EspNowSerial(wifi_mode_t mode = WIFI_MODE_STA);
+
+    /**
+     * @brief Initialize ESP-NOW communication.
+     *
+     * @param channel WiFi channel to use (1–14)
+     * @param broadcast Enable broadcast mode if true
+     * @param pmk Primary Master Key (PMK) for encrypted communication
+     *
+     * @attention
+     * - When broadcast mode is enabled:
+     *   - Do NOT add peers
+     *   - Do NOT set a PMK
+     * - When broadcast mode is disabled:
+     *   - Add peers explicitly
+     *   - Set a PMK if encrypted peers are used
+     *
+     * @return esp_err_t ESP_OK on success, error code otherwise
+     */
+    esp_err_t begin(uint8_t channel, bool broadcast, CcmpMasterKey pmk = nullptr);
+
+    /**
+     * @brief Deinitialize ESP-NOW and release resources.
+     */
+    void end();
+
+    /**
+     * @brief Add an unencrypted peer.
+     *
+     * @param peer_address MAC address of the RX peer
+     * @return esp_err_t ESP_OK on success, error code otherwise
+     *
+     * @see esp_now_add_peer()
+     */
+    esp_err_t add_peer(EspMacAddress peer_address);
+
+    /**
+     * @brief Add an encrypted peer.
+     *
+     * @param peer_address MAC address of the RX peer
+     * @param lmk Local Master Key (LMK) used for encryption
+     * @return esp_err_t ESP_OK on success, error code otherwise
+     *
+     * @see esp_now_add_peer()
+     */
+    esp_err_t add_peer_encrypted(EspMacAddress peer_address, CcmpMasterKey lmk);
+
+    /**
+     * @brief Get the number of bytes available for reading.
+     *
+     * @return Number of bytes currently available in the RX buffer
+     */
+    int available() override;
+
+    /**
+     * @brief Read a single byte from the stream.
+     *
+     * @return Byte value (0–255) or -1 if no data is available
+     */
+    int read() override;
+
+    /**
+     * @brief Read multiple bytes from the stream.
+     *
+     * @param buffer Destination buffer
+     * @param length Maximum number of bytes to read
+     * @return Number of bytes actually read
+     */
+    size_t readBytes(uint8_t *buffer, size_t length) override;
+
+    /**
+     * @brief Peek the next byte in the stream without removing it.
+     *
+     * @return Byte value (0–255) or -1 if no data is available
+     */
+    int peek() override;
+
+    /**
+     * @brief Write a single byte to the stream.
+     *
+     * @note This method is not recommended for high-throughput transfers.
+     *
+     * @param value Byte to send
+     * @return Number of bytes written (1 on success, 0 on failure)
+     */
+    size_t write(uint8_t value) override;
+
+    /**
+     * @brief Write multiple bytes to the stream.
+     *
+     * @param buffer Source buffer
+     * @param size Number of bytes to send
+     * @return Number of bytes successfully written
+     */
+    size_t write(const uint8_t *buffer, size_t size) override;
+
+private:
+
+    /**
+     * @brief ESP-NOW send-complete callback.
+     *
+     * @param mac_addr MAC address of the destination peer
+     * @param status Transmission status
+     */
+    static void _on_data_sent(const uint8_t *mac_addr,
+                              esp_now_send_status_t status);
+
+    /**
+     * @brief ESP-NOW receive callback.
+     *
+     * @param mac_addr MAC address of the sender
+     * @param data Pointer to received data
+     * @param len Length of received data in bytes
+     */
+    static void _on_data_recv(const uint8_t *mac_addr,
+                              const uint8_t *data,
+                              int len);
+
+    wifi_mode_t _wifi_mode;                     ///< Configured WiFi mode
+    wifi_interface_t _wifi_if;                  ///< WiFi interface in use
+
+    bool _has_pmk{false};                       ///< Indicates whether PMK is set
+    bool _broadcast{false};                     ///< Broadcast mode flag
+
+    static EspNowSerial *_instance;             ///< Singleton instance for callbacks
+
+    uint8_t _data_sent{1};                      ///< TX completion flag
+
+    IoQueue<ESP_NOW_MAX_DATA_LEN> _rx_queue;    ///< Internal RX buffer
+};
+
+#endif // _ESPNOW_SERIAL_H_

+ 8 - 0
src/EspNowSerial/defines.h

@@ -0,0 +1,8 @@
+#ifndef _ESP_NOW_DEFINES_H_
+#define _ESP_NOW_DEFINES_H_
+
+#ifndef ESP_NOW_RX_QUEUE_SIZE
+    #define ESP_NOW_RX_QUEUE_SIZE ESP_NOW_MAX_DATA_LEN
+#endif // ESP_NOW_RX_QUEUE_SIZE
+
+#endif // _ESP_NOW_DEFINES_H_

+ 44 - 0
src/InterSerialBridge/InterSerialBridge.h

@@ -0,0 +1,44 @@
+#ifndef _INTER_SERIAL_BRIDGE_H_
+#define _INTER_SERIAL_BRIDGE_H_
+
+#include <stream.h>
+
+template <size_t _BUFFER_SIZE>
+class InterSerialBridge
+{
+public:
+    InterSerialBridge(Stream &device1, Stream &device2)
+    : _device1{device1}, _device2{device2} 
+    {
+
+    }
+    
+    void loop_callback()
+    {
+        _buffer_size = _device1.available();
+
+        if (_buffer_size) 
+        {
+            _buffer_size = _device1.readBytes(_buffer, _buffer_size);
+            _device2.write(_buffer, _buffer_size);
+            _device2.flush();
+        }
+
+        _buffer_size = _device2.available();
+
+        if (_buffer_size)
+        {
+            _buffer_size = _device2.readBytes(_buffer, _buffer_size);
+            _device1.write(_buffer, _buffer_size);
+            _device1.flush();
+        }
+    }
+
+private:
+    Stream & _device1;
+    Stream & _device2;
+    uint8_t _buffer[_BUFFER_SIZE];
+    uint8_t _buffer_size{0};
+};
+
+#endif // _INTER_SERIAL_BRIDGE_H_

+ 178 - 0
src/IoQueue/IoQueue.h

@@ -0,0 +1,178 @@
+#ifndef _IO_QUEUE_H_
+#define _IO_QUEUE_H_
+
+#include <cinttypes>
+#include <cstring>
+#include <freertos/queue.h>
+
+/**
+ * @brief Fixed-capacity, non-blocking byte queue wrapper for FreeRTOS.
+ *
+ * IoQueue is a lightweight template wrapper around a statically allocated
+ * FreeRTOS queue. It is intended for fast, non-blocking byte-oriented I/O
+ * (e.g. RX buffers, stream backends).
+ *
+ * @tparam _CAPACITY Maximum number of bytes the queue can hold
+ *
+ * @note
+ * - The queue stores bytes only (uint8_t)
+ * - All operations are non-blocking (timeout = 0)
+ * - Memory is statically allocated (no heap usage)
+ */
+template <size_t _CAPACITY>
+class IoQueue
+{
+public:
+
+  /**
+   * @brief Initialize the queue.
+   *
+   * Must be called before any read or write operation.
+   * Uses statically allocated storage.
+   */
+  void init()
+  {
+    _xQueue = xQueueCreateStatic(
+        _CAPACITY,
+        sizeof(uint8_t),
+        _pucQueueStorage,
+        &_xQueueBuffer
+    );
+  }
+
+  /**
+   * @brief Get the number of bytes currently stored in the queue.
+   *
+   * @return Number of bytes available for reading
+   */
+  int32_t size()
+  {
+    return static_cast<int32_t>(uxQueueMessagesWaiting(_xQueue));
+  }
+
+  /**
+   * @brief Get the remaining free capacity of the queue.
+   *
+   * @return Number of bytes that can still be written
+   */
+  int32_t free_size()
+  {
+    return static_cast<int32_t>(_CAPACITY) - size();
+  }
+
+  /**
+   * @brief Deinitialize the queue.
+   *
+   * Deletes the underlying FreeRTOS queue.
+   * The instance must not be used after this call unless reinitialized.
+   */
+  void deinit()
+  {
+    vQueueDelete(_xQueue);
+  }
+
+  /**
+   * @brief Peek the first byte without removing it from the queue.
+   *
+   * @return
+   * - Byte value (0–255) if available
+   * - -1 if the queue is empty
+   */
+  int32_t peek()
+  {
+    uint8_t value;
+
+    if (xQueuePeek(_xQueue, &value, 0) == pdTRUE)
+      return value;
+    else
+      return -1;
+  }
+
+  /**
+   * @brief Read and remove a single byte from the queue.
+   *
+   * @return
+   * - Byte value (0–255) if available
+   * - -1 if the queue is empty
+   */
+  int32_t read_byte()
+  {
+    uint8_t value;
+
+    if (xQueueReceive(_xQueue, &value, 0) == pdTRUE)
+      return value;
+    else
+      return -1;
+  }
+
+  /**
+   * @brief Read multiple bytes from the queue.
+   *
+   * Reads up to @p size bytes or until the queue becomes empty.
+   *
+   * @param data Destination buffer
+   * @param size Maximum number of bytes to read
+   * @return Number of bytes actually read
+   */
+  int32_t read_data(uint8_t *data, int32_t size)
+  {
+    uint8_t value;
+    int32_t ret_size = 0;
+
+    for (int32_t i = 0; i < size; i++)
+    {
+      if (xQueueReceive(_xQueue, &value, 0) == pdTRUE)
+      {
+        data[i] = value;
+        ret_size++;
+      }
+    }
+
+    return ret_size;
+  }
+
+  /**
+   * @brief Write a single byte to the queue.
+   *
+   * @param value Byte to write
+   * @return
+   * - 1 if the byte was written successfully
+   * - 0 if the queue is full
+   */
+  int32_t write_byte(uint8_t value)
+  {
+    return xQueueSend(_xQueue, &value, 0) == pdTRUE;
+  }
+
+  /**
+   * @brief Write multiple bytes to the queue.
+   *
+   * Writes as many bytes as possible until the queue becomes full.
+   *
+   * @param data Source buffer
+   * @param size Number of bytes to write
+   * @return Number of bytes actually written
+   */
+  int32_t write_data(uint8_t *data, int32_t size)
+  {
+    int32_t ret_size = 0;
+
+    for (int32_t i = 0; i < size; i++)
+    {
+      if (xQueueSend(_xQueue, &data[i], 0) == pdTRUE)
+      {
+        ret_size++;
+      }
+    }
+
+    return ret_size;
+  }
+
+private:
+
+  uint8_t       _pucQueueStorage[_CAPACITY];  ///< Static storage for queue items
+  StaticQueue_t _xQueueBuffer;                ///< FreeRTOS queue control structure
+  QueueHandle_t _xQueue;                      ///< Queue handle
+};
+
+#endif // _IO_QUEUE_H_