Compare commits

...
Author SHA1 Message Date
Ricardo Guzman 759cddd0e5 date update 2026-09-22 14:28:29 -03:00
Ricardo Guzman 8186ff4fbd updated digi cleanPath and process 2026-09-22 14:27:48 -03:00
Ricardo Guzman 3720fe9881 added fix for mic encoded syslog 2026-09-16 17:12:27 -03:00
Ricardo Guzman 5d04c56fd2 tnc2 added 2026-09-15 11:52:09 -03:00
Ricardo Guzman ee65f953d6 date update 2026-09-07 10:54:24 -03:00
Ricardo Guzman c952ea0159 Merge pull request #456 from cwb/fix-oled-screen-height
display: fix SCREEN_HEIGHT definition and maxLines for SH1106 OLED
2026-09-07 10:52:20 -03:00
Ricardo Guzman 21a42ef157 update de iconos 2026-09-07 10:46:39 -03:00
Ricardo Guzman 53ee07a7c1 heard direct and repeated 2026-08-27 22:45:08 -04:00
Ricardo Guzman 12a0f5bed1 update a 24px symbol 2026-08-25 20:20:09 -04:00
Ricardo Guzman 27f34c5f67 48px 2026-08-25 20:04:12 -04:00
Craig 08bfb013ce display: fix SCREEN_HEIGHT definition and maxLines for SH1106 OLED 2026-08-25 17:03:01 -07:00
Ricardo Guzman d573c66fd7 casi 2026-08-25 19:50:37 -04:00
Ricardo Guzman e0dc2f81ee ready for testing 2026-08-24 23:18:25 -04:00
Ricardo Guzman 6e4dc3c301 update needed for t-decks 2026-08-16 12:28:40 -04:00
Ricardo Guzman d2e42c8571 readme update2 2026-08-16 12:01:31 -04:00
25 changed files with 547 additions and 101 deletions

No files matched your search

+2 -1
View File
@@ -5,6 +5,7 @@
"platformio.platformio-ide"
],
"unwantedRecommendations": [
"ms-vscode.cpptools-extension-pack"
"ms-vscode.cpptools-extension-pack",
"pioarduino.pioarduino-ide"
]
}
+10 -8
View File
@@ -16,12 +16,6 @@ ____________________________________________________
____________________________________________________
## You can support this project to continue to grow:
[<img src="https://github.com/richonguzman/LoRa_APRS_Tracker/raw/main/images/github-sponsors.png">](https://github.com/sponsors/richonguzman) [<img src="https://github.com/richonguzman/LoRa_APRS_Tracker/raw/main/images/paypalme.png">](http://paypal.me/richonguzman)
____________________________________________________
## New Web/UI
| New Menu | Map |
@@ -30,18 +24,24 @@ ____________________________________________________
____________________________________________________
## You can support this project to continue to grow:
[<img src="https://github.com/richonguzman/LoRa_APRS_Tracker/raw/main/images/github-sponsors.png">](https://github.com/sponsors/richonguzman) [<img src="https://github.com/richonguzman/LoRa_APRS_Tracker/raw/main/images/paypalme.png">](http://paypal.me/richonguzman)
____________________________________________________
## SUPPORTED BOARDS (<a href="https://github.com/richonguzman/LoRa_APRS_iGate/wiki/Supported-Boards-and-Buying-Links" target="_blank">Buying links</a>).
(NOTE: all boards with 433-868-915 MHz versions)
- TTGO Lilygo LoRa32 T3S3 V1.2 and LoRa32 V2.1 (V1.6 is the same).
- TTGO Lilygo LoRa32 LoRa32 V2.1 (V1.6 is the same) and T3S3 V1.2.
- TTGO T-Beam V1.0 , V1.1, V1.2 (also variations with SX1262 and SX1268 LoRa Modules) and Supreme V3.
- T-Deck Plus (and also regular T-Deck with/without GPS).
- HELTEC V2, V3, V3.2, T114, Wireless Stick, Wireless Stick Lite V3/V3.2, HT-CT62, Wireless Tracker, Wireless Paper.
- HELTEC V2, V3, V3.2, V4, T114, Wireless Stick, Wireless Stick Lite V3/V3.2, HT-CT62, Wireless Tracker, Wireless Paper.
- RAK Wireless 4631 + 19007(or 19003)
@@ -57,6 +57,8 @@ ____________________________________________________
- Wemos Lolin32 Oled + SX1278 DIY Version.
- and many more...
<br />
# Timeline (Versions):
+1 -1
View File
@@ -38,7 +38,7 @@ lib_deps =
ESP32Async/AsyncTCP @ 3.4.10
ESP32Async/ESPAsyncWebServer @ 3.11.1
mikalhart/TinyGPSPlus @ 1.0.3
richonguzman/APRSPacketLib @ 1.0.4
richonguzman/APRSPacketLib @ 1.0.5
display_libs =
adafruit/Adafruit GFX Library @ 1.12.6
adafruit/Adafruit SSD1306 @ 2.5.17
Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+19 -2
View File
@@ -1520,7 +1520,7 @@ body{background:var(--msh-bg);color:var(--msh-text);font-family:"Inter",system-u
<label
for="tnc.enableSerial"
class="form-label"
>Enable Serial KISS</label
>Enable Serial</label
>
</div>
</div>
@@ -1552,7 +1552,7 @@ body{background:var(--msh-bg);color:var(--msh-text);font-family:"Inter",system-u
<label
for="tnc.acceptOwn"
class="form-label"
>Accept own frames via KISS</label
>Accept own frames</label
>
</div>
</div>
@@ -1572,6 +1572,23 @@ body{background:var(--msh-bg);color:var(--msh-text);font-family:"Inter",system-u
</div>
</div>
</div>
<div class="row mt-2">
<div class="col-6">
<label
for="tnc.kissProtocol"
class="form-label"
>Protocol</label
>
<select
class="form-select form-select"
name="tnc.kissProtocol"
id="tnc.kissProtocol"
>
<option value="KISS" selected>KISS</option>
<option value="TNC2">TNC2</option>
</select>
</div>
</div>
</div>
</div>
+310 -9
View File
@@ -226,6 +226,7 @@ function loadSettings(settings) {
document.getElementById("tnc.enableSerial").checked = settings.tnc.enableSerial;
document.getElementById("tnc.acceptOwn").checked = settings.tnc.acceptOwn;
document.getElementById("tnc.aprsBridgeActive").checked = settings.tnc.aprsBridgeActive;
document.getElementById("tnc.kissProtocol").value = settings.tnc.kissProtocol ? "KISS" : "TNC2";
}
// MQTT
@@ -490,7 +491,6 @@ function toggleWiFiAutoAPFields() {
if (autoAPConfig) autoAPConfig.style.display = isEnabled ? 'block' : 'none';
}
document.querySelector(".new button").addEventListener("click", function () {
const networksContainer = document.querySelector(".list-networks");
@@ -659,10 +659,274 @@ function setMapMessage(text) {
}
}
/*
* APRS symbol renderer for Stations Map
*
* s.symbol contains APRS table + symbol code, for example:
* "/>" Primary table
* "\\>" Alternate table
* "L&" Alternate table with overlay
*
* Sprite layout:
* aprs-symbols-24-0.png = Primary table
* aprs-symbols-24-1.png = Alternate table
* aprs-symbols-24-2.png = Overlay characters
*/
function getAprsSymbolIcon(symbol, callsign = "") {
if (!symbol || String(symbol).length < 2) {
return null;
}
const value = String(symbol);
let table = value.charAt(0);
let code = value.charAt(1);
let overlay = null;
/*
* Standard APRS:
* "/" = Primary
* "\\" = Alternate
*
* A-Z / 0-9 / a-j in table position are overlays
* for the Alternate table.
*/
if (
table !== "/" &&
table !== "\\"
) {
overlay = table;
table = "\\";
}
const ascii = code.charCodeAt(0);
/*
* APRS printable symbol codes are ASCII 33..126.
*/
if (
ascii < 33 ||
ascii > 126
) {
return null;
}
const index = ascii - 33;
/*
* Sprite sheet contains 16 columns x 6 rows.
* Each cell occupies 24x24 CSS px; the
* "background-size" below tells the browser to scale the sheet back
* down to its original 24px-per-cell layout, so all position math
* stays in 24px units regardless of source resolution.
*/
const columns = 16;
const sheetSize = "384px 144px"; // 16*24 x 6*24, same for all 3 sheets
const col = index % columns;
const row = Math.floor(index / columns);
const sprite =
table === "/"
? "/aprs-symbols-24-0.png"
: "/aprs-symbols-24-1.png";
const x = -(col * 24);
const y = -(row * 24);
let overlayHtml = "";
if (overlay) {
let displayOverlay = overlay;
/*
* APRS compressed numeric overlay:
* a-j -> 0-9
*/
if (
overlay >= "a" &&
overlay <= "j"
) {
displayOverlay =
String(
overlay.charCodeAt(0) -
"a".charCodeAt(0)
);
}
/*
* The overlay sprite sheet does NOT follow the ascii-33 layout used
* by the primary/alternate tables above; it has its own grid:
* "0" at row 0 col 15, "1".."9" at row 1 col 0-8, "A".."P" at row 2,
* "Q".."Z" at row 3 col 0-9. Verified by hand against the PNG.
*/
let overlayCol = null;
let overlayRow = null;
if (displayOverlay === "0") {
overlayCol = 15;
overlayRow = 0;
} else if (displayOverlay >= "1" && displayOverlay <= "9") {
overlayCol = displayOverlay.charCodeAt(0) - "1".charCodeAt(0);
overlayRow = 1;
} else if (displayOverlay >= "A" && displayOverlay <= "P") {
overlayCol = displayOverlay.charCodeAt(0) - "A".charCodeAt(0);
overlayRow = 2;
} else if (displayOverlay >= "Q" && displayOverlay <= "Z") {
overlayCol = displayOverlay.charCodeAt(0) - "Q".charCodeAt(0);
overlayRow = 3;
}
if (overlayCol !== null) {
const ox = -(overlayCol * 24);
const oy = -(overlayRow * 24);
overlayHtml = `
<span style="
position:absolute;
left:0;
top:0;
width:24px;
height:24px;
background-image:url('/aprs-symbols-24-2.png');
background-repeat:no-repeat;
background-position:${ox}px ${oy}px;
background-size:${sheetSize};
pointer-events:none;
"></span>
`;
}
}
return L.divIcon({
className: "",
html: `
<div style="
position:relative;
width:24px;
height:24px;
background-image:url('${sprite}');
background-repeat:no-repeat;
background-position:${x}px ${y}px;
background-size:${sheetSize};
">
${overlayHtml}
<span style="
position:absolute;
left:27px;
top:6px;
font:600 10px Arial,sans-serif;
line-height:12px;
white-space:nowrap;
color:#111;
text-shadow:
-1px -1px 0 #fff,
1px -1px 0 #fff,
-1px 1px 0 #fff,
1px 1px 0 #fff;
pointer-events:none;
">${callsign}</span>
</div>
`,
iconSize: [24, 24],
iconAnchor: [12, 12],
popupAnchor: [0, -12]
});
}
let mapStationsCache = [];
let mapRouteLayer = null;
function getUsedDigipeaters(path) {
if (!path) return [];
return path
.split(",")
.map((x) => x.trim())
.filter((x) => x.endsWith("*"))
.map((x) => x.replace(/\*+$/, ""))
.filter((x) =>
x &&
!x.startsWith("WIDE") &&
!x.startsWith("TRACE") &&
!x.startsWith("TCPIP") &&
!x.startsWith("TCPXX") &&
!x.startsWith("qA")
);
}
function describeMapPath(station) {
const digis = getUsedDigipeaters(station.path);
// heardDirect viene del firmware: true si ALGUNA VEZ se oyo directo,
// aunque el ultimo paquete haya llegado repetido.
if (station.heardDirect && digis.length) {
return "Direct+Via: " + digis.join(" → ");
}
if (digis.length) {
return "Via: " + digis.join(" → ");
}
return "Direct";
}
function clearMapRoute() {
if (mapRouteLayer && mapInstance) {
mapInstance.removeLayer(mapRouteLayer);
}
mapRouteLayer = null;
}
function drawStationRoute(station) {
if (!mapInstance || !iGateLatLng) return;
clearMapRoute();
const points = [
L.latLng(station.lat, station.lon)
];
const digis = getUsedDigipeaters(station.path);
digis.forEach((digiCall) => {
const digi = mapStationsCache.find((s) =>
String(s.callsign || "").toUpperCase() === digiCall.toUpperCase()
);
if (digi && !(digi.lat === 0 && digi.lon === 0)) {
points.push(L.latLng(digi.lat, digi.lon));
}
});
points.push(iGateLatLng);
const routeColor = digis.length > 0 ? "#2e7d32" : "#1565c0";
mapRouteLayer = L.polyline(points, {
color: routeColor,
weight: 3,
opacity: 0.9
}).addTo(mapInstance);
}
function loadMapStations(stations) {
if (!mapMarkers) return;
mapStationsCache = stations || [];
mapMarkers.clearLayers();
clearMapRoute();
let drawn = 0;
(stations || []).forEach((s) => {
@@ -671,15 +935,52 @@ function loadMapStations(stations) {
const popup = `<b>${s.callsign}</b>`
+ (s.lastHeard ? `<br>Last: ${s.lastHeard}` : "")
+ `<br>RSSI ${s.RSSI} / SNR ${s.SNR}`
+ `<br>Packets: ${s.count}`;
+ `<br>Packets: ${s.count}`
+ `<br><b>${describeMapPath(s)}</b>`
+ (s.path ? `<br><small>Path: ${s.path}</small>` : "");
const aprsIcon =
getAprsSymbolIcon(s.symbol, s.callsign);
if (aprsIcon) {
const marker = L.marker(
[s.lat, s.lon],
{
icon: aprsIcon
}
)
.bindPopup(popup)
.addTo(mapMarkers);
marker.on("mouseover", function () {
drawStationRoute(s);
});
marker.on("mouseout", function () {
clearMapRoute();
});
} else { // Fallback: station with missing or invalid APRS symbol
const marker = L.circleMarker([s.lat, s.lon], {
radius: 6,
color: "#1d6fe0",
weight: 2,
fillColor: "#3b8cff",
fillOpacity: 0.9
})
.bindPopup(popup)
.addTo(mapMarkers);
marker.on("mouseover", function () {
drawStationRoute(s);
});
marker.on("mouseout", function () {
clearMapRoute();
});
}
L.circleMarker([s.lat, s.lon], {
radius: 6,
color: "#1d6fe0", // borde
weight: 2,
fillColor: "#3b8cff", // relleno
fillOpacity: 0.9
}).bindPopup(popup).addTo(mapMarkers);
drawn++;
});
+1
View File
@@ -132,6 +132,7 @@ public:
bool enableSerial;
bool acceptOwn;
bool aprsBridgeActive;
bool kissProtocol; // true = KISS, false = TNC2 -- applies to both Serial and TCP server
};
class OTA {
+1
View File
@@ -32,6 +32,7 @@ namespace GPS_Utils {
String decodeEncodedGPS(const String& packet);
String getReceivedGPS(const String& packet);
String getDistanceAndComment(const String& packet);
String getDistanceAndCommentFromMicE(const String& packet);
void setup();
void getData();
+3 -1
View File
@@ -32,12 +32,14 @@ struct MapStation {
float snr; // ultimo SNR
uint16_t count; // nro de paquetes oidos de esa estacion
char lastHeard[10]; // "HH:MM:SS" si hay NTP valido, "" si no
char path[96]; // last path received
uint32_t lastHeardMillis; // millis() -> ordenar / expirar (no se muestra)
bool heardDirect; // true si ALGUNA VEZ se oyo sin digipeaters (no se resetea)
};
namespace MAP_Utils {
void upsert(const String& callsign, float latitude, float longitude, const String& symbol, int rssi, float snr);
void upsert(const String& callsign, float latitude, float longitude, const String& path, const String& symbol, int rssi, float snr);
String getStationsJson();
}
+3
View File
@@ -29,6 +29,9 @@ board_build.embed_files =
data_embed/leaflet.css.gz
data_embed/leaflet.js.gz
data_embed/favicon.png.gz
data_embed/aprs-symbols-24-0.png.gz
data_embed/aprs-symbols-24-1.png.gz
data_embed/aprs-symbols-24-2.png.gz
extra_scripts =
pre:tools/compress.py
debug_tool = esp-prog
+2 -2
View File
@@ -68,8 +68,8 @@ ___________________________________________________________________*/
#endif
String versionDate = "2026-08-16";
String versionNumber = "4.0.0";
String versionDate = "2026-09-22";
String versionNumber = "4.0.1";
Configuration Config;
WiFiClient aprsIsClient;
WiFiClient mqttClient;
+5 -1
View File
@@ -157,6 +157,7 @@ bool Configuration::writeFile() {
data["tnc"]["enableSerial"] = tnc.enableSerial;
data["tnc"]["acceptOwn"] = tnc.acceptOwn;
data["tnc"]["aprsBridgeActive"] = tnc.aprsBridgeActive;
data["tnc"]["kissProtocol"] = tnc.kissProtocol;
data["mqtt"]["active"] = mqtt.active;
data["mqtt"]["server"] = mqtt.server;
@@ -369,11 +370,13 @@ bool Configuration::readFile() {
if (data["tnc"]["enableServer"].isNull() ||
data["tnc"]["enableSerial"].isNull() ||
data["tnc"]["acceptOwn"].isNull() ||
data["tnc"]["aprsBridgeActive"].isNull()) needsRewrite = true;
data["tnc"]["aprsBridgeActive"].isNull() ||
data["tnc"]["kissProtocol"].isNull()) needsRewrite = true;
tnc.enableServer = data["tnc"]["enableServer"] | false;
tnc.enableSerial = data["tnc"]["enableSerial"] | false;
tnc.acceptOwn = data["tnc"]["acceptOwn"] | false;
tnc.aprsBridgeActive = data["tnc"]["aprsBridgeActive"] | false;
tnc.kissProtocol = data["tnc"]["kissProtocol"] | true;
if (data["mqtt"]["active"].isNull() ||
data["mqtt"]["server"].isNull() ||
@@ -538,6 +541,7 @@ void Configuration::setDefaultValues() {
tnc.enableSerial = false;
tnc.acceptOwn = false;
tnc.aprsBridgeActive = false;
tnc.kissProtocol = true;
mqtt.active = false;
mqtt.server = "";
+31 -11
View File
@@ -42,13 +42,32 @@ extern bool backupDigiMode;
namespace DIGI_Utils {
String cleanPath(String path) {
String terms[] = {"WIDE1*,", "WIDE2*,", "*"};
for (String term : terms) {
int index = path.indexOf(term);
if (index != -1) path.remove(index, term.length()); // less memory than: tempPath.replace("*", "");
String cleanPath(const String& path) {
String result;
unsigned int start = 0;
while (true) {
int delim = path.indexOf(',', start);
int end = (delim == -1) ? path.length() : delim;
String token = path.substring(start, end);
if (token != "WIDE1*" && token != "WIDE2*") {
if (result.length() > 0) result += ",";
result += token;
}
if (delim == -1) break;
start = delim + 1;
}
return path;
return result;
}
static int pathTokenIndex(const String& path, const String& token) {
unsigned int start = 0;
while (start < path.length()) {
int end = path.indexOf(",", start);
if (end == -1) end = path.length();
if (path.substring(start, end) == token) return start;
start = end + 1;
}
return -1;
}
String processMode3Path(const String& path, const String& stationCallsign) {
@@ -94,12 +113,13 @@ namespace DIGI_Utils {
tempPath.replace("WIDE1-1", stationCallsign + "*");
} else if (tempPath.indexOf("WIDE2-") != -1 && digiMode == 2) { // WIDE2-n Digipeater
tempPath = cleanPath(path);
if (tempPath.indexOf("WIDE2-1") != -1) {
tempPath.replace("WIDE2-1", stationCallsign + "*");
} else if (tempPath.indexOf("WIDE2-2") != -1) {
tempPath.replace("WIDE2-2", stationCallsign + "*,WIDE2-1");
int idx = pathTokenIndex(tempPath, "WIDE2-1");
if (idx != -1) {
tempPath = tempPath.substring(0, idx) + stationCallsign + "*" + tempPath.substring(idx + 7);
} else {
return "";
idx = pathTokenIndex(tempPath, "WIDE2-2");
if (idx == -1) return "";
tempPath = tempPath.substring(0, idx) + stationCallsign + "*,WIDE2-1" + tempPath.substring(idx + 7);
}
} else if (digiMode == 3) { // Repeat if station callsign is in path (free to repeat).
tempPath = processMode3Path(tempPath, stationCallsign);
+10 -7
View File
@@ -57,19 +57,20 @@
String lastEpaperText;
#else
#include <Adafruit_GFX.h>
#if defined RPC_LORA_DIGIGATE_1W
#define SCREEN_HEIGHT 32
#else
#define SCREEN_HEIGHT 64
#endif
#ifdef HAS_SH1106
#include <Adafruit_SH110X.h>
Adafruit_SH1106G display(128, 64, &Wire, OLED_RST);
Adafruit_SH1106G display(128, SCREEN_HEIGHT, &Wire, OLED_RST);
#else
#include <Adafruit_SSD1306.h>
#ifdef HELTEC_WSL_V3_DISPLAY
Adafruit_SSD1306 display(128, 64, &Wire1, OLED_RST);
Adafruit_SSD1306 display(128, SCREEN_HEIGHT, &Wire1, OLED_RST);
#else
#if defined RPC_LORA_DIGIGATE_1W
#define SCREEN_HEIGHT 32
#else
#define SCREEN_HEIGHT 64
#endif
Adafruit_SSD1306 display(128, SCREEN_HEIGHT, &Wire, OLED_RST);
#endif
#endif
@@ -107,6 +108,8 @@ void displaySetup() {
maxLines = 6;
#elif (SCREEN_HEIGHT == 32)
maxLines = 3;
#else
maxLines = 6;
#endif
#ifdef HAS_EPAPER
display.landscape();
+22 -25
View File
@@ -154,10 +154,6 @@ namespace GPS_Utils {
int indexOfAt = packet.indexOf(":@");
if (indexOfAt > 10) return getReceivedGPS(packet);
const uint8_t nonEncondedLatitudeOffset = 9; // "N" / "S"
const uint8_t nonEncondedLongitudeOffset = 19; // "E" / "W"
const uint8_t encodedByteOffset = 14;
int indexOfExclamation = packet.indexOf(":!");
int indexOfEqual = packet.indexOf(":=");
int baseIndex = - 1;
@@ -168,32 +164,33 @@ namespace GPS_Utils {
}
if (baseIndex == -1) return " _ / _ / _ ";
int latitudeIndex = baseIndex + nonEncondedLatitudeOffset;
int longitudeIndex = baseIndex + nonEncondedLongitudeOffset;
int encodedByteIndex = baseIndex + encodedByteOffset;
int packetLength = packet.length();
int packetLength = packet.length();
if (baseIndex + 2 >= packetLength) return " _ / _ / _ ";
if (latitudeIndex < packetLength && longitudeIndex < packetLength) {
char latChar = packet[latitudeIndex];
char lngChar = packet[longitudeIndex];
if ((latChar == 'N' || latChar == 'S') && (lngChar == 'E' || lngChar == 'W')) return getReceivedGPS(packet);
}
if (encodedByteIndex < packetLength) {
char byteChar = packet[encodedByteIndex];
if (byteChar == 'G' || byteChar == 'Q' || byteChar == '[' || byteChar == 'H' || byteChar == 'X' || byteChar == '3') return decodeEncodedGPS(packet);
}
// Spec-based: the first character right after the DTI decides the format.
// Uncompressed positions always start with a latitude digit.
// Compressed positions always start with the symbol table id: '/', '\', or an overlay A-Z/a-j.
char firstInfoChar = packet[baseIndex + 2];
if (firstInfoChar >= '0' && firstInfoChar <= '9') return getReceivedGPS(packet);
if (firstInfoChar == '/' || firstInfoChar == '\\' ||
(firstInfoChar >= 'A' && firstInfoChar <= 'Z') ||
(firstInfoChar >= 'a' && firstInfoChar <= 'j')) return decodeEncodedGPS(packet);
return " _ / _ / _ ";
}
/*String getDistanceAndCommentFromMicE(const String& packet) {
int indexOfMicE1 = packet.indexOf(":`");
int indexOfMicE2 = packet.indexOf(":\'");
String getDistanceAndCommentFromMicE(const String& packet) {
// Mic-E encodes latitude in the AX.25 destination field and longitude
// in the info field with its own +28 offset scheme -- nothing like
// the non-encoded/base91 formats above. APRSPacketLib::processReceivedPacket
// already implements that decode (used for the map upsert in
// lora_utils.cpp), so it's reused here instead of duplicating it.
if (packet.length() < 4) return " _ / _ / _ ";
if (indexOfMicE1 > 10 || indexOfMicE2 > 10) {
return decodeEncodedGPS(packet);
}
return " _ / _ / _ ";
}*/
APRSPacket aprsPacket = APRSPacketLib::processReceivedPacket(packet.substring(3), 0, 0, 0);
if (aprsPacket.type != 4) return " _ / _ / _ "; // 4 = Mic-E
return buildDistanceAndComment(aprsPacket.latitude, aprsPacket.longitude, "");
}
void setup() {
#ifdef HAS_GPS
+2 -2
View File
@@ -291,8 +291,8 @@ namespace LoRa_Utils {
receivedPackets.push_back(receivedPacket);
APRSPacket aprsPacket = APRSPacketLib::processReceivedPacket(packet.substring(3), rssi, snr, freqError);
if (aprsPacket.type == 0 || aprsPacket.type == 4) { // 0 = GPS, 4 = Mic-E (los que traen posición)
MAP_Utils::upsert(aprsPacket.sender, aprsPacket.latitude, aprsPacket.longitude, aprsPacket.overlay + aprsPacket.symbol, aprsPacket.rssi, aprsPacket.snr);
if (aprsPacket.type == 0 || aprsPacket.type == 4) { // 0 = GPS, 4 = Mic-E (only ones with position)
MAP_Utils::upsert(aprsPacket.sender, aprsPacket.latitude, aprsPacket.longitude, aprsPacket.path, aprsPacket.overlay + aprsPacket.symbol, aprsPacket.rssi, aprsPacket.snr);
}
}
+11 -9
View File
@@ -29,9 +29,7 @@ std::vector<MapStation> mapStations;
namespace MAP_Utils {
// Copia la hora NTP "HH:MM:SS" si es valida; si no hay red (NTP devuelve
// un texto de fallback) deja el campo vacio para no guardar hora falsa.
void writeFormatedTime(char* dst, size_t size) {
void writeFormatedTime(char* dst, size_t size) { // gets NTP time ("HH:MM:SS") and writes it to dst if valid
String t = NTP_Utils::getFormatedTime();
if (t.length() == 8 && t.charAt(2) == ':') {
t.toCharArray(dst, size);
@@ -40,10 +38,8 @@ namespace MAP_Utils {
}
}
// Inserta una estacion nueva o actualiza la existente (dedup por callsign).
// Llamar solo cuando el parser ya decodifico una posicion valida.
void upsert(const String& callsign, float latitude, float longitude, const String& symbol, int rssi, float snr) {
for (auto &s : mapStations) { // 1) ya existe -> actualizar
void upsert(const String& callsign, float latitude, float longitude, const String& path, const String& symbol, int rssi, float snr) { // Upsert station only with valid position with callsign dedup.
for (auto &s : mapStations) { // 1) update
if (callsign.equals(s.callsign)) {
s.latitude = latitude;
s.longitude = longitude;
@@ -51,13 +47,15 @@ namespace MAP_Utils {
s.snr = snr;
s.count++;
symbol.toCharArray(s.symbol, sizeof(s.symbol));
path.toCharArray(s.path, sizeof(s.path));
if (path.indexOf('*') == -1) s.heardDirect = true; // se oyo directo alguna vez; no se resetea si despues llega repetido
writeFormatedTime(s.lastHeard, sizeof(s.lastHeard));
s.lastHeardMillis = millis();
return;
}
}
if (mapStations.size() >= MAX_MAP_STATIONS) { // 2) llena -> descartar la mas vieja
if (mapStations.size() >= MAX_MAP_STATIONS) { // 2) full? delete oldest one
size_t oldest = 0;
for (size_t i = 1; i < mapStations.size(); i++) {
if (mapStations[i].lastHeardMillis < mapStations[oldest].lastHeardMillis) {
@@ -67,14 +65,16 @@ namespace MAP_Utils {
mapStations.erase(mapStations.begin() + oldest);
}
MapStation st = {}; // 3) insertar nueva
MapStation st = {}; // 3) insert new
callsign.toCharArray(st.callsign, sizeof(st.callsign));
symbol.toCharArray(st.symbol, sizeof(st.symbol));
path.toCharArray(st.path, sizeof(st.path));
st.latitude = latitude;
st.longitude = longitude;
st.rssi = rssi;
st.snr = snr;
st.count = 1;
st.heardDirect = (path.indexOf('*') == -1); // primer paquete: directo si no trae digis usados
writeFormatedTime(st.lastHeard, sizeof(st.lastHeard));
st.lastHeardMillis = millis();
mapStations.push_back(st);
@@ -102,6 +102,8 @@ namespace MAP_Utils {
data[i]["SNR"] = mapStations[i].snr;
data[i]["count"] = mapStations[i].count;
data[i]["lastHeard"] = mapStations[i].lastHeard;
data[i]["path"] = mapStations[i].path;
data[i]["heardDirect"] = mapStations[i].heardDirect;
}
String buffer;
+2 -3
View File
@@ -85,9 +85,8 @@ namespace SYSLOG_Utils {
syslogPacket.concat(" ---> ");
syslogPacket.concat(String(nextChar)); // restore the actual indicator received
syslogPacket.concat(packet.substring(colonIndex + 2));
// append distance for Mic-E, same as the GPS path
//syslogPacket.concat(" / ");
//syslogPacket.concat(GPS_Utils::getDistanceAndComment(packet));
syslogPacket.concat(" / ");
syslogPacket.concat(GPS_Utils::getDistanceAndCommentFromMicE(packet));
} else if (nextChar == ';') {
syslogPacket.concat("OBJECT / ");
syslogPacket.concat(sender);
+51 -17
View File
@@ -78,7 +78,23 @@ namespace TNC_Utils {
}
}
void handleInputData(char character, int bufferIndex) {
void processInputFrame(const String& frame, int bufferIndex, const String& protocolLabel) {
if (bufferIndex != -1) {
Utils::print("<--- Got from TNC (" + protocolLabel + ") : ");
Utils::println(frame);
}
String sender = frame.substring(0,frame.indexOf(">"));
if (Config.tnc.acceptOwn || sender != Config.callsign) {
if (Config.loramodule.txActive) STATION_Utils::addToOutputPacketBuffer(frame);
if (Config.tnc.aprsBridgeActive && Config.aprs_is.active && passcodeValid && aprsIsClient.connected()) APRS_IS_Utils::upload(frame);
} else {
Utils::println("Ignored own frame from " + protocolLabel);
}
}
void handleInputDataKISS(char character, int bufferIndex) {
String* data = (bufferIndex == -1) ? &inputSerialBuffer : &inputServerBuffer[bufferIndex];
if (data->length() == 0 && character != (char)FEND) return;
@@ -89,19 +105,7 @@ namespace TNC_Utils {
const String& frame = decodeKISS(*data, isDataFrame);
if (isDataFrame) {
if (bufferIndex != -1) {
Utils::print("<--- Got from TNC : ");
Utils::println(frame);
}
String sender = frame.substring(0,frame.indexOf(">"));
if (Config.tnc.acceptOwn || sender != Config.callsign) {
if (Config.loramodule.txActive) STATION_Utils::addToOutputPacketBuffer(frame);
if (Config.tnc.aprsBridgeActive && Config.aprs_is.active && passcodeValid && aprsIsClient.connected()) APRS_IS_Utils::upload(frame);
} else {
Utils::println("Ignored own frame from KISS");
}
processInputFrame(frame, bufferIndex, "KISS");
}
data->clear();
}
@@ -111,6 +115,36 @@ namespace TNC_Utils {
}
}
void handleInputDataTNC2(char character, int bufferIndex) {
String* data = (bufferIndex == -1) ? &inputSerialBuffer : &inputServerBuffer[bufferIndex];
if (character == '\r') return;
if (character == '\n') {
String frame = *data;
frame.trim();
data->clear();
if (frame.length() > 0 && frame.indexOf(':') != -1 && frame.indexOf('>') != -1) {
processInputFrame(frame, bufferIndex, "TNC2");
}
return;
}
data->concat(character);
if (data->length() > 255) {
data->clear();
}
}
void handleInputData(char character, int bufferIndex) {
if (Config.tnc.kissProtocol) {
handleInputDataKISS(character, bufferIndex);
} else {
handleInputDataTNC2(character, bufferIndex);
}
}
void readFromClients() {
for (int i = 0; i < MAX_CLIENTS; i++) {
auto client = clients[i];
@@ -138,13 +172,13 @@ namespace TNC_Utils {
void sendToClients(const String& packet, bool stripBytes) {
String cleanPacket = stripBytes ? packet.substring(3): packet;
const String kissEncoded = encodeKISS(cleanPacket);
const String encoded = Config.tnc.kissProtocol ? encodeKISS(cleanPacket) : (cleanPacket + "\r\n");
for (int i = 0; i < MAX_CLIENTS; i++) {
auto client = clients[i];
if (client != nullptr) {
if (client->connected()) {
client->print(kissEncoded);
client->print(encoded);
client->flush();
} else {
delete client;
@@ -158,7 +192,7 @@ namespace TNC_Utils {
void sendToSerial(const String& packet, bool stripBytes) {
String cleanPacket = stripBytes ? packet.substring(3): packet;
Serial.print(encodeKISS(cleanPacket));
Serial.print(Config.tnc.kissProtocol ? encodeKISS(cleanPacket) : (cleanPacket + "\r\n"));
Serial.flush();
}
+52
View File
@@ -62,6 +62,18 @@ extern const unsigned char favicon_data[] asm("_binary_data_embed_favicon_png_gz
extern const unsigned char favicon_data_end[] asm("_binary_data_embed_favicon_png_gz_end");
extern const size_t favicon_data_len = favicon_data_end - favicon_data;
extern const unsigned char aprs_symbols_24_0_data[] asm("_binary_data_embed_aprs_symbols_24_0_png_gz_start");
extern const unsigned char aprs_symbols_24_0_data_end[] asm("_binary_data_embed_aprs_symbols_24_0_png_gz_end");
extern const size_t aprs_symbols_24_0_data_len = aprs_symbols_24_0_data_end - aprs_symbols_24_0_data;
extern const unsigned char aprs_symbols_24_1_data[] asm("_binary_data_embed_aprs_symbols_24_1_png_gz_start");
extern const unsigned char aprs_symbols_24_1_data_end[] asm("_binary_data_embed_aprs_symbols_24_1_png_gz_end");
extern const size_t aprs_symbols_24_1_data_len = aprs_symbols_24_1_data_end - aprs_symbols_24_1_data;
extern const unsigned char aprs_symbols_24_2_data[] asm("_binary_data_embed_aprs_symbols_24_2_png_gz_start");
extern const unsigned char aprs_symbols_24_2_data_end[] asm("_binary_data_embed_aprs_symbols_24_2_png_gz_end");
extern const size_t aprs_symbols_24_2_data_len = aprs_symbols_24_2_data_end - aprs_symbols_24_2_data;
namespace WEB_Utils {
@@ -92,6 +104,42 @@ namespace WEB_Utils {
request->send(response);
}
void handleAprsSymbols24Primary(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response = request->beginResponse(
200,
"image/png",
(const uint8_t*)aprs_symbols_24_0_data,
aprs_symbols_24_0_data_len
);
response->addHeader("Content-Encoding", "gzip");
response->addHeader("Cache-Control", "max-age=3600");
request->send(response);
}
void handleAprsSymbols24Alternate(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response = request->beginResponse(
200,
"image/png",
(const uint8_t*)aprs_symbols_24_1_data,
aprs_symbols_24_1_data_len
);
response->addHeader("Content-Encoding", "gzip");
response->addHeader("Cache-Control", "max-age=3600");
request->send(response);
}
void handleAprsSymbols24Overlay(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response = request->beginResponse(
200,
"image/png",
(const uint8_t*)aprs_symbols_24_2_data,
aprs_symbols_24_2_data_len
);
response->addHeader("Content-Encoding", "gzip");
response->addHeader("Cache-Control", "max-age=3600");
request->send(response);
}
void handleReadConfiguration(AsyncWebServerRequest *request) {
if(Config.webadmin.active && !request->authenticate(Config.webadmin.username.c_str(), Config.webadmin.password.c_str()))
return request->requestAuthentication();
@@ -274,6 +322,7 @@ namespace WEB_Utils {
Config.tnc.enableSerial = request->hasParam("tnc.enableSerial", true);
Config.tnc.acceptOwn = request->hasParam("tnc.acceptOwn", true);
Config.tnc.aprsBridgeActive = request->hasParam("tnc.aprsBridgeActive", true);
Config.tnc.kissProtocol = getParamStringSafe("tnc.kissProtocol", Config.tnc.kissProtocol ? "KISS" : "TNC2") != "TNC2";
Config.mqtt.active = request->hasParam("mqtt.active", true);
if (Config.mqtt.active) {
@@ -401,6 +450,9 @@ namespace WEB_Utils {
server.on("/leaflet.css", HTTP_GET, handleLeafletStyle);
server.on("/leaflet.js", HTTP_GET, handleLeafletScript);
server.on("/favicon.png", HTTP_GET, handleFavicon);
server.on("/aprs-symbols-24-0.png", HTTP_GET, handleAprsSymbols24Primary);
server.on("/aprs-symbols-24-1.png", HTTP_GET, handleAprsSymbols24Alternate);
server.on("/aprs-symbols-24-2.png", HTTP_GET, handleAprsSymbols24Overlay);
OTA_Utils::setup(&server); // Include OTA Updater for WebServer
+3
View File
@@ -29,6 +29,9 @@ files = [
'data_embed/leaflet.js',
'data_embed/leaflet.css',
'data_embed/favicon.png',
'data_embed/aprs-symbols-24-0.png',
'data_embed/aprs-symbols-24-1.png',
'data_embed/aprs-symbols-24-2.png',
]
string_to_find_str = "String"
+3 -1
View File
@@ -5,12 +5,14 @@ platform = espressif32 @ 6.3.1
board_build.partitions = min_spiffs.csv
board = esp32-s3-devkitc-1
board_build.mcu = esp32s3
board_build.embed_files =
board_build.embed_files =
data_embed/index.html.gz
data_embed/style.css.gz
data_embed/script.js.gz
data_embed/bootstrap.css.gz
data_embed/bootstrap.js.gz
data_embed/leaflet.css.gz
data_embed/leaflet.js.gz
data_embed/favicon.png.gz
extra_scripts =
pre:tools/compress.py
+3 -1
View File
@@ -5,12 +5,14 @@ platform = espressif32 @ 6.3.1
board_build.partitions = min_spiffs.csv
board = esp32-s3-devkitc-1
board_build.mcu = esp32s3
board_build.embed_files =
board_build.embed_files =
data_embed/index.html.gz
data_embed/style.css.gz
data_embed/script.js.gz
data_embed/bootstrap.css.gz
data_embed/bootstrap.js.gz
data_embed/leaflet.css.gz
data_embed/leaflet.js.gz
data_embed/favicon.png.gz
extra_scripts =
pre:tools/compress.py