Compare commits

..

20 Commits

Author SHA1 Message Date
Ricardo Guzman (Richonguzman) b448e2fc6b ADC_CTRL_PIN fix 2026-03-05 10:06:22 -03:00
Ricardo Guzman (Richonguzman) c33720a5fb VEXT_CTRL_PIN definition missing fix 2026-03-04 12:24:16 -03:00
Ricardo Guzman (Richonguzman) e57fad6666 aprsis.active update in digi.ecoMode == 1 ? 2026-03-04 11:21:26 -03:00
Ricardo Guzman (Richonguzman) 2418291ac9 digiEcoMode forces APRS.active = false 2026-03-03 11:22:41 -03:00
Ricardo Guzman (Richonguzman) 796ba35357 add new ESP32 device 9M2IBR 2026-02-26 12:28:52 -03:00
Ricardo Guzman (Richonguzman) 57873e4181 readme and date update 2026-02-26 11:51:59 -03:00
Ricardo Guzman (Richonguzman) 453222d69f new board added 9M2IBR 2026-02-26 11:08:49 -03:00
Ricardo Guzman (Richonguzman) fbb347fb7f new update to kiss utils 2026-02-26 10:31:45 -03:00
Ricardo Guzman (Richonguzman) 50fd831fdb kiss correction of missing new indexOf 2026-02-26 10:23:40 -03:00
Ricardo Guzman (Richonguzman) 3a2c0304d0 status logic improved 2026-02-25 12:25:40 -03:00
Ricardo Guzman (Richonguzman) b24c2b2527 update build for heltec v2 915 2026-02-25 10:57:07 -03:00
Ricardo Guzman (Richonguzman) db0da96d7f ReadMe update 2026-02-25 10:45:49 -03:00
Ricardo Guzman (Richonguzman) bf6a0faa90 killing blank spaces 2026-02-24 23:33:58 -03:00
Ricardo Guzman (Richonguzman) 61cf118a3b cleaning Headers 2026-02-24 23:27:36 -03:00
Ricardo Guzman (Richonguzman) 44719083a6 new callsignIsValid update 2026-02-24 21:26:20 -03:00
Ricardo Guzman (Richonguzman) 808d740477 update indexOf kill 2026-02-24 19:29:20 -03:00
Ricardo Guzman (Richonguzman) 23c8257c80 date update 2026-02-24 07:55:53 -03:00
Ricardo Guzman (Richonguzman) db4582db13 telemetry EUP beacon update 2026-02-24 07:55:32 -03:00
Ricardo Guzman (Richonguzman) 09e06d36e8 checkCallsignList update 2026-02-24 06:50:27 -03:00
Ricardo Guzman (Richonguzman) e6d44c1b7f replaced string for hash in 25segBuffer 2026-02-23 18:59:27 -03:00
47 changed files with 528 additions and 411 deletions
+4
View File
@@ -19,6 +19,8 @@ jobs:
chip: esp32s3
- name: heltec-lora32-v2
chip: esp32
- name: heltec-lora32-v2_915
chip: esp32
- name: heltec_wifi_lora_32_V3
chip: esp32s3
- name: heltec_wifi_lora_32_V3_2
@@ -97,6 +99,8 @@ jobs:
chip: esp32s3
- name: TROY_LoRa_APRS
chip: esp32
- name: ESP32_9M2IBR_1W_LoRa_GPS
chip: esp32
steps:
- uses: actions/checkout@v3
+5
View File
@@ -51,6 +51,11 @@ ____________________________________________________
<br />
# Timeline (Versions):
- 2026-02-26 9M2IBR ESP32 1W (400M30S) + GPS board added.
- 2026-02-25 Code Improvements: reduced String comparisons and improved logic for faster code execution.
- 2026-02-15 Digipeater code/logic improved.
- 2026-02-08 Heltec V2 915MHz added.
- 2026-02-05 Improved "digiBackupMode" to check "APRS-IS" server connection.
- 2026-01-07 Tactical Callsign added.
- 2026-01-05 Heltec V4 support added.
- 2025-12-22 Heltec Wireless Paper V1.2 and VisionMaster E290 Added. Thanks HA5SZI.
+1
View File
@@ -21,6 +21,7 @@
#include <Arduino.h>
#define SCREEN_ADDRESS 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
void displaySetup();
+1 -1
View File
@@ -34,7 +34,7 @@ namespace POWER_Utils {
void vext_ctrl_OFF();
#endif
#ifdef ADC_CTRL
#ifdef ADC_CTRL_PIN
void adc_ctrl_ON();
void adc_ctrl_OFF();
#endif
+1
View File
@@ -21,6 +21,7 @@
#include <Arduino.h>
namespace SLEEP_Utils {
void setup();
+1 -2
View File
@@ -37,8 +37,7 @@ namespace STATION_Utils {
void deleteNotHeard();
void updateLastHeard(const String& station);
bool wasHeard(const String& station);
void clean25SegBuffer();
bool check25SegBuffer(const String& station, const String& textMessage);
bool isIn25SegHashBuffer(const String& station, const String& textMessage);
void processOutputPacketBufferUltraEcoMode();
void processOutputPacketBuffer();
void addToOutputPacketBuffer(const String& packet, bool flag = false);
+1
View File
@@ -27,6 +27,7 @@ namespace TELEMETRY_Utils {
void sendEquationsUnitsParameters();
String generateEncodedTelemetryBytes(float value, bool counterBytes, byte telemetryType);
String generateEncodedTelemetry();
void checkEUPInterval();
}
+2 -3
View File
@@ -67,8 +67,8 @@ ___________________________________________________________________*/
#endif
String versionDate = "2026-02-20";
String versionNumber = "3.2";
String versionDate = "2026-03-05";
String versionNumber = "3.2.2";
Configuration Config;
WiFiClient aprsIsClient;
WiFiClient mqttClient;
@@ -188,7 +188,6 @@ void loop() {
}
if (Config.loramodule.txActive && (Config.digi.mode == 2 || Config.digi.mode == 3 || backupDigiMode)) { // If Digi enabled
STATION_Utils::clean25SegBuffer();
DIGI_Utils::processLoRaPacket(packet); // Send received packet to Digi
}
+48 -40
View File
@@ -135,30 +135,30 @@ namespace APRS_IS_Utils {
}
String checkForStartingBytes(const String& packet) {
if (packet.indexOf("\x3c\xff\x01") != -1) {
return packet.substring(0, packet.indexOf("\x3c\xff\x01"));
} else {
return packet;
}
int index = packet.indexOf("\x3c\xff\x01");
return (index != -1) ? packet.substring(0, index) : packet;
}
String buildPacketToUpload(const String& packet) {
String packetToUpload = packet.substring(3, packet.indexOf(":"));
int colonIndex = packet.indexOf(":");
String packetToUpload = packet.substring(3, colonIndex);
if (Config.aprs_is.active && passcodeValid && Config.aprs_is.messagesToRF) {
packetToUpload += ",qAR,";
} else {
packetToUpload += ",qAO,";
}
packetToUpload += Config.callsign;
packetToUpload += checkForStartingBytes(packet.substring(packet.indexOf(":")));
packetToUpload += checkForStartingBytes(packet.substring(colonIndex));
return packetToUpload;
}
bool processReceivedLoRaMessage(const String& sender, const String& packet, bool thirdParty) {
String receivedMessage;
if (packet.indexOf("{") > 0) { // ack?
int leftCurlyBraceIndex = packet.indexOf("{");
int colonIndex = packet.indexOf(":");
if (leftCurlyBraceIndex > 0) { // ack?
String ackMessage = "ack";
ackMessage.concat(packet.substring(packet.indexOf("{") + 1));
ackMessage.concat(packet.substring(leftCurlyBraceIndex + 1));
ackMessage.trim();
//Serial.println(ackMessage);
@@ -180,9 +180,9 @@ namespace APRS_IS_Utils {
addToBuffer += ":";
addToBuffer += ackMessage;
STATION_Utils::addToOutputPacketBuffer(addToBuffer);
receivedMessage = packet.substring(packet.indexOf(":") + 1, packet.indexOf("{"));
receivedMessage = packet.substring(colonIndex + 1, leftCurlyBraceIndex);
} else {
receivedMessage = packet.substring(packet.indexOf(":") + 1);
receivedMessage = packet.substring(colonIndex + 1);
}
if (receivedMessage.indexOf("?") == 0) {
if (!Config.display.alwaysOn && Config.display.timeout != 0) {
@@ -207,29 +207,30 @@ namespace APRS_IS_Utils {
if (Sender != Config.callsign && Utils::callsignIsValid(Sender)) {
STATION_Utils::updateLastHeard(Sender);
Utils::typeOfPacket(packet.substring(3), 0); // LoRa-APRS
const String& AddresseeAndMessage = packet.substring(packet.indexOf("::") + 2);
int doubleColonIndex = packet.indexOf("::");
const String& AddresseeAndMessage = packet.substring(doubleColonIndex + 2);
String Addressee = AddresseeAndMessage.substring(0, AddresseeAndMessage.indexOf(":"));
Addressee.trim();
bool queryMessage = false;
if (packet.indexOf("::") > 10 && Addressee == Config.callsign) { // its a message for me!
if (doubleColonIndex > 10 && Addressee == Config.callsign) { // its a message for me!
queryMessage = processReceivedLoRaMessage(Sender, checkForStartingBytes(AddresseeAndMessage), false);
}
if (!queryMessage) {
const String& aprsPacket = buildPacketToUpload(packet);
if (!Config.display.alwaysOn && Config.display.timeout != 0) {
displayToggle(true);
}
lastScreenOn = millis();
#ifdef HAS_A7670
stationBeacon = true;
A7670_Utils::uploadToAPRSIS(aprsPacket);
stationBeacon = false;
#else
upload(aprsPacket);
#endif
Utils::println("---> Uploaded to APRS-IS");
displayShow(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine, 0);
if (queryMessage) return;
const String& aprsPacket = buildPacketToUpload(packet);
if (!Config.display.alwaysOn && Config.display.timeout != 0) {
displayToggle(true);
}
lastScreenOn = millis();
#ifdef HAS_A7670
stationBeacon = true;
A7670_Utils::uploadToAPRSIS(aprsPacket);
stationBeacon = false;
#else
upload(aprsPacket);
#endif
Utils::println("---> Uploaded to APRS-IS");
displayShow(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine, 0);
}
}
}
@@ -245,26 +246,30 @@ namespace APRS_IS_Utils {
outputPacket.concat(",TCPIP,");
outputPacket.concat(Config.callsign);
outputPacket.concat("*");
int colonEqualIndex = packet.indexOf(":=");
int doubleColonIndex = packet.indexOf("::");
int colonInvAccentIndex = packet.indexOf(":`");
switch (packetType) {
case 0: // gps
if (packet.indexOf(":=") > 0) {
outputPacket += packet.substring(packet.indexOf(":="));
if (colonEqualIndex > 0) {
outputPacket += packet.substring(colonEqualIndex);
} else {
outputPacket += packet.substring(packet.indexOf(":!"));
}
break;
case 1: // messages
outputPacket += packet.substring(packet.indexOf("::"));
outputPacket += packet.substring(doubleColonIndex);
break;
case 2: // status
outputPacket += packet.substring(packet.indexOf(":>"));
break;
case 3: // telemetry
outputPacket += packet.substring(packet.indexOf("::"));
outputPacket += packet.substring(doubleColonIndex);
break;
case 4: // mic-e
if (packet.indexOf(":`") > 0) {
outputPacket += packet.substring(packet.indexOf(":`"));
if (colonInvAccentIndex > 0) {
outputPacket += packet.substring(colonInvAccentIndex);
} else {
outputPacket += packet.substring(packet.indexOf(":'"));
}
@@ -316,18 +321,21 @@ namespace APRS_IS_Utils {
if (packet.startsWith("#")) {
if (Config.digi.backupDigiMode) lastServerCheck = currentTime;
} else {
if (Config.aprs_is.messagesToRF && packet.indexOf("::") > 0) {
int doubleColonIndex = packet.indexOf("::");
if (Config.aprs_is.messagesToRF && doubleColonIndex > 0) {
String Sender = packet.substring(0, packet.indexOf(">"));
const String& AddresseeAndMessage = packet.substring(packet.indexOf("::") + 2);
String Addressee = AddresseeAndMessage.substring(0, AddresseeAndMessage.indexOf(":"));
const String& AddresseeAndMessage = packet.substring(doubleColonIndex + 2);
int colonIndex = AddresseeAndMessage.indexOf(":");
String Addressee = AddresseeAndMessage.substring(0, colonIndex);
Addressee.trim();
if (Addressee == Config.callsign) { // its for me!
String receivedMessage;
if (AddresseeAndMessage.indexOf("{") > 0) { // ack?
int curlyBraceIndex = AddresseeAndMessage.indexOf("{");
if (curlyBraceIndex > 0) { // ack?
processAckMessage(Sender, AddresseeAndMessage);
receivedMessage = AddresseeAndMessage.substring(AddresseeAndMessage.indexOf(":") + 1, AddresseeAndMessage.indexOf("{"));
receivedMessage = AddresseeAndMessage.substring(colonIndex + 1, curlyBraceIndex);
} else {
receivedMessage = AddresseeAndMessage.substring(AddresseeAndMessage.indexOf(":") + 1);
receivedMessage = AddresseeAndMessage.substring(colonIndex + 1);
}
if (receivedMessage.indexOf("?") == 0) {
Utils::println("Rx Query (APRS-IS) : " + packet);
+2 -2
View File
@@ -154,7 +154,7 @@ namespace BATTERY_Utils {
}
#else
#ifdef ADC_CTRL
#ifdef ADC_CTRL_PIN
POWER_Utils::adc_ctrl_ON();
#endif
@@ -180,7 +180,7 @@ namespace BATTERY_Utils {
delay(3);
}
#ifdef ADC_CTRL
#ifdef ADC_CTRL_PIN
POWER_Utils::adc_ctrl_OFF();
#ifdef HELTEC_WP_V1
+1
View File
@@ -87,6 +87,7 @@ bool Configuration::writeFile() {
data["digi"]["mode"] = digi.mode;
data["digi"]["ecoMode"] = digi.ecoMode;
if (digi.ecoMode == 1) data["aprs_is"]["active"] = false;
#if defined(HAS_A7670)
if (digi.ecoMode == 1) data["digi"]["ecoMode"] = 2;
#endif
+23 -23
View File
@@ -61,14 +61,14 @@ namespace DIGI_Utils {
int digiMode = Config.digi.mode;
String tempPath = path;
if (tempPath.indexOf("WIDE1-1") != -1 && (digiMode == 2 || digiMode == 3)) { // WIDE1-1 Digipeater
if (tempPath.indexOf("*") != -1 ) return ""; // "*" shouldn't be in WIDE1-1 (only) type of packet
if (tempPath.indexOf("WIDE1-1") != -1 && (digiMode == 2 || digiMode == 3)) { // WIDE1-1
if (tempPath.indexOf("*") != -1 ) return ""; // "*" shouldn't be in WIDE1-1 (only) type of packet
tempPath.replace("WIDE1-1", stationCallsign + "*");
} else if (tempPath.indexOf("WIDE2-") != -1 && digiMode == 3) { // WIDE2-n Digipeater
tempPath = cleanPathAsterisks(path);
if (path.indexOf("WIDE2-1") != -1) {
if (tempPath.indexOf("WIDE2-1") != -1) {
tempPath.replace("WIDE2-1", stationCallsign + "*");
} else if (path.indexOf("WIDE2-2") != -1) {
} else if (tempPath.indexOf("WIDE2-2") != -1) {
tempPath.replace("WIDE2-2", stationCallsign + "*,WIDE2-1");
} else {
return "";
@@ -149,28 +149,28 @@ namespace DIGI_Utils {
if (Sender == stationCallsign) return; // Avoid listening to self packets
if (!thirdPartyPacket && Config.tacticalCallsign == "" && !Utils::callsignIsValid(Sender)) return; // No thirdParty + no tactical y no valid callsign
if (STATION_Utils::check25SegBuffer(Sender, temp.substring(temp.indexOf(":") + 2))) {
STATION_Utils::updateLastHeard(Sender);
Utils::typeOfPacket(temp, 2); // Digi
bool queryMessage = false;
int doubleColonIndex = temp.indexOf("::");
if (doubleColonIndex > 10) { // it's a message
String AddresseeAndMessage = temp.substring(doubleColonIndex + 2);
String Addressee = AddresseeAndMessage.substring(0, AddresseeAndMessage.indexOf(":"));
Addressee.trim();
if (Addressee == stationCallsign) { // it's a message for me!
queryMessage = APRS_IS_Utils::processReceivedLoRaMessage(Sender, AddresseeAndMessage, thirdPartyPacket);
}
}
if (queryMessage) return; // answer should not be repeated.
if (STATION_Utils::isIn25SegHashBuffer(Sender, temp.substring(temp.indexOf(":") + 2))) return;
String loraPacket = generateDigipeatedPacket(packet.substring(3), thirdPartyPacket);
if (loraPacket != "") {
STATION_Utils::addToOutputPacketBuffer(loraPacket);
if (Config.digi.ecoMode != 1) displayToggle(true);
lastScreenOn = millis();
STATION_Utils::updateLastHeard(Sender);
Utils::typeOfPacket(temp, 2); // Digi
bool queryMessage = false;
int doubleColonIndex = temp.indexOf("::");
if (doubleColonIndex > 10) { // it's a message
String AddresseeAndMessage = temp.substring(doubleColonIndex + 2);
String Addressee = AddresseeAndMessage.substring(0, AddresseeAndMessage.indexOf(":"));
Addressee.trim();
if (Addressee == stationCallsign) { // it's a message for me!
queryMessage = APRS_IS_Utils::processReceivedLoRaMessage(Sender, AddresseeAndMessage, thirdPartyPacket);
}
}
if (queryMessage) return; // answer should not be repeated.
String loraPacket = generateDigipeatedPacket(packet.substring(3), thirdPartyPacket);
if (loraPacket != "") {
STATION_Utils::addToOutputPacketBuffer(loraPacket);
if (Config.digi.ecoMode != 1) displayToggle(true);
lastScreenOn = millis();
}
}
}
+8 -8
View File
@@ -57,7 +57,7 @@
String lastEpaperText;
#else
#include <Adafruit_GFX.h>
#if defined(TTGO_T_Beam_S3_SUPREME_V3)
#ifdef HAS_SH1106
#include <Adafruit_SH110X.h>
Adafruit_SH1106G display(128, 64, &Wire, OLED_RST);
#else
@@ -117,7 +117,7 @@ void displaySetup() {
digitalWrite(OLED_RST, HIGH);
#endif
#if defined(TTGO_T_Beam_S3_SUPREME_V3)
#ifdef HAS_SH1106
if (display.begin(0x3c, false)) {
displayFound = true;
if (Config.display.turn180) display.setRotation(2);
@@ -157,7 +157,7 @@ void displayToggle(bool toggle) {
display.printCenter("EPAPER Display Disabled by toggle...");
display.update();
#else
#if defined(TTGO_T_Beam_S3_SUPREME_V3)
#ifdef HAS_SH1106
if (displayFound) display.oled_command(SH110X_DISPLAYON);
#else
if (displayFound) display.ssd1306_command(SSD1306_DISPLAYON);
@@ -171,7 +171,7 @@ void displayToggle(bool toggle) {
#ifdef HAS_EPAPER
display.update();
#else
#if defined(TTGO_T_Beam_S3_SUPREME_V3)
#ifdef HAS_SH1106
if (displayFound) display.oled_command(SH110X_DISPLAYOFF);
#else
if (displayFound) display.ssd1306_command(SSD1306_DISPLAYOFF);
@@ -222,7 +222,7 @@ void displayShow(const String& header, const String& line1, const String& line2,
#else
if (displayFound) {
display.clearDisplay();
#if defined(TTGO_T_Beam_S3_SUPREME_V3)
#ifdef HAS_SH1106
display.setTextColor(SH110X_WHITE);
#else
display.setTextColor(WHITE);
@@ -234,7 +234,7 @@ void displayShow(const String& header, const String& line1, const String& line2,
display.setCursor(0, 8 + (8 * i));
display.println(*lines[i]);
}
#if defined(TTGO_T_Beam_S3_SUPREME_V3)
#ifdef HAS_SH1106
display.setContrast(1);
#else
display.ssd1306_command(SSD1306_SETCONTRAST);
@@ -288,7 +288,7 @@ void displayShow(const String& header, const String& line1, const String& line2,
#else
if (displayFound) {
display.clearDisplay();
#if defined(TTGO_T_Beam_S3_SUPREME_V3)
#ifdef HAS_SH1106
display.setTextColor(SH110X_WHITE);
#else
display.setTextColor(WHITE);
@@ -301,7 +301,7 @@ void displayShow(const String& header, const String& line1, const String& line2,
display.setCursor(0, 16 + (8 * i));
display.println(*lines[i]);
}
#if defined(TTGO_T_Beam_S3_SUPREME_V3)
#ifdef HAS_SH1106
display.setContrast(1);
#else
display.ssd1306_command(SSD1306_SETCONTRAST);
+7 -16
View File
@@ -29,29 +29,20 @@ bool validateKISSFrame(const String& kissFormattedFrame) {
}
String encodeAddressAX25(String tnc2Address) {
bool hasBeenDigipited = tnc2Address.indexOf('*') != -1;
int tnc2AddressIndex = tnc2Address.indexOf('-');
if (tnc2AddressIndex == -1) {
if (hasBeenDigipited) {
tnc2Address = tnc2Address.substring(0, tnc2Address.length() - 1);
}
bool hasBeenDigipited = tnc2Address.indexOf('*') != -1;
if (tnc2Address.indexOf('-') == -1) {
if (hasBeenDigipited) tnc2Address = tnc2Address.substring(0, tnc2Address.length() - 1);
tnc2Address += "-0";
}
int separatorIndex = tnc2AddressIndex;
int ssid = tnc2Address.substring(separatorIndex + 1).toInt();
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 = ' ';
}
char addressChar = ' ';
if (tnc2Address.length() > i && i < separatorIndex) addressChar = tnc2Address.charAt(i);
kissAddress += (char)(addressChar << 1);
}
kissAddress += (char)((ssid << 1) | 0b01100000 | (hasBeenDigipited ? HAS_BEEN_DIGIPITED_MASK : 0));
return kissAddress;
}
+4 -4
View File
@@ -263,8 +263,8 @@ namespace POWER_Utils {
pinMode(Config.battery.externalVoltagePin, INPUT);
}
#ifdef VEXT_CTRL
pinMode(VEXT_CTRL,OUTPUT); // GPS + TFT on HELTEC Wireless_Tracker and only for Oled in HELTEC V3
#ifdef VEXT_CTRL_PIN
pinMode(VEXT_CTRL_PIN,OUTPUT); // GPS + TFT on HELTEC Wireless_Tracker and only for Oled in HELTEC V3
vext_ctrl_ON();
#endif
@@ -272,8 +272,8 @@ namespace POWER_Utils {
if (Config.beacon.gpsActive && Config.digi.ecoMode != 1) activateGPS();
#endif
#ifdef ADC_CTRL
pinMode(ADC_CTRL, OUTPUT);
#ifdef ADC_CTRL_PIN
pinMode(ADC_CTRL_PIN, OUTPUT);
adc_ctrl_OFF();
#endif
+57 -48
View File
@@ -25,6 +25,8 @@
#include "utils.h"
#include <vector>
#define SECS_TO_WAIT 3 // soon to be deleted...
extern Configuration Config;
extern uint32_t lastRxTime;
@@ -40,17 +42,16 @@ std::vector<LastHeardStation> lastHeardObjects;
struct OutputPacketBuffer {
String packet;
bool isBeacon;
OutputPacketBuffer(const String& p, bool b) : packet(p), isBeacon(b) {}
};
std::vector<OutputPacketBuffer> outputPacketBuffer;
struct Packet25SegBuffer {
uint32_t receivedTime;
String station;
String payload;
uint32_t hash;
};
std::vector<Packet25SegBuffer> packet25SegBuffer;
bool saveNewDigiEcoModeConfig = false;
bool packetIsBeacon = false;
@@ -59,19 +60,18 @@ namespace STATION_Utils {
std::vector<String> loadCallsignList(const String& list) {
std::vector<String> loadedList;
int start = 0;
int listLength = list.length();
String callsigns = list;
callsigns.trim();
while (start < listLength) {
while (start < listLength && list[start] == ' ') start++; // avoid blank spaces
if (start >= listLength) break;
while (callsigns.length() > 0) { // != ""
int spaceIndex = callsigns.indexOf(" ");
if (spaceIndex == -1) { // No more spaces, add the last part
loadedList.push_back(callsigns);
break;
}
loadedList.push_back(callsigns.substring(0, spaceIndex));
callsigns = callsigns.substring(spaceIndex + 1);
callsigns.trim(); // Trim in case of multiple spaces
int end = start;
while (end < listLength && list[end] != ' ') end++; // find another blank space or reach listLength
loadedList.emplace_back(list.substring(start, end));
start = end + 1; // keep on searching if listLength not reached
}
return loadedList;
}
@@ -85,8 +85,9 @@ namespace STATION_Utils {
for (size_t i = 0; i < list.size(); i++) {
int wildcardIndex = list[i].indexOf("*");
if (wildcardIndex >= 0) {
String wildcard = list[i].substring(0, wildcardIndex);
if (callsign.startsWith(wildcard)) return true;
if (wildcardIndex >= 2 && callsign.length() >= wildcardIndex && strncmp(callsign.c_str(), list[i].c_str(), wildcardIndex) == 0) {
return true;
}
} else {
if (list[i] == callsign) return true;
}
@@ -127,36 +128,33 @@ namespace STATION_Utils {
}
void deleteNotHeard() {
std::vector<LastHeardStation> lastHeardStation_temp;
for (int i = 0; i < lastHeardStations.size(); i++) {
if (millis() - lastHeardStations[i].lastHeardTime < Config.rememberStationTime * 60 * 1000) {
lastHeardStation_temp.push_back(lastHeardStations[i]);
uint32_t currentTime = millis();
uint32_t timeout = Config.rememberStationTime * 60UL * 1000UL;
for (int i = lastHeardStations.size() - 1; i >= 0; i--) {
if (currentTime - lastHeardStations[i].lastHeardTime >= timeout) {
lastHeardStations.erase(lastHeardStations.begin() + i);
}
}
lastHeardStations.clear();
for (int j = 0; j < lastHeardStation_temp.size(); j++) {
lastHeardStations.push_back(lastHeardStation_temp[j]);
}
lastHeardStation_temp.clear();
}
void updateLastHeard(const String& station) {
deleteNotHeard();
bool stationHeard = false;
for (int i = 0; i < lastHeardStations.size(); i++) {
uint32_t currentTime = millis();
for (size_t i = 0; i < lastHeardStations.size(); i++) {
if (lastHeardStations[i].station == station) {
lastHeardStations[i].lastHeardTime = millis();
stationHeard = true;
break;
lastHeardStations[i].lastHeardTime = currentTime;
Utils::showActiveStations();
return;
}
}
if (!stationHeard) lastHeardStations.emplace_back(LastHeardStation{millis(), station});
lastHeardStations.emplace_back(LastHeardStation{currentTime, station});
Utils::showActiveStations();
}
bool wasHeard(const String& station) {
deleteNotHeard();
for (int i = 0; i < lastHeardStations.size(); i++) {
for (size_t i = 0; i < lastHeardStations.size(); i++) {
if (lastHeardStations[i].station == station) {
Utils::println(" ---> Listened Station");
return true;
@@ -166,18 +164,33 @@ namespace STATION_Utils {
return false;
}
void clean25SegBuffer() {
if (!packet25SegBuffer.empty() && (millis() - packet25SegBuffer[0].receivedTime) > 25 * 1000) packet25SegBuffer.erase(packet25SegBuffer.begin());
}
bool check25SegBuffer(const String& station, const String& textMessage) {
if (!packet25SegBuffer.empty()) {
for (int i = 0; i < packet25SegBuffer.size(); i++) {
if (packet25SegBuffer[i].station == station && packet25SegBuffer[i].payload == textMessage) return false;
void clean25SegHashBuffer() {
uint32_t currentTime = millis();
for (int i = packet25SegBuffer.size() - 1; i >= 0; i--) {
if ((currentTime - packet25SegBuffer[i].receivedTime) > 25 * 1000) {
packet25SegBuffer.erase(packet25SegBuffer.begin() + i);
}
}
packet25SegBuffer.emplace_back(Packet25SegBuffer{millis(), station, textMessage});
return true;
}
uint32_t makeHash(const String& station, const String& payload) { // DJB2 Hash
uint32_t h = 5381;
for (size_t i = 0; i < station.length(); i++)
h = ((h << 5) + h) + station[i];
for (size_t i = 0; i < payload.length(); i++)
h = ((h << 5) + h) + payload[i];
return h;
}
bool isIn25SegHashBuffer(const String& station, const String& textMessage) {
clean25SegHashBuffer();
uint32_t newHash = makeHash(station, textMessage);
uint32_t currentTime = millis();
for (int i = 0; i < packet25SegBuffer.size(); i++) {
if (packet25SegBuffer[i].hash == newHash) return true;
}
packet25SegBuffer.push_back({currentTime, newHash});
return false;
}
void processOutputPacketBufferUltraEcoMode() {
@@ -201,7 +214,7 @@ namespace STATION_Utils {
}
void processOutputPacketBuffer() {
int timeToWait = 3 * 1000; // 3 segs between packet Tx and also Rx ???
int timeToWait = SECS_TO_WAIT * 1000; // 3 segs between packet Tx and also Rx ???
uint32_t lastRx = millis() - lastRxTime;
uint32_t lastTx = millis() - lastTxTime;
if (outputPacketBuffer.size() > 0 && lastTx > timeToWait && lastRx > timeToWait) {
@@ -229,11 +242,7 @@ namespace STATION_Utils {
}
void addToOutputPacketBuffer(const String& packet, bool flag) {
OutputPacketBuffer entry;
entry.packet = packet;
entry.isBeacon = flag;
outputPacketBuffer.push_back(entry);
outputPacketBuffer.emplace_back(OutputPacketBuffer{packet, flag});
}
}
+12 -4
View File
@@ -29,10 +29,11 @@
#include "display.h"
extern Configuration Config;
extern bool sendStartTelemetry;
extern Configuration Config;
int telemetryCounter = random(1,999);
int telemetryCounter = random(1,999);
uint32_t telemetryEUPTime = 0;
bool sendEUP = false; // Equations Units Parameters
namespace TELEMETRY_Utils {
@@ -89,7 +90,7 @@ namespace TELEMETRY_Utils {
sendBaseTelemetryPacket("EQNS.", getEquationCoefficients());
sendBaseTelemetryPacket("UNIT.", getUnitLabels());
sendBaseTelemetryPacket("PARM.", getParameterNames());
sendStartTelemetry = false;
sendEUP = false;
}
String generateEncodedTelemetryBytes(float value, bool counterBytes, byte telemetryType) {
@@ -127,4 +128,11 @@ namespace TELEMETRY_Utils {
return telemetry;
}
void checkEUPInterval() {
if (telemetryEUPTime == 0 || millis() - telemetryEUPTime > 24UL * 60UL * 60UL * 1000UL) {
sendEUP = true;
telemetryEUPTime = millis();
}
}
}
+65 -51
View File
@@ -58,11 +58,11 @@ extern bool backupDigiMode;
extern bool shouldSleepLowVoltage;
extern bool transmitFlag;
extern bool passcodeValid;
extern bool sendEUP; // Equations Units Parameters
extern std::vector<LastHeardStation> lastHeardStations;
bool statusAfterBoot = true;
bool sendStartTelemetry = true;
bool statusUpdate = true;
bool beaconUpdate = false;
uint32_t lastBeaconTx = 0;
uint32_t lastScreenOn = millis();
@@ -75,26 +75,30 @@ String secondaryBeaconPacket;
namespace Utils {
void processStatus() {
String status = APRSPacketLib::generateBasePacket(Config.callsign, "APLRG1", Config.beacon.path);
bool sendOverAPRSIS = Config.beacon.sendViaAPRSIS && Config.aprs_is.active && WiFi.status() == WL_CONNECTED;
bool sendOverRF = !Config.beacon.sendViaAPRSIS && Config.beacon.sendViaRF;
if (WiFi.status() == WL_CONNECTED && Config.aprs_is.active && Config.beacon.sendViaAPRSIS) {
delay(1000);
status.concat(",qAC:>");
status.concat(Config.beacon.statusPacket);
APRS_IS_Utils::upload(status);
SYSLOG_Utils::log(2, status, 0, 0.0, 0); // APRSIS TX
if (!sendOverAPRSIS && !sendOverRF) {
statusUpdate = false;
return;
}
if (statusAfterBoot && !Config.beacon.sendViaAPRSIS && Config.beacon.sendViaRF) {
status.concat(":>");
status.concat(Config.beacon.statusPacket);
STATION_Utils::addToOutputPacketBuffer(status, true); // treated also as beacon on Tx Freq
String statusPacket = APRSPacketLib::generateBasePacket(Config.callsign, "APLRG1", Config.beacon.path);
statusPacket += sendOverAPRSIS ? ",qAC:>" : ":>";
statusPacket += Config.beacon.statusPacket;
if (sendOverAPRSIS) {
APRS_IS_Utils::upload(statusPacket);
SYSLOG_Utils::log(2, statusPacket, 0, 0.0, 0); // APRSIS TX
} else {
STATION_Utils::addToOutputPacketBuffer(statusPacket, true); // treated also as beacon on Tx Freq
}
statusAfterBoot = false;
statusUpdate = false;
lastStatusTx = millis();
}
void checkStatusInterval() {
if (lastStatusTx == 0 || millis() - lastStatusTx > DAY_MS) statusAfterBoot = true;
if (lastStatusTx == 0 || millis() - lastStatusTx > DAY_MS) statusUpdate = true;
}
String getLocalIP() {
@@ -157,7 +161,8 @@ namespace Utils {
if (beaconUpdate) {
if (!Config.display.alwaysOn && Config.display.timeout != 0) displayToggle(true);
if (sendStartTelemetry &&
TELEMETRY_Utils::checkEUPInterval();
if (sendEUP &&
Config.battery.sendVoltageAsTelemetry &&
!Config.wxsensor.active &&
(Config.battery.sendInternalVoltage || Config.battery.sendExternalVoltage) &&
@@ -290,7 +295,7 @@ namespace Utils {
}
checkStatusInterval();
if (statusAfterBoot && Config.beacon.statusActive && !Config.beacon.statusPacket.isEmpty()) processStatus();
if (statusUpdate && Config.beacon.statusActive && !Config.beacon.statusPacket.isEmpty()) processStatus();
}
void checkDisplayInterval() {
@@ -403,9 +408,9 @@ namespace Utils {
if (mode == 1) { // low voltage detected after a while
displayToggle(false);
}
#ifdef VEXT_CTRL
#ifdef VEXT_CTRL_PIN
#ifndef HELTEC_WSL_V3
digitalWrite(VEXT_CTRL, LOW);
digitalWrite(VEXT_CTRL_PIN, LOW);
#endif
#endif
LoRa_Utils::sleepRadio();
@@ -417,44 +422,53 @@ namespace Utils {
bool callsignIsValid(const String& callsign) {
if (callsign == "WLNK-1") return true;
int totalCallsignLength = callsign.length();
if (totalCallsignLength < 4) return false;
String cleanCallsign;
int hypenCallsignIndex = callsign.indexOf("-");
if (hypenCallsignIndex > 0) { // SSID Validation
cleanCallsign = callsign.substring(0, hypenCallsignIndex);
String ssid = callsign.substring(hypenCallsignIndex + 1);
if (ssid.indexOf("-") != -1 || ssid.length() > 2) return false;
if (ssid.length() == 2 && ssid[0] == '0') return false;
for (int i = 0; i < ssid.length(); i++) {
if (!isAlphaNumeric(ssid[i])) return false;
int hyphenIndex = callsign.indexOf("-");
int baseCallsignLength = (hyphenIndex > 0) ? hyphenIndex : totalCallsignLength;
if (hyphenIndex > 0) { // SSID Validation
if (hyphenIndex < 4) return false; // base Callsign must have at least 4 characters
int ssidStart = hyphenIndex + 1;
int ssidLength = totalCallsignLength - ssidStart;
if (ssidLength == 0 || ssidLength > 2) return false;
if (callsign.indexOf('-', ssidStart) != -1) return false; // avoid another "-" in ssid
if (ssidLength == 2 && callsign[ssidStart] == '0') return false; // ssid can't start with "0"
for (int i = ssidStart; i < totalCallsignLength; i++) {
if (!isDigit(callsign[i])) return false;
}
}
if (baseCallsignLength < 4 || baseCallsignLength > 6) return false;
bool padded = false; // Callsigns with 4 characters like A0AA are padded into 5 characters for further analisis : A0AA --> _A0AA
if (baseCallsignLength == 4 && isAlpha(callsign[0]) && isDigit(callsign[1]) && isAlpha(callsign[2]) && isAlpha(callsign[3])) padded = true;
char c0, c1, c2, c3;
if (padded) {
c0 = ' ';
c1 = callsign[0];
c2 = callsign[1];
c3 = callsign[2];
} else {
cleanCallsign = callsign;
c0 = callsign[0];
c1 = callsign[1];
c2 = callsign[2];
c3 = callsign[3];
}
if (!isDigit(c2) || !isAlpha(c3)) { // __0A__ must be validated
if (c0 != 'R' && !isDigit(c1) && !isAlpha(c2)) return false; // to accepto R0A___
}
if (cleanCallsign.length() < 4 || cleanCallsign.length() > 6) return false;
bool isValid =
((isAlphaNumeric(c0) || c0 == ' ') && isAlpha(c1)) || // AA0A (+A+A) + _A0AA (+A) + 0A0A (+A+A)
(isAlpha(c0) && isDigit(c1)) || // A00A (+A+A)
(c0 == 'R' && baseCallsignLength == 6 && isDigit(c1) && isAlpha(c2) && isAlpha(c3) && isAlpha(callsign[4])); // R0AA (+A+A)
if (!isValid) return false; // also 00__ avoided
if (cleanCallsign.length() < 6 && isAlpha(cleanCallsign[0]) && isDigit(cleanCallsign[1]) && isAlpha(cleanCallsign[2]) && isAlpha(cleanCallsign[3]) ) {
cleanCallsign = " " + cleanCallsign; // A0AA --> _A0AA
}
if (!isDigit(cleanCallsign[2]) || !isAlpha(cleanCallsign[3])) { // __0A__ must be validated
if (cleanCallsign[0] != 'R' && !isDigit(cleanCallsign[1]) && !isAlpha(cleanCallsign[2])) return false; // to accepto R0A___
}
bool isValid = false;
if ((isAlphaNumeric(cleanCallsign[0]) || cleanCallsign[0] == ' ') && isAlpha(cleanCallsign[1])) {
isValid = true; // AA0A (+A+A) + _A0AA (+A) + 0A0A (+A+A)
} else if (isAlpha(cleanCallsign[0]) && isDigit(cleanCallsign[1])) {
isValid = true; // A00A (+A+A)
} else if (cleanCallsign[0] == 'R' && cleanCallsign.length() == 6 && isDigit(cleanCallsign[1]) && isAlpha(cleanCallsign[2]) && isAlpha(cleanCallsign[3]) && isAlpha(cleanCallsign[4])) {
isValid = true; // R0AA (+A+A)
}
if (!isValid) return false; // also 00__ avoided
if (cleanCallsign.length() > 4) { // to validate ____AA
for (int i = 5; i <= cleanCallsign.length(); i++) {
if (!isAlpha(cleanCallsign[i - 1])) return false;
if (baseCallsignLength > 4 ) { // to validate ____AA
for (int i = 4; i < baseCallsignLength; i++) {
if (!isAlpha(callsign[i])) return false;
}
}
return true;
@@ -0,0 +1,63 @@
/* Copyright (C) 2025 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef BOARD_PINOUT_H_
#define BOARD_PINOUT_H_
// LoRa Radio
#define HAS_SX1268
#define HAS_1W_LORA
#define HAS_TCXO
#define RADIO_SCLK_PIN 18
#define RADIO_MISO_PIN 19
#define RADIO_MOSI_PIN 23
#define RADIO_CS_PIN 5
#define RADIO_RST_PIN 27
#define RADIO_DIO1_PIN 12
#define RADIO_BUSY_PIN 14
#define RADIO_RXEN 32
#define RADIO_TXEN 25
#define RADIO_WAKEUP_PIN RADIO_DIO1_PIN
#define GPIO_WAKEUP_PIN GPIO_SEL_12
// I2C
#define USE_WIRE_WITH_OLED_PINS
// Display
#define HAS_DISPLAY
#define HAS_SH1106
#undef OLED_SDA
#undef OLED_SCL
#undef OLED_RST
#define OLED_SDA 21
#define OLED_SCL 22
#define OLED_RST -1 // Reset pin # (or -1 if sharing Arduino reset pin)
// GPS
#define HAS_GPS
#define GPS_BAUDRATE 9600
#define GPS_RX 17
#define GPS_TX 16
// Aditional Config
#define INTERNAL_LED_PIN 2
#define BATTERY_PIN 35
#endif
@@ -0,0 +1,12 @@
[env:ESP32_9M2IBR_1W_LoRa_GPS]
board = esp32dev
build_flags =
${common.build_flags}
-D RADIOLIB_EXCLUDE_LR11X0=1
-D RADIOLIB_EXCLUDE_SX127X=1
-D RADIOLIB_EXCLUDE_SX128X=1
-D ESP32_9M2IBR_1W_LoRa_GPS
lib_deps =
${common.lib_deps}
${common.display_libs}
adafruit/Adafruit SH110X @ 2.1.10
+1 -1
View File
@@ -49,7 +49,7 @@
#define INTERNAL_LED_PIN 25
#define BATTERY_PIN 37
#define ADC_CTRL 21
#define ADC_CTRL_PIN 21
#define ADC_CTRL_ON_STATE LOW
#endif
@@ -35,6 +35,7 @@
// Display
#define HAS_DISPLAY
#define HAS_SH1106
#undef OLED_SDA
#undef OLED_SCL