mirror of
https://github.com/f4exb/sdrangel.git
synced 2026-08-10 21:43:31 -04:00
08ff033dc3
cppcheck reported an out-of-bounds access in APRSPacket::parseItem() when parsing item names. The parser checked the string length using an incorrect boundary condition, allowing an index equal to the string length to be accessed. Validate the item terminator before indexing the string, and ensure malformed packets without a valid terminator are rejected. Also simplify the item name parsing logic while enforcing the APRS 3-9 character item name limit. Signed-off-by: Robin Getz <rgetz503@gmail.com>
1219 lines
40 KiB
C++
1219 lines
40 KiB
C++
///////////////////////////////////////////////////////////////////////////////////
|
|
// Copyright (C) 2021-2022 Jon Beniston, M7RCE <jon@beniston.com> //
|
|
// Copyright (C) 2022 Peter Beckman <beckman@angryox.com> //
|
|
// Copyright (C) 2023 Daniele Forsi <iu5hkx@gmail.com> //
|
|
// //
|
|
// This program 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 as version 3 of the License, or //
|
|
// (at your option) any later version. //
|
|
// //
|
|
// This program 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 V3 for more details. //
|
|
// //
|
|
// You should have received a copy of the GNU General Public License //
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
|
|
///////////////////////////////////////////////////////////////////////////////////
|
|
|
|
#include <QRegularExpression>
|
|
#include <QStringList>
|
|
#include <QDateTime>
|
|
|
|
#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
|
|
bool APRSPacket::decode(AX25Packet packet)
|
|
{
|
|
// Check type, PID and length of packet
|
|
if ((packet.m_type == "UI") && (packet.m_pid == "f0") && (packet.m_data.length() >= 1))
|
|
{
|
|
// Check destination address
|
|
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;
|
|
m_via = packet.m_via;
|
|
m_data = packet.m_data;
|
|
|
|
// UTF-8 is supported: http://aprs.org/aprs12/utf-8.txt
|
|
QString data = QString::fromUtf8(packet.m_data);
|
|
|
|
if (packet.m_to.startsWith("GPS") || packet.m_to.startsWith("SPC") || packet.m_to.startsWith("SYM"))
|
|
{
|
|
// FIXME: Trailing letters xyz specify a symbol
|
|
}
|
|
|
|
// Source address SSID can be used to specify a symbol
|
|
|
|
// First byte of information field is data type ID
|
|
char dataType = data[0].toLatin1();
|
|
int idx = 1;
|
|
switch (dataType)
|
|
{
|
|
case '!': // Position without timestamp or Ultimeter 2000 WX Station
|
|
parsePosition(data, idx);
|
|
if (m_symbolCode == '_')
|
|
parseWeather(data, idx, false);
|
|
else if (m_symbolCode == '@')
|
|
parseStorm(data, idx);
|
|
else
|
|
{
|
|
parseDataExension(data, idx);
|
|
parseComment(data, idx);
|
|
}
|
|
break;
|
|
case '#': // Peet Bros U-II Weather Station
|
|
case '$': // Raw GPS data or Ultimeter 2000
|
|
case '%': // Agrelo DFJr / MicroFinder
|
|
break;
|
|
case ')': // Item
|
|
parseItem(data, idx);
|
|
parsePosition(data, idx);
|
|
parseDataExension(data, idx);
|
|
parseComment(data, idx);
|
|
break;
|
|
case '*': // Peet Bros U-II Weather Station
|
|
break;
|
|
case '/': // Position with timestamp (no APRS messaging)
|
|
parseTime(data, idx);
|
|
parsePosition(data, idx);
|
|
if (m_symbolCode == '_')
|
|
parseWeather(data, idx, false);
|
|
else if (m_symbolCode == '@')
|
|
parseStorm(data, idx);
|
|
else
|
|
{
|
|
parseDataExension(data, idx);
|
|
parseComment(data, idx);
|
|
}
|
|
break;
|
|
case ':': // Message
|
|
parseMessage(data, idx);
|
|
break;
|
|
case ';': // Object
|
|
parseObject(data, idx);
|
|
parseTime(data, idx);
|
|
parsePosition(data, idx);
|
|
if (m_symbolCode == '_')
|
|
parseWeather(data, idx, false);
|
|
else if (m_symbolCode == '@')
|
|
parseStorm(data, idx);
|
|
else
|
|
{
|
|
parseDataExension(data, idx);
|
|
parseComment(data, idx);
|
|
}
|
|
break;
|
|
case '<': // Station Capabilities
|
|
break;
|
|
case '=': // Position without timestamp (with APRS messaging)
|
|
parsePosition(data, idx);
|
|
if (m_symbolCode == '_')
|
|
parseWeather(data, idx, false);
|
|
else if (m_symbolCode == '@')
|
|
parseStorm(data, idx);
|
|
else
|
|
{
|
|
parseDataExension(data, idx);
|
|
parseComment(data, idx);
|
|
}
|
|
break;
|
|
case '>': // Status
|
|
parseStatus(data, idx);
|
|
break;
|
|
case '?': // Query
|
|
break;
|
|
case '@': // Position with timestamp (with APRS messaging)
|
|
parseTime(data, idx);
|
|
parsePosition(data, idx);
|
|
if (m_symbolCode == '_')
|
|
parseWeather(data, idx, false);
|
|
else if (m_symbolCode == '@')
|
|
parseStorm(data, idx);
|
|
else
|
|
{
|
|
parseDataExension(data, idx);
|
|
parseComment(data, idx);
|
|
}
|
|
break;
|
|
case 'T': // Telemetry data
|
|
parseTelemetry(data, idx);
|
|
break;
|
|
case '_': // Weather report (without position)
|
|
parseTimeMDHM(data, idx);
|
|
parseWeather(data, idx, true);
|
|
break;
|
|
case '`': // Mic-E Information Field Data (current)
|
|
case '\'': // Mic-E Information Field Data (old)
|
|
parseMicE(data, idx, m_to);
|
|
break;
|
|
case '{': // User-defined APRS packet format
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
if (m_hasSymbol)
|
|
{
|
|
int num = m_symbolCode - '!';
|
|
m_symbolImage = QString("aprs/aprs/aprs-symbols-24-%1-%2.png").arg(m_symbolTable == '/' ? 0 : 1).arg(num, 2, 10, QChar('0'));
|
|
}
|
|
|
|
return true;
|
|
} else {
|
|
qDebug() << "APRSPacket::decode: AX.25 Destination did not match known regexp " << m_to;
|
|
}
|
|
} else {
|
|
qDebug() << "APRSPacket::decode: Not APRS: type=" << packet.m_type << " pid=" << packet.m_pid << " length=" << packet.m_data.length();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
int APRSPacket::charToInt(QString&s, int idx)
|
|
{
|
|
char c = s[idx].toLatin1();
|
|
return c == ' ' ? 0 : c - '0';
|
|
}
|
|
|
|
bool APRSPacket::parseTime(QString& info, int& idx)
|
|
{
|
|
if (info.length() < idx+7)
|
|
return false;
|
|
|
|
QDateTime currentDateTime;
|
|
|
|
if (info[idx+6]=='h')
|
|
{
|
|
// HMS format
|
|
if (info[idx].isDigit()
|
|
&& info[idx+1].isDigit()
|
|
&& info[idx+2].isDigit()
|
|
&& info[idx+3].isDigit()
|
|
&& info[idx+4].isDigit()
|
|
&& info[idx+5].isDigit())
|
|
{
|
|
int hour = charToInt(info, idx) * 10 + charToInt(info, idx+1);
|
|
int min = charToInt(info, idx+2) * 10 + charToInt(info, idx+3);
|
|
int sec = charToInt(info, idx+4) * 10 + charToInt(info, idx+5);
|
|
|
|
if (hour > 23)
|
|
return false;
|
|
if (min > 59)
|
|
return false;
|
|
if (sec > 60) // Can have 60 seconds when there's a leap second
|
|
return false;
|
|
|
|
m_utc = true;
|
|
m_timestamp = QDateTime(QDate::currentDate(), QTime(hour, min, sec));
|
|
m_hasTimestamp = true;
|
|
|
|
idx += 7;
|
|
return true;
|
|
}
|
|
else
|
|
return false;
|
|
}
|
|
else if ((info[idx+6]=='z') || (info[idx+6]=='/'))
|
|
{
|
|
// DHM format
|
|
if (info[idx].isDigit()
|
|
&& info[idx+1].isDigit()
|
|
&& info[idx+2].isDigit()
|
|
&& info[idx+3].isDigit()
|
|
&& info[idx+4].isDigit()
|
|
&& info[idx+5].isDigit())
|
|
{
|
|
int day = charToInt(info, idx) * 10 + charToInt(info, idx+1);
|
|
int hour = charToInt(info, idx+2) * 10 + charToInt(info, idx+3);
|
|
int min = charToInt(info, idx+4) * 10 + charToInt(info, idx+5);
|
|
|
|
if (day > 31)
|
|
return false;
|
|
if (hour > 23)
|
|
return false;
|
|
if (min > 59)
|
|
return false;
|
|
|
|
m_utc = info[idx+6]=='z';
|
|
currentDateTime = m_utc ? QDateTime::currentDateTimeUtc() : QDateTime::currentDateTime();
|
|
m_timestamp = QDateTime(QDate(currentDateTime.date().year(), currentDateTime.date().month(), day), QTime(hour, min, 0));
|
|
m_hasTimestamp = true;
|
|
|
|
idx += 7;
|
|
return true;
|
|
}
|
|
else
|
|
return false;
|
|
}
|
|
else
|
|
return false;
|
|
}
|
|
|
|
// Time format used in weather reports without position
|
|
bool APRSPacket::parseTimeMDHM(QString& info, int& idx)
|
|
{
|
|
if (info.length() < idx+8)
|
|
return false;
|
|
|
|
if (info[idx].isDigit()
|
|
&& info[idx+1].isDigit()
|
|
&& info[idx+2].isDigit()
|
|
&& info[idx+3].isDigit()
|
|
&& info[idx+4].isDigit()
|
|
&& info[idx+5].isDigit()
|
|
&& info[idx+6].isDigit()
|
|
&& info[idx+7].isDigit())
|
|
{
|
|
int month = charToInt(info, idx) * 10 + charToInt(info, idx+1);
|
|
int day = charToInt(info, idx+2) * 10 + charToInt(info, idx+3);
|
|
int hour = charToInt(info, idx+4) * 10 + charToInt(info, idx+5);
|
|
int min = charToInt(info, idx+6) * 10 + charToInt(info, idx+7);
|
|
|
|
if (month > 12)
|
|
return false;
|
|
if (day > 31)
|
|
return false;
|
|
if (hour > 23)
|
|
return false;
|
|
if (min > 59)
|
|
return false;
|
|
|
|
m_utc = true;
|
|
QDateTime currentDateTime = QDateTime::currentDateTimeUtc();
|
|
m_timestamp = QDateTime(QDate(currentDateTime.date().year(), month, day), QTime(hour, min, 0));
|
|
m_hasTimestamp = true;
|
|
|
|
return true;
|
|
}
|
|
else
|
|
return false;
|
|
}
|
|
|
|
// Position ambigutiy can be specified by using spaces instead of digits in lats and longs
|
|
bool APRSPacket::isLatLongChar(const QChar c)
|
|
{
|
|
return (c.isDigit() || c == ' ');
|
|
}
|
|
|
|
bool APRSPacket::parsePosition(QString& info, int& idx)
|
|
{
|
|
float latitude;
|
|
float longitude;
|
|
char table;
|
|
char code;
|
|
|
|
if (info.length() < idx+8+1+9+1)
|
|
return false;
|
|
|
|
// Latitude
|
|
if (info[idx].isDigit()
|
|
&& info[idx+1].isDigit()
|
|
&& isLatLongChar(info[idx+2])
|
|
&& isLatLongChar(info[idx+3])
|
|
&& (info[idx+4]=='.')
|
|
&& isLatLongChar(info[idx+5])
|
|
&& isLatLongChar(info[idx+6])
|
|
&& ((info[idx+7]=='N') || (info[idx+7]=='S')))
|
|
{
|
|
int deg = charToInt(info, idx) * 10 + charToInt(info, idx+1);
|
|
int min = charToInt(info, idx+2) * 10 + charToInt(info, idx+3);
|
|
int hundreths = charToInt(info, idx+5) * 10 + charToInt(info, idx+6);
|
|
bool north = (info[idx+7]=='N');
|
|
if (deg > 90)
|
|
return false;
|
|
else if ((deg == 90) && ((min != 0) || (hundreths != 0)))
|
|
return false;
|
|
latitude = ((float)deg) + min/60.0 + hundreths/60.0/100.0;
|
|
if (!north)
|
|
latitude = -latitude;
|
|
idx += 8;
|
|
}
|
|
else
|
|
return false;
|
|
|
|
// Symbol table identifier
|
|
table = info[idx++].toLatin1();
|
|
|
|
// Longitude
|
|
if (info[idx].isDigit()
|
|
&& info[idx+1].isDigit()
|
|
&& info[idx+2].isDigit()
|
|
&& isLatLongChar(info[idx+3])
|
|
&& isLatLongChar(info[idx+4])
|
|
&& (info[idx+5]=='.')
|
|
&& isLatLongChar(info[idx+6])
|
|
&& isLatLongChar(info[idx+7])
|
|
&& ((info[idx+8]=='E') || (info[idx+8]=='W')))
|
|
{
|
|
int deg = charToInt(info, idx) * 100 + charToInt(info, idx+1) * 10 + charToInt(info, idx+2);
|
|
int min = charToInt(info, idx+3) * 10 + charToInt(info, idx+4);
|
|
int hundreths = charToInt(info, idx+6) * 10 + charToInt(info, idx+7);
|
|
bool east = (info[idx+8]=='E');
|
|
if (deg > 180)
|
|
return false;
|
|
else if ((deg == 180) && ((min != 0) || (hundreths != 0)))
|
|
return false;
|
|
longitude = ((float)deg) + min/60.0 + hundreths/60.0/100.0;
|
|
if (!east)
|
|
longitude = -longitude;
|
|
idx += 9;
|
|
}
|
|
else
|
|
return false;
|
|
|
|
// Symbol table code
|
|
code = info[idx++].toLatin1();
|
|
|
|
// Update state as we have a valid position
|
|
m_latitude = latitude;
|
|
m_longitude = longitude;
|
|
m_hasPosition = true;
|
|
m_symbolTable = table;
|
|
m_symbolCode = code;
|
|
m_hasSymbol = true;
|
|
return true;
|
|
}
|
|
|
|
bool APRSPacket::parseDataExension(QString& info, int& idx)
|
|
{
|
|
int heightMap[] = {10, 20, 40, 80, 160, 320, 640, 1280, 2560, 5120};
|
|
QStringList directivityMap = {"Omni", "NE", "E", "SE", "S", "SW", "W", "NW", "N", ""};
|
|
|
|
int remainingLength = info.length() - idx;
|
|
if (remainingLength < 7)
|
|
return true;
|
|
QString s = info.right(remainingLength);
|
|
|
|
// Course and speed
|
|
QRegularExpression courseSpeed("^([0-9]{3})\\/([0-9]{3})");
|
|
QRegularExpressionMatch match;
|
|
match = courseSpeed.match(s);
|
|
if (match.hasMatch())
|
|
{
|
|
m_course = match.capturedTexts()[1].toInt();
|
|
m_speed = match.capturedTexts()[2].toInt();
|
|
m_hasCourseAndSpeed = true;
|
|
idx += 7;
|
|
return true;
|
|
}
|
|
|
|
// Station radio details
|
|
QRegularExpression phg("^PHG([0-9])([0-9])([0-9])([0-9])");
|
|
match = phg.match(s);
|
|
if (match.hasMatch())
|
|
{
|
|
// Transmitter power
|
|
int powerCode = match.capturedTexts()[1].toInt();
|
|
int powerMap[] = {0, 1, 4, 9, 16, 25, 36, 49, 64, 81};
|
|
m_powerWatts = powerMap[powerCode];
|
|
|
|
// Antenna height
|
|
int heightCode = match.capturedTexts()[2].toInt();
|
|
m_antennaHeightFt = heightMap[heightCode];
|
|
|
|
// Antenna gain
|
|
m_antennaGainDB = match.capturedTexts()[3].toInt();
|
|
|
|
// Antenna directivity
|
|
int directivityCode = match.capturedTexts()[4].toInt();
|
|
m_antennaDirectivity = directivityMap[directivityCode];
|
|
|
|
m_hasStationDetails = true;
|
|
|
|
idx += 7;
|
|
return true;
|
|
}
|
|
|
|
// Radio range
|
|
QRegularExpression rng("^RNG([0-9]{4})");
|
|
match = rng.match(s);
|
|
if (match.hasMatch())
|
|
{
|
|
m_radioRangeMiles = match.capturedTexts()[1].toInt();
|
|
m_hasRadioRange = true;
|
|
idx += 7;
|
|
return true;
|
|
}
|
|
|
|
// Omni-DF strength
|
|
QRegularExpression dfs("^DFS([0-9])([0-9])([0-9])([0-9])");
|
|
match = dfs.match(s);
|
|
if (match.hasMatch())
|
|
{
|
|
// Strength S-points
|
|
m_dfStrength = match.capturedTexts()[1].toInt();
|
|
|
|
// Antenna height
|
|
int heightCode = match.capturedTexts()[2].toInt();
|
|
m_dfHeightFt = heightMap[heightCode];
|
|
|
|
// Antenna gain
|
|
m_dfGainDB = match.capturedTexts()[3].toInt();
|
|
|
|
// Antenna directivity
|
|
int directivityCode = match.capturedTexts()[4].toInt();
|
|
m_dfAntennaDirectivity = directivityMap[directivityCode];
|
|
|
|
m_hasDf = true;
|
|
idx += 7;
|
|
return true;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool APRSPacket::parseComment(QString& info, int& idx)
|
|
{
|
|
int commentLength = info.length() - idx;
|
|
if (commentLength > 0)
|
|
{
|
|
m_comment = info.right(commentLength);
|
|
|
|
// Comment can contain altitude anywhere in it. Of the form /A=001234 in feet
|
|
QRegularExpression re("\\/A=([0-9]{6})");
|
|
QRegularExpressionMatch match = re.match(m_comment);
|
|
if (match.hasMatch())
|
|
{
|
|
m_altitudeFt = match.capturedTexts()[1].toInt();
|
|
m_hasAltitude = true;
|
|
// Strip it out of comment if at start of string
|
|
if (match.capturedStart(0) == 0)
|
|
m_comment = m_comment.mid(9);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool APRSPacket::parseInt(QString& info, int& idx, int chars, int& value, bool& hasValue)
|
|
{
|
|
int total = 0;
|
|
bool negative = false;
|
|
bool noValue = false;
|
|
|
|
for (int i = 0; i < chars; i++)
|
|
{
|
|
if (info[idx].isDigit())
|
|
{
|
|
total = total * 10;
|
|
total += info[idx].toLatin1() - '0';
|
|
}
|
|
else if ((i == 0) && (info[idx] == '-'))
|
|
negative = true;
|
|
else if ((info[idx] == '.') || (info[idx] == ' '))
|
|
noValue = true;
|
|
else
|
|
return false;
|
|
idx++;
|
|
}
|
|
if (!noValue)
|
|
{
|
|
if (negative)
|
|
value = -total;
|
|
else
|
|
value = total;
|
|
hasValue = true;
|
|
}
|
|
else
|
|
hasValue = false;
|
|
return true;
|
|
}
|
|
|
|
bool APRSPacket::parseWeather(QString& info, int& idx, bool positionLess)
|
|
{
|
|
if (!positionLess)
|
|
{
|
|
if (!parseInt(info, idx, 3, m_windDirection, m_hasWindDirection))
|
|
return false;
|
|
if (info[idx++] != '/')
|
|
return false;
|
|
if (!parseInt(info, idx, 3, m_windSpeed, m_hasWindSpeed))
|
|
return false;
|
|
}
|
|
|
|
// Weather data
|
|
bool done = false;
|
|
while (!done && (idx < info.length()))
|
|
{
|
|
switch (info[idx++].toLatin1())
|
|
{
|
|
case 'c': // Wind direction
|
|
if (!parseInt(info, idx, 3, m_windDirection, m_hasWindDirection))
|
|
return false;
|
|
break;
|
|
case 's': // Wind speed
|
|
if (!parseInt(info, idx, 3, m_windSpeed, m_hasWindSpeed))
|
|
return false;
|
|
break;
|
|
case 'g': // Gust
|
|
if (!parseInt(info, idx, 3, m_gust, m_hasGust))
|
|
return false;
|
|
break;
|
|
case 't': // Temp
|
|
if (!parseInt(info, idx, 3, m_temp, m_hasTemp))
|
|
{
|
|
qDebug() << "Failed parsing temp: idx" << idx;
|
|
return false;
|
|
}
|
|
break;
|
|
case 'r': // Rain last hour
|
|
if (!parseInt(info, idx, 3, m_rainLastHr, m_hasRainLastHr))
|
|
return false;
|
|
break;
|
|
case 'p': // Rain last 24 hours
|
|
if (!parseInt(info, idx, 3, m_rainLast24Hrs, m_hasRainLast24Hrs))
|
|
return false;
|
|
break;
|
|
case 'P': // Rain since midnight
|
|
if (!parseInt(info, idx, 3, m_rainSinceMidnight, m_hasRainSinceMidnight))
|
|
return false;
|
|
break;
|
|
case 'h': // Humidity
|
|
if (!parseInt(info, idx, 2, m_humidity, m_hasHumidity))
|
|
return false;
|
|
break;
|
|
case 'b': // Barometric pressure
|
|
if (!parseInt(info, idx, 5, m_barometricPressure, m_hasBarometricPressure))
|
|
return false;
|
|
break;
|
|
case 'L': // Luminosity <999
|
|
if (!parseInt(info, idx, 3, m_luminosity, m_hasLuminsoity))
|
|
return false;
|
|
break;
|
|
case 'l': // Luminosity >= 1000
|
|
if (!parseInt(info, idx, 3, m_luminosity, m_hasLuminsoity))
|
|
return false;
|
|
m_luminosity += 1000;
|
|
break;
|
|
case 'S': // Snowfall
|
|
if (!parseInt(info, idx, 3, m_snowfallLast24Hrs, m_hasSnowfallLast24Hrs))
|
|
return false;
|
|
break;
|
|
case '#': // Raw rain counter
|
|
if (!parseInt(info, idx, 3, m_rawRainCounter, m_hasRawRainCounter))
|
|
return false;
|
|
break;
|
|
case 'X': // Radiation level
|
|
if (!parseInt(info, idx, 3, m_radiationLevel, m_hasRadiationLevel))
|
|
return false;
|
|
break;
|
|
case 'F': // Floor water level
|
|
if (!parseInt(info, idx, 4, m_floodLevel, m_hasFloodLevel))
|
|
return false;
|
|
break;
|
|
case 'V': // Battery volts
|
|
if (!parseInt(info, idx, 3, m_batteryVolts, m_hasBatteryVolts))
|
|
return false;
|
|
break;
|
|
default:
|
|
done = true;
|
|
break;
|
|
}
|
|
}
|
|
if (done)
|
|
{
|
|
// APRS 1.1 spec says remaining fields are s/w and weather unit type
|
|
// But few real-world packets actually seem to conform to the spec
|
|
idx--;
|
|
int remaining = info.length() - idx;
|
|
m_weatherUnitType = info.right(remaining);
|
|
idx += remaining;
|
|
}
|
|
|
|
m_hasWeather = true;
|
|
return true;
|
|
}
|
|
|
|
|
|
bool APRSPacket::parseStorm(QString& info, int& idx)
|
|
{
|
|
bool unused;
|
|
|
|
if (!parseInt(info, idx, 3, m_stormDirection, unused))
|
|
return false;
|
|
if (info[idx++] != '/')
|
|
return false;
|
|
if (!parseInt(info, idx, 3, m_stormSpeed, unused))
|
|
return false;
|
|
if (info[idx++] != '/')
|
|
return false;
|
|
QString type = info.mid(idx, 2);
|
|
idx += 2;
|
|
if (type == "TS")
|
|
m_stormType = "Tropical storm";
|
|
else if (type == "HC")
|
|
m_stormType = "Hurrican";
|
|
else if (type == "TD")
|
|
m_stormType = "Tropical depression";
|
|
else
|
|
m_stormType = type;
|
|
|
|
if (info[idx++] != '/') // Sustained wind speed
|
|
return false;
|
|
if (!parseInt(info, idx, 3, m_stormSustainedWindSpeed, unused))
|
|
return false;
|
|
if (info[idx++] != '^') // Peak wind gusts
|
|
return false;
|
|
if (!parseInt(info, idx, 3, m_stormPeakWindGusts, unused))
|
|
return false;
|
|
if (info[idx++] != '/') // Central pressure
|
|
return false;
|
|
if (!parseInt(info, idx, 4, m_stormCentralPresure, unused))
|
|
return false;
|
|
if (info[idx++] != '>') // Radius hurrican winds
|
|
return false;
|
|
if (!parseInt(info, idx, 3, m_stormRadiusHurricanWinds, unused))
|
|
return false;
|
|
if (info[idx++] != '&') // Radius tropical storm winds
|
|
return false;
|
|
if (!parseInt(info, idx, 3, m_stormRadiusTropicalStormWinds, unused))
|
|
return false;
|
|
m_hasStormData = true;
|
|
// Optional field
|
|
if (info.length() >= idx + 4)
|
|
{
|
|
if (info[idx] != '%') // Radius whole gail
|
|
return true;
|
|
idx++;
|
|
if (!parseInt(info, idx, 3, m_stormRadiusWholeGail, m_hasStormRadiusWholeGail))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool APRSPacket::parseObject(QString& info, int& idx)
|
|
{
|
|
if (info.length() < idx+10)
|
|
return false;
|
|
|
|
// Object names are 9 chars
|
|
m_objectName = info.mid(idx, 9).trimmed();
|
|
idx += 9;
|
|
|
|
if (info[idx] == '*')
|
|
m_objectLive = true;
|
|
else if (info[idx] == '_')
|
|
m_objectKilled = true;
|
|
else
|
|
return false;
|
|
idx++;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool APRSPacket::parseItem(QString& info, int& idx)
|
|
{
|
|
// Item names are 3-9 characters long and terminated by '!' or '_'
|
|
// Require the minimum 3-character name plus the terminator.
|
|
if (info.length() < (idx + 3 + 1))
|
|
return false;
|
|
|
|
m_objectName.clear();
|
|
|
|
for (int i = 0; (i < 9) && (idx < info.length()); ++i)
|
|
{
|
|
const QChar c = info[idx];
|
|
if (c == '!' || c == '_')
|
|
break;
|
|
|
|
m_objectName.append(c);
|
|
++idx;
|
|
}
|
|
|
|
if (idx >= info.length())
|
|
return false;
|
|
|
|
if (info[idx] != '!' && info[idx] != '_')
|
|
return false;
|
|
|
|
if (info[idx] == '!')
|
|
m_objectLive = true;
|
|
else if (info[idx] == '_')
|
|
m_objectKilled = true;
|
|
idx++;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool APRSPacket::parseStatus(QString& info, int& idx)
|
|
{
|
|
QString remaining = info.mid(idx);
|
|
|
|
QRegularExpression timestampRE("^([0-9]{6})z"); // DHM timestamp
|
|
QRegularExpression maidenheadRE("^([A-Z]{2}[0-9]{2}[A-Z]{0,2})[/\\\\]."); // Maidenhead grid locator and symbol
|
|
QRegularExpressionMatch matchTimestamp = timestampRE.match(remaining);
|
|
QRegularExpressionMatch matchMaidenhead = maidenheadRE.match(remaining);
|
|
|
|
if (matchTimestamp.hasMatch())
|
|
{
|
|
parseTime(info, idx);
|
|
m_status = info.mid(idx);
|
|
idx += m_status.length();
|
|
}
|
|
else if (matchMaidenhead.hasMatch())
|
|
{
|
|
m_maidenhead = matchMaidenhead.capturedTexts()[1];
|
|
idx += m_maidenhead.length();
|
|
m_symbolTable = info[idx++].toLatin1();
|
|
m_symbolCode = info[idx++].toLatin1();
|
|
m_hasSymbol = true;
|
|
if (info[idx] == ' ')
|
|
{
|
|
idx++;
|
|
m_status = info.mid(idx);
|
|
idx += m_status.length();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
m_status = remaining;
|
|
idx += m_status.length();
|
|
}
|
|
m_hasStatus = true;
|
|
|
|
// Check for beam heading and power in meteor scatter status reports
|
|
int len = m_status.length();
|
|
if (len >= 3)
|
|
{
|
|
if (m_status[len-3] == '^')
|
|
{
|
|
bool error = false;
|
|
char h = m_status[len-2].toLatin1();
|
|
char p = m_status[len-1].toLatin1();
|
|
|
|
if (isdigit(h))
|
|
m_beamHeading = (h - '0') * 10;
|
|
else if (isupper(h))
|
|
m_beamHeading = (h - 'A') * 10 + 100;
|
|
else
|
|
error = true;
|
|
|
|
switch (p)
|
|
{
|
|
case '1': m_beamPower = 10; break;
|
|
case '2': m_beamPower = 40; break;
|
|
case '3': m_beamPower = 90; break;
|
|
case '4': m_beamPower = 160; break;
|
|
case '5': m_beamPower = 250; break;
|
|
case '6': m_beamPower = 360; break;
|
|
case '7': m_beamPower = 490; break;
|
|
case '8': m_beamPower = 640; break;
|
|
case '9': m_beamPower = 810; break;
|
|
case ':': m_beamPower = 1000; break;
|
|
case ';': m_beamPower = 1210; break;
|
|
case '<': m_beamPower = 1440; break;
|
|
case '=': m_beamPower = 1690; break;
|
|
case '>': m_beamPower = 1960; break;
|
|
case '?': m_beamPower = 2250; break;
|
|
case '@': m_beamPower = 2560; break;
|
|
case 'A': m_beamPower = 2890; break;
|
|
case 'B': m_beamPower = 3240; break;
|
|
case 'C': m_beamPower = 3610; break;
|
|
case 'D': m_beamPower = 4000; break;
|
|
case 'E': m_beamPower = 4410; break;
|
|
case 'F': m_beamPower = 4840; break;
|
|
case 'G': m_beamPower = 5290; break;
|
|
case 'H': m_beamPower = 5760; break;
|
|
case 'I': m_beamPower = 6250; break;
|
|
case 'J': m_beamPower = 6760; break;
|
|
case 'K': m_beamPower = 7290; break;
|
|
default: error = true; break;
|
|
}
|
|
if (!error)
|
|
{
|
|
m_hasBeam = true;
|
|
m_status = m_status.left(len - 3);
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool APRSPacket::parseMessage(QString& info, int& idx)
|
|
{
|
|
if (info.length() < idx+10)
|
|
return false;
|
|
|
|
// Addressee is fixed width
|
|
if (info[idx+9] != ':')
|
|
return false;
|
|
m_addressee = info.mid(idx, 9).trimmed();
|
|
idx += 10;
|
|
|
|
// Message
|
|
m_message = info.mid(idx);
|
|
idx += m_message.length();
|
|
|
|
// Check if telemetry parameter/unit names
|
|
if (m_message.startsWith("PARM."))
|
|
{
|
|
bool done = false;
|
|
QString s("");
|
|
int i = 5;
|
|
while (!done)
|
|
{
|
|
if (i >= m_message.length())
|
|
{
|
|
if (!s.isEmpty())
|
|
m_telemetryNames.append(s);
|
|
done = true;
|
|
}
|
|
else if (m_message[i] == ',')
|
|
{
|
|
if (!s.isEmpty())
|
|
m_telemetryNames.append(s);
|
|
i++;
|
|
s = "";
|
|
}
|
|
else
|
|
s.append(m_message[i++]);
|
|
}
|
|
}
|
|
else if (m_message.startsWith("UNIT."))
|
|
{
|
|
bool done = false;
|
|
QString s("");
|
|
int i = 5;
|
|
while (!done)
|
|
{
|
|
if (i >= m_message.length())
|
|
{
|
|
if (!s.isEmpty())
|
|
m_telemetryLabels.append(s);
|
|
done = true;
|
|
}
|
|
else if (m_message[i] == ',')
|
|
{
|
|
if (!s.isEmpty())
|
|
m_telemetryLabels.append(s);
|
|
i++;
|
|
s = "";
|
|
}
|
|
else
|
|
s.append(m_message[i++]);
|
|
}
|
|
}
|
|
else if (m_message.startsWith("EQNS."))
|
|
{
|
|
bool done = false;
|
|
QString s("");
|
|
int i = 5;
|
|
QList <QString> telemetryCoefficients;
|
|
while (!done)
|
|
{
|
|
if (i >= m_message.length())
|
|
{
|
|
if (!s.isEmpty())
|
|
telemetryCoefficients.append(s);
|
|
done = true;
|
|
}
|
|
else if (m_message[i] == ',')
|
|
{
|
|
if (!s.isEmpty())
|
|
telemetryCoefficients.append(s);
|
|
i++;
|
|
s = "";
|
|
}
|
|
else
|
|
s.append(m_message[i++]);
|
|
}
|
|
m_hasTelemetryCoefficients = 0;
|
|
for (int j = 0; j < telemetryCoefficients.length() / 3; j++)
|
|
{
|
|
m_telemetryCoefficientsA[j] = telemetryCoefficients[j*3].toDouble();
|
|
m_telemetryCoefficientsB[j] = telemetryCoefficients[j*3+1].toDouble();
|
|
m_telemetryCoefficientsC[j] = telemetryCoefficients[j*3+2].toDouble();
|
|
m_hasTelemetryCoefficients++;
|
|
}
|
|
}
|
|
else if (m_message.startsWith("BITS."))
|
|
{
|
|
QString s("");
|
|
int i = 5;
|
|
for (int j = 0; j < 8; j++)
|
|
{
|
|
if (i >= m_message.length())
|
|
m_telemetryBitSense[j] = m_message[i] == '1';
|
|
else
|
|
m_telemetryBitSense[j] = true;
|
|
i++;
|
|
}
|
|
m_hasTelemetryBitSense = true;
|
|
m_telemetryProjectName = m_message.mid(i);
|
|
i += m_telemetryProjectName.length();
|
|
}
|
|
else
|
|
{
|
|
// Check for message number
|
|
QRegularExpression noRE("\\{([0-9]{1,5})$");
|
|
QRegularExpressionMatch match = noRE.match(m_message);
|
|
if (match.hasMatch())
|
|
{
|
|
m_messageNo = match.capturedTexts()[1];
|
|
m_message = m_message.left(m_message.length() - m_messageNo.length() - 1);
|
|
}
|
|
}
|
|
m_hasMessage = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool APRSPacket::parseTelemetry(QString& info, int& idx)
|
|
{
|
|
if (info[idx] == '#')
|
|
{
|
|
// Telemetry report
|
|
idx++;
|
|
if ((info[idx] == 'M') && (info[idx+1] == 'I') && (info[idx+2] == 'C'))
|
|
idx += 3;
|
|
else if (isdigit(info[idx].toLatin1()) && isdigit(info[idx+1].toLatin1()) && isdigit(info[idx+2].toLatin1()))
|
|
{
|
|
m_seqNo = info.mid(idx, 3).toInt();
|
|
m_hasSeqNo = true;
|
|
idx += 3;
|
|
}
|
|
else
|
|
return false;
|
|
|
|
if (info[idx] == ',')
|
|
idx++;
|
|
parseInt(info, idx, 3, m_a1, m_a1HasValue);
|
|
if (info[idx++] != ',')
|
|
return false;
|
|
parseInt(info, idx, 3, m_a2, m_a2HasValue);
|
|
if (info[idx++] != ',')
|
|
return false;
|
|
parseInt(info, idx, 3, m_a3, m_a3HasValue);
|
|
if (info[idx++] != ',')
|
|
return false;
|
|
parseInt(info, idx, 3, m_a4, m_a4HasValue);
|
|
if (info[idx++] != ',')
|
|
return false;
|
|
parseInt(info, idx, 3, m_a5, m_a5HasValue);
|
|
if (info[idx++] != ',')
|
|
return false;
|
|
for (int i = 0; i < 8; i++)
|
|
m_b[i] = info[idx++] == '1';
|
|
m_bHasValue = true;
|
|
|
|
m_telemetryComment = info.mid(idx);
|
|
idx += m_telemetryComment.length();
|
|
m_hasTelemetry = true;
|
|
return true;
|
|
}
|
|
else
|
|
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) {
|
|
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 = "";
|
|
QString messageBits = "";
|
|
int messageType = 0; // 0 = Standard, 1 = Custom
|
|
int longitudeOffset = 0;
|
|
// 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<QString, QString> 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()) {
|
|
m_comment = "Mic-E";
|
|
for (int i = 0; i < 6; i++) {
|
|
int charInt = charToIntAscii(dest, i);
|
|
if (inRange(48, 57, charInt)) {
|
|
latDigits.append(QString::number(charInt % 48));
|
|
} else if (inRange(65, 74, charInt)) {
|
|
latDigits.append(QString::number(charInt % 65));
|
|
} else if (inRange(80, 89, charInt)) {
|
|
latDigits.append(QString::number(charInt % 80));
|
|
} else {
|
|
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)) { // P-Z, Standard
|
|
messageBits.append('1');
|
|
messageType = 0;
|
|
} else if (inRange(65, 75, charInt)) { // A-K, Custom
|
|
messageBits.append('1');
|
|
messageType = 1;
|
|
}
|
|
}
|
|
|
|
// Latitude Direction
|
|
if (i == 3 && inRange(80, 90, charInt)) {
|
|
latitudeDirection = 1; // North
|
|
}
|
|
|
|
// Longitude Offset
|
|
if (i == 4 && inRange(80, 90, charInt)) {
|
|
longitudeOffset = 100;
|
|
}
|
|
|
|
// Longitude Direction
|
|
if (i == 5 && inRange(80, 90, charInt)) {
|
|
longitudeDirection = -1; // West
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// 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 Hundredths 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; 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))
|
|
deg -= 80;
|
|
if (inRange(190, 199, deg))
|
|
deg -= 190;
|
|
|
|
int min = (charToIntAscii(info, idx+1) - 28) % 60;
|
|
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
|
|
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_course = course;
|
|
m_speed = speed;
|
|
m_hasCourseAndSpeed = true;
|
|
} else {
|
|
qDebug() << "APRSPacket::parseMicE: Information Data invalid ASCII range " << info;
|
|
return false;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// 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;
|
|
|
|
// 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()) {
|
|
QList<int> micEAltitudeMultipliers = {91 * 91, 91, 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 += (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;
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|