diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index c33cadda..4edca23d 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -72,6 +72,11 @@ static UITask ui_task(display); #endif +#ifdef BRIDGE_OVER_SERIAL +#include "bridge/serial/SerialBridge.h" +bridge::SerialBridge *bridge_interface; +#endif + #define FIRMWARE_ROLE "repeater" #define PACKET_LOG_FILE "/packet_log" @@ -752,7 +757,14 @@ void setup() { } #endif - if (!radio_init()) { halt(); } +#ifdef BRIDGE_OVER_SERIAL + bridge_interface = new bridge::SerialBridge(); + bridge_interface->setup(); +#endif + + if (!radio_init()) { + halt(); + } fast_rng.begin(radio_get_rng_seed()); @@ -825,6 +837,10 @@ void loop() { command[0] = 0; // reset command buffer } +#ifdef BRIDGE_OVER_SERIAL + bridge_interface->loop(); +#endif + the_mesh.loop(); sensors.loop(); } diff --git a/platformio.ini b/platformio.ini index 90e7cfb0..36cdf760 100644 --- a/platformio.ini +++ b/platformio.ini @@ -32,6 +32,7 @@ build_flags = -w -DNDEBUG -DRADIOLIB_STATIC_ONLY=1 -DRADIOLIB_GODMODE=1 build_src_filter = +<*.cpp> + + + ; ----------------- ESP32 --------------------- diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index 7f39dc49..08cca61e 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -1,7 +1,7 @@ #include "Dispatcher.h" #if MESH_PACKET_LOGGING - #include +#include #endif #include @@ -104,6 +104,7 @@ void Dispatcher::loop() { processRecvPacket(pkt); } } + checkRecv(); checkSend(); } @@ -115,6 +116,18 @@ void Dispatcher::checkRecv() { { uint8_t raw[MAX_TRANS_UNIT+1]; int len = _radio->recvRaw(raw, MAX_TRANS_UNIT); + +#ifdef BRIDGE_OVER_SERIAL + // We are basically stamping metadata from the last RF packet into something that came from serial, + // it works for now. But long term the code on checkRecv() should be refactored to be able to deal + // with both use cases without dupeing the existing code. I've chosen [for now] not to dupe and just + // fake the metadata. + + if (len == 0) { + len = bridge_interface->getPacket(raw); + } +#endif + if (len > 0) { logRxRaw(_radio->getLastSNR(), _radio->getLastRSSI(), raw, len); @@ -280,7 +293,11 @@ void Dispatcher::checkSend() { } outbound_expiry = futureMillis(max_airtime); - #if MESH_PACKET_LOGGING +#ifdef BRIDGE_OVER_SERIAL + bridge_interface->sendPacket(outbound); +#endif + +#if MESH_PACKET_LOGGING Serial.print(getLogDateTime()); Serial.printf(": TX, len=%d (type=%d, route=%s, payload_len=%d)", len, outbound->getPayloadType(), outbound->isRouteDirect() ? "D" : "F", outbound->payload_len); @@ -290,7 +307,7 @@ void Dispatcher::checkSend() { } else { Serial.printf("\n"); } - #endif +#endif } } } @@ -328,5 +345,4 @@ bool Dispatcher::millisHasNowPassed(unsigned long timestamp) const { unsigned long Dispatcher::futureMillis(int millis_from_now) const { return _ms->getMillis() + millis_from_now; } - } \ No newline at end of file diff --git a/src/Dispatcher.h b/src/Dispatcher.h index 2200f81b..98184b50 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -6,6 +6,11 @@ #include #include +#ifdef BRIDGE_OVER_SERIAL +#include "bridge/serial/SerialBridge.h" +extern bridge::SerialBridge *bridge_interface; +#endif + namespace mesh { /** diff --git a/src/bridge/serial/Packet.h b/src/bridge/serial/Packet.h new file mode 100644 index 00000000..fbb4155a --- /dev/null +++ b/src/bridge/serial/Packet.h @@ -0,0 +1,58 @@ +/** + * MeshCore - A new lightweight, hybrid routing mesh protocol for packet radios + * Copyright (c) 2025 Scott Powell / rippleradios.com + * + * This project is maintained by the contributors listed in + * https://github.com/ripplebiz/MeshCore/graphs/contributors + * + * MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ +#pragma once + +#include "MeshCore.h" + +#include + +namespace bridge { + +/* + * +----------------------------------------------------+ + * | SERIAL PACKET SPEC | + * +-----------+---------+------------------------------+ + * | TYPE | NAME | DESCRIPTION | + * +-----------+---------+------------------------------+ + * | uint16_t | MAGIC | The packet start marker | + * | uint16_t | LEN | Length of the payload | + * | uint16_t | CRC | Checksum for error checking | + * | uint8_t[] | PAYLOAD | Actual rf packet data | + * +-----------+---------+------------------------------+ + */ +#define SERIAL_PKT_MAGIC 0xdead + +struct Packet { + uint16_t magic, len, crc; + uint8_t payload[MAX_TRANS_UNIT]; + + Packet() : magic(SERIAL_PKT_MAGIC), len(0), crc(0) {} +}; + +} // namespace bridge \ No newline at end of file diff --git a/src/bridge/serial/PacketQueue.h b/src/bridge/serial/PacketQueue.h new file mode 100644 index 00000000..a83d8f2e --- /dev/null +++ b/src/bridge/serial/PacketQueue.h @@ -0,0 +1,113 @@ +/** + * MeshCore - A new lightweight, hybrid routing mesh protocol for packet radios + * Copyright (c) 2025 Scott Powell / rippleradios.com + * + * This project is maintained by the contributors listed in + * https://github.com/ripplebiz/MeshCore/graphs/contributors + * + * MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ +#pragma once + +#include "MeshCore.h" + +#include +#include + +#if MESH_PACKET_LOGGING +#include +#endif + +#ifndef MAX_QUEUE_SIZE +#define MAX_QUEUE_SIZE 8 +#endif + +namespace bridge { + +class PacketQueue { +private: + struct { + size_t len; + uint8_t bytes[MAX_TRANS_UNIT]; + } buffer[MAX_QUEUE_SIZE]; + +protected: + uint16_t head = 0, tail = 0; + +public: + size_t available() const { return (tail - head + MAX_QUEUE_SIZE) % MAX_QUEUE_SIZE; } + + size_t enqueue(const uint8_t *bytes, const uint8_t len) { + if (len == 0 || len > MAX_TRANS_UNIT) { +#if BRIDGE_DEBUG + Serial.printf("BRIDGE: enqueue() failed len=%d\n", len); +#endif + return 0; + } + + if ((tail + 1) % MAX_QUEUE_SIZE == head) { // Buffer full + head = (head + 1) % MAX_QUEUE_SIZE; // Overwrite oldest packet + } + + buffer[tail].len = len; + memcpy(buffer[tail].bytes, bytes, len); + +#if MESH_PACKET_LOGGING + Serial.printf("BRIDGE: enqueue() len=%d payload[5]=", len); + for (size_t i = 0; i < len && i < 5; ++i) { + Serial.printf("0x%02x ", buffer[tail].bytes[i]); + } + Serial.printf("\n"); +#endif + + tail = (tail + 1) % MAX_QUEUE_SIZE; + return len; + } + + size_t enqueue(const mesh::Packet *pkt) { + uint8_t bytes[MAX_TRANS_UNIT]; + const size_t len = pkt->writeTo(bytes); + return enqueue(bytes, len); + } + + size_t dequeue(uint8_t *bytes) { + if (head == tail) { // Buffer empty + return 0; + } + + const size_t len = buffer[head].len; + memcpy(bytes, buffer[head].bytes, len); + head = (head + 1) % MAX_QUEUE_SIZE; + +#if MESH_PACKET_LOGGING + Serial.printf("BRIDGE: dequeue() payload[5]="); + for (size_t i = 0; i < len && i < 5; ++i) { + Serial.printf("0x%02x ", bytes[i]); + } + Serial.printf("\n"); +#endif + + return len; + } +}; + +} // namespace bridge diff --git a/src/bridge/serial/SerialBridge.cpp b/src/bridge/serial/SerialBridge.cpp new file mode 100644 index 00000000..1e83f9e6 --- /dev/null +++ b/src/bridge/serial/SerialBridge.cpp @@ -0,0 +1,135 @@ +/** + * MeshCore - A new lightweight, hybrid routing mesh protocol for packet radios + * Copyright (c) 2025 Scott Powell / rippleradios.com + * + * This project is maintained by the contributors listed in + * https://github.com/ripplebiz/MeshCore/graphs/contributors + * + * MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ +#include "SerialBridge.h" + +#include "MeshCore.h" +#include "Packet.h" + +// Alternative for ESP32 +// Alternative for RP2040 +#include + +namespace bridge { +#ifdef BRIDGE_OVER_SERIAL + +#if !defined(BRIDGE_OVER_SERIAL_RX) || !defined(BRIDGE_OVER_SERIAL_TX) +#error You must define RX and TX pins +#endif + +void SerialBridge::setup() { +#if defined(ESP32) + BRIDGE_OVER_SERIAL.setPins(BRIDGE_OVER_SERIAL_RX, BRIDGE_OVER_SERIAL_TX); +#elif defined(RP2040_PLATFORM) + BRIDGE_OVER_SERIAL.setPinout(BRIDGE_OVER_SERIAL_TX, BRIDGE_OVER_SERIAL_RX); +#else +#error SerialBridge was not tested on the current platform +#endif + BRIDGE_OVER_SERIAL.begin(115200); + Serial.printf("Bridge over serial: enabled\n"); +} + +void SerialBridge::loop() { + readFromSerial(); + writeToSerial(); +} + +bool SerialBridge::shouldRetransmit(const mesh::Packet *pkt) { + if (pkt->isMarkedDoNotRetransmit()) { + return false; + } +} + +size_t SerialBridge::getPacket(uint8_t *bytes) { + return rx_queue.dequeue(bytes); +} + +size_t SerialBridge::sendPacket(const mesh::Packet *pkt) { + if (shouldRetransmit(pkt)) return 0; + const size_t len = tx_queue.enqueue(pkt); + return len; +} + +void SerialBridge::readFromSerial() { + static constexpr uint16_t size = sizeof(bridge::Packet) + 1; + static uint8_t buffer[size]; + static uint16_t tail = 0; + + while (BRIDGE_OVER_SERIAL.available()) { + buffer[tail] = (uint8_t)BRIDGE_OVER_SERIAL.read(); + tail = (tail + 1) % size; + +#if BRIDGE_OVER_SERIAL_DEBUG + Serial.printf("%02x ", buffer[(tail - 1 + size) % size]); +#endif + + // Check for complete packet by looking back to where the magic number should be + uint16_t head = (tail - sizeof(bridge::Packet) + size) % size; + const uint16_t magic = buffer[head] | (buffer[(head + 1) % size] << 8); + + if (magic == SERIAL_PKT_MAGIC) { + const uint16_t len = buffer[(head + 2) % size] | (buffer[(head + 3) % size] << 8); + const uint16_t crc = buffer[(head + 4) % size] | (buffer[(head + 5) % size] << 8); + + uint8_t payload[MAX_TRANS_UNIT]; + for (size_t i = 0; i < len; i++) { + payload[i] = buffer[(head + 6 + i) % size]; + } + + const bool valid = verifyCRC(payload, len, crc); + +#if MESH_PACKET_LOGGING + Serial.printf("BRIDGE: Read from serial len=%d crc=0x%04x\n", len, crc); +#endif + + if (verifyCRC(payload, len, crc)) { +#if MESH_PACKET_LOGGING + Serial.printf("BRIDGE: Received packet was validated\n"); +#endif + rx_queue.enqueue(payload, len); + } + } + } +} + +void SerialBridge::writeToSerial() { + bridge::Packet pkt; + if (!tx_queue.available()) return; + pkt.len = tx_queue.dequeue(pkt.payload); + + if (pkt.len > 0) { + pkt.crc = SerialBridge::calculateCRC(pkt.payload, pkt.len); + BRIDGE_OVER_SERIAL.write((uint8_t *)&pkt, sizeof(bridge::Packet)); +#if MESH_PACKET_LOGGING + Serial.printf("BRIDGE: Write to serial len=%d crc=0x%04x\n", pkt.len, pkt.crc); +#endif + } +} + +#endif +} // namespace bridge diff --git a/src/bridge/serial/SerialBridge.h b/src/bridge/serial/SerialBridge.h new file mode 100644 index 00000000..80bb2360 --- /dev/null +++ b/src/bridge/serial/SerialBridge.h @@ -0,0 +1,73 @@ +/** + * MeshCore - A new lightweight, hybrid routing mesh protocol for packet radios + * Copyright (c) 2025 Scott Powell / rippleradios.com + * + * This project is maintained by the contributors listed in + * https://github.com/ripplebiz/MeshCore/graphs/contributors + * + * MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ +#pragma once + +#include "Packet.h" +#include "PacketQueue.h" + +#include + +namespace bridge { + +class SerialBridge { +private: + PacketQueue rx_queue, tx_queue; + +protected: + bool shouldRetransmit(const mesh::Packet *); + +public: + void loop(); + void setup(); + void readFromSerial(); + void writeToSerial(); + + size_t getPacket(uint8_t *); + size_t sendPacket(const mesh::Packet *); + + static uint16_t calculateCRC(const uint8_t *bytes, size_t len) { + // Fletcher-16 + // https://en.wikipedia.org/wiki/Fletcher%27s_checksum + uint8_t sum1 = 0, sum2 = 0; + + for (size_t i = 0; i < len; i++) { + sum1 = (sum1 + bytes[i]) % 255; + sum2 = (sum2 + sum1) % 255; + } + + return (sum2 << 8) | sum1; + }; + + static bool verifyCRC(const uint8_t *bytes, size_t len, uint16_t crc) { + uint16_t computedChecksum = calculateCRC(bytes, len); + return (computedChecksum == crc); + } +}; + +} // namespace bridge