From 2a5cc33711e65e4eaa33fe7193808c05a8edb011 Mon Sep 17 00:00:00 2001 From: SQ2CPA Date: Sat, 9 Mar 2024 16:25:23 +0100 Subject: [PATCH] feat: tnc server --- src/LoRa_APRS_iGate.cpp | 18 +++-- src/kiss_protocol.h | 23 ++++++ src/kiss_utils.cpp | 170 ++++++++++++++++++++++++++++++++++++++++ src/kiss_utils.h | 15 ++++ src/tnc_utils.cpp | 113 ++++++++++++++++++++++++++ src/tnc_utils.h | 15 ++++ 6 files changed, 349 insertions(+), 5 deletions(-) create mode 100644 src/kiss_protocol.h create mode 100644 src/kiss_utils.cpp create mode 100644 src/kiss_utils.h create mode 100644 src/tnc_utils.cpp create mode 100644 src/tnc_utils.h diff --git a/src/LoRa_APRS_iGate.cpp b/src/LoRa_APRS_iGate.cpp index e77a4e4..04db05a 100644 --- a/src/LoRa_APRS_iGate.cpp +++ b/src/LoRa_APRS_iGate.cpp @@ -14,6 +14,7 @@ #include "gps_utils.h" #include "bme_utils.h" #include "web_utils.h" +#include "tnc_utils.h" #include "display.h" #include "utils.h" #include @@ -82,6 +83,7 @@ void setup() { SYSLOG_Utils::setup(); BME_Utils::setup(); WEB_Utils::setup(); + TNC_Utils::setup(); } void loop() { @@ -101,6 +103,8 @@ void loop() { APRS_IS_Utils::connect(); } + TNC_Utils::loop(); + Utils::checkDisplayInterval(); Utils::checkBeaconInterval(); @@ -112,12 +116,16 @@ void loop() { APRS_IS_Utils::checkStatus(); // Need that to update display, maybe split this and send APRSIS status to display func? - if (Config.aprs_is.active) { // If APRSIS enabled - APRS_IS_Utils::loop(packet); // Send received packet to APRSIS - } + if (packet != "") { + if (Config.aprs_is.active) { // If APRSIS enabled + APRS_IS_Utils::loop(packet); // Send received packet to APRSIS + } - if (Config.digi.mode == 2) { // If Digi enabled - DIGI_Utils::loop(packet); // Send received packet to Digi + if (Config.digi.mode == 2) { // If Digi enabled + DIGI_Utils::loop(packet); // Send received packet to Digi + } + + TNC_Utils::sendToClients(packet); // Send received packet to TNC KISS } show_display(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine, 0); diff --git a/src/kiss_protocol.h b/src/kiss_protocol.h new file mode 100644 index 0000000..f4eb7f9 --- /dev/null +++ b/src/kiss_protocol.h @@ -0,0 +1,23 @@ +#define DCD_ON 0x03 + +#define FEND 0xC0 +#define FESC 0xDB +#define TFEND 0xDC +#define TFESC 0xDD + +#define CMD_UNKNOWN 0xFE +#define CMD_DATA 0x00 +#define CMD_HARDWARE 0x06 + +#define HW_RSSI 0x21 + +#define CMD_ERROR 0x90 +#define ERROR_INITRADIO 0x01 +#define ERROR_TXFAILED 0x02 +#define ERROR_QUEUE_FULL 0x04 + +#define APRS_CONTROL_FIELD 0x03 +#define APRS_INFORMATION_FIELD 0xf0 + +#define HAS_BEEN_DIGIPITED_MASK 0b10000000 +#define IS_LAST_ADDRESS_POSITION_MASK 0b1 \ No newline at end of file diff --git a/src/kiss_utils.cpp b/src/kiss_utils.cpp new file mode 100644 index 0000000..ad0f93f --- /dev/null +++ b/src/kiss_utils.cpp @@ -0,0 +1,170 @@ +#include +#include "kiss_protocol.h" + +bool validateTNC2Frame(const String& tnc2FormattedFrame) { + return (tnc2FormattedFrame.indexOf(':') != -1) && (tnc2FormattedFrame.indexOf('>') != -1); +} + +bool validateKISSFrame(const String& kissFormattedFrame) { + return kissFormattedFrame.charAt(0) == (char)FEND && kissFormattedFrame.charAt(kissFormattedFrame.length() - 1) == (char)FEND; +} + +String encodeAddressAX25(String tnc2Address) { + bool hasBeenDigipited = tnc2Address.indexOf('*') != -1; + + if (tnc2Address.indexOf('-') == -1) { + if (hasBeenDigipited) { + tnc2Address = tnc2Address.substring(0, tnc2Address.length() - 1); + } + + tnc2Address += "-0"; + } + + int separatorIndex = tnc2Address.indexOf('-'); + int ssid = tnc2Address.substring(separatorIndex + 1).toInt(); + + String kissAddress = ""; + for (int i = 0; i < 6; ++i) { + char addressChar; + if (tnc2Address.length() > i && i < separatorIndex) { + addressChar = tnc2Address.charAt(i); + } else { + addressChar = ' '; + } + kissAddress += (char)(addressChar << 1); + } + + kissAddress += (char)((ssid << 1) | 0b01100000 | (hasBeenDigipited ? HAS_BEEN_DIGIPITED_MASK : 0)); + return kissAddress; +} + +String decodeAddressAX25(const String& ax25Address, bool& isLast, bool isRelay) { + String address = ""; + for (int i = 0; i < 6; ++i) { + uint8_t currentCharacter = ax25Address.charAt(i); + currentCharacter >>= 1; + if (currentCharacter != ' ') { + address += (char)currentCharacter; + } + } + auto ssidChar = (uint8_t)ax25Address.charAt(6); + bool hasBeenDigipited = ssidChar & HAS_BEEN_DIGIPITED_MASK; + isLast = ssidChar & IS_LAST_ADDRESS_POSITION_MASK; + ssidChar >>= 1; + + int ssid = 0b1111 & ssidChar; + + if (ssid) { + address += '-'; + address += ssid; + } + if (isRelay && hasBeenDigipited) { + address += '*'; + } + + return address; +} + +String encapsulateKISS(const String& ax25Frame, uint8_t cmd) { + String kissFrame = ""; + kissFrame += (char)FEND; + kissFrame += (char)(0x0f & cmd); + + for (int i = 0; i < ax25Frame.length(); ++i) { + char currentChar = ax25Frame.charAt(i); + if (currentChar == (char)FEND) { + kissFrame += (char)FESC; + kissFrame += (char)TFEND; + } else if (currentChar == (char)FESC) { + kissFrame += (char)FESC; + kissFrame += (char)TFESC; + } else { + kissFrame += currentChar; + } + } + kissFrame += (char)FEND; // end of frame + return kissFrame; +} + + +String decapsulateKISS(const String& frame) { + String ax25Frame = ""; + for (int i = 2; i < frame.length() - 1; ++i) { + char currentChar = frame.charAt(i); + if (currentChar == (char)FESC) { + char nextChar = frame.charAt(i + 1); + if (nextChar == (char)TFEND) { + ax25Frame += (char)FEND; + } else if (nextChar == (char)TFESC) { + ax25Frame += (char)FESC; + } + i++; + } else { + ax25Frame += currentChar; + } + } + + return ax25Frame; +} + +String encodeKISS(const String& frame) { + String ax25Frame = ""; + + if (validateTNC2Frame(frame)) { + String address = ""; + bool dstAddresWritten = false; + for (int p = 0; p <= frame.indexOf(':'); p++) { + char currentChar = frame.charAt(p); + if (currentChar == ':' || currentChar == '>' || currentChar == ',') { + if (!dstAddresWritten && (currentChar == ',' || currentChar == ':')) { + ax25Frame = encodeAddressAX25(address) + ax25Frame; + dstAddresWritten = true; + } else { + ax25Frame += encodeAddressAX25(address); + } + address = ""; + } else { + address += currentChar; + } + } + + auto lastAddressChar = (uint8_t)ax25Frame.charAt(ax25Frame.length() - 1); + ax25Frame.setCharAt(ax25Frame.length() - 1, (char)(lastAddressChar | IS_LAST_ADDRESS_POSITION_MASK)); + ax25Frame += (char)APRS_CONTROL_FIELD; + ax25Frame += (char)APRS_INFORMATION_FIELD; + ax25Frame += frame.substring(frame.indexOf(':') + 1); + } + + String kissFrame = encapsulateKISS(ax25Frame, CMD_DATA); + return kissFrame; +} + +String decodeKISS(const String& inputFrame, bool& dataFrame) { + String frame = ""; + + if (validateKISSFrame(inputFrame)) { + dataFrame = inputFrame.charAt(1) == CMD_DATA; + if (dataFrame) { + String ax25Frame = decapsulateKISS(inputFrame); + bool isLast = false; + String dstAddr = decodeAddressAX25(ax25Frame.substring(0, 7), isLast, false); + String srcAddr = decodeAddressAX25(ax25Frame.substring(7, 14), isLast, false); + + frame = srcAddr + ">" + dstAddr; + + int digiInfoIndex = 14; + while (!isLast && digiInfoIndex + 7 < ax25Frame.length()) { + String digiAddr = decodeAddressAX25(ax25Frame.substring(digiInfoIndex, digiInfoIndex + 7), isLast, true); + frame += ',' + digiAddr; + digiInfoIndex += 7; + } + + frame += ':'; + frame += ax25Frame.substring(digiInfoIndex + 2); + } else { + frame += inputFrame; + } + } + + return frame; +} \ No newline at end of file diff --git a/src/kiss_utils.h b/src/kiss_utils.h new file mode 100644 index 0000000..d2e2a93 --- /dev/null +++ b/src/kiss_utils.h @@ -0,0 +1,15 @@ +#ifndef KISS_UTILS_H_ +#define KISS_UTILS_H_ + +#include + +String encodeAddressAX25(String tnc2Address); +String decodeAddressAX25(const String& ax25Address, bool& isLast, bool isRelay); + +String encapsulateKISS(const String& ax25Frame, uint8_t cmd); +String decapsulateKISS(const String& frame); + +String encodeKISS(const String& frame); +String decodeKISS(const String& inputFrame, bool& dataFrame); + +#endif \ No newline at end of file diff --git a/src/tnc_utils.cpp b/src/tnc_utils.cpp new file mode 100644 index 0000000..0798059 --- /dev/null +++ b/src/tnc_utils.cpp @@ -0,0 +1,113 @@ +#include +#include "kiss_utils.h" +#include "kiss_protocol.h" +#include "lora_utils.h" + +#define MAX_CLIENTS 4 +#define INPUT_TNC_BUFFER_SIZE (2 + MAX_CLIENTS) + +#define TNC_PORT 8001 + +WiFiClient* clients[MAX_CLIENTS]; + +WiFiServer tncServer(TNC_PORT); + +String inputBuffer[INPUT_TNC_BUFFER_SIZE]; + +namespace TNC_Utils { + void setup() { + tncServer.stop(); + tncServer.begin(); + } + + void checkNewClients() { + WiFiClient new_client = tncServer.available(); + + if (new_client.connected()) { + for (int i = 0; i < MAX_CLIENTS; i++) { + WiFiClient* client = clients[i]; + + if (client == nullptr) { + clients[i] = new WiFiClient(new_client); + + Serial.println("New TNC client connected"); + + break; + } + } + } + } + + void handleInputData(char character, int bufferIndex) { + String* inTNCData = &inputBuffer[bufferIndex]; + + if (inTNCData->length() == 0 && character != (char)FEND) { + return; + } + + inTNCData->concat(character); + + if (character == (char)FEND && inTNCData->length() > 3) { + bool isDataFrame = false; + const String& frame = decodeKISS(*inTNCData, isDataFrame); + + if (isDataFrame) { + Serial.print("---> Got from TNC : "); + Serial.println(frame); + + LoRa_Utils::sendNewPacket("APRS", frame); + } + + inTNCData->clear(); + } + + if (inTNCData->length() > 255) { + inTNCData->clear(); + } + } + + void readFromClients() { + for (int i = 0; i < MAX_CLIENTS; i++) { + auto client = clients[i]; + if (client != nullptr) { + if (client->connected()) { + while (client->available() > 0) { + char character = client->read(); + handleInputData(character, 2 + i); + } + } else { + delete client; + clients[i] = nullptr; + } + } + } + } + + void sendToClients(String packet) { + packet = packet.substring(3); + + const String kissEncoded = encodeKISS(packet); + + for (int i = 0; i < MAX_CLIENTS; i++) { + auto client = clients[i]; + if (client != nullptr) { + if (client->connected()) { + client->print(kissEncoded); + client->flush(); + } else { + delete client; + clients[i] = nullptr; + } + } + } + + Serial.print("---> Sent to TNC : "); + Serial.println(packet); + } + + void loop() { + checkNewClients(); + + readFromClients(); + } +} \ No newline at end of file diff --git a/src/tnc_utils.h b/src/tnc_utils.h new file mode 100644 index 0000000..8464a9a --- /dev/null +++ b/src/tnc_utils.h @@ -0,0 +1,15 @@ +#ifndef TNC_UTILS_H_ +#define TNC_UTILS_H_ + +#include + +namespace TNC_Utils { + + void setup(); + void loop(); + + void sendToClients(String packet); + +} + +#endif \ No newline at end of file