From 668b2c74908ec8de0c6e639fd660aeb93e20afdd Mon Sep 17 00:00:00 2001 From: Peter Beckman Date: Wed, 19 Jan 2022 00:23:33 -0500 Subject: [PATCH 01/11] First attempt at implementing APRS Mic-E Message Decoding --- sdrbase/util/aprs.cpp | 135 ++++++++++++++++++++++++++++++++++++++++++ sdrbase/util/aprs.h | 2 + 2 files changed, 137 insertions(+) diff --git a/sdrbase/util/aprs.cpp b/sdrbase/util/aprs.cpp index ee2029e13..e2f139daf 100644 --- a/sdrbase/util/aprs.cpp +++ b/sdrbase/util/aprs.cpp @@ -143,6 +143,10 @@ bool APRSPacket::decode(AX25Packet packet) parseTimeMDHM(packet.m_dataASCII, idx); parseWeather(packet.m_dataASCII, idx, true); break; + case '`': // Mic-E Information Field Data (current) + case '\'': // Mic-E Information Field Data (old) + parseMicE(packet.m_dataASCII, idx, m_to); + break; case '{': // User-defined APRS packet format break; default: @@ -168,6 +172,12 @@ int APRSPacket::charToInt(QString&s, int idx) return c == ' ' ? 0 : c - '0'; } +// Mic-E Byte in Range +bool APRSPacket::inRange(unsigned low, unsigned high, unsigned x) +{ + return (low <= x && x <= high); +} + bool APRSPacket::parseTime(QString& info, int& idx) { if (info.length() < idx+7) @@ -988,3 +998,128 @@ bool APRSPacket::parseTelemetry(QString& info, int& idx) else return false; } + +bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) +{ + // Mic-E Location data is encoded in the AX.25 Destination Address + if (dest.length() < 6) + return false; + + // Mic-E Information data is 8 bytes minimum, 13-14 with altitude + if (info.length() < idx+8) + return false; + + QString latDigits = ''; + QString messageBits = ''; + int messageType = 0; // 0 = Standard, 1 = Custom + int latitudeOffset = 0; + float latitudeDirection = -1; // Assume South because North is easier to code + float longitudeDirection = 1; + + QRegExp re("^[A-LP-Z0-9]{3}[L-Z0-9]{3}.?$"); // 6-7 bytes + if (re.exactMatch(dest)) { + for (int i = 0; i < 6; i++) { + int charInt = charToInt(dest, i); + if (inRange(48, 57, charInt)) { + latDigits.append(static_cast(dest[i] % 48)); + } else if (inRange(65, 74, charInt)) { + latDigits.append(static_cast(dest[i] % 48)); + } else if (inRange(80, 89, charInt)) { + latDigits.append(static_cast(dest[i] % 80)); + } else { + latDigits.append(' '); + } + + // Message Type is encoded in 3 bits + if (i < 3) { + if (inRange(48, 57, charInt) || charInt == 76) // 0-9 or L + messageBits.append('0'); + else if (inRange(80, 90, charInt)) { // A-K, Standard + messageBits.append('1'); + messageType = 0; + } else if (inRange(65, 75, charInt)) { // P-Z, Custom + messageBits.append('1'); + messageType = 1; + } + } + + // Latitude Direction + if (i == 3) { + if (inRange(65, 75, charInt)) + latitudeDirection = 1; + } + + // Longitude Offset + if (i == 4) { + if (inRange(65, 75, charInt)) + latitudeOffset = 100; + } + + // Longitude Direction + if (i == 5) { + if (inRange(65, 75, charInt)) + longitudeDirection = -1; + } + } + + m_latitude = (((float)latDigits.mid(0, 2)) + latDigits.mid(2, 2)/60.00 + latDigits.mid(4, 2)/60.0/100.0) * latitudeDirection; + + } else + return false; + + // Mic-E Data is encoded in ASCII. + // 0: Longitude Degrees, 0-360 + // 1: Longitude Minutes, 0-59 + // 2: Longitude Hundreths of a minute, 0-99 + // 3: Speed, units of 10 + // 4: Speed, units of 1, Course, Units of 100 + // 5: Course, 0-99 degrees + // 6: Symbol Code + // 7: Symbol Table ID, / = standard, \ = alternate + if (inRange(38, 127, charToInt(info, idx)) + && inRange(38, 97, charToInt(info, idx+1)) + && inRange(28, 127, charToInt(info, idx+2)) + && inRange(48, 127, charToInt(info, idx+3)) + && inRange(28, 125, charToInt(info, idx+4)) + && inRange(28, 127, charToInt(info, idx+5)) + && inRange(33, 126, charToInt(info, idx+6)) + && (charToInt(info, idx+7) == 47 || charToInt(info, idx+7) == 92) + ) + { + // Longitude + int deg = charToInt(info, idx) - 28; + // Destination Byte 5, ASCII P through Z is offset of +100 + if (inRange(80, 90, charToInt(dest, 4))) + deg += 100; + if (inRange(180, 189, deg)) + deg -= 80; + if (inRange(190, 199, deg)) + deg -= 190; + + // Crap. You need the Longitude Offset from the Destination Address. + // How do I get that here??? + + int min = (charToInt(info, idx+1) - 28) % 60; + int hundreths = charToInt(info, idx+2); + + // Course and Speed + + int speed = ((charToInt(info, idx+3) - 28) * 10) % 800; // Speed in 10 kts units + float decoded_speed_course = (charToInt(info, idx+4) - 28) / 10; + speed += floor(decoded_speed_course); // Speed in 1 kt units, added to above + int course = (((charToInt(info, idx+4) - 28) % 10) * 100) % 400; + int course += charToInt(info, idx+5) - 28; + + m_longitude = (((float)deg) + min/60.00 + hundreths/60.0/100.0) * longitudeDirection; + m_hasPosition = true; + m_symbolTable = info[idx+7].toLatin1(); + m_symbolCode = info[idx+6].toLatin1(); + m_hasSymbol = true; + } else + return false; + + return true; + // Altitude + // #TODO +} + diff --git a/sdrbase/util/aprs.h b/sdrbase/util/aprs.h index d3933c539..c97742b86 100644 --- a/sdrbase/util/aprs.h +++ b/sdrbase/util/aprs.h @@ -461,6 +461,7 @@ private: int charToInt(QString &s, int idx); bool parseTime(QString& info, int& idx); bool parseTimeMDHM(QString& info, int& idx); + bool inRange(unsigned low, unsigned high, unsigned x); bool isLatLongChar(QCharRef c); bool parsePosition(QString& info, int& idx); bool parseDataExension(QString& info, int& idx); @@ -473,6 +474,7 @@ private: bool parseStatus(QString& info, int& idx); bool parseMessage(QString& info, int& idx); bool parseTelemetry(QString& info, int& idx); + bool parseMicE(QString& info, int& idx, QString& dest); }; #endif // INCLUDE_APRS_H From f07600f24e04a2331def89de99d0c45037c7b54f Mon Sep 17 00:00:00 2001 From: Peter Beckman Date: Wed, 19 Jan 2022 00:33:08 -0500 Subject: [PATCH 02/11] Found a few bugs in Code Review --- sdrbase/util/aprs.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/sdrbase/util/aprs.cpp b/sdrbase/util/aprs.cpp index e2f139daf..fcbdc4088 100644 --- a/sdrbase/util/aprs.cpp +++ b/sdrbase/util/aprs.cpp @@ -1012,7 +1012,7 @@ bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) QString latDigits = ''; QString messageBits = ''; int messageType = 0; // 0 = Standard, 1 = Custom - int latitudeOffset = 0; + int longitudeOffset = 0; float latitudeDirection = -1; // Assume South because North is easier to code float longitudeDirection = 1; @@ -1023,7 +1023,7 @@ bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) if (inRange(48, 57, charInt)) { latDigits.append(static_cast(dest[i] % 48)); } else if (inRange(65, 74, charInt)) { - latDigits.append(static_cast(dest[i] % 48)); + latDigits.append(static_cast(dest[i] % 65)); } else if (inRange(80, 89, charInt)) { latDigits.append(static_cast(dest[i] % 80)); } else { @@ -1052,7 +1052,7 @@ bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) // Longitude Offset if (i == 4) { if (inRange(65, 75, charInt)) - latitudeOffset = 100; + longitudeOffset = 100; } // Longitude Direction @@ -1063,6 +1063,7 @@ bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) } m_latitude = (((float)latDigits.mid(0, 2)) + latDigits.mid(2, 2)/60.00 + latDigits.mid(4, 2)/60.0/100.0) * latitudeDirection; + m_hasPosition = true; } else return false; @@ -1086,11 +1087,9 @@ bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) && (charToInt(info, idx+7) == 47 || charToInt(info, idx+7) == 92) ) { - // Longitude - int deg = charToInt(info, idx) - 28; + // Longitude, plus offset encoded in the AX.25 Destination + int deg = (charToInt(info, idx) - 28) + longitudeOffset; // Destination Byte 5, ASCII P through Z is offset of +100 - if (inRange(80, 90, charToInt(dest, 4))) - deg += 100; if (inRange(180, 189, deg)) deg -= 80; if (inRange(190, 199, deg)) @@ -1118,8 +1117,9 @@ bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) } else return false; - return true; // Altitude // #TODO + + return true; } From d9943f697645caa734abf23c437aa33106724406 Mon Sep 17 00:00:00 2001 From: Peter Beckman Date: Wed, 19 Jan 2022 14:30:15 -0500 Subject: [PATCH 03/11] Adding brackets, fixing QString double quotes, changing inRange() to inline --- sdrbase/util/aprs.cpp | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/sdrbase/util/aprs.cpp b/sdrbase/util/aprs.cpp index fcbdc4088..2778af1b2 100644 --- a/sdrbase/util/aprs.cpp +++ b/sdrbase/util/aprs.cpp @@ -21,6 +21,11 @@ #include "aprs.h" +inline bool inRange(unsigned low, unsigned high, unsigned x) +{ + return (low <= x && x <= high); +} + // See: http://www.aprs.org/doc/APRS101.PDF // Currently we only decode what we want to display on the map @@ -172,12 +177,6 @@ int APRSPacket::charToInt(QString&s, int idx) return c == ' ' ? 0 : c - '0'; } -// Mic-E Byte in Range -bool APRSPacket::inRange(unsigned low, unsigned high, unsigned x) -{ - return (low <= x && x <= high); -} - bool APRSPacket::parseTime(QString& info, int& idx) { if (info.length() < idx+7) @@ -1009,8 +1008,8 @@ bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) if (info.length() < idx+8) return false; - QString latDigits = ''; - QString messageBits = ''; + QString latDigits = ""; + QString messageBits = ""; int messageType = 0; // 0 = Standard, 1 = Custom int longitudeOffset = 0; float latitudeDirection = -1; // Assume South because North is easier to code @@ -1020,7 +1019,7 @@ bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) if (re.exactMatch(dest)) { for (int i = 0; i < 6; i++) { int charInt = charToInt(dest, i); - if (inRange(48, 57, charInt)) { + if (inRange(48, 57, charInt)) { latDigits.append(static_cast(dest[i] % 48)); } else if (inRange(65, 74, charInt)) { latDigits.append(static_cast(dest[i] % 65)); @@ -1032,9 +1031,9 @@ bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) // Message Type is encoded in 3 bits if (i < 3) { - if (inRange(48, 57, charInt) || charInt == 76) // 0-9 or L + if (inRange(48, 57, charInt) || charInt == 76) { // 0-9 or L messageBits.append('0'); - else if (inRange(80, 90, charInt)) { // A-K, Standard + } else if (inRange(80, 90, charInt)) { // A-K, Standard messageBits.append('1'); messageType = 0; } else if (inRange(65, 75, charInt)) { // P-Z, Custom @@ -1065,8 +1064,9 @@ bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) m_latitude = (((float)latDigits.mid(0, 2)) + latDigits.mid(2, 2)/60.00 + latDigits.mid(4, 2)/60.0/100.0) * latitudeDirection; m_hasPosition = true; - } else + } else { return false; + } // Mic-E Data is encoded in ASCII. // 0: Longitude Degrees, 0-360 @@ -1114,8 +1114,9 @@ bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) m_symbolTable = info[idx+7].toLatin1(); m_symbolCode = info[idx+6].toLatin1(); m_hasSymbol = true; - } else + } else { return false; + } // Altitude // #TODO From 2d397207a613d2c64a7f131a5effd4a62596c175 Mon Sep 17 00:00:00 2001 From: Peter Beckman Date: Wed, 19 Jan 2022 14:32:08 -0500 Subject: [PATCH 04/11] Removing inRange from .h in favor of inline method --- sdrbase/util/aprs.h | 1 - 1 file changed, 1 deletion(-) diff --git a/sdrbase/util/aprs.h b/sdrbase/util/aprs.h index c97742b86..5a20810ef 100644 --- a/sdrbase/util/aprs.h +++ b/sdrbase/util/aprs.h @@ -461,7 +461,6 @@ private: int charToInt(QString &s, int idx); bool parseTime(QString& info, int& idx); bool parseTimeMDHM(QString& info, int& idx); - bool inRange(unsigned low, unsigned high, unsigned x); bool isLatLongChar(QCharRef c); bool parsePosition(QString& info, int& idx); bool parseDataExension(QString& info, int& idx); From cd9bf5770e979cb2f8266fd473cf86ab63c89766 Mon Sep 17 00:00:00 2001 From: Peter Beckman Date: Wed, 19 Jan 2022 15:12:50 -0500 Subject: [PATCH 05/11] Fixing course, latitude calculation, latitude character appending --- sdrbase/util/aprs.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sdrbase/util/aprs.cpp b/sdrbase/util/aprs.cpp index 2778af1b2..064bd3319 100644 --- a/sdrbase/util/aprs.cpp +++ b/sdrbase/util/aprs.cpp @@ -1019,12 +1019,12 @@ bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) if (re.exactMatch(dest)) { for (int i = 0; i < 6; i++) { int charInt = charToInt(dest, i); - if (inRange(48, 57, charInt)) { - latDigits.append(static_cast(dest[i] % 48)); + if (inRange(48, 57, charInt)) { + latDigits.append(static_cast(charToInt(dest, i) % 48)); } else if (inRange(65, 74, charInt)) { - latDigits.append(static_cast(dest[i] % 65)); + latDigits.append(static_cast(charToInt(dest, i) % 65)); } else if (inRange(80, 89, charInt)) { - latDigits.append(static_cast(dest[i] % 80)); + latDigits.append(static_cast(charToInt(dest, i) % 80)); } else { latDigits.append(' '); } @@ -1061,7 +1061,7 @@ bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) } } - m_latitude = (((float)latDigits.mid(0, 2)) + latDigits.mid(2, 2)/60.00 + latDigits.mid(4, 2)/60.0/100.0) * latitudeDirection; + m_latitude = (latDigits.mid(0, 2).toFloat() + latDigits.mid(2, 2).toFloat()/60.00 + latDigits.mid(4, 2).toFloat()/60.0/100.0) * latitudeDirection; m_hasPosition = true; } else { @@ -1087,7 +1087,7 @@ bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) && (charToInt(info, idx+7) == 47 || charToInt(info, idx+7) == 92) ) { - // Longitude, plus offset encoded in the AX.25 Destination + // Longitude, plus offset encoded in the AX.25 Destination int deg = (charToInt(info, idx) - 28) + longitudeOffset; // Destination Byte 5, ASCII P through Z is offset of +100 if (inRange(180, 189, deg)) @@ -1107,7 +1107,7 @@ bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) float decoded_speed_course = (charToInt(info, idx+4) - 28) / 10; speed += floor(decoded_speed_course); // Speed in 1 kt units, added to above int course = (((charToInt(info, idx+4) - 28) % 10) * 100) % 400; - int course += charToInt(info, idx+5) - 28; + course += charToInt(info, idx+5) - 28; m_longitude = (((float)deg) + min/60.00 + hundreths/60.0/100.0) * longitudeDirection; m_hasPosition = true; From de66c8cb3a9dbd8236ceea1b25272e6319a1f6e5 Mon Sep 17 00:00:00 2001 From: Peter Beckman Date: Wed, 19 Jan 2022 15:39:20 -0500 Subject: [PATCH 06/11] Adding Debug lines to APRSPacket::parseMicE, adding regex match for Mic-E APRS --- sdrbase/util/aprs.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/sdrbase/util/aprs.cpp b/sdrbase/util/aprs.cpp index 064bd3319..44d876501 100644 --- a/sdrbase/util/aprs.cpp +++ b/sdrbase/util/aprs.cpp @@ -36,7 +36,8 @@ bool APRSPacket::decode(AX25Packet packet) { // Check destination address QRegExp re("^(AIR.*|ALL.*|AP.*|BEACON|CQ.*|GPS.*|DF.*|DGPS.*|DRILL.*|DX.*|ID.*|JAVA.*|MAIL.*|MICE.*|QST.*|QTH.*|RTCM.*|SKY.*|SPACE.*|SPC.*|SYM.*|TEL.*|TEST.*|TLM.*|WX.*|ZIP.*)"); - if (re.exactMatch(packet.m_to)) + QRegExp re_mice("^[A-LP-Z0-9]{3}[L-Z0-9]{3}.?$"); // Mic-E Encoded Destination, 6-7 bytes + if (re.exactMatch(packet.m_to) || re_mice.exactMatch(packet.m_to)) { m_from = packet.m_from; m_to = packet.m_to; @@ -165,9 +166,14 @@ bool APRSPacket::decode(AX25Packet packet) } return true; + } else { + qDebug() << "APRSPacket::decode: AX.25 Destination did not match known regexp " << m_to; } + } else { + qDebug() << "APRSPacket::decode: Invalid value in type=" << packet.m_type << " pid=" << packet.m_pid << " length of " << packet.m_dataASCII; } + return false; } @@ -1002,10 +1008,12 @@ bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) { // Mic-E Location data is encoded in the AX.25 Destination Address if (dest.length() < 6) + qDebug() << "APRSPacket::parseMicE: Destination invalid length " << dest; return false; // Mic-E Information data is 8 bytes minimum, 13-14 with altitude if (info.length() < idx+8) + qDebug() << "APRSPacket::parseMicE: Information Data invalid length " << info; return false; QString latDigits = ""; @@ -1065,6 +1073,7 @@ bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) m_hasPosition = true; } else { + qDebug() << "APRSPacket::parseMicE: Destination invalid regexp match " << dest; return false; } @@ -1104,7 +1113,7 @@ bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) // Course and Speed int speed = ((charToInt(info, idx+3) - 28) * 10) % 800; // Speed in 10 kts units - float decoded_speed_course = (charToInt(info, idx+4) - 28) / 10; + float decoded_speed_course = (float)(charToInt(info, idx+4) - 28) / 10.0; speed += floor(decoded_speed_course); // Speed in 1 kt units, added to above int course = (((charToInt(info, idx+4) - 28) % 10) * 100) % 400; course += charToInt(info, idx+5) - 28; @@ -1115,12 +1124,15 @@ bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) m_symbolCode = info[idx+6].toLatin1(); m_hasSymbol = true; } else { + qDebug() << "APRSPacket::parseMicE: Information Data invalid ASCII range " << info; return false; } // Altitude // #TODO + qDebug() << "APRSPacket::parseMicE: Successfully Decoded Maybe"; + return true; } From 8c4419af1b8e0b0a7a96a3c3a463b8c9fafb05c7 Mon Sep 17 00:00:00 2001 From: Peter Beckman Date: Wed, 19 Jan 2022 23:20:45 -0500 Subject: [PATCH 07/11] Tested and working version; Adds Altitude, Status, Comment --- sdrbase/util/aprs.cpp | 150 +++++++++++++++++++++++++++++++----------- 1 file changed, 110 insertions(+), 40 deletions(-) diff --git a/sdrbase/util/aprs.cpp b/sdrbase/util/aprs.cpp index 44d876501..89762b01c 100644 --- a/sdrbase/util/aprs.cpp +++ b/sdrbase/util/aprs.cpp @@ -16,16 +16,26 @@ /////////////////////////////////////////////////////////////////////////////////// #include +#include #include #include #include "aprs.h" +#include "util/units.h" inline bool inRange(unsigned low, unsigned high, unsigned x) { return (low <= x && x <= high); } +// Required for Mic-E Decoding +inline int charToIntAscii(QString&s, int idx) +{ + char c = s.toLatin1().at(idx); + return int(c); +} + + // See: http://www.aprs.org/doc/APRS101.PDF // Currently we only decode what we want to display on the map @@ -35,9 +45,9 @@ bool APRSPacket::decode(AX25Packet packet) if ((packet.m_type == "UI") && (packet.m_pid == "f0") && (packet.m_dataASCII.length() >= 1)) { // Check destination address - QRegExp re("^(AIR.*|ALL.*|AP.*|BEACON|CQ.*|GPS.*|DF.*|DGPS.*|DRILL.*|DX.*|ID.*|JAVA.*|MAIL.*|MICE.*|QST.*|QTH.*|RTCM.*|SKY.*|SPACE.*|SPC.*|SYM.*|TEL.*|TEST.*|TLM.*|WX.*|ZIP.*)"); - QRegExp re_mice("^[A-LP-Z0-9]{3}[L-Z0-9]{3}.?$"); // Mic-E Encoded Destination, 6-7 bytes - if (re.exactMatch(packet.m_to) || re_mice.exactMatch(packet.m_to)) + QRegularExpression re("^(AIR.*|ALL.*|AP.*|BEACON|CQ.*|GPS.*|DF.*|DGPS.*|DRILL.*|DX.*|ID.*|JAVA.*|MAIL.*|MICE.*|QST.*|QTH.*|RTCM.*|SKY.*|SPACE.*|SPC.*|SYM.*|TEL.*|TEST.*|TLM.*|WX.*|ZIP.*)"); + QRegularExpression re_mice("^[A-LP-Z0-9]{3}[L-Z0-9]{3}.?$"); // Mic-E Encoded Destination, 6-7 bytes + if (re.match(packet.m_to).hasMatch() || re_mice.match(packet.m_to).hasMatch()) { m_from = packet.m_from; m_to = packet.m_to; @@ -1004,17 +1014,21 @@ bool APRSPacket::parseTelemetry(QString& info, int& idx) return false; } +// Mic-E Implementation by Peter Beckman KM4BBB github:ooglek bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) { + info = info.toLatin1(); // Mic-E Location data is encoded in the AX.25 Destination Address - if (dest.length() < 6) + if (dest.length() < 6) { qDebug() << "APRSPacket::parseMicE: Destination invalid length " << dest; return false; + } // Mic-E Information data is 8 bytes minimum, 13-14 with altitude - if (info.length() < idx+8) + if (info.length() < idx+8) { qDebug() << "APRSPacket::parseMicE: Information Data invalid length " << info; return false; + } QString latDigits = ""; QString messageBits = ""; @@ -1023,18 +1037,29 @@ bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) float latitudeDirection = -1; // Assume South because North is easier to code float longitudeDirection = 1; - QRegExp re("^[A-LP-Z0-9]{3}[L-Z0-9]{3}.?$"); // 6-7 bytes - if (re.exactMatch(dest)) { + QHash messageTypeLookup; + messageTypeLookup["111"] = "Off Duty"; + messageTypeLookup["110"] = "En Route"; + messageTypeLookup["101"] = "In Service"; + messageTypeLookup["100"] = "Returning"; + messageTypeLookup["011"] = "Committed"; + messageTypeLookup["010"] = "Special"; + messageTypeLookup["001"] = "Priority"; + messageTypeLookup["000"] = "Emergency"; + + QRegularExpression re("^[A-LP-Z0-9]{3}[L-Z0-9]{3}.?$"); // 6-7 bytes + if (re.match(dest).hasMatch()) { + m_comment = "Mic-E"; for (int i = 0; i < 6; i++) { - int charInt = charToInt(dest, i); + int charInt = charToIntAscii(dest, i); if (inRange(48, 57, charInt)) { - latDigits.append(static_cast(charToInt(dest, i) % 48)); + latDigits.append(QString::number(charInt % 48)); } else if (inRange(65, 74, charInt)) { - latDigits.append(static_cast(charToInt(dest, i) % 65)); + latDigits.append(QString::number(charInt % 65)); } else if (inRange(80, 89, charInt)) { - latDigits.append(static_cast(charToInt(dest, i) % 80)); + latDigits.append(QString::number(charInt % 80)); } else { - latDigits.append(' '); + latDigits.append('0'); } // Message Type is encoded in 3 bits @@ -1069,9 +1094,15 @@ bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) } } + if (messageTypeLookup.find(messageBits) != messageTypeLookup.end()) { + m_status = messageTypeLookup[messageBits]; + if (messageType == 1) { + m_status.append(" (custom)"); + } + m_hasStatus = true; + } m_latitude = (latDigits.mid(0, 2).toFloat() + latDigits.mid(2, 2).toFloat()/60.00 + latDigits.mid(4, 2).toFloat()/60.0/100.0) * latitudeDirection; m_hasPosition = true; - } else { qDebug() << "APRSPacket::parseMicE: Destination invalid regexp match " << dest; return false; @@ -1084,54 +1115,93 @@ bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) // 3: Speed, units of 10 // 4: Speed, units of 1, Course, Units of 100 // 5: Course, 0-99 degrees - // 6: Symbol Code - // 7: Symbol Table ID, / = standard, \ = alternate - if (inRange(38, 127, charToInt(info, idx)) - && inRange(38, 97, charToInt(info, idx+1)) - && inRange(28, 127, charToInt(info, idx+2)) - && inRange(48, 127, charToInt(info, idx+3)) - && inRange(28, 125, charToInt(info, idx+4)) - && inRange(28, 127, charToInt(info, idx+5)) - && inRange(33, 126, charToInt(info, idx+6)) - && (charToInt(info, idx+7) == 47 || charToInt(info, idx+7) == 92) + if (inRange(38, 127, charToIntAscii(info, idx)) + && inRange(38, 97, charToIntAscii(info, idx+1)) + && inRange(28, 127, charToIntAscii(info, idx+2)) + && inRange(48, 127, charToIntAscii(info, idx+3)) + && inRange(28, 125, charToIntAscii(info, idx+4)) + && inRange(28, 127, charToIntAscii(info, idx+5)) ) { // Longitude, plus offset encoded in the AX.25 Destination - int deg = (charToInt(info, idx) - 28) + longitudeOffset; - // Destination Byte 5, ASCII P through Z is offset of +100 + // Destination Byte 5, ASCII P through Z indicates an offset of +100 + int deg = (charToIntAscii(info, idx) - 28) + longitudeOffset; if (inRange(180, 189, deg)) deg -= 80; if (inRange(190, 199, deg)) deg -= 190; - // Crap. You need the Longitude Offset from the Destination Address. - // How do I get that here??? - - int min = (charToInt(info, idx+1) - 28) % 60; - int hundreths = charToInt(info, idx+2); + int min = (charToIntAscii(info, idx+1) - 28) % 60; + int hundreths = charToIntAscii(info, idx+2); // Course and Speed - int speed = ((charToInt(info, idx+3) - 28) * 10) % 800; // Speed in 10 kts units - float decoded_speed_course = (float)(charToInt(info, idx+4) - 28) / 10.0; + int speed = ((charToIntAscii(info, idx+3) - 28) * 10) % 800; // Speed in 10 kts units + float decoded_speed_course = (float)(charToIntAscii(info, idx+4) - 28) / 10.0; speed += floor(decoded_speed_course); // Speed in 1 kt units, added to above - int course = (((charToInt(info, idx+4) - 28) % 10) * 100) % 400; - course += charToInt(info, idx+5) - 28; + int course = (((charToIntAscii(info, idx+4) - 28) % 10) * 100) % 400; + course += charToIntAscii(info, idx+5) - 28; m_longitude = (((float)deg) + min/60.00 + hundreths/60.0/100.0) * longitudeDirection; m_hasPosition = true; - m_symbolTable = info[idx+7].toLatin1(); - m_symbolCode = info[idx+6].toLatin1(); - m_hasSymbol = true; + m_course = course; + m_speed = speed; + m_hasCourseAndSpeed = true; } else { qDebug() << "APRSPacket::parseMicE: Information Data invalid ASCII range " << info; return false; } - // Altitude - // #TODO + // 6: Symbol Code + // 7: Symbol Table ID, / = standard, \ = alternate, "," = Telemetry + if (inRange(33, 126, charToIntAscii(info, idx+6)) + && (charToIntAscii(info, idx+7) == 47 || charToIntAscii(info, idx+7) == 92) + ) + { + m_symbolTable = info[idx+7].toLatin1(); + m_symbolCode = info[idx+6].toLatin1(); + m_hasSymbol = true; + } - qDebug() << "APRSPacket::parseMicE: Successfully Decoded Maybe"; + // Altitude, encoded in Status Message + float altitude = -10000; + QRegularExpression re_mice_altitude("[\]>]?(.{3})}"); // 4-5 bytes, we only need the 3 to get altitude, e.g. "4T} + QRegularExpressionMatch altitude_str = re_mice_altitude.match(info); + if (altitude_str.hasMatch()) { + QHash micEAltitudeMultipliers; + micEAltitudeMultipliers[0] = 91 * 91; + micEAltitudeMultipliers[1] = 91; + micEAltitudeMultipliers[2] = 1; + + for (int i = 0; i < 3; i++) { + QString altmatch = altitude_str.captured(1); + int charInt = charToIntAscii(altmatch, i); + if (!inRange(33, 127, charInt)) { + qDebug() << "APRSPacket::parseMicE: Invalid Altitude Byte Found pos:" << QString::number(i) << " ascii int:" << QString::number(charInt); + break; + } + altitude += micEAltitudeMultipliers[i] * (float)(charInt - 33); + } + + if (altitude >= 0) { + m_altitudeFt = std::round(Units::metresToFeet(altitude)); + m_hasAltitude = true; + } + } + + // Mic-E Text Format + if (info.length() >= 9) { + QString mice_status = info.mid(9); + if (altitude_str.hasMatch() && mice_status.indexOf(altitude_str.captured(0)) != -1) { + mice_status.replace(altitude_str.captured(0), ""); + } + + m_comment += " " + mice_status; + // TODO Implement the APRS 1.2 Mic-E Text Format http://www.aprs.org/aprs12/mic-e-types.txt + // Consider the Kenwood leading characters + } + + // TODO Implement Mic-E Telemetry Data -- need to modify regexp for the Symbol Table Identifier to include comma (,) return true; } From 9e5ebe0f1d48f797c350d085f7a006988d68bdee Mon Sep 17 00:00:00 2001 From: Peter Beckman Date: Wed, 19 Jan 2022 23:41:25 -0500 Subject: [PATCH 08/11] Was seeing some invalid Coordinates, I'll dig into that overnight once I log enough packets --- sdrbase/util/aprs.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sdrbase/util/aprs.cpp b/sdrbase/util/aprs.cpp index 89762b01c..4ed38818f 100644 --- a/sdrbase/util/aprs.cpp +++ b/sdrbase/util/aprs.cpp @@ -1034,8 +1034,8 @@ bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) QString messageBits = ""; int messageType = 0; // 0 = Standard, 1 = Custom int longitudeOffset = 0; - float latitudeDirection = -1; // Assume South because North is easier to code - float longitudeDirection = 1; + float latitudeDirection = 1; // Assume North + float longitudeDirection = -1; // Assume West QHash messageTypeLookup; messageTypeLookup["111"] = "Off Duty"; @@ -1077,8 +1077,8 @@ bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) // Latitude Direction if (i == 3) { - if (inRange(65, 75, charInt)) - latitudeDirection = 1; + if (!inRange(65, 75, charInt)) + latitudeDirection = -1; } // Longitude Offset @@ -1089,8 +1089,8 @@ bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) // Longitude Direction if (i == 5) { - if (inRange(65, 75, charInt)) - longitudeDirection = -1; + if (!inRange(65, 75, charInt)) + longitudeDirection = 1; } } From f75c77f47c988744c282ed97b513b8d2f93ac451 Mon Sep 17 00:00:00 2001 From: Peter Beckman Date: Thu, 20 Jan 2022 14:48:26 -0500 Subject: [PATCH 09/11] Compacting Hash/List definitions, adding a few comments for future OSS contributors --- sdrbase/util/aprs.cpp | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/sdrbase/util/aprs.cpp b/sdrbase/util/aprs.cpp index 4ed38818f..3fd69e0eb 100644 --- a/sdrbase/util/aprs.cpp +++ b/sdrbase/util/aprs.cpp @@ -1037,15 +1037,16 @@ bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) float latitudeDirection = 1; // Assume North float longitudeDirection = -1; // Assume West - QHash messageTypeLookup; - messageTypeLookup["111"] = "Off Duty"; - messageTypeLookup["110"] = "En Route"; - messageTypeLookup["101"] = "In Service"; - messageTypeLookup["100"] = "Returning"; - messageTypeLookup["011"] = "Committed"; - messageTypeLookup["010"] = "Special"; - messageTypeLookup["001"] = "Priority"; - messageTypeLookup["000"] = "Emergency"; + QHash messageTypeLookup = { + {"111", "Off Duty"}, + {"110", "En Route"}, + {"101", "In Service"}, + {"100", "Returning"}, + {"011", "Committed"}, + {"010", "Special"}, + {"001", "Priority"}, + {"000", "Emergency"} + }; QRegularExpression re("^[A-LP-Z0-9]{3}[L-Z0-9]{3}.?$"); // 6-7 bytes if (re.match(dest).hasMatch()) { @@ -1163,15 +1164,18 @@ bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) m_hasSymbol = true; } - // Altitude, encoded in Status Message + // Altitude, encoded in Status Message in meters, converted to feet, above -10000 meters + // e.g. "4T} -> Doublequote is 34, digit 4 is 52, Capital T is 84. Subtract 33 from each -> 1, 19, 51 + // Multiply -> (1 * 91 * 91) + (19 * 91) + (51 * 1) - 10000 = 61 meters Mean Sea Level (MSL) + // ASCII Integer Character Range is 33 to 127 float altitude = -10000; - QRegularExpression re_mice_altitude("[\]>]?(.{3})}"); // 4-5 bytes, we only need the 3 to get altitude, e.g. "4T} + + // 4-5 bytes, we only need the 3 to get altitude, e.g. "4T} + // Some HTs prefix the altitude with ']' or '>', so we match that optionally but ignore it + QRegularExpression re_mice_altitude("[\]>]?(.{3})}"); QRegularExpressionMatch altitude_str = re_mice_altitude.match(info); if (altitude_str.hasMatch()) { - QHash micEAltitudeMultipliers; - micEAltitudeMultipliers[0] = 91 * 91; - micEAltitudeMultipliers[1] = 91; - micEAltitudeMultipliers[2] = 1; + QList micEAltitudeMultipliers = {91 * 91, 91, 1}; for (int i = 0; i < 3; i++) { QString altmatch = altitude_str.captured(1); @@ -1180,9 +1184,10 @@ bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) qDebug() << "APRSPacket::parseMicE: Invalid Altitude Byte Found pos:" << QString::number(i) << " ascii int:" << QString::number(charInt); break; } - altitude += micEAltitudeMultipliers[i] * (float)(charInt - 33); + altitude += (float)(charInt - 33) * micEAltitudeMultipliers.at(i); } + // Assume that the Mic-E transmission is Above Ground Level if (altitude >= 0) { m_altitudeFt = std::round(Units::metresToFeet(altitude)); m_hasAltitude = true; From f4e2226fb49b0499eca382d6c531d054b315de3f Mon Sep 17 00:00:00 2001 From: Peter Beckman Date: Thu, 20 Jan 2022 15:20:16 -0500 Subject: [PATCH 10/11] Fixed ASCII Range for Latitude Direction and compacted code, fixed escape char in Altitude Regexp --- sdrbase/util/aprs.cpp | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/sdrbase/util/aprs.cpp b/sdrbase/util/aprs.cpp index 3fd69e0eb..b3918a159 100644 --- a/sdrbase/util/aprs.cpp +++ b/sdrbase/util/aprs.cpp @@ -1034,8 +1034,9 @@ bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) QString messageBits = ""; int messageType = 0; // 0 = Standard, 1 = Custom int longitudeOffset = 0; - float latitudeDirection = 1; // Assume North - float longitudeDirection = -1; // Assume West + // Assume South & East, as North & West are encoded using consecutive Characters, easier and shorter to code + float latitudeDirection = -1; // South + float longitudeDirection = 1; // East QHash messageTypeLookup = { {"111", "Off Duty"}, @@ -1060,38 +1061,35 @@ bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) } else if (inRange(80, 89, charInt)) { latDigits.append(QString::number(charInt % 80)); } else { - latDigits.append('0'); + latDigits.append('0'); // Standard states "space" but we put a zero for math } // Message Type is encoded in 3 bits if (i < 3) { if (inRange(48, 57, charInt) || charInt == 76) { // 0-9 or L messageBits.append('0'); - } else if (inRange(80, 90, charInt)) { // A-K, Standard + } else if (inRange(80, 90, charInt)) { // P-Z, Standard messageBits.append('1'); messageType = 0; - } else if (inRange(65, 75, charInt)) { // P-Z, Custom + } else if (inRange(65, 75, charInt)) { // A-K, Custom messageBits.append('1'); messageType = 1; } } // Latitude Direction - if (i == 3) { - if (!inRange(65, 75, charInt)) - latitudeDirection = -1; + if (i == 3 && inRange(80, 90, charInt)) { + latitudeDirection = 1; // North } // Longitude Offset - if (i == 4) { - if (inRange(65, 75, charInt)) - longitudeOffset = 100; + if (i == 4 && inRange(80, 90, charInt)) { + longitudeOffset = 100; } // Longitude Direction - if (i == 5) { - if (!inRange(65, 75, charInt)) - longitudeDirection = 1; + if (i == 5 && inRange(80, 90, charInt)) { + longitudeDirection = -1; // West } } @@ -1172,7 +1170,7 @@ bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) // 4-5 bytes, we only need the 3 to get altitude, e.g. "4T} // Some HTs prefix the altitude with ']' or '>', so we match that optionally but ignore it - QRegularExpression re_mice_altitude("[\]>]?(.{3})}"); + QRegularExpression re_mice_altitude("[\\]>]?(.{3})}"); QRegularExpressionMatch altitude_str = re_mice_altitude.match(info); if (altitude_str.hasMatch()) { QList micEAltitudeMultipliers = {91 * 91, 91, 1}; From e9a805824d77c3c7ed0bd96fdaf90beb09c08ae7 Mon Sep 17 00:00:00 2001 From: Peter Beckman Date: Thu, 20 Jan 2022 16:18:05 -0500 Subject: [PATCH 11/11] Updating the DC+28 Encoding Range to include the alternate encoding --- sdrbase/util/aprs.cpp | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/sdrbase/util/aprs.cpp b/sdrbase/util/aprs.cpp index b3918a159..e4b1effec 100644 --- a/sdrbase/util/aprs.cpp +++ b/sdrbase/util/aprs.cpp @@ -1107,22 +1107,16 @@ bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) return false; } - // Mic-E Data is encoded in ASCII. - // 0: Longitude Degrees, 0-360 - // 1: Longitude Minutes, 0-59 - // 2: Longitude Hundreths of a minute, 0-99 - // 3: Speed, units of 10 - // 4: Speed, units of 1, Course, Units of 100 - // 5: Course, 0-99 degrees - if (inRange(38, 127, charToIntAscii(info, idx)) - && inRange(38, 97, charToIntAscii(info, idx+1)) - && inRange(28, 127, charToIntAscii(info, idx+2)) - && inRange(48, 127, charToIntAscii(info, idx+3)) - && inRange(28, 125, charToIntAscii(info, idx+4)) - && inRange(28, 127, charToIntAscii(info, idx+5)) + // Mic-E Data is encoded in ASCII Characters + if (inRange(38, 127, charToIntAscii(info, idx)) // 0: Longitude Degrees, 0-360 + && inRange(38, 97, charToIntAscii(info, idx+1)) // 1: Longitude Minutes, 0-59 + && inRange(28, 127, charToIntAscii(info, idx+2)) // 2: Longitude Hundreths of a minute, 0-99 + && inRange(28, 127, charToIntAscii(info, idx+3)) // 3: Speed (tens), 0-800 + && inRange(28, 125, charToIntAscii(info, idx+4)) // 4: Speed (ones), 0-9, and Course (hundreds), {0, 100, 200, 300} + && inRange(28, 127, charToIntAscii(info, idx+5)) // 5: Course, 0-99 degrees ) { - // Longitude, plus offset encoded in the AX.25 Destination + // Longitude; Degrees plus offset encoded in the AX.25 Destination // Destination Byte 5, ASCII P through Z indicates an offset of +100 int deg = (charToIntAscii(info, idx) - 28) + longitudeOffset; if (inRange(180, 189, deg)) @@ -1134,7 +1128,8 @@ bool APRSPacket::parseMicE(QString& info, int& idx, QString& dest) int hundreths = charToIntAscii(info, idx+2); // Course and Speed - + // Speed (SP+28, units of 10) can use two encodings: ASCII 28-47 and 108-127 are the same + // Speed & Course (DC+28, Speed units of 1, Course units of 100 e.g. 0, 100, 200, 300) uses two encodings int speed = ((charToIntAscii(info, idx+3) - 28) * 10) % 800; // Speed in 10 kts units float decoded_speed_course = (float)(charToIntAscii(info, idx+4) - 28) / 10.0; speed += floor(decoded_speed_course); // Speed in 1 kt units, added to above